21a4d0af5b
## 改动 - 新增低价审核与用户反馈的批量通过、批量拒绝接口 - 单条仍保持独立事务、审计、发奖和通知;单项失败不影响同批其它记录 - 批量响应返回每条记录的成功状态或失败原因,供前端保留失败项重试 - 反馈审核补充行锁,降低并发重复发奖风险 ## 验证 - `ruff check`(相关路由、Schema、测试) - `pytest tests/test_admin_write.py -q`:22 passed --------- Co-authored-by: unknown <798648091@qq.com> Reviewed-on: #164 Co-authored-by: linkeyu <linkeyu@wonderable.ai> Co-committed-by: linkeyu <linkeyu@wonderable.ai>
185 lines
7.5 KiB
Python
185 lines
7.5 KiB
Python
"""admin 上报更低价审核:列表 + 统计(读)+ 通过(发金币)/拒绝(写,带审计)。
|
|
|
|
数据由客户端 POST /api/v1/report 写入 price_report 表(提交即 pending);本路由是运营后台
|
|
对它的人工审核窗口。**通过** → 给上报用户钱包发固定金币(PRICE_REPORT_REWARD_COINS):
|
|
改状态 + 发金币(wallet.grant_coins)+ 审计同一事务一起 commit(原子,仿 users.grant_user_coins),
|
|
绝不只改状态不发钱或反之。通过后下发「爆料审核通过」通知(站内 + push,PRD #11);
|
|
客户端也可轮询 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 (
|
|
PriceReportBulkItemResult,
|
|
PriceReportBulkRejectRequest,
|
|
PriceReportBulkRequest,
|
|
PriceReportBulkResult,
|
|
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
|
|
from app.services import notification_events
|
|
|
|
router = APIRouter(
|
|
prefix="/admin/api/price-reports",
|
|
tags=["admin-price-report"],
|
|
dependencies=[Depends(get_current_admin)],
|
|
)
|
|
|
|
|
|
def _approve_price_report(
|
|
db: AdminDb, admin: AdminUser, report_id: int, ip: str, *, bulk: bool = False
|
|
) -> PriceReport:
|
|
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
|
|
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="上报更低价审核通过",
|
|
)
|
|
detail = {"reward_coins": coins, "user_id": rep.user_id}
|
|
if bulk:
|
|
detail["bulk"] = True
|
|
write_audit(
|
|
db, admin, action="price_report.approve", target_type="price_report", target_id=report_id,
|
|
detail=detail, ip=ip, commit=False,
|
|
)
|
|
db.commit()
|
|
notification_events.notify_report_approved(db, rep)
|
|
return rep
|
|
|
|
|
|
def _reject_price_report(
|
|
db: AdminDb, admin: AdminUser, report_id: int, reason: str, ip: str, *, bulk: bool = False
|
|
) -> PriceReport:
|
|
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}),不可重复操作")
|
|
mutations.review_price_report(db, rep, status="rejected", reject_reason=reason, commit=False)
|
|
detail = {"reason": reason, "user_id": rep.user_id}
|
|
if bulk:
|
|
detail["bulk"] = True
|
|
write_audit(
|
|
db, admin, action="price_report.reject", target_type="price_report", target_id=report_id,
|
|
detail=detail, ip=ip, commit=False,
|
|
)
|
|
db.commit()
|
|
return rep
|
|
|
|
|
|
def _bulk_result(items: list[PriceReportBulkItemResult]) -> PriceReportBulkResult:
|
|
success = sum(1 for item in items if item.ok)
|
|
return PriceReportBulkResult(
|
|
total=len(items), success=success, failed=len(items) - success, items=items,
|
|
)
|
|
|
|
|
|
@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("/bulk/approve", response_model=PriceReportBulkResult, summary="批量通过上报(发固定金币)")
|
|
def bulk_approve_price_reports(
|
|
body: PriceReportBulkRequest,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> PriceReportBulkResult:
|
|
results: list[PriceReportBulkItemResult] = []
|
|
ip = get_client_ip(request)
|
|
for report_id in body.ids:
|
|
try:
|
|
rep = _approve_price_report(db, admin, report_id, ip, bulk=True)
|
|
results.append(PriceReportBulkItemResult(id=report_id, ok=True, status=rep.status))
|
|
except HTTPException as exc:
|
|
db.rollback()
|
|
results.append(PriceReportBulkItemResult(id=report_id, ok=False, error=str(exc.detail)))
|
|
except Exception: # noqa: BLE001 - 单笔失败不打断整批
|
|
db.rollback()
|
|
results.append(PriceReportBulkItemResult(id=report_id, ok=False, error="系统异常"))
|
|
return _bulk_result(results)
|
|
|
|
|
|
@router.post("/bulk/reject", response_model=PriceReportBulkResult, summary="批量拒绝上报")
|
|
def bulk_reject_price_reports(
|
|
body: PriceReportBulkRejectRequest,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> PriceReportBulkResult:
|
|
results: list[PriceReportBulkItemResult] = []
|
|
ip = get_client_ip(request)
|
|
for report_id in body.ids:
|
|
try:
|
|
rep = _reject_price_report(db, admin, report_id, body.reason, ip, bulk=True)
|
|
results.append(PriceReportBulkItemResult(id=report_id, ok=True, status=rep.status))
|
|
except HTTPException as exc:
|
|
db.rollback()
|
|
results.append(PriceReportBulkItemResult(id=report_id, ok=False, error=str(exc.detail)))
|
|
except Exception: # noqa: BLE001 - 单笔失败不打断整批
|
|
db.rollback()
|
|
results.append(PriceReportBulkItemResult(id=report_id, ok=False, error="系统异常"))
|
|
return _bulk_result(results)
|
|
|
|
|
|
@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:
|
|
_approve_price_report(db, admin, report_id, get_client_ip(request))
|
|
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:
|
|
_reject_price_report(db, admin, report_id, body.reason, get_client_ip(request))
|
|
return OkResponse()
|