Files
meida_backend/apps/api/routers/quota.py
T
xsl 746b66da01 feat(api): vertical slice POST /v1/agent/chat and the Celery ping path
One request exercises every layer: JWT auth, quota reservation, a versioned
prompt through the LLM gateway, a cost row, quota commit or refund, and the
unified envelope. The trivial ping task proves the queue routing and the
tk_task transitions before any real long-running task is written.

Routes are mounted at the /api/v1 base path of 架构文档 §9.1; /v1/* is
rewritten to it at the ASGI layer so both spellings work from one route table.
2026-08-14 00:26:39 +08:00

62 lines
2.0 KiB
Python
Executable File

"""``GET /quota`` and ``GET /quota/ledger`` (架构文档 §9.3 配额).
FR-BILL-07: 额度相关文案中**不得出现任何价格、购买、会员、升级字样**。
Only two recovery paths may ever be surfaced: 等待恢复 and 做任务换额度.
"""
from __future__ import annotations
from fastapi import APIRouter, Query
from apps.api.deps import BillingServiceDep, CurrentUserId
from apps.api.schemas import LedgerEntry, QuotaResponse
from packages.agent.service import QUOTA_KEY_AGENT_CHAT
from packages.shared.response import ok
router = APIRouter(prefix="/quota", tags=["quota"])
@router.get("", summary="Current period quota for a feature")
async def get_quota(
user_id: CurrentUserId,
billing: BillingServiceDep,
quota_key: str = Query(default=QUOTA_KEY_AGENT_CHAT, max_length=64),
) -> dict[str, object]:
snapshot = await billing.get_quota(user_id=user_id, quota_key=quota_key)
return ok(
QuotaResponse(
quota_key=snapshot.quota_key,
period_key=snapshot.period_key,
used=snapshot.used,
limit_value=snapshot.limit_value,
bonus_value=snapshot.bonus_value,
remaining=snapshot.remaining,
reset_at=snapshot.reset_at,
).model_dump()
)
@router.get("/ledger", summary="Quota ledger — 流水是真相,余额是缓存")
async def get_ledger(
user_id: CurrentUserId,
billing: BillingServiceDep,
limit: int = Query(default=50, ge=1, le=200),
) -> dict[str, object]:
rows = await billing.list_ledger(user_id=user_id, limit=limit)
return ok(
[
LedgerEntry(
tx_id=str(row.tx_id),
delta=row.delta,
tx_type=row.tx_type,
status=row.status,
reason=row.reason,
quota_key=row.quota_key,
period_key=row.period_key,
balance_after=row.balance_after,
created_at=row.created_at,
).model_dump()
for row in rows
]
)