feat(compare): 比价记录落库 + 战绩里程碑(真发金币)
- comparison_record 表 + 上报(幂等 user+trace)/列表/详情 接口;服务端派生 best/saved/is_source_best/status + information(done 帧文案/失败具体原因) - 战绩里程碑:comparison_milestone_claim 表 + 进度/领取接口,按成功比价次数 解锁,复用 grant_coins 真发金币(RECORD_MILESTONES 120/180/300/500/800/1200) - 迁移 record/information/milestone 三个,单 head;rewards.py 集中档位配置 - 测试 test_compare_record(8)+test_compare_milestone(7) 全绿;docs/api 12a-12e + 索引 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""比价战绩里程碑 endpoint(福利页「记录比价战绩」)。
|
||||
|
||||
路由前缀 `/api/v1/compare`:
|
||||
GET /milestones 进度与各档领取状态
|
||||
POST /milestones/{milestone}/claim 领取某档奖励
|
||||
|
||||
**均需鉴权**。解锁进度 = 该用户 status='success' 的 comparison_record 条数;每档领一次。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from app.api.deps import CurrentUser, DbSession
|
||||
from app.repositories import comparison_milestone as crud_milestone
|
||||
from app.schemas.compare_record import (
|
||||
MilestoneClaimResultOut,
|
||||
MilestoneStateOut,
|
||||
MilestoneStatusOut,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("shagua.compare_milestone")
|
||||
|
||||
router = APIRouter(prefix="/api/v1/compare", tags=["compare-milestone"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/milestones",
|
||||
response_model=MilestoneStatusOut,
|
||||
summary="比价战绩里程碑进度",
|
||||
)
|
||||
def get_milestones(user: CurrentUser, db: DbSession) -> MilestoneStatusOut:
|
||||
st = crud_milestone.get_status(db, user.id)
|
||||
return MilestoneStatusOut(
|
||||
success_count=st.success_count,
|
||||
claimable_count=st.claimable_count,
|
||||
milestones=[
|
||||
MilestoneStateOut(milestone=m.milestone, coin=m.coin, state=m.state)
|
||||
for m in st.milestones
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/milestones/{milestone}/claim",
|
||||
response_model=MilestoneClaimResultOut,
|
||||
summary="领取比价战绩里程碑奖励",
|
||||
)
|
||||
def claim_milestone(
|
||||
milestone: int, user: CurrentUser, db: DbSession
|
||||
) -> MilestoneClaimResultOut:
|
||||
try:
|
||||
coin, balance = crud_milestone.claim(db, user.id, milestone)
|
||||
except crud_milestone.UnknownMilestoneError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="unknown milestone"
|
||||
) from e
|
||||
except crud_milestone.MilestoneLockedError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="milestone locked"
|
||||
) from e
|
||||
except crud_milestone.AlreadyClaimedError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="milestone already claimed"
|
||||
) from e
|
||||
|
||||
logger.info(
|
||||
"compare milestone claimed user_id=%d milestone=%d coin=%d",
|
||||
user.id,
|
||||
milestone,
|
||||
coin,
|
||||
)
|
||||
return MilestoneClaimResultOut(
|
||||
milestone=milestone, coin_awarded=coin, coin_balance=balance
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
"""比价记录 endpoint(「我的比价记录」数据源)。
|
||||
|
||||
路由前缀 `/api/v1/compare`:
|
||||
POST /record 上报一次比价结果(幂等:同 user+trace_id 覆盖)
|
||||
GET /records 比价记录列表(游标分页)
|
||||
GET /records/{id} 单条详情(含 raw_payload 全量)
|
||||
|
||||
**均需鉴权**(CurrentUser)——与同文件无关的不鉴权透传 `compare.py` 分开:那个是
|
||||
转发壳(MVP 不鉴权),这里是按用户维度落库的业务接口,必须有 user_id。
|
||||
|
||||
注:本轮只做 server 端,客户端(android 仓)在 done 帧后调 POST /record 上报的改动
|
||||
另起一轮(见 app-server docs/待办与技术债.md P1)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
|
||||
from app.api.deps import CurrentUser, DbSession
|
||||
from app.repositories import comparison as crud_compare
|
||||
from app.schemas.compare_record import (
|
||||
ComparisonRecordCreatedOut,
|
||||
ComparisonRecordDetailOut,
|
||||
ComparisonRecordIn,
|
||||
ComparisonRecordPage,
|
||||
ComparisonRecordOut,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("shagua.compare_record")
|
||||
|
||||
router = APIRouter(prefix="/api/v1/compare", tags=["compare-record"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/record",
|
||||
response_model=ComparisonRecordCreatedOut,
|
||||
summary="上报一次比价结果(幂等)",
|
||||
)
|
||||
def report_record(
|
||||
payload: ComparisonRecordIn, user: CurrentUser, db: DbSession
|
||||
) -> ComparisonRecordCreatedOut:
|
||||
rec = crud_compare.upsert_record(db, user_id=user.id, payload=payload)
|
||||
logger.info(
|
||||
"compare record user=%s trace=%s biz=%s status=%s saved=%s",
|
||||
user.id,
|
||||
rec.trace_id,
|
||||
rec.business_type,
|
||||
rec.status,
|
||||
rec.saved_amount_cents,
|
||||
)
|
||||
return ComparisonRecordCreatedOut(id=rec.id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/records",
|
||||
response_model=ComparisonRecordPage,
|
||||
summary="比价记录列表(游标分页)",
|
||||
)
|
||||
def list_records(
|
||||
user: CurrentUser,
|
||||
db: DbSession,
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
cursor: int | None = Query(None, description="上一页末条 id"),
|
||||
) -> ComparisonRecordPage:
|
||||
items, next_cursor = crud_compare.list_records(
|
||||
db, user.id, limit=limit, cursor=cursor
|
||||
)
|
||||
return ComparisonRecordPage(
|
||||
items=[ComparisonRecordOut.model_validate(it) for it in items],
|
||||
next_cursor=next_cursor,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/records/{record_id}",
|
||||
response_model=ComparisonRecordDetailOut,
|
||||
summary="比价记录详情(含 raw_payload)",
|
||||
)
|
||||
def get_record(
|
||||
record_id: int, user: CurrentUser, db: DbSession
|
||||
) -> ComparisonRecordDetailOut:
|
||||
rec = crud_compare.get_record(db, user.id, record_id)
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="record not found")
|
||||
return ComparisonRecordDetailOut.model_validate(rec)
|
||||
Reference in New Issue
Block a user