From e9c0a80d446fc56dceac294ed9bbd670c7a38651 Mon Sep 17 00:00:00 2001 From: guke Date: Fri, 10 Jul 2026 14:24:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E4=B8=A4=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E9=80=90=E8=A1=8C=E8=A1=A5=E3=80=8C=E6=9C=AC=E6=AC=A1=E5=B9=BF?= =?UTF-8?q?=E5=91=8A=E6=94=B6=E7=9B=8A=E3=80=8D(ad=5Frevenue=5Fyuan)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按本页 trace_id 一次性聚合 ad_ecpm_record 展示收益(元),挂到领券数据 看板明细行(dict 字段)与比价记录列表项(ORM 瞬态属性),防 N+1 范式与 现有 _attach_user_info / _ad_coins_by_trace 一致。 - CouponDataRow: 新增 ad_revenue_yuan: float = 0.0 字段 - AdminComparisonListItem: 新增 ad_revenue_yuan: float = 0.0 字段 - coupon_data.py: _session_to_row 增 ad_revenue_yuan 参数;coupon_data_report 在分页后调用 crud_ecpm.revenue_yuan_by_trace 按本页批量聚合 - queries.py: list_comparison_records 在 _attach_user_info 后瞬态挂 ad_revenue_yuan - 新增 tests/test_board_ad_revenue.py:两个看板各一个集成测试 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/admin/repositories/coupon_data.py | 7 ++- app/admin/repositories/queries.py | 6 +++ app/admin/schemas/comparison.py | 1 + app/admin/schemas/coupon_data.py | 3 ++ tests/test_board_ad_revenue.py | 66 +++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 tests/test_board_ad_revenue.py diff --git a/app/admin/repositories/coupon_data.py b/app/admin/repositories/coupon_data.py index a290df1..7fd104e 100644 --- a/app/admin/repositories/coupon_data.py +++ b/app/admin/repositories/coupon_data.py @@ -18,6 +18,7 @@ from sqlalchemy.orm import Session from app.core import rewards from app.models.coupon_state import CouponClaimRecord, CouponSession from app.models.user import User +from app.repositories import ad_ecpm as crud_ecpm from app.repositories.coupon_state import DEFAULT_PLATFORMS, coupon_id_to_platform @@ -85,7 +86,7 @@ def _success_rates(rows: list) -> dict: } -def _session_to_row(r, phone: str | None = None, nickname: str | None = None) -> dict: +def _session_to_row(r, phone: str | None = None, nickname: str | None = None, ad_revenue_yuan: float = 0.0) -> dict: """CouponSession ORM → 明细行 dict(主表「领券数据」与「用户全部领券」抽屉共用)。""" return { "id": r.id, @@ -104,6 +105,7 @@ def _session_to_row(r, phone: str | None = None, nickname: str | None = None) -> "started_at": r.started_at, "claimed_count": r.claimed_count, "trace_url": r.trace_url, + "ad_revenue_yuan": ad_revenue_yuan, } @@ -246,10 +248,11 @@ def coupon_data_report( select(User.id, User.phone, User.nickname).where(User.id.in_(uids)) ).all() } + rev_map = crud_ecpm.revenue_yuan_by_trace(db, [r.trace_id for r in page]) items = [] for r in page: phone, nickname = user_map.get(r.user_id, (None, None)) if r.user_id is not None else (None, None) - items.append(_session_to_row(r, phone, nickname)) + items.append(_session_to_row(r, phone, nickname, ad_revenue_yuan=rev_map.get(r.trace_id, 0.0))) return { "summary": summary, diff --git a/app/admin/repositories/queries.py b/app/admin/repositories/queries.py index 1142460..501dba9 100644 --- a/app/admin/repositories/queries.py +++ b/app/admin/repositories/queries.py @@ -32,6 +32,7 @@ from app.models.wallet import ( InviteCashTransaction, WithdrawOrder, ) +from app.repositories import ad_ecpm # 「最近活跃」计入的行为事件(与大盘 DAU/留存活跃口径一致:开始比价 + 开始领券) _ACTIVE_EVENTS = (COMPARE_START_EVENT, COUPON_START_EVENT) @@ -298,6 +299,11 @@ def list_comparison_records( limit=limit, cursor=cursor, ) _attach_user_info(db, items) + # 「本次比价看广告的预估收益」:按本页 trace_id 一次性聚合(同 _attach_user_info 逐页范式)。 + # ad_revenue_yuan 非 ORM 列,仅瞬态挂实例上供 AdminComparisonListItem(from_attributes)读出。 + rev = ad_ecpm.revenue_yuan_by_trace(db, [it.trace_id for it in items]) + for it in items: + it.ad_revenue_yuan = rev.get(it.trace_id, 0.0) return items, next_cursor, total diff --git a/app/admin/schemas/comparison.py b/app/admin/schemas/comparison.py index 5d4155c..3a5ae19 100644 --- a/app/admin/schemas/comparison.py +++ b/app/admin/schemas/comparison.py @@ -41,6 +41,7 @@ class AdminComparisonListItem(BaseModel): rom_name: str | None = None android_version: str | None = None app_version: str | None = None + ad_revenue_yuan: float = 0.0 # 本次比价看的信息流广告预估收益(元),queries 瞬态挂 ORM 实例上 created_at: datetime diff --git a/app/admin/schemas/coupon_data.py b/app/admin/schemas/coupon_data.py index ec4c56e..c560397 100644 --- a/app/admin/schemas/coupon_data.py +++ b/app/admin/schemas/coupon_data.py @@ -70,6 +70,9 @@ class CouponDataRow(BaseModel): started_at: datetime = Field(..., description="发起时刻(明细「时间」列)") claimed_count: int | None = None trace_url: str | None = Field(None, description="pricebot 公网 trace 链接(仅 completed 有);admin 渲染可点链接,无则显示可复制 trace_id") + ad_revenue_yuan: float = Field( + 0.0, description="本次领券看的信息流广告预估收益(元);按 trace_id 聚合 ad_ecpm_record" + ) class CouponDataOut(BaseModel): diff --git a/tests/test_board_ad_revenue.py b/tests/test_board_ad_revenue.py new file mode 100644 index 0000000..72d578c --- /dev/null +++ b/tests/test_board_ad_revenue.py @@ -0,0 +1,66 @@ +"""两个看板逐行「本次广告收益」(元):按 trace_id 聚合 ad_ecpm_record。""" +from __future__ import annotations + +from datetime import UTC, date, datetime + +from sqlalchemy import delete + +from app.admin.repositories import queries +from app.admin.repositories.coupon_data import coupon_data_report +from app.db.session import SessionLocal +from app.models.ad_ecpm import AdEcpmRecord +from app.models.comparison import ComparisonRecord +from app.models.coupon_state import CouponSession + + +def _ecpm(trace_id: str, ecpm_raw: str, session_id: str, scene: str) -> AdEcpmRecord: + return AdEcpmRecord( + user_id=1, ad_type="draw", feed_scene=scene, ad_session_id=session_id, + ecpm_raw=ecpm_raw, trace_id=trace_id, report_date="2020-01-02", + created_at=datetime(2020, 1, 2, tzinfo=UTC), + ) + + +def test_coupon_data_report_includes_ad_revenue() -> None: + """领券看板明细行带本次广告收益;200+300 分 → 0.005 元。""" + db = SessionLocal() + try: + db.add(CouponSession( + trace_id="rev-cp-1", device_id="d1", status="completed", app_env="prod", + platforms=["meituan-waimai"], platform_success=["meituan-waimai"], + started_at=datetime(2020, 1, 2, tzinfo=UTC), started_date=date(2020, 1, 2), + )) + db.add_all([ + _ecpm("rev-cp-1", "200", "cp-sess-a", "coupon"), + _ecpm("rev-cp-1", "300", "cp-sess-b", "coupon"), + ]) + db.flush() + res = coupon_data_report(db, date_from="2020-01-02", date_to="2020-01-02", app_env="prod") + row = next(r for r in res["items"] if r["trace_id"] == "rev-cp-1") + assert row["ad_revenue_yuan"] == 0.005 + finally: + db.rollback() + db.close() + + +def test_comparison_list_includes_ad_revenue() -> None: + """比价记录列表项带本次广告收益;200 分 → 0.002 元。 + + 用独有 user_id 过滤,确保本行必落在第一页(避免共享测试库里同 user 记录多、分页把它挤掉)。 + """ + db = SessionLocal() + try: + db.add(ComparisonRecord( + trace_id="rev-cmp-1", user_id=987654, status="success", business_type="food", + created_at=datetime(2020, 1, 2, tzinfo=UTC), + )) + db.add(_ecpm("rev-cmp-1", "200", "cmp-sess-a", "comparison")) + db.commit() + items, _next, _total = queries.list_comparison_records(db, user_id=987654) + row = next(it for it in items if it.trace_id == "rev-cmp-1") + assert row.ad_revenue_yuan == 0.002 + finally: + db.execute(delete(AdEcpmRecord).where(AdEcpmRecord.trace_id.in_(["rev-cmp-1"]))) + db.execute(delete(ComparisonRecord).where(ComparisonRecord.trace_id == "rev-cmp-1")) + db.commit() + db.close()