"""admin 上报更低价审核:列表 + 统计(读)+ 通过(发金币)/拒绝(写,带审计)。 数据由客户端 POST /api/v1/report 写入 price_report 表(提交即 pending);本路由是运营后台 对它的人工审核窗口。**通过** → 给上报用户钱包发固定金币(PRICE_REPORT_REWARD_COINS): 改状态 + 发金币(wallet.grant_coins)+ 审计同一事务一起 commit(原子,仿 users.grant_user_coins), 绝不只改状态不发钱或反之。客户端轮询 GET /api/v1/report/records 自动看到结果(无需推送)。 """ from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, Request from app.admin.audit import write_audit from app.admin.deps import AdminDb, get_client_ip, get_current_admin, require_role from app.admin.repositories import mutations, queries from app.admin.schemas.common import CursorPage, OkResponse from app.admin.schemas.price_report import ( PriceReportOut, PriceReportRejectRequest, PriceReportSummary, ) from app.core.rewards import PRICE_REPORT_REWARD_COINS from app.models.admin import AdminUser from app.models.price_report import PriceReport from app.repositories import wallet as wallet_repo router = APIRouter( prefix="/admin/api/price-reports", tags=["admin-price-report"], dependencies=[Depends(get_current_admin)], ) @router.get("", response_model=CursorPage[PriceReportOut], summary="上报更低价列表(筛选+分页)") def list_price_reports( db: AdminDb, status: Annotated[str | None, Query()] = None, user_id: Annotated[int | None, Query()] = None, sort_by: Annotated[str, Query(pattern="^(id|created_at)$")] = "id", sort_order: Annotated[str, Query(pattern="^(asc|desc)$")] = "desc", limit: Annotated[int, Query(ge=1, le=100)] = 20, cursor: Annotated[int | None, Query()] = None, ) -> CursorPage[PriceReportOut]: items, next_cursor, total = queries.list_price_reports( db, status=status, user_id=user_id, sort_by=sort_by, sort_order=sort_order, limit=limit, cursor=cursor, ) return CursorPage( items=[PriceReportOut.model_validate(r) for r in items], next_cursor=next_cursor, total=total, ) @router.get("/summary", response_model=PriceReportSummary, summary="上报审核统计(各状态计数)") def price_report_summary(db: AdminDb) -> PriceReportSummary: return PriceReportSummary.model_validate(queries.price_report_summary(db)) @router.post("/{report_id}/approve", response_model=OkResponse, summary="通过上报(发固定金币)") def approve_price_report( report_id: int, request: Request, admin: Annotated[AdminUser, Depends(require_role("operator"))], db: AdminDb, ) -> OkResponse: # 行锁(SELECT FOR UPDATE):并发/连点双请求会都读到 pending → 各发一次金币双倍发奖, # 锁住该行串行化,第二个请求拿锁后看到 approved → 走 400。SQLite 下 FOR UPDATE 为 no-op。 rep = db.get(PriceReport, report_id, with_for_update=True) if rep is None: raise HTTPException(status_code=404, detail="上报记录不存在") if rep.status != "pending": raise HTTPException(status_code=400, detail=f"该上报已审核过(当前 {rep.status}),不可重复操作") coins = PRICE_REPORT_REWARD_COINS # 改状态 + 发金币 + 审计同一事务(commit=False),最后一起 commit:改了就有痕、发了就留账 mutations.review_price_report(db, rep, status="approved", reward_coins=coins, commit=False) wallet_repo.grant_coins( db, rep.user_id, coins, biz_type="price_report_reward", ref_id=str(rep.id), remark="上报更低价审核通过", ) write_audit( db, admin, action="price_report.approve", target_type="price_report", target_id=report_id, detail={"reward_coins": coins, "user_id": rep.user_id}, ip=get_client_ip(request), commit=False, ) db.commit() return OkResponse() @router.post("/{report_id}/reject", response_model=OkResponse, summary="拒绝上报") def reject_price_report( report_id: int, body: PriceReportRejectRequest, request: Request, admin: Annotated[AdminUser, Depends(require_role("operator"))], db: AdminDb, ) -> OkResponse: rep = db.get(PriceReport, report_id, with_for_update=True) # 行锁,同 approve(防并发重复审核) if rep is None: raise HTTPException(status_code=404, detail="上报记录不存在") if rep.status != "pending": raise HTTPException(status_code=400, detail=f"该上报已审核过(当前 {rep.status}),不可重复操作") reason = body.reason.strip() mutations.review_price_report(db, rep, status="rejected", reject_reason=reason, commit=False) write_audit( db, admin, action="price_report.reject", target_type="price_report", target_id=report_id, detail={"reason": reason, "user_id": rep.user_id}, ip=get_client_ip(request), commit=False, ) db.commit() return OkResponse()