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>
113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
"""admin 反馈工单 schemas。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
from app.core.rewards import FEEDBACK_REWARD_MAX_COINS
|
|
|
|
|
|
class FeedbackOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
user_id: int
|
|
content: str
|
|
contact: str
|
|
# 反馈来源入口:profile(「我的」页)/ comparison(比价结果页)——admin 展示/筛选「反馈类型」
|
|
source: str = "profile"
|
|
# 比价反馈的问题场景(找错商品/优惠不对…);普通反馈为 None
|
|
scene: str | None = None
|
|
images: list[str] | None = None
|
|
status: str
|
|
reject_reason: str | None = None
|
|
reward_coins: int | None = None
|
|
review_note: str | None = None
|
|
# 运营给用户的回复留言(用户端可见,采纳/未采纳都可填)
|
|
admin_reply: str | None = None
|
|
reviewed_by_admin_id: int | None = None
|
|
reviewed_at: datetime | None = None
|
|
created_at: datetime
|
|
# 提交端环境快照(feedback 表列):提交版本号 / 机型OS版本;改版前的历史反馈为 None
|
|
app_version: str | None = None
|
|
device_model: str | None = None
|
|
rom_name: str | None = None
|
|
android_version: str | None = None
|
|
# 联表瞬态字段(queries._attach_user_info 挂):列表展示完整手机号,点手机号查该用户全部反馈
|
|
phone: str | None = None
|
|
nickname: str | None = None
|
|
|
|
|
|
class FeedbackApproveRequest(BaseModel):
|
|
reward_coins: int = Field(
|
|
ge=1,
|
|
le=FEEDBACK_REWARD_MAX_COINS,
|
|
description="采纳后发放金币数",
|
|
)
|
|
note: str | None = Field(default=None, max_length=256, description="采纳要点/审核备注(内部)")
|
|
reply: str | None = Field(default=None, max_length=256, description="给用户的回复留言,用户端可见")
|
|
|
|
|
|
class FeedbackRejectRequest(BaseModel):
|
|
reason: str = Field(min_length=1, max_length=256, description="未采纳原因,用户端可见")
|
|
note: str | None = Field(default=None, max_length=256, description="运营内部审核备注")
|
|
reply: str | None = Field(default=None, max_length=256, description="给用户的回复留言,用户端可见")
|
|
|
|
|
|
class FeedbackBulkRequest(BaseModel):
|
|
ids: list[int] = Field(min_length=1, max_length=50, description="待审核反馈 ID 列表")
|
|
|
|
@field_validator("ids")
|
|
@classmethod
|
|
def _ids_must_be_unique(cls, ids: list[int]) -> list[int]:
|
|
if len(ids) != len(set(ids)):
|
|
raise ValueError("反馈 ID 不能重复")
|
|
return ids
|
|
|
|
|
|
class FeedbackBulkApproveRequest(FeedbackBulkRequest):
|
|
reward_coins: int = Field(
|
|
ge=1,
|
|
le=FEEDBACK_REWARD_MAX_COINS,
|
|
description="每条采纳反馈发放的金币数",
|
|
)
|
|
note: str | None = Field(default=None, max_length=256, description="采纳要点/审核备注(内部)")
|
|
reply: str | None = Field(default=None, max_length=256, description="给用户的回复留言,用户端可见")
|
|
|
|
|
|
class FeedbackBulkRejectRequest(FeedbackBulkRequest):
|
|
reason: str = Field(min_length=1, max_length=256, description="批量未采纳原因,用户端可见")
|
|
note: str | None = Field(default=None, max_length=256, description="运营内部审核备注")
|
|
reply: str | None = Field(default=None, max_length=256, description="给用户的回复留言,用户端可见")
|
|
|
|
@field_validator("reason")
|
|
@classmethod
|
|
def _reason_not_blank(cls, value: str) -> str:
|
|
if not value.strip():
|
|
raise ValueError("未采纳原因不能为空")
|
|
return value.strip()
|
|
|
|
|
|
class FeedbackBulkItemResult(BaseModel):
|
|
id: int
|
|
ok: bool
|
|
status: str | None = None
|
|
error: str | None = None
|
|
|
|
|
|
class FeedbackBulkResult(BaseModel):
|
|
total: int
|
|
success: int
|
|
failed: int
|
|
items: list[FeedbackBulkItemResult]
|
|
|
|
|
|
class FeedbackSummary(BaseModel):
|
|
"""审核台顶部各状态计数(pending 含历史 new 态)。"""
|
|
|
|
pending: int
|
|
adopted: int
|
|
rejected: int
|
|
total: int
|