From df2b31726ca3d507b433ce1f876b4b850a194049 Mon Sep 17 00:00:00 2001 From: zzhyyyyy <2685922758@qq.com> Date: Thu, 2 Jul 2026 16:43:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=8D=E9=A6=88=E5=8A=A0=E6=9D=A5?= =?UTF-8?q?=E6=BA=90/=E5=9C=BA=E6=99=AF/=E8=BF=90=E8=90=A5=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=20+=20=E6=8F=90=E7=8E=B0=E7=B1=BB=E5=9E=8B=E7=AD=9B?= =?UTF-8?q?=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈后台:区分反馈类型(比价反馈/普通反馈)+ 审核可给用户留言;提现后台:按提现类型筛选。 - 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 --- alembic/versions/feedback_type_reply.py | 48 +++++++++++++++++++ app/admin/repositories/mutations.py | 5 +- app/admin/repositories/queries.py | 15 ++++-- app/admin/routers/feedback.py | 7 +++ app/admin/routers/withdraw.py | 3 ++ app/admin/schemas/feedback.py | 10 +++- app/admin/schemas/wallet.py | 2 + app/api/v1/feedback.py | 25 +++++++++- app/models/feedback.py | 11 +++++ app/repositories/feedback.py | 4 ++ app/schemas/feedback.py | 4 ++ tests/test_admin_read.py | 61 +++++++++++++++++++++++++ tests/test_admin_write.py | 29 ++++++++++++ tests/test_feedback.py | 37 +++++++++++++++ 14 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 alembic/versions/feedback_type_reply.py diff --git a/alembic/versions/feedback_type_reply.py b/alembic/versions/feedback_type_reply.py new file mode 100644 index 0000000..e532dbd --- /dev/null +++ b/alembic/versions/feedback_type_reply.py @@ -0,0 +1,48 @@ +"""feedback 加反馈来源/场景/运营回复(source / scene / admin_reply) + +admin「用户反馈」页要展示并筛选「反馈类型」(比价反馈 / 普通反馈),并支持审核时给用户留言: + - source: 反馈来源入口(profile=「我的」页 / comparison=比价结果页)。NOT NULL, + 旧数据 + 普通反馈默认 profile(server_default)。加索引供 admin 按类型筛选。 + - scene: 比价反馈的问题场景(找错商品/优惠不对…),普通反馈为 NULL。 + - admin_reply: 运营给用户的回复留言(用户端可见),随「我的反馈」历史下发。 + +均为新增列(SQLite 原生支持 add_column);downgrade 的 drop_column 在 SQLite 走 batch 兜底。 + +Revision ID: feedback_type_reply +Revises: 761ef181ce7c +Create Date: 2026-07-02 00:00:00.000000 +""" +from collections.abc import Sequence + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "feedback_type_reply" +down_revision: str | Sequence[str] | None = "761ef181ce7c" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "feedback", + sa.Column( + "source", + sa.String(length=16), + nullable=False, + server_default="profile", + ), + ) + op.add_column("feedback", sa.Column("scene", sa.String(length=32), nullable=True)) + op.add_column("feedback", sa.Column("admin_reply", sa.String(length=256), nullable=True)) + op.create_index("ix_feedback_source", "feedback", ["source"]) + + +def downgrade() -> None: + with op.batch_alter_table("feedback") as batch_op: + batch_op.drop_index("ix_feedback_source") + batch_op.drop_column("admin_reply") + batch_op.drop_column("scene") + batch_op.drop_column("source") diff --git a/app/admin/repositories/mutations.py b/app/admin/repositories/mutations.py index 7470c39..f667811 100644 --- a/app/admin/repositories/mutations.py +++ b/app/admin/repositories/mutations.py @@ -65,16 +65,19 @@ def review_feedback( reward_coins: int | None = None, reject_reason: str | None = None, review_note: str | None = None, + admin_reply: str | None = None, commit: bool = True, ) -> Feedback: - """审核反馈:置 adopted/rejected + 记录奖励/原因/审核人/审核时间。 + """审核反馈:置 adopted/rejected + 记录奖励/原因/回复留言/审核人/审核时间。 发金币(wallet.grant_coins)由 router 在同一事务里调,确保状态、金币流水、审计一起提交。 + admin_reply=给用户的回复留言(用户端可见),与 review_note(内部备注)区分。 """ feedback.status = status feedback.reward_coins = reward_coins feedback.reject_reason = reject_reason feedback.review_note = review_note + feedback.admin_reply = admin_reply feedback.reviewed_by_admin_id = reviewed_by_admin_id feedback.reviewed_at = datetime.now(CN_TZ).replace(tzinfo=None) if commit: diff --git a/app/admin/repositories/queries.py b/app/admin/repositories/queries.py index 51ef145..f0de886 100644 --- a/app/admin/repositories/queries.py +++ b/app/admin/repositories/queries.py @@ -390,6 +390,7 @@ def list_all_withdraw_orders( *, user_id: int | None = None, status: str | None = None, + source: str | None = None, keyword: str | None = None, date_from: datetime | None = None, date_to: datetime | None = None, @@ -409,6 +410,9 @@ def list_all_withdraw_orders( stmt = stmt.where(WithdrawOrder.user_id == user_id) if status: stmt = stmt.where(WithdrawOrder.status == status) + # 提现类型:coin_cash(福利页提现)/ invite_cash(邀请提现);None=全部 + if source: + stmt = stmt.where(WithdrawOrder.source == source) kw = (keyword or "").strip() if kw: @@ -534,6 +538,7 @@ def list_feedbacks( db: Session, *, status: str | None = None, + source: str | None = None, user_id: int | None = None, content: str | None = None, created_from: datetime | None = None, @@ -543,13 +548,15 @@ def list_feedbacks( limit: int = 20, cursor: int | None = None, ) -> tuple[list[Feedback], int | None, int]: - """反馈工单列表。支持 状态 / 用户ID / 内容模糊 / 提交时间范围 筛选,按 id·提交时间排序。 - **offset 分页**(cursor=offset):任意列排序下游标语义统一(同 [list_users]),代价是翻页期间 - 数据变动可能错位一条——admin 低频场景可接受。返回 (items, next_cursor, total),total 供页码分页。 - created_at 为 timestamptz,日期入参统一转 tz-aware UTC 比较。""" + """反馈工单列表。支持 状态 / 反馈类型(source) / 用户ID / 内容模糊 / 提交时间范围 筛选, + 按 id·提交时间排序。**offset 分页**(cursor=offset):任意列排序下游标语义统一(同 [list_users]), + 代价是翻页期间数据变动可能错位一条——admin 低频场景可接受。返回 (items, next_cursor, total), + total 供页码分页。created_at 为 timestamptz,日期入参统一转 tz-aware UTC 比较。""" stmt = select(Feedback) if status: stmt = stmt.where(Feedback.status == status) + if source: + stmt = stmt.where(Feedback.source == source) if user_id is not None: stmt = stmt.where(Feedback.user_id == user_id) if content and content.strip(): diff --git a/app/admin/routers/feedback.py b/app/admin/routers/feedback.py index 8f27bda..f177ebd 100644 --- a/app/admin/routers/feedback.py +++ b/app/admin/routers/feedback.py @@ -36,6 +36,8 @@ def _ensure_pending(fb: Feedback) -> None: 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, @@ -48,6 +50,7 @@ def list_feedbacks( items, next_cursor, total = queries.list_feedbacks( db, status=status, + source=source, user_id=user_id, content=content, created_from=created_from, @@ -101,6 +104,7 @@ def approve_feedback( status="adopted", reward_coins=payload.reward_coins, review_note=payload.note, + admin_reply=payload.reply, reviewed_by_admin_id=admin.id, commit=False, ) @@ -123,6 +127,7 @@ def approve_feedback( "after": "adopted", "reward_coins": payload.reward_coins, "note": payload.note, + "reply": payload.reply, }, ip=get_client_ip(request), commit=False, @@ -152,6 +157,7 @@ def reject_feedback( status="rejected", reject_reason=payload.reason, review_note=payload.note, + admin_reply=payload.reply, reviewed_by_admin_id=admin.id, commit=False, ) @@ -166,6 +172,7 @@ def reject_feedback( "after": "rejected", "reason": payload.reason, "note": payload.note, + "reply": payload.reply, }, ip=get_client_ip(request), commit=False, diff --git a/app/admin/routers/withdraw.py b/app/admin/routers/withdraw.py index a36ad54..dbc006d 100644 --- a/app/admin/routers/withdraw.py +++ b/app/admin/routers/withdraw.py @@ -50,6 +50,8 @@ def list_withdraws( db: AdminDb, user_id: Annotated[int | None, Query()] = None, status: Annotated[str | None, Query()] = None, + # 提现类型筛选:coin_cash(福利页提现)/ invite_cash(邀请提现);None=全部 + source: Annotated[str | None, Query(pattern="^(coin_cash|invite_cash)$")] = None, keyword: Annotated[str | None, Query(max_length=100)] = None, date_from: Annotated[datetime | None, Query()] = None, date_to: Annotated[datetime | None, Query()] = None, @@ -69,6 +71,7 @@ def list_withdraws( db, user_id=user_id, status=status, + source=source, keyword=keyword, date_from=date_from, date_to=date_to, diff --git a/app/admin/schemas/feedback.py b/app/admin/schemas/feedback.py index 3988b87..bb588cf 100644 --- a/app/admin/schemas/feedback.py +++ b/app/admin/schemas/feedback.py @@ -15,11 +15,17 @@ class FeedbackOut(BaseModel): 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 @@ -39,12 +45,14 @@ class FeedbackApproveRequest(BaseModel): le=FEEDBACK_REWARD_MAX_COINS, description="采纳后发放金币数", ) - note: str | None = Field(default=None, 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 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 FeedbackSummary(BaseModel): diff --git a/app/admin/schemas/wallet.py b/app/admin/schemas/wallet.py index 10cc8ed..06b4c7e 100644 --- a/app/admin/schemas/wallet.py +++ b/app/admin/schemas/wallet.py @@ -41,6 +41,8 @@ class WithdrawOrderOut(BaseModel): user_id: int out_bill_no: str amount_cents: int + # 提现类型:coin_cash(福利页提现,金币兑换的现金)/ invite_cash(邀请提现,邀请奖励金) + source: str = "coin_cash" user_name: str | None = None # 提现实名(审核核对 + 打款用) status: str wechat_state: str | None = None diff --git a/app/api/v1/feedback.py b/app/api/v1/feedback.py index 5455d21..48b8dcd 100644 --- a/app/api/v1/feedback.py +++ b/app/api/v1/feedback.py @@ -31,7 +31,18 @@ router = APIRouter(prefix="/api/v1/feedback", tags=["feedback"]) _MAX_IMAGES = 6 _CONTENT_MAX = 200 _CONTACT_MAX = 128 +_SCENE_MAX = 32 _VALID_RECORD_STATUS = {"pending", "adopted", "rejected"} +_VALID_SOURCES = {"profile", "comparison"} + + +def _resolve_source(source: str, scene: str) -> str: + """反馈来源:客户端显式传的 source 优先(profile / comparison);未传或非法时, + 按 scene 有无派生——比价结果页反馈才带 scene,故 scene 非空即视作 comparison。""" + src = source.strip().lower() + if src in _VALID_SOURCES: + return src + return "comparison" if scene.strip() else "profile" def _app_status(db_status: str) -> str: @@ -46,10 +57,12 @@ def _record_out(fb) -> FeedbackRecordOut: return FeedbackRecordOut( id=fb.id, content=fb.content, + scene=getattr(fb, "scene", None), images=fb.images or [], status=_app_status(fb.status), reject_reason=getattr(fb, "reject_reason", None), reward_coins=getattr(fb, "reward_coins", None), + admin_reply=getattr(fb, "admin_reply", None), created_at=fb.created_at, ) @@ -61,6 +74,11 @@ async def submit_feedback( content: str = Form(...), # 原型改版后客户端不再采集联系方式;保留字段以兼容旧端 + 后续可能复用,默认空串。 contact: str = Form(default=""), + # 反馈来源入口:profile(「我的」页)/ comparison(比价结果页)。新端显式传;旧端不带 → + # 空串 → 按 scene 有无派生。admin 据此展示/筛选「反馈类型」。 + source: str = Form(default=""), + # 比价结果页反馈的「问题场景」(找错商品/优惠不对…);普通反馈不带 → 空串 → 存 NULL。 + scene: str = Form(default=""), # 提交端环境快照(admin 反馈页展示「提交版本号」「机型OS版本」);旧端不带 → 空串 → 存 NULL。 app_version: str = Form(default=""), device_model: str = Form(default=""), @@ -76,6 +94,8 @@ async def submit_feedback( raise HTTPException(status_code=400, detail="反馈内容过长") if len(contact) > _CONTACT_MAX: raise HTTPException(status_code=400, detail="联系方式过长") + scene = scene.strip()[:_SCENE_MAX] + resolved_source = _resolve_source(source, scene) files = [f for f in (images or []) if f is not None and f.filename] if len(files) > _MAX_IMAGES: @@ -91,10 +111,13 @@ async def submit_feedback( fb = feedback_repo.create_feedback( db, user_id=user.id, content=content, contact=contact, images=urls, + source=resolved_source, scene=scene or None, app_version=app_version.strip(), device_model=device_model.strip(), rom_name=rom_name.strip(), android_version=android_version.strip(), ) - logger.info("feedback id=%d user_id=%d images=%d", fb.id, user.id, len(urls)) + logger.info( + "feedback id=%d user_id=%d source=%s images=%d", fb.id, user.id, resolved_source, len(urls) + ) return FeedbackOut.model_validate(fb) diff --git a/app/models/feedback.py b/app/models/feedback.py index fb3d68a..1907605 100644 --- a/app/models/feedback.py +++ b/app/models/feedback.py @@ -23,6 +23,14 @@ class Feedback(Base): ) content: Mapped[str] = mapped_column(Text, nullable=False) contact: Mapped[str] = mapped_column(String(128), nullable=False) + # 反馈来源入口:profile(「我的」页反馈入口)/ comparison(比价结果页反馈入口)。 + # admin 据此展示/筛选「反馈类型」。旧数据与「我的」页反馈均为 profile(server_default)。 + # 客户端显式传 source 优先;未传时由 scene 有无派生(见 api/v1/feedback.submit_feedback)。 + source: Mapped[str] = mapped_column( + String(16), nullable=False, default="profile", server_default="profile", index=True + ) + # 比价反馈的「问题场景」(找错商品/优惠不对/比价太慢…),比价结果页反馈才有;普通反馈为 NULL。 + scene: Mapped[str | None] = mapped_column(String(32), nullable=True) # 截图 URL 列表(相对路径,如 ["/media/feedback/u1_ab12.jpg"]);无图为 None images: Mapped[list[str] | None] = mapped_column(JSON, nullable=True) # 提交时的端环境快照(admin 排查用;客户端改版带上后的新反馈才有,历史数据为 NULL) @@ -36,6 +44,9 @@ class Feedback(Base): reward_coins: Mapped[int | None] = mapped_column(Integer, nullable=True) # 审核批注:采纳时可写采纳要点,未采纳时也可保留运营侧备注 review_note: Mapped[str | None] = mapped_column(String(256), nullable=True) + # 运营给用户的回复留言(**用户端可见**,采纳/未采纳都可填,随「我的反馈」历史下发)。 + # 与 review_note(内部备注,用户不可见)、reject_reason(未采纳原因)区分开。 + admin_reply: Mapped[str | None] = mapped_column(String(256), nullable=True) reviewed_by_admin_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("admin_user.id"), nullable=True ) diff --git a/app/repositories/feedback.py b/app/repositories/feedback.py index aa74678..9cdbeda 100644 --- a/app/repositories/feedback.py +++ b/app/repositories/feedback.py @@ -17,6 +17,8 @@ def create_feedback( content: str, contact: str, images: list[str] | None, + source: str = "profile", + scene: str | None = None, app_version: str | None = None, device_model: str | None = None, rom_name: str | None = None, @@ -26,6 +28,8 @@ def create_feedback( user_id=user_id, content=content, contact=contact, + source=source, + scene=scene or None, images=images or None, app_version=app_version or None, device_model=device_model or None, diff --git a/app/schemas/feedback.py b/app/schemas/feedback.py index d903cca..0007155 100644 --- a/app/schemas/feedback.py +++ b/app/schemas/feedback.py @@ -30,10 +30,14 @@ class FeedbackQrConfigOut(BaseModel): class FeedbackRecordOut(BaseModel): id: int content: str + # 比价反馈的问题场景(找错商品/优惠不对…);普通反馈为 None + scene: str | None = None images: list[str] = Field(default_factory=list) status: str reject_reason: str | None = None reward_coins: int | None = None + # 运营给用户的回复留言(用户端可见,采纳/未采纳都可能有) + admin_reply: str | None = None created_at: datetime diff --git a/tests/test_admin_read.py b/tests/test_admin_read.py index c9d3781..1babd3c 100644 --- a/tests/test_admin_read.py +++ b/tests/test_admin_read.py @@ -190,6 +190,67 @@ def test_feedback_list(admin_client: TestClient, admin_token: str) -> None: assert all(f["status"] == "pending" for f in r.json()["items"]) +def test_feedback_list_filter_by_source(admin_client: TestClient, admin_token: str) -> None: + """反馈列表按反馈类型(source)筛选;返回项带 source/scene 字段。""" + db = SessionLocal() + try: + uid = user_repo.upsert_user_for_login(db, phone="13800009001", register_channel="sms").id + db.add(Feedback(user_id=uid, content="比价反馈", contact="", status="pending", + source="comparison", scene="找错商品")) + db.add(Feedback(user_id=uid, content="普通反馈", contact="", status="pending", + source="profile")) + db.commit() + finally: + db.close() + + r = admin_client.get( + "/admin/api/feedbacks", params={"user_id": uid, "source": "comparison"}, + headers=_auth(admin_token), + ) + assert r.status_code == 200, r.text + items = r.json()["items"] + assert items and all(it["source"] == "comparison" for it in items) + assert any(it["scene"] == "找错商品" for it in items) + + r = admin_client.get( + "/admin/api/feedbacks", params={"user_id": uid, "source": "profile"}, + headers=_auth(admin_token), + ) + assert all(it["source"] == "profile" for it in r.json()["items"]) + + # 非法 source 值 → 422 + assert admin_client.get( + "/admin/api/feedbacks", params={"source": "bogus"}, headers=_auth(admin_token) + ).status_code == 422 + + +def test_withdraw_list_exposes_source_and_filters( + admin_client: TestClient, admin_token: str +) -> None: + """提现列表带 source 字段(coin_cash/invite_cash),并可按提现类型筛选。""" + db = SessionLocal() + try: + uid = user_repo.upsert_user_for_login(db, phone="13800009002", register_channel="sms").id + db.add(WithdrawOrder(user_id=uid, out_bill_no=f"src{uid}coin0001", + amount_cents=100, status="success", source="coin_cash")) + db.add(WithdrawOrder(user_id=uid, out_bill_no=f"src{uid}invite001", + amount_cents=200, status="success", source="invite_cash")) + db.commit() + finally: + db.close() + + r = admin_client.get("/admin/api/withdraws", params={"user_id": uid}, headers=_auth(admin_token)) + assert r.status_code == 200, r.text + assert {it["source"] for it in r.json()["items"]} == {"coin_cash", "invite_cash"} + + r = admin_client.get( + "/admin/api/withdraws", params={"user_id": uid, "source": "invite_cash"}, + headers=_auth(admin_token), + ) + items = r.json()["items"] + assert items and all(it["source"] == "invite_cash" for it in items) + + def test_ad_coin_audit_full_count_truncate_and_only_mismatch( admin_client: TestClient, admin_token: str ) -> None: diff --git a/tests/test_admin_write.py b/tests/test_admin_write.py index 95f57e9..ec6d39d 100644 --- a/tests/test_admin_write.py +++ b/tests/test_admin_write.py @@ -368,6 +368,35 @@ def test_reject_feedback_records_reason_and_no_coin( db.close() +def test_feedback_review_stores_admin_reply( + admin_client: TestClient, operator_token: str +) -> None: + """审核可给用户留言(admin_reply,用户端可见),采纳/拒绝都能带,并回显在响应里。""" + fid = _seed_feedback("13900000021") + r = admin_client.post( + f"/admin/api/feedbacks/{fid}/approve", + json={"reward_coins": 100, "note": "内部备注", "reply": "感谢反馈,已采纳~"}, + headers=_auth(operator_token), + ) + assert r.status_code == 200, r.text + assert r.json()["admin_reply"] == "感谢反馈,已采纳~" + db = SessionLocal() + try: + fb = db.get(Feedback, fid) + assert fb.admin_reply == "感谢反馈,已采纳~" and fb.review_note == "内部备注" + finally: + db.close() + + fid2 = _seed_feedback("13900000022") + r = admin_client.post( + f"/admin/api/feedbacks/{fid2}/reject", + json={"reason": "无法复现", "reply": "已收到,后续跟进"}, + headers=_auth(operator_token), + ) + assert r.status_code == 200, r.text + assert r.json()["admin_reply"] == "已收到,后续跟进" + + # ===== admin 账号管理(super_admin) ===== def test_create_and_update_admin(admin_client: TestClient, super_token: str) -> None: diff --git a/tests/test_feedback.py b/tests/test_feedback.py index 8ec524f..b5dafa2 100644 --- a/tests/test_feedback.py +++ b/tests/test_feedback.py @@ -36,6 +36,43 @@ def test_feedback_config_returns_default(client) -> None: assert body["group_name"] == "傻瓜比价官方群" +def test_submit_feedback_derives_source_from_scene(client) -> None: + """比价结果页反馈带 scene → source=comparison 并存 scene;「我的」页不带 → profile; + 客户端显式传 source 优先(即便没 scene 也判 comparison)。历史接口带回 scene。""" + token = _login(client, "13622000010") + h = {"Authorization": f"Bearer {token}"} + + r = client.post("/api/v1/feedback", data={"content": "比价太慢了", "scene": "比价太慢"}, headers=h) + assert r.status_code == 200, r.text + fid_cmp = r.json()["id"] + + r = client.post("/api/v1/feedback", data={"content": "普通建议"}, headers=h) + assert r.status_code == 200, r.text + fid_prof = r.json()["id"] + + r = client.post( + "/api/v1/feedback", data={"content": "只填了描述", "source": "comparison"}, headers=h + ) + assert r.status_code == 200, r.text + fid_explicit = r.json()["id"] + + db = SessionLocal() + try: + cmp_fb = db.get(Feedback, fid_cmp) + assert cmp_fb.source == "comparison" and cmp_fb.scene == "比价太慢" + prof_fb = db.get(Feedback, fid_prof) + assert prof_fb.source == "profile" and prof_fb.scene is None + assert db.get(Feedback, fid_explicit).source == "comparison" + finally: + db.close() + + r = client.get("/api/v1/feedback/records", headers=h) + assert r.status_code == 200, r.text + scenes = {item["id"]: item["scene"] for item in r.json()["records"]} + assert scenes[fid_cmp] == "比价太慢" + assert scenes[fid_prof] is None + + def test_feedback_records_maps_existing_statuses(client) -> None: phone = "13622000003" token = _login(client, phone) -- 2.52.0