"""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