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>
273 lines
8.8 KiB
Python
273 lines
8.8 KiB
Python
"""admin 反馈工单:列表筛选 + 审核采纳/拒绝(带金币发放与审计)。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
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.feedback import (
|
|
FeedbackApproveRequest,
|
|
FeedbackBulkApproveRequest,
|
|
FeedbackBulkItemResult,
|
|
FeedbackBulkRejectRequest,
|
|
FeedbackBulkResult,
|
|
FeedbackOut,
|
|
FeedbackRejectRequest,
|
|
FeedbackSummary,
|
|
)
|
|
from app.models.admin import AdminUser
|
|
from app.models.feedback import Feedback
|
|
from app.repositories import wallet as wallet_repo
|
|
from app.services import notification_events
|
|
|
|
router = APIRouter(
|
|
prefix="/admin/api/feedbacks",
|
|
tags=["admin-feedback"],
|
|
dependencies=[Depends(get_current_admin)],
|
|
)
|
|
|
|
|
|
def _ensure_pending(fb: Feedback) -> None:
|
|
if fb.status not in {"pending", "new"}:
|
|
raise HTTPException(status_code=400, detail="反馈已审核")
|
|
|
|
|
|
def _approve_feedback(
|
|
db: AdminDb,
|
|
admin: AdminUser,
|
|
feedback_id: int,
|
|
payload: FeedbackApproveRequest | FeedbackBulkApproveRequest,
|
|
ip: str,
|
|
*,
|
|
bulk: bool = False,
|
|
) -> FeedbackOut:
|
|
fb = db.get(Feedback, feedback_id, with_for_update=True)
|
|
if fb is None:
|
|
raise HTTPException(status_code=404, detail="反馈不存在")
|
|
_ensure_pending(fb)
|
|
|
|
before = fb.status
|
|
mutations.review_feedback(
|
|
db,
|
|
fb,
|
|
status="adopted",
|
|
reward_coins=payload.reward_coins,
|
|
review_note=payload.note,
|
|
admin_reply=payload.reply,
|
|
reviewed_by_admin_id=admin.id,
|
|
commit=False,
|
|
)
|
|
wallet_repo.grant_coins(
|
|
db,
|
|
fb.user_id,
|
|
payload.reward_coins,
|
|
biz_type="feedback_reward",
|
|
ref_id=str(fb.id),
|
|
remark="意见反馈被采纳",
|
|
)
|
|
detail = {
|
|
"before": before,
|
|
"after": "adopted",
|
|
"reward_coins": payload.reward_coins,
|
|
"note": payload.note,
|
|
"reply": payload.reply,
|
|
}
|
|
if bulk:
|
|
detail["bulk"] = True
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="feedback.approve",
|
|
target_type="feedback",
|
|
target_id=feedback_id,
|
|
detail=detail,
|
|
ip=ip,
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
db.refresh(fb)
|
|
out = FeedbackOut.model_validate(fb)
|
|
notification_events.notify_feedback_reward(db, fb)
|
|
return out
|
|
|
|
|
|
def _reject_feedback(
|
|
db: AdminDb,
|
|
admin: AdminUser,
|
|
feedback_id: int,
|
|
payload: FeedbackRejectRequest | FeedbackBulkRejectRequest,
|
|
ip: str,
|
|
*,
|
|
bulk: bool = False,
|
|
) -> FeedbackOut:
|
|
fb = db.get(Feedback, feedback_id, with_for_update=True)
|
|
if fb is None:
|
|
raise HTTPException(status_code=404, detail="反馈不存在")
|
|
_ensure_pending(fb)
|
|
|
|
before = fb.status
|
|
mutations.review_feedback(
|
|
db,
|
|
fb,
|
|
status="rejected",
|
|
reject_reason=payload.reason,
|
|
review_note=payload.note,
|
|
admin_reply=payload.reply,
|
|
reviewed_by_admin_id=admin.id,
|
|
commit=False,
|
|
)
|
|
detail = {
|
|
"before": before,
|
|
"after": "rejected",
|
|
"reason": payload.reason,
|
|
"note": payload.note,
|
|
"reply": payload.reply,
|
|
}
|
|
if bulk:
|
|
detail["bulk"] = True
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="feedback.reject",
|
|
target_type="feedback",
|
|
target_id=feedback_id,
|
|
detail=detail,
|
|
ip=ip,
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
db.refresh(fb)
|
|
out = FeedbackOut.model_validate(fb)
|
|
notification_events.notify_feedback_reply(db, fb)
|
|
return out
|
|
|
|
|
|
def _bulk_result(items: list[FeedbackBulkItemResult]) -> FeedbackBulkResult:
|
|
success = sum(1 for item in items if item.ok)
|
|
return FeedbackBulkResult(
|
|
total=len(items), success=success, failed=len(items) - success, items=items,
|
|
)
|
|
|
|
|
|
@router.get("", response_model=CursorPage[FeedbackOut], summary="反馈工单列表")
|
|
def list_feedbacks(
|
|
db: AdminDb,
|
|
status: Annotated[str | None, Query()] = None,
|
|
# 反馈类型筛选:profile(普通反馈)/ comparison(比价反馈);None=全部
|
|
source: Annotated[str | None, Query(pattern="^(profile|comparison)$")] = None,
|
|
user_id: Annotated[int | None, Query()] = None,
|
|
content: Annotated[str | None, Query(max_length=100)] = None,
|
|
created_from: Annotated[datetime | None, Query()] = None,
|
|
created_to: Annotated[datetime | 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[FeedbackOut]:
|
|
items, next_cursor, total = queries.list_feedbacks(
|
|
db,
|
|
status=status,
|
|
source=source,
|
|
user_id=user_id,
|
|
content=content,
|
|
created_from=created_from,
|
|
created_to=created_to,
|
|
sort_by=sort_by,
|
|
sort_order=sort_order,
|
|
limit=limit,
|
|
cursor=cursor,
|
|
)
|
|
return CursorPage(
|
|
items=[FeedbackOut.model_validate(f) for f in items],
|
|
next_cursor=next_cursor,
|
|
total=total,
|
|
)
|
|
|
|
|
|
@router.get("/summary", response_model=FeedbackSummary, summary="反馈审核统计(各状态计数)")
|
|
def feedback_summary(db: AdminDb) -> FeedbackSummary:
|
|
return FeedbackSummary.model_validate(queries.feedback_summary(db))
|
|
|
|
|
|
@router.post("/bulk/approve", response_model=FeedbackBulkResult, summary="批量采纳反馈并发金币")
|
|
def bulk_approve_feedbacks(
|
|
body: FeedbackBulkApproveRequest,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> FeedbackBulkResult:
|
|
results: list[FeedbackBulkItemResult] = []
|
|
ip = get_client_ip(request)
|
|
for feedback_id in body.ids:
|
|
try:
|
|
out = _approve_feedback(db, admin, feedback_id, body, ip, bulk=True)
|
|
results.append(FeedbackBulkItemResult(id=feedback_id, ok=True, status=out.status))
|
|
except HTTPException as exc:
|
|
db.rollback()
|
|
results.append(FeedbackBulkItemResult(id=feedback_id, ok=False, error=str(exc.detail)))
|
|
except Exception: # noqa: BLE001 - 单笔失败不打断整批
|
|
db.rollback()
|
|
results.append(FeedbackBulkItemResult(id=feedback_id, ok=False, error="系统异常"))
|
|
return _bulk_result(results)
|
|
|
|
|
|
@router.post("/bulk/reject", response_model=FeedbackBulkResult, summary="批量拒绝采纳反馈")
|
|
def bulk_reject_feedbacks(
|
|
body: FeedbackBulkRejectRequest,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> FeedbackBulkResult:
|
|
results: list[FeedbackBulkItemResult] = []
|
|
ip = get_client_ip(request)
|
|
for feedback_id in body.ids:
|
|
try:
|
|
out = _reject_feedback(db, admin, feedback_id, body, ip, bulk=True)
|
|
results.append(FeedbackBulkItemResult(id=feedback_id, ok=True, status=out.status))
|
|
except HTTPException as exc:
|
|
db.rollback()
|
|
results.append(FeedbackBulkItemResult(id=feedback_id, ok=False, error=str(exc.detail)))
|
|
except Exception: # noqa: BLE001 - 单笔失败不打断整批
|
|
db.rollback()
|
|
results.append(FeedbackBulkItemResult(id=feedback_id, ok=False, error="系统异常"))
|
|
return _bulk_result(results)
|
|
|
|
|
|
@router.post("/{feedback_id}/handle", response_model=OkResponse, summary="标记反馈已处理")
|
|
def handle_feedback(
|
|
feedback_id: int,
|
|
_admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> OkResponse:
|
|
fb = db.get(Feedback, feedback_id)
|
|
if fb is None:
|
|
raise HTTPException(status_code=404, detail="反馈不存在")
|
|
raise HTTPException(status_code=400, detail="请使用采纳或拒绝接口审核反馈")
|
|
|
|
|
|
@router.post("/{feedback_id}/approve", response_model=FeedbackOut, summary="采纳反馈并发金币")
|
|
def approve_feedback(
|
|
feedback_id: int,
|
|
payload: FeedbackApproveRequest,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> FeedbackOut:
|
|
return _approve_feedback(db, admin, feedback_id, payload, get_client_ip(request))
|
|
|
|
|
|
@router.post("/{feedback_id}/reject", response_model=FeedbackOut, summary="拒绝采纳反馈")
|
|
def reject_feedback(
|
|
feedback_id: int,
|
|
payload: FeedbackRejectRequest,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> FeedbackOut:
|
|
return _reject_feedback(db, admin, feedback_id, payload, get_client_ip(request))
|