e1bd0e3ef7
改了什么:新增新版大盘日期窗口聚合、广告收益拆分、比价耗时字段、美团 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。
472 lines
16 KiB
Python
472 lines
16 KiB
Python
"""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, CoinTransaction, WithdrawOrder
|
|
from app.repositories import user as user_repo
|
|
from app.repositories import wallet as wallet_repo
|
|
|
|
|
|
@pytest.fixture()
|
|
def admin_client() -> TestClient:
|
|
return TestClient(admin_app)
|
|
|
|
|
|
@pytest.fixture()
|
|
def admin_token() -> str:
|
|
db = SessionLocal()
|
|
try:
|
|
if admin_repo.get_by_username(db, "m2_admin") is None:
|
|
admin_repo.create_admin(db, username="m2_admin", password="m2-pass", role="super_admin")
|
|
finally:
|
|
db.close()
|
|
c = TestClient(admin_app)
|
|
r = c.post("/admin/api/auth/login", json={"username": "m2_admin", "password": "m2-pass"})
|
|
return r.json()["access_token"]
|
|
|
|
|
|
def _auth(token: str) -> dict:
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
def _seed_user_with_data(phone: str) -> int:
|
|
"""造一个用户 + 金币流水 + 现金流水 + 提现单 + 反馈,返回 user_id。"""
|
|
db = SessionLocal()
|
|
try:
|
|
user = user_repo.upsert_user_for_login(db, phone=phone, register_channel="sms")
|
|
uid = user.id
|
|
wallet_repo.grant_coins(db, uid, 5000, biz_type="signin", remark="测试发金币")
|
|
db.commit()
|
|
db.add(CashTransaction(
|
|
user_id=uid, amount_cents=-100, balance_after_cents=0, biz_type="withdraw", remark="t"
|
|
))
|
|
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="pending"))
|
|
db.commit()
|
|
return uid
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_dashboard_overview(admin_client: TestClient, admin_token: str) -> None:
|
|
_seed_user_with_data("13800000001")
|
|
r = admin_client.get("/admin/api/stats/overview", headers=_auth(admin_token))
|
|
assert r.status_code == 200, r.text
|
|
data = r.json()
|
|
assert data["users"]["total"] >= 1
|
|
assert data["coins"]["granted_total"] >= 5000
|
|
assert "success_rate" in data["comparison"]
|
|
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:
|
|
uid = _seed_user_with_data("13800000002")
|
|
r = admin_client.get("/admin/api/users", headers=_auth(admin_token))
|
|
assert r.status_code == 200, r.text
|
|
assert "items" in r.json()
|
|
|
|
r = admin_client.get(f"/admin/api/users/{uid}", headers=_auth(admin_token))
|
|
assert r.status_code == 200, r.text
|
|
d = r.json()
|
|
assert d["user"]["id"] == uid
|
|
assert d["coin_balance"] == 5000
|
|
assert d["withdraw_total"] >= 1
|
|
assert d["feedback_total"] >= 1
|
|
|
|
assert admin_client.get("/admin/api/users/999999", headers=_auth(admin_token)).status_code == 404
|
|
|
|
|
|
def test_user_filter_by_status(admin_client: TestClient, admin_token: str) -> None:
|
|
_seed_user_with_data("13800000003")
|
|
r = admin_client.get("/admin/api/users", params={"status": "active"}, headers=_auth(admin_token))
|
|
assert r.status_code == 200
|
|
assert all(u["status"] == "active" for u in r.json()["items"])
|
|
|
|
|
|
def test_wallet_and_withdraw_lists(admin_client: TestClient, admin_token: str) -> None:
|
|
uid = _seed_user_with_data("13800000004")
|
|
r = admin_client.get(
|
|
"/admin/api/wallet/coin-transactions", params={"user_id": uid}, headers=_auth(admin_token)
|
|
)
|
|
assert r.status_code == 200
|
|
assert len(r.json()["items"]) >= 1
|
|
|
|
r = admin_client.get("/admin/api/withdraws", params={"user_id": uid}, headers=_auth(admin_token))
|
|
assert r.status_code == 200
|
|
assert len(r.json()["items"]) >= 1
|
|
assert all(o["status"] == "success" for o in r.json()["items"])
|
|
|
|
|
|
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": "pending"}, headers=_auth(admin_token))
|
|
assert r.status_code == 200
|
|
assert all(f["status"] == "pending" for f in r.json()["items"])
|
|
|
|
|
|
def test_read_apis_require_auth(admin_client: TestClient) -> None:
|
|
"""所有 M2 读接口未带 token → 401(router 级 get_current_admin 守卫)。"""
|
|
for path in [
|
|
"/admin/api/stats/overview",
|
|
"/admin/api/users",
|
|
"/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
|