feat: 反馈加来源/场景/运营回复 + 提现类型筛选 (#105)
用户反馈后台:区分反馈类型(比价反馈/普通反馈)+ 审核可给用户留言;提现后台:按提现类型筛选。 - 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> --------- Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #105 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
This commit was merged in pull request #105.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user