- 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「占位值」说法顺手更正。
91 lines
3.1 KiB
Python
Executable File
91 lines
3.1 KiB
Python
Executable File
"""``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()
|
||
)
|