docs(admin): 补充管理后台 API 与数据库文档 (#12)
新增 admin 模块接口文档(auth login/me、admins CRUD、audit-logs、feedback 处理、stats 概览、user coins/detail/status、wallet 流水、withdraw 对账/刷新/列表) + admin_user/admin_audit_log 两张表文档,更新 api/database README 索引。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: OuYingJun1024 <1034284404@qq.com> Reviewed-on: #12 Co-authored-by: ouzhou <ouzhou@wonderable.ai> Co-committed-by: ouzhou <ouzhou@wonderable.ai>
This commit was merged in pull request #12.
This commit is contained in:
@@ -33,7 +33,8 @@ def _food_payload(trace_id: str) -> dict:
|
||||
],
|
||||
"comparison_results": [
|
||||
{"platform_id": "meituan", "platform_name": "美团", "package": "com.sankuai.meituan",
|
||||
"price": 123.50, "is_source": False, "rank": 1, "coupon_saved": 7.0},
|
||||
"price": 123.50, "is_source": False, "rank": 1,
|
||||
"coupon_saved": 7.0, "coupon_name": "外卖大额神券"},
|
||||
{"platform_id": "taobao_flash", "platform_name": "淘宝闪购", "package": "com.taobao.taobao",
|
||||
"price": 128.50, "is_source": True, "rank": 2},
|
||||
{"platform_id": "jd_waimai", "platform_name": "京东外卖", "package": "com.jingdong.app.mall",
|
||||
@@ -79,7 +80,7 @@ def test_report_and_derive(client) -> None:
|
||||
|
||||
|
||||
def test_coupon_saved_passthrough(client) -> None:
|
||||
"""红包优惠额 coupon_saved 上报→落库→读出;仅目标平台带值,源平台不带(null)。"""
|
||||
"""红包优惠额 coupon_saved + 来源名 coupon_name 上报→落库→读出;仅目标平台带值,源平台不带(null)。"""
|
||||
token = _login(client, "13800002010")
|
||||
rid = client.post(
|
||||
"/api/v1/compare/record", json=_food_payload("trace-coupon"), headers=_auth(token)
|
||||
@@ -87,7 +88,9 @@ def test_coupon_saved_passthrough(client) -> None:
|
||||
d = client.get(f"/api/v1/compare/records/{rid}", headers=_auth(token)).json()
|
||||
by_pid = {x["platform_id"]: x for x in d["comparison_results"]}
|
||||
assert by_pid["meituan"]["coupon_saved"] == 7.0 # 目标平台带值
|
||||
assert by_pid["meituan"]["coupon_name"] == "外卖大额神券" # 来源名透传(pricebot#38)
|
||||
assert by_pid["taobao_flash"].get("coupon_saved") is None # 源平台不带/为 null
|
||||
assert by_pid["taobao_flash"].get("coupon_name") is None # 源平台无来源名
|
||||
|
||||
|
||||
def test_source_is_cheapest_no_saving(client) -> None:
|
||||
|
||||
+53
-6
@@ -133,10 +133,10 @@ def test_exchange_info(client) -> None:
|
||||
|
||||
|
||||
def test_exchange_flow(client) -> None:
|
||||
"""先供款够兑换下限的金币 → 兑换 1 元 → 金币扣、现金加 → 现金流水有记录。
|
||||
"""先供款够兑 1 元的金币 → 兑换 1 元 → 金币扣、现金加 → 现金流水有记录。
|
||||
|
||||
打开消息提醒任务已降到 1000 金币(不再 = 兑换下限), 不能再靠领任务供款;
|
||||
直接 grant_coins 注入 MIN_EXCHANGE_COIN(= COIN_PER_YUAN = 10000)当种子。
|
||||
兑换下限已降到 1 分(MIN_EXCHANGE_COIN = COIN_PER_CENT = 100), 但本用例验证兑换 1 元,
|
||||
直接 grant_coins 注入 COIN_PER_YUAN(= 10000)当种子(够兑 1 元)。
|
||||
"""
|
||||
phone = "13800001005"
|
||||
token = _login(client, phone)
|
||||
@@ -144,7 +144,7 @@ def test_exchange_flow(client) -> None:
|
||||
with SessionLocal() as db:
|
||||
user = get_user_by_phone(db, phone)
|
||||
assert user is not None
|
||||
crud_wallet.grant_coins(db, user.id, MIN_EXCHANGE_COIN, biz_type="test_seed", remark="测试供款")
|
||||
crud_wallet.grant_coins(db, user.id, COIN_PER_YUAN, biz_type="test_seed", remark="测试供款")
|
||||
db.commit()
|
||||
|
||||
# 兑换 10000 金币 → 100 分
|
||||
@@ -180,6 +180,51 @@ def test_exchange_flow(client) -> None:
|
||||
assert page["items"][0]["biz_type"] == "exchange_in"
|
||||
|
||||
|
||||
def test_exchange_min_floor_one_cent(client) -> None:
|
||||
"""锁定本次下调的兑换下限:正好兑下限额(MIN_EXCHANGE_COIN=100 金币=1 分)应成功。
|
||||
|
||||
这是本 PR 的核心新能力(下限 10000→100,可兑 1 分起)。test_exchange_flow 兑的是
|
||||
1 元(10000),在旧下限下也通过,证明不了新下限;本用例供款并兑换正好等于下限的
|
||||
100 金币,断言到账 1 分。若有人把 MIN_EXCHANGE_COIN 改回 10000,100 会因低于下限被
|
||||
判 400,本用例随之失败,从而把新下限值钉死。
|
||||
"""
|
||||
phone = "13800001010"
|
||||
token = _login(client, phone)
|
||||
# 供款: 正好注入一个下限额度的金币(=COIN_PER_CENT=100)
|
||||
with SessionLocal() as db:
|
||||
user = get_user_by_phone(db, phone)
|
||||
assert user is not None
|
||||
crud_wallet.grant_coins(
|
||||
db, user.id, MIN_EXCHANGE_COIN, biz_type="test_seed", remark="测试供款"
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# 兑换下限额 100 金币 → 1 分
|
||||
r = client.post(
|
||||
"/api/v1/wallet/exchange",
|
||||
json={"coin_amount": MIN_EXCHANGE_COIN},
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
res = r.json()
|
||||
assert res["coin_amount"] == MIN_EXCHANGE_COIN
|
||||
assert res["cash_added_cents"] == 1 # coins_to_cents(100) == 1
|
||||
assert res["coin_balance"] == 0
|
||||
assert res["cash_balance_cents"] == 1
|
||||
|
||||
# 金币流水有一笔 exchange_out(=-100)
|
||||
r = client.get("/api/v1/wallet/coin-transactions", headers=_auth(token))
|
||||
out_txn = next(t for t in r.json()["items"] if t["biz_type"] == "exchange_out")
|
||||
assert out_txn["amount"] == -MIN_EXCHANGE_COIN
|
||||
|
||||
# 现金流水有且仅有一笔 exchange_in(=+1 分)
|
||||
r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
|
||||
cash = r.json()["items"]
|
||||
assert len(cash) == 1
|
||||
assert cash[0]["amount_cents"] == 1
|
||||
assert cash[0]["biz_type"] == "exchange_in"
|
||||
|
||||
|
||||
def test_exchange_insufficient_and_invalid(client) -> None:
|
||||
token = _login(client, "13800001006")
|
||||
|
||||
@@ -191,10 +236,10 @@ def test_exchange_insufficient_and_invalid(client) -> None:
|
||||
)
|
||||
assert r.status_code == 409
|
||||
|
||||
# 低于最小额 → 400
|
||||
# 低于最小额(下限现为 1 分=100;取 MIN_EXCHANGE_COIN-1=99,既低于下限又非整分)→ 400
|
||||
r = client.post(
|
||||
"/api/v1/wallet/exchange",
|
||||
json={"coin_amount": MIN_EXCHANGE_COIN - COIN_PER_CENT},
|
||||
json={"coin_amount": MIN_EXCHANGE_COIN - 1},
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert r.status_code == 400
|
||||
@@ -226,6 +271,8 @@ def test_savings_summary_and_battle(client) -> None:
|
||||
assert b["streak_days"] >= 1
|
||||
assert b["week_saved_cents"] >= 0
|
||||
assert 0 <= b["beat_percent"] <= 100
|
||||
# 完成比价次数与 summary 的 order_count 同源(均 = 有效记录数)
|
||||
assert b["compare_count"] == s["order_count"]
|
||||
|
||||
|
||||
def test_savings_seeder_idempotent(client) -> None:
|
||||
|
||||
Reference in New Issue
Block a user