df2b31726c
用户反馈后台:区分反馈类型(比价反馈/普通反馈)+ 审核可给用户留言;提现后台:按提现类型筛选。 - feedback 表加 source(profile/comparison)/scene/admin_reply + 迁移;提交接口 /api/v1/feedback 接收 source/scene,来源判定显式 source 优先、否则据 scene 有无派生(有=comparison); /records 带回 scene + admin_reply。 - admin 反馈:列表加「反馈类型」筛选;采纳/拒绝支持存 admin_reply(给用户的回复,用户端可见); FeedbackOut 带出 source/scene/admin_reply。 - admin 提现:WithdrawOut 暴露 source,列表加「提现类型」筛选(coin_cash=福利页提现 / invite_cash=邀请提现)。 - 补 4 项测试:来源派生、回复存取、反馈按类型筛选、提现按类型筛选。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
183 lines
5.6 KiB
Python
183 lines
5.6 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,
|
|
FeedbackOut,
|
|
FeedbackRejectRequest,
|
|
FeedbackSummary,
|
|
)
|
|
from app.models.admin import AdminUser
|
|
from app.models.feedback import Feedback
|
|
from app.repositories import wallet as wallet_repo
|
|
|
|
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="反馈已审核")
|
|
|
|
|
|
@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("/{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:
|
|
fb = db.get(Feedback, feedback_id)
|
|
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="意见反馈被采纳",
|
|
)
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="feedback.approve",
|
|
target_type="feedback",
|
|
target_id=feedback_id,
|
|
detail={
|
|
"before": before,
|
|
"after": "adopted",
|
|
"reward_coins": payload.reward_coins,
|
|
"note": payload.note,
|
|
"reply": payload.reply,
|
|
},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
db.refresh(fb)
|
|
return FeedbackOut.model_validate(fb)
|
|
|
|
|
|
@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:
|
|
fb = db.get(Feedback, feedback_id)
|
|
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,
|
|
)
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="feedback.reject",
|
|
target_type="feedback",
|
|
target_id=feedback_id,
|
|
detail={
|
|
"before": before,
|
|
"after": "rejected",
|
|
"reason": payload.reason,
|
|
"note": payload.note,
|
|
"reply": payload.reply,
|
|
},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
db.refresh(fb)
|
|
return FeedbackOut.model_validate(fb)
|