feat: 接入数据大盘聚合与美团 CPS 对账
改了什么:新增新版大盘日期窗口聚合、广告收益拆分、比价耗时字段、美团 CPS 拉单入库与大盘展示,并同步反馈审核和邀请奖励下线口径。 验证:python -m pytest tests/test_admin_read.py tests/test_admin_write.py tests/test_compare_record.py tests/test_invite.py tests/test_feedback.py -q。
This commit is contained in:
+352
-5
@@ -1,14 +1,20 @@
|
||||
"""Admin M2 读接口测试:大盘聚合 + 用户/流水/提现/反馈列表 + 鉴权拦截。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.admin.main import admin_app
|
||||
from app.admin.repositories import admin_user as admin_repo
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.ad_ecpm import AdEcpmRecord
|
||||
from app.models.comparison import ComparisonRecord
|
||||
from app.models.cps_order import CpsOrder
|
||||
from app.models.feedback import Feedback
|
||||
from app.models.wallet import CashTransaction, WithdrawOrder
|
||||
from app.models.wallet import CashTransaction, CoinTransaction, WithdrawOrder
|
||||
from app.repositories import user as user_repo
|
||||
from app.repositories import wallet as wallet_repo
|
||||
|
||||
@@ -49,7 +55,7 @@ def _seed_user_with_data(phone: str) -> int:
|
||||
db.add(WithdrawOrder(
|
||||
user_id=uid, out_bill_no=f"test{uid}billno0001", amount_cents=100, status="success"
|
||||
))
|
||||
db.add(Feedback(user_id=uid, content="测试反馈内容", contact="wx_test", status="new"))
|
||||
db.add(Feedback(user_id=uid, content="测试反馈内容", contact="wx_test", status="pending"))
|
||||
db.commit()
|
||||
return uid
|
||||
finally:
|
||||
@@ -64,7 +70,347 @@ def test_dashboard_overview(admin_client: TestClient, admin_token: str) -> None:
|
||||
assert data["users"]["total"] >= 1
|
||||
assert data["coins"]["granted_total"] >= 5000
|
||||
assert "success_rate" in data["comparison"]
|
||||
assert data["cps"]["available"] is False
|
||||
assert data["period"]["trend"]
|
||||
assert {"date", "active_users", "new_users", "comparisons"} <= set(
|
||||
data["period"]["trend"][0]
|
||||
)
|
||||
assert data["period"]["comparison"]["average_duration_ms"] is None
|
||||
assert data["cps"]["available"] is True
|
||||
|
||||
|
||||
def test_dashboard_average_duration_from_total_ms(
|
||||
admin_client: TestClient, admin_token: str
|
||||
) -> None:
|
||||
day = "2020-02-23"
|
||||
created_at = datetime(2020, 2, 23, 12, 0)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
uid = user_repo.upsert_user_for_login(
|
||||
db, phone="13800000008", register_channel="sms"
|
||||
).id
|
||||
db.add_all([
|
||||
ComparisonRecord(
|
||||
user_id=uid,
|
||||
trace_id=f"duration-a-{uuid4().hex}",
|
||||
business_type="food",
|
||||
status="success",
|
||||
items=[],
|
||||
comparison_results=[],
|
||||
skipped_dish_names=[],
|
||||
total_ms=10000,
|
||||
created_at=created_at,
|
||||
),
|
||||
ComparisonRecord(
|
||||
user_id=uid,
|
||||
trace_id=f"duration-b-{uuid4().hex}",
|
||||
business_type="food",
|
||||
status="failed",
|
||||
items=[],
|
||||
comparison_results=[],
|
||||
skipped_dish_names=[],
|
||||
total_ms=30000,
|
||||
created_at=created_at,
|
||||
),
|
||||
])
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r = admin_client.get(
|
||||
"/admin/api/stats/overview",
|
||||
params={"date_from": day, "date_to": day},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["period"]["comparison"]["average_duration_ms"] == 20000
|
||||
|
||||
|
||||
def test_dashboard_meituan_cps_from_cps_order(
|
||||
admin_client: TestClient, admin_token: str
|
||||
) -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
db.add_all([
|
||||
CpsOrder(
|
||||
order_id=f"mt-cps-hit-{uuid4().hex[:16]}",
|
||||
pay_price_cents=3200,
|
||||
commission_cents=160,
|
||||
commission_rate="500",
|
||||
mt_status="3",
|
||||
pay_time=datetime(2020, 2, 24, 3, 0, tzinfo=timezone.utc),
|
||||
raw={},
|
||||
),
|
||||
CpsOrder(
|
||||
order_id=f"mt-cps-miss-{uuid4().hex[:16]}",
|
||||
pay_price_cents=2600,
|
||||
commission_cents=3,
|
||||
commission_rate="10",
|
||||
mt_status="6",
|
||||
pay_time=datetime(2020, 2, 24, 5, 0, tzinfo=timezone.utc),
|
||||
raw={},
|
||||
),
|
||||
CpsOrder(
|
||||
order_id=f"mt-cps-cancel-{uuid4().hex[:16]}",
|
||||
pay_price_cents=3000,
|
||||
commission_cents=150,
|
||||
commission_rate="500",
|
||||
mt_status="4",
|
||||
pay_time=datetime(2020, 2, 24, 6, 0, tzinfo=timezone.utc),
|
||||
raw={},
|
||||
),
|
||||
])
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r = admin_client.get(
|
||||
"/admin/api/stats/overview",
|
||||
params={"date_from": "2020-02-24", "date_to": "2020-02-24"},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
cps = r.json()["cps"]
|
||||
assert cps["available"] is True
|
||||
assert cps["meituan_order_count"] >= 2
|
||||
assert cps["meituan_commission_cents"] >= 163
|
||||
assert cps["meituan_hit_count"] >= 1
|
||||
assert cps["meituan_miss_count"] >= 1
|
||||
assert cps["meituan_hit_rate"] == 0.5
|
||||
|
||||
|
||||
def test_admin_cps_reconcile_writes_meituan_orders(
|
||||
admin_client: TestClient, admin_token: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
order_id = f"mt-api-{uuid4().hex[:16]}"
|
||||
|
||||
def fake_query_order(**kwargs):
|
||||
assert kwargs["start_time"] > 0
|
||||
assert kwargs["end_time"] > kwargs["start_time"]
|
||||
assert kwargs["query_time_type"] == 1
|
||||
return {
|
||||
"code": 0,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"scrollId": "next-scroll",
|
||||
"dataList": [
|
||||
{
|
||||
"businessLine": 1,
|
||||
"orderId": order_id,
|
||||
"payTime": 1582502400,
|
||||
"payPrice": "100.00",
|
||||
"updateTime": 1582503000,
|
||||
"commissionRate": "300",
|
||||
"profit": "3.00",
|
||||
"refundProfit": "null",
|
||||
"status": "6",
|
||||
"tradeType": 1,
|
||||
"sid": "testsid",
|
||||
"actId": 123,
|
||||
"productName": "测试美团订单",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.admin.repositories.cps_orders.meituan.query_order",
|
||||
fake_query_order,
|
||||
)
|
||||
|
||||
r = admin_client.post(
|
||||
"/admin/api/cps/orders/reconcile",
|
||||
params={"date_from": "2020-02-24", "date_to": "2020-02-24"},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["inserted"] == 1
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
order = db.query(CpsOrder).filter(CpsOrder.order_id == order_id).one()
|
||||
assert order.commission_cents == 300
|
||||
assert order.commission_rate == "300"
|
||||
assert order.mt_status == "6"
|
||||
assert order.product_name == "测试美团订单"
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_admin_comparison_records_return_total_ms(
|
||||
admin_client: TestClient, admin_token: str
|
||||
) -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = user_repo.upsert_user_for_login(
|
||||
db, phone="13800000009", register_channel="sms"
|
||||
)
|
||||
rec = ComparisonRecord(
|
||||
user_id=user.id,
|
||||
trace_id=f"admin-duration-{uuid4().hex}",
|
||||
business_type="food",
|
||||
status="success",
|
||||
items=[],
|
||||
comparison_results=[],
|
||||
skipped_dish_names=[],
|
||||
total_ms=45678,
|
||||
created_at=datetime(2020, 2, 25, 12, 0),
|
||||
)
|
||||
db.add(rec)
|
||||
db.commit()
|
||||
rec_id = rec.id
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r = admin_client.get(
|
||||
"/admin/api/comparison-records",
|
||||
params={"phone": "13800000009"},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
data = r.json()
|
||||
assert data["total"] >= 1
|
||||
row = next(item for item in data["items"] if item["id"] == rec_id)
|
||||
assert row["total_ms"] == 45678
|
||||
assert row["phone"] == "13800000009"
|
||||
|
||||
r = admin_client.get(
|
||||
f"/admin/api/comparison-records/{rec_id}",
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["total_ms"] == 45678
|
||||
|
||||
|
||||
def test_ad_revenue_report_by_ad_type(admin_client: TestClient, admin_token: str) -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
uid = user_repo.upsert_user_for_login(
|
||||
db, phone="13800000006", register_channel="sms"
|
||||
).id
|
||||
db.add(AdEcpmRecord(
|
||||
user_id=uid,
|
||||
ad_type="reward_video",
|
||||
ad_session_id=f"test-reward-video-{uuid4().hex}",
|
||||
adn="pangle",
|
||||
slot_id="test-slot",
|
||||
ecpm_raw="1200",
|
||||
report_date="2026-06-25",
|
||||
created_at=datetime(2026, 6, 25, 8, 0, tzinfo=timezone.utc),
|
||||
))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r = admin_client.get(
|
||||
"/admin/api/ad-revenue-report",
|
||||
params={"date_from": "2026-06-25", "date_to": "2026-06-25"},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
data = r.json()
|
||||
assert data["total_impressions"] >= 1
|
||||
assert data["by_ad_type"]["reward_video"]["impressions"] >= 1
|
||||
assert data["by_ad_type"]["reward_video"]["revenue_yuan"] > 0
|
||||
|
||||
|
||||
def test_ad_revenue_report_keeps_feed_scene(admin_client: TestClient, admin_token: str) -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
uid = user_repo.upsert_user_for_login(
|
||||
db, phone="13800000016", register_channel="sms"
|
||||
).id
|
||||
db.add_all([
|
||||
AdEcpmRecord(
|
||||
user_id=uid,
|
||||
ad_type="draw",
|
||||
feed_scene="coupon",
|
||||
ad_session_id=f"test-draw-coupon-{uuid4().hex}",
|
||||
adn="pangle",
|
||||
slot_id="coupon-slot",
|
||||
ecpm_raw="1200",
|
||||
report_date="2026-06-24",
|
||||
created_at=datetime(2026, 6, 24, 8, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
AdEcpmRecord(
|
||||
user_id=uid,
|
||||
ad_type="draw",
|
||||
feed_scene="comparison",
|
||||
ad_session_id=f"test-draw-comparison-{uuid4().hex}",
|
||||
adn="pangle",
|
||||
slot_id="comparison-slot",
|
||||
ecpm_raw="800",
|
||||
report_date="2026-06-24",
|
||||
created_at=datetime(2026, 6, 24, 9, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
])
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r = admin_client.get(
|
||||
"/admin/api/ad-revenue-report",
|
||||
params={"date_from": "2026-06-24", "date_to": "2026-06-24"},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
data = r.json()
|
||||
scene_rows = {
|
||||
item["feed_scene"]: item
|
||||
for item in data["items"]
|
||||
if item["user_id"] == uid and item["ad_type"] == "draw"
|
||||
}
|
||||
assert scene_rows["coupon"]["impressions"] == 1
|
||||
assert scene_rows["coupon"]["revenue_yuan"] > 0
|
||||
assert scene_rows["comparison"]["impressions"] == 1
|
||||
assert scene_rows["comparison"]["revenue_yuan"] > 0
|
||||
|
||||
|
||||
def test_dashboard_coin_category_mapping_without_feed_scene(
|
||||
admin_client: TestClient, admin_token: str
|
||||
) -> None:
|
||||
day = "2020-02-20"
|
||||
created_at = datetime(2020, 2, 20, 12, 0)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
uid = user_repo.upsert_user_for_login(
|
||||
db, phone="13800000007", register_channel="sms"
|
||||
).id
|
||||
rows = [
|
||||
("reward_video", 100),
|
||||
("coupon_reward", 80),
|
||||
("compare_reward", 60),
|
||||
("signin", 30),
|
||||
("price_report_reward", 40),
|
||||
("feed_ad_reward", 70),
|
||||
("admin_grant", 1000),
|
||||
("invite_inviter", 200),
|
||||
]
|
||||
balance = 0
|
||||
for biz_type, amount in rows:
|
||||
balance += amount
|
||||
db.add(CoinTransaction(
|
||||
user_id=uid,
|
||||
amount=amount,
|
||||
balance_after=balance,
|
||||
biz_type=biz_type,
|
||||
ref_id=f"coin-map-{biz_type}",
|
||||
created_at=created_at,
|
||||
))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r = admin_client.get(
|
||||
"/admin/api/stats/overview",
|
||||
params={"date_from": day, "date_to": day},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
coins = r.json()["period"]["coins"]
|
||||
assert coins["coupon_reward_coin_total"] >= 180
|
||||
assert coins["comparison_reward_coin_total"] >= 60
|
||||
assert coins["regular_task_coin_total"] >= 70
|
||||
assert coins["regular_task_coin_total"] < coins["granted_total"]
|
||||
|
||||
|
||||
def test_user_list_and_detail(admin_client: TestClient, admin_token: str) -> None:
|
||||
@@ -107,9 +453,9 @@ def test_wallet_and_withdraw_lists(admin_client: TestClient, admin_token: str) -
|
||||
|
||||
def test_feedback_list(admin_client: TestClient, admin_token: str) -> None:
|
||||
_seed_user_with_data("13800000005")
|
||||
r = admin_client.get("/admin/api/feedbacks", params={"status": "new"}, headers=_auth(admin_token))
|
||||
r = admin_client.get("/admin/api/feedbacks", params={"status": "pending"}, headers=_auth(admin_token))
|
||||
assert r.status_code == 200
|
||||
assert all(f["status"] == "new" for f in r.json()["items"])
|
||||
assert all(f["status"] == "pending" for f in r.json()["items"])
|
||||
|
||||
|
||||
def test_read_apis_require_auth(admin_client: TestClient) -> None:
|
||||
@@ -120,5 +466,6 @@ def test_read_apis_require_auth(admin_client: TestClient) -> None:
|
||||
"/admin/api/wallet/coin-transactions",
|
||||
"/admin/api/withdraws",
|
||||
"/admin/api/feedbacks",
|
||||
"/admin/api/ad-revenue-report",
|
||||
]:
|
||||
assert admin_client.get(path).status_code == 401, path
|
||||
|
||||
@@ -74,7 +74,7 @@ def _seed_feedback(phone: str) -> int:
|
||||
uid = _seed_user(phone)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
fb = Feedback(user_id=uid, content="测试", contact="wx", status="new")
|
||||
fb = Feedback(user_id=uid, content="测试", contact="wx", status="pending")
|
||||
db.add(fb)
|
||||
db.commit()
|
||||
return fb.id
|
||||
@@ -184,18 +184,67 @@ def test_super_admin_can_grant_coins(admin_client: TestClient, super_token: str)
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
# ===== 反馈处理 =====
|
||||
# ===== 反馈审核 =====
|
||||
|
||||
def test_handle_feedback(admin_client: TestClient, operator_token: str) -> None:
|
||||
def test_approve_feedback_grants_coins_and_audit(admin_client: TestClient, operator_token: str) -> None:
|
||||
fid = _seed_feedback("13900000006")
|
||||
r = admin_client.post(f"/admin/api/feedbacks/{fid}/handle", headers=_auth(operator_token))
|
||||
assert r.status_code == 200
|
||||
r = admin_client.post(
|
||||
f"/admin/api/feedbacks/{fid}/approve",
|
||||
json={"reward_coins": 600, "note": "建议已采纳"},
|
||||
headers=_auth(operator_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
db = SessionLocal()
|
||||
try:
|
||||
assert db.get(Feedback, fid).status == "handled"
|
||||
fb = db.get(Feedback, fid)
|
||||
assert fb.status == "adopted"
|
||||
assert fb.reward_coins == 600
|
||||
assert fb.review_note == "建议已采纳"
|
||||
assert fb.reviewed_by_admin_id is not None
|
||||
assert db.get(CoinAccount, fb.user_id).coin_balance == 600
|
||||
txns = db.execute(
|
||||
select(CoinTransaction).where(
|
||||
CoinTransaction.user_id == fb.user_id,
|
||||
CoinTransaction.biz_type == "feedback_reward",
|
||||
CoinTransaction.ref_id == str(fid),
|
||||
)
|
||||
).scalars().all()
|
||||
assert len(txns) == 1 and txns[0].amount == 600
|
||||
logs = db.execute(
|
||||
select(AdminAuditLog).where(
|
||||
AdminAuditLog.action == "feedback.approve",
|
||||
AdminAuditLog.target_id == str(fid),
|
||||
)
|
||||
).scalars().all()
|
||||
assert len(logs) == 1 and logs[0].detail["reward_coins"] == 600
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r = admin_client.post(
|
||||
f"/admin/api/feedbacks/{fid}/approve",
|
||||
json={"reward_coins": 600},
|
||||
headers=_auth(operator_token),
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_reject_feedback_writes_reason(admin_client: TestClient, operator_token: str) -> None:
|
||||
fid = _seed_feedback("13900000016")
|
||||
r = admin_client.post(
|
||||
f"/admin/api/feedbacks/{fid}/reject",
|
||||
json={"reason": "暂未提供足够复现信息", "note": "可引导补充截图"},
|
||||
headers=_auth(operator_token),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
db = SessionLocal()
|
||||
try:
|
||||
fb = db.get(Feedback, fid)
|
||||
assert fb.status == "rejected"
|
||||
assert fb.reject_reason == "暂未提供足够复现信息"
|
||||
assert fb.review_note == "可引导补充截图"
|
||||
assert db.get(CoinAccount, fb.user_id) is None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# ===== admin 账号管理(super_admin) =====
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ def _food_payload(trace_id: str) -> dict:
|
||||
"skipped_dish_names": ["黑牛肉卷"],
|
||||
"total_dish_count": 3,
|
||||
"information": "在美团找到同店,到手价 ¥123.50",
|
||||
"total_ms": 12345,
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +72,8 @@ def test_report_and_derive(client) -> None:
|
||||
assert d["information"] == "在美团找到同店,到手价 ¥123.50"
|
||||
assert d["store_name"] == "海底捞(朝阳店)"
|
||||
assert d["total_dish_count"] == 3
|
||||
assert d["total_ms"] == 12345
|
||||
assert d["raw_payload"]["total_ms"] == 12345
|
||||
assert d["skipped_dish_count"] == 1
|
||||
assert d["skipped_dish_names"] == ["黑牛肉卷"]
|
||||
assert len(d["comparison_results"]) == 3
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""用户反馈接口测试。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.feedback import Feedback
|
||||
from app.repositories import user as user_repo
|
||||
|
||||
|
||||
def _login(client, phone: str) -> str:
|
||||
client.post("/api/v1/auth/sms/send", json={"phone": phone})
|
||||
r = client.post("/api/v1/auth/sms/login", json={"phone": phone, "code": "123456"})
|
||||
assert r.status_code == 200, r.text
|
||||
return r.json()["access_token"]
|
||||
|
||||
|
||||
def test_feedback_records_empty_returns_200(client) -> None:
|
||||
token = _login(client, "13622000001")
|
||||
|
||||
r = client.get("/api/v1/feedback/records", headers={"Authorization": f"Bearer {token}"})
|
||||
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json() == {
|
||||
"records": [],
|
||||
"counts": {"all": 0, "pending": 0, "adopted": 0, "rejected": 0},
|
||||
}
|
||||
|
||||
|
||||
def test_feedback_config_returns_default(client) -> None:
|
||||
token = _login(client, "13622000002")
|
||||
|
||||
r = client.get("/api/v1/feedback/config", headers={"Authorization": f"Bearer {token}"})
|
||||
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["enabled"] is True
|
||||
assert body["group_name"] == "傻瓜比价官方群"
|
||||
|
||||
|
||||
def test_feedback_records_maps_existing_statuses(client) -> None:
|
||||
phone = "13622000003"
|
||||
token = _login(client, phone)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = user_repo.get_user_by_phone(db, phone)
|
||||
assert user is not None
|
||||
db.add(Feedback(user_id=user.id, content="待处理反馈", contact="", status="pending"))
|
||||
db.add(
|
||||
Feedback(
|
||||
user_id=user.id,
|
||||
content="已采纳反馈",
|
||||
contact="",
|
||||
status="adopted",
|
||||
reward_coins=800,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
Feedback(
|
||||
user_id=user.id,
|
||||
content="未采纳反馈",
|
||||
contact="",
|
||||
status="rejected",
|
||||
reject_reason="暂未提供可复现信息",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r = client.get("/api/v1/feedback/records", headers={"Authorization": f"Bearer {token}"})
|
||||
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["counts"] == {"all": 3, "pending": 1, "adopted": 1, "rejected": 1}
|
||||
by_status = {item["status"]: item for item in body["records"]}
|
||||
assert by_status["adopted"]["reward_coins"] == 800
|
||||
assert by_status["rejected"]["reject_reason"] == "暂未提供可复现信息"
|
||||
+21
-23
@@ -1,4 +1,4 @@
|
||||
"""好友邀请测试:邀请码、绑定、双方发金币、幂等、自邀/无效码屏蔽、指纹兜底归因。
|
||||
"""好友邀请测试:邀请码、绑定、幂等、自邀/无效码屏蔽、指纹兜底归因。
|
||||
|
||||
用 sms mock 登录拿 token(同 test_welfare),再跑邀请闭环。
|
||||
"""
|
||||
@@ -10,8 +10,6 @@ from sqlalchemy import select
|
||||
|
||||
from app.core.rewards import (
|
||||
INVITE_FP_WINDOW_DAYS,
|
||||
INVITE_INVITEE_COINS,
|
||||
INVITE_INVITER_COINS,
|
||||
INVITE_NEW_USER_WINDOW_HOURS,
|
||||
)
|
||||
from app.db.session import SessionLocal
|
||||
@@ -56,8 +54,8 @@ def test_invite_me_returns_stable_code(client) -> None:
|
||||
assert _my_code(client, token) == body["invite_code"]
|
||||
|
||||
|
||||
def test_bind_flow_both_get_coins(client) -> None:
|
||||
"""B 用 A 的码绑定 → 双方各得 1 万金币;A 战绩 +1。"""
|
||||
def test_bind_flow_records_relation_without_invite_coins(client) -> None:
|
||||
"""B 用 A 的码绑定 → 邀请关系生效;邀请金币已下线。"""
|
||||
a = _login(client, "13800002002")
|
||||
b = _login(client, "13800002003")
|
||||
a_code = _my_code(client, a)
|
||||
@@ -69,20 +67,20 @@ def test_bind_flow_both_get_coins(client) -> None:
|
||||
assert r.status_code == 200, r.text
|
||||
res = r.json()
|
||||
assert res["status"] == "success"
|
||||
assert res["coins_awarded"] == INVITE_INVITEE_COINS
|
||||
assert res["coins_awarded"] == 0
|
||||
|
||||
# 双方金币到账
|
||||
assert _coin_balance(client, b) == INVITE_INVITEE_COINS
|
||||
assert _coin_balance(client, a) == INVITE_INVITER_COINS
|
||||
# 邀请金币已下线,双方余额不因邀请变化
|
||||
assert _coin_balance(client, b) == 0
|
||||
assert _coin_balance(client, a) == 0
|
||||
|
||||
# A 的战绩:已邀 1 人,累计获得 = 邀请人那份
|
||||
# A 的战绩:已邀 1 人,邀请金币保持 0
|
||||
r = client.get("/api/v1/invite/me", headers=_auth(a))
|
||||
assert r.json()["invited_count"] == 1
|
||||
assert r.json()["coins_earned"] == INVITE_INVITER_COINS
|
||||
assert r.json()["coins_earned"] == 0
|
||||
|
||||
|
||||
def test_bind_idempotent_no_double_reward(client) -> None:
|
||||
"""同一被邀请人二次绑定 → already_bound,不重复发奖。"""
|
||||
def test_bind_idempotent_no_duplicate_relation(client) -> None:
|
||||
"""同一被邀请人二次绑定 → already_bound,不重复绑定。"""
|
||||
a = _login(client, "13800002004")
|
||||
b = _login(client, "13800002005")
|
||||
a_code = _my_code(client, a)
|
||||
@@ -99,14 +97,14 @@ def test_bind_idempotent_no_double_reward(client) -> None:
|
||||
assert r2.json()["status"] == "already_bound"
|
||||
assert r2.json()["coins_awarded"] == 0
|
||||
|
||||
# 余额没变(没二次发奖),C 也没拿到邀请奖励
|
||||
# 余额没变,C 也没有邀请金币
|
||||
assert _coin_balance(client, b) == bal_b
|
||||
assert _coin_balance(client, a) == bal_a
|
||||
assert _coin_balance(client, c) == 0
|
||||
|
||||
|
||||
def test_self_invite_blocked(client) -> None:
|
||||
"""填自己的邀请码 → self_invite,不发奖。"""
|
||||
"""填自己的邀请码 → self_invite,不产生金币。"""
|
||||
a = _login(client, "13800002007")
|
||||
a_code = _my_code(client, a)
|
||||
r = client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(a))
|
||||
@@ -116,7 +114,7 @@ def test_self_invite_blocked(client) -> None:
|
||||
|
||||
|
||||
def test_invalid_code(client) -> None:
|
||||
"""无效邀请码 → invalid_code,不发奖。"""
|
||||
"""无效邀请码 → invalid_code,不产生金币。"""
|
||||
b = _login(client, "13800002008")
|
||||
r = client.post("/api/v1/invite/bind", json={"invite_code": "ZZZZZZ"}, headers=_auth(b))
|
||||
assert r.status_code == 200, r.text
|
||||
@@ -145,7 +143,7 @@ def test_invite_requires_auth(client) -> None:
|
||||
|
||||
|
||||
def test_old_user_not_eligible(client) -> None:
|
||||
"""新用户闸:被邀请人注册超过窗口 → not_eligible,双方都不发奖。"""
|
||||
"""新用户闸:被邀请人注册超过窗口 → not_eligible,双方都不产生金币。"""
|
||||
a = _login(client, "13800002030")
|
||||
a_code = _my_code(client, a)
|
||||
b = _login(client, "13800002031")
|
||||
@@ -217,13 +215,13 @@ def test_bind_by_fingerprint_success(client) -> None:
|
||||
)
|
||||
assert r2.status_code == 200, r2.text
|
||||
assert r2.json()["status"] == "success"
|
||||
# 双方各发金币
|
||||
assert _coin_balance(client, a) == INVITE_INVITER_COINS
|
||||
assert _coin_balance(client, b) == INVITE_INVITEE_COINS
|
||||
# 邀请金币已下线,双方余额不因邀请变化
|
||||
assert _coin_balance(client, a) == 0
|
||||
assert _coin_balance(client, b) == 0
|
||||
|
||||
|
||||
def test_bind_by_fingerprint_not_found(client) -> None:
|
||||
"""没有匹配的指纹记录 → fp_not_found,不发奖。"""
|
||||
"""没有匹配的指纹记录 → fp_not_found,不产生金币。"""
|
||||
b = _login(client, "13800002043")
|
||||
r = client.post(
|
||||
"/api/v1/invite/bind",
|
||||
@@ -300,7 +298,7 @@ def test_bind_no_code_no_fingerprint(client) -> None:
|
||||
# =====================================================================
|
||||
|
||||
def test_invitees_basic(client) -> None:
|
||||
"""A 邀 B、C → 列表返 2 条、total=2、没设资料的名字=脱敏手机号、头像 null、金币对。"""
|
||||
"""A 邀 B、C → 列表返 2 条、total=2、没设资料的名字=脱敏手机号、头像 null、金币为 0。"""
|
||||
a = _login(client, "13800002050")
|
||||
a_code = _my_code(client, a)
|
||||
for phone in ("13800002051", "13800002052"):
|
||||
@@ -317,7 +315,7 @@ def test_invitees_basic(client) -> None:
|
||||
names = {it["display_name"] for it in body["items"]}
|
||||
assert names == {"138****2051", "138****2052"}
|
||||
assert all(it["avatar_url"] is None for it in body["items"])
|
||||
assert all(it["coins"] == INVITE_INVITER_COINS for it in body["items"])
|
||||
assert all(it["coins"] == 0 for it in body["items"])
|
||||
|
||||
|
||||
def test_invitees_order_desc(client) -> None:
|
||||
|
||||
Reference in New Issue
Block a user