Files
meida_backend/apps/api/routers/quota.py
T
xsl 76a0502ea3 feat(billing): 任务换额度定稿闭环——claim 凭据校验(40311)+ 定稿 4 任务目录
- claim_task_bonus 在幂等快捷路径前做完成凭据校验:report_feedback 查当日 rp_feedback、first_report 查 succeeded rp_report,无凭据 40311 且不打流水;sign_in/share_report 不校验(产品决策:签到行为即领取,分享内测期接受端侧自报,hard_cap 锁死刷量收益)
- task_bonus_rules 默认值与 .env.example 同步产品定稿(产品方案 §3.2,2026-09-11):sign_in 对话+10/day、report_feedback 发型+2/day、share_report 换装+3/day、first_report 重生成+1/lifetime;下架 invite_friend/profile_complete
- billing/repo 新增只读凭据探测 has_feedback_between/has_completed_report(import-linter 禁 billing→report,直查 shared.models 的 rp_* 表,永远只读)
- 测试:test_task_bonus_eligibility 5 用例(40311 两分支/凭据后 200/40910/40410/零流水)

偏差说明:test_task_bonus 与 test_task_bonus_rules 断言数值随定稿更新(发放/幂等/封顶语义未动);quota.py docstring「占位值」说法顺手更正。
2026-09-12 00:23:43 +08:00

91 lines
3.1 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""``GET /quota``、``GET /quota/ledger``、``POST /quota/tasks/{task_key}/claim``.
FR-BILL-07: 额度相关文案中**不得出现任何价格、购买、会员、升级字样**。
Only two recovery paths may ever be surfaced: 等待恢复 and 做任务换额度.
"""
from __future__ import annotations
from fastapi import APIRouter, Path, Query
from apps.api.deps import BillingServiceDep, CurrentUserId, SettingsDep
from apps.api.schemas import LedgerEntry, QuotaResponse
from packages.agent.service import QUOTA_KEY_AGENT_CHAT
from packages.billing.schemas import TaskBonusClaimResponse
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_granted=snapshot.bonus_granted,
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
]
)
@router.post("/tasks/{task_key}/claim", summary="做任务换额度:领取任务奖励(FR-BILL-03)")
async def claim_task_bonus(
user_id: CurrentUserId,
billing: BillingServiceDep,
settings: SettingsDep,
task_key: str = Path(max_length=64),
) -> dict[str, object]:
"""同一任务同一周期只能领一次(重复领返回 40910),受规则封顶(40310).
奖励数值由 ``TASK_BONUS_RULES_JSON`` 配置驱动(FR-BILL-04),2026-09-11 起默认值
即产品定稿(产品方案 §3.2);report_feedback/first_report 有服务端凭据校验(40311)。
"""
claim = await billing.claim_task_bonus(
user_id=user_id,
task_key=task_key,
rules=dict(settings.task_bonus_rules),
)
return ok(
TaskBonusClaimResponse(
task_key=claim.task_key,
quota_key=claim.quota_key,
amount=claim.amount,
bonus_granted=claim.bonus_granted,
remaining=claim.remaining,
).model_dump()
)