Files
meida_backend/packages/billing/repo.py
T
xsl 7c67506474 feat(domains): identity, billing, task services and domain placeholders
Billing implements reserve -> commit -> refund against bl_quota and bl_ledger
with row-level locking and idempotency keys; the refund commits on its own
transaction so a failed request cannot silently keep a user's quota. Task
implements the unified task centre over the state machine. Identity issues and
resolves users. Remaining domains are placeholders whose READMEs state what
belongs there and what must not.
2026-08-14 00:26:39 +08:00

80 lines
2.4 KiB
Python
Executable File

"""Data access for the billing domain. **Only this module may touch bl_* tables.**
架构文档 §3.2 硬性规则 1: 每个域只能通过对方暴露的 service 接口调用,禁止直接
查对方的表。import-linter enforces it (contract "no cross-domain repository access").
"""
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
from packages.shared.models import Ledger, Quota, QuotaRule
async def get_rule(session: AsyncSession, quota_key: str) -> QuotaRule | None:
return await session.get(QuotaRule, quota_key)
async def lock_or_create_quota(
session: AsyncSession,
*,
user_id: int,
quota_key: str,
period_key: str,
limit_value: int,
) -> Quota:
"""Get the period row with a row lock, creating it on first use.
``ON CONFLICT DO NOTHING`` + ``SELECT ... FOR UPDATE`` makes concurrent first
requests for the same period safe without an advisory lock.
"""
await session.execute(
pg_insert(Quota)
.values(
user_id=user_id,
quota_key=quota_key,
period_key=period_key,
used=0,
limit_value=limit_value,
bonus_value=0,
)
.on_conflict_do_nothing(index_elements=["user_id", "quota_key", "period_key"])
)
result = await session.execute(
select(Quota)
.where(
Quota.user_id == user_id,
Quota.quota_key == quota_key,
Quota.period_key == period_key,
)
.with_for_update()
)
row = result.scalar_one()
return row
async def get_ledger_by_idempotency_key(session: AsyncSession, key: str) -> Ledger | None:
result = await session.execute(select(Ledger).where(Ledger.idempotency_key == key))
return result.scalar_one_or_none()
async def get_ledger(session: AsyncSession, tx_id: int) -> Ledger | None:
return await session.get(Ledger, tx_id)
async def list_ledger(session: AsyncSession, user_id: int, limit: int = 50) -> list[Ledger]:
result = await session.execute(
select(Ledger)
.where(Ledger.user_id == user_id)
.order_by(Ledger.created_at.desc(), Ledger.tx_id.desc())
.limit(limit)
)
return list(result.scalars().all())
def add_ledger(session: AsyncSession, ledger: Ledger) -> Ledger:
session.add(ledger)
return ledger