930eff822c
## 背景 admin「领券数据」「比价记录」两个看板此前只能看到场景级(所有领券/比价)的广告收益, 无法定位「这一次领券/比价具体赚了多少」。根因:收益表 `ad_ecpm_record` 缺 `trace_id`, 无法与领券会话 / 比价记录按 trace 关联。 ## 改动 - **模型/迁移**:`ad_ecpm_record` 新增 `trace_id`(String(64), index, nullable); 迁移 `ad_ecpm_trace_id` 加列 + 索引 `ix_ad_ecpm_record_trace_id`,并**收敛当前两个 alembic head**(`11c44afbea58` selfstat + `merge_pages_override_coupon_slot`)为单 head。 - **上报链路**:`EcpmReportIn` 增 `trace_id` 字段;`/api/v1/ad/ecpm-report` 透传; `create_ecpm_record` 落库。 - **收益聚合**:新增 `revenue_yuan_by_trace(db, trace_ids)`——按 trace_id 聚合展示收益, 单条 = `min(eCPM元, ¥500钳顶)/1000`,与广告收益报表 `ad_revenue.py` **同口径**; 只吃当前页的 trace_id(逐页批量,索引命中,无 N+1)。 - **两个看板**:`CouponDataRow` / `AdminComparisonListItem` 增 `ad_revenue_yuan`; `coupon_data_report`(主表 + 用户抽屉)与 `list_comparison_records` 分页后逐页补该字段。 - **测试**:`tests/test_ad_ecpm_trace_revenue.py`(聚合/钳顶/落库)、 `tests/test_board_ad_revenue.py`(两看板 + 抽屉)。 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #131
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
"""ad_ecpm_record.trace_id 落库 + 按 trace 聚合广告收益(元)。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy import delete
|
|
|
|
from app.db.session import SessionLocal
|
|
from app.models.ad_ecpm import AdEcpmRecord
|
|
from app.repositories import ad_ecpm as crud_ecpm
|
|
|
|
|
|
def _ecpm(trace_id: str, ecpm_raw: str, session_id: str) -> AdEcpmRecord:
|
|
"""构造一条 Draw 展示 eCPM(不 commit;ad_session_id 全局唯一,须各不相同)。"""
|
|
return AdEcpmRecord(
|
|
user_id=1,
|
|
ad_type="draw",
|
|
feed_scene="comparison",
|
|
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_revenue_yuan_by_trace_sums_and_clamps() -> None:
|
|
"""同一 trace 多条展示求和;收益=min(eCPM元,¥500)/1000;无展示的 trace 不出现。
|
|
|
|
ecpm 200 分→2.0 元/千次→0.002 元/次;300 分→0.003;合计 0.005。
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
db.add_all([
|
|
_ecpm("t1", "200", "sess-t1-a"),
|
|
_ecpm("t1", "300", "sess-t1-b"),
|
|
_ecpm("t2", "0", "sess-t2-a"),
|
|
_ecpm("t_cap", "60000", "sess-cap-a"), # 600 元 CPM > ¥500 钳顶
|
|
])
|
|
db.flush()
|
|
rev = crud_ecpm.revenue_yuan_by_trace(db, ["t1", "t2", "t3", "t_cap"])
|
|
assert rev["t1"] == 0.005
|
|
assert rev["t2"] == 0.0 # 有展示但 eCPM=0 → 0 元(仍在结果里)
|
|
assert "t3" not in rev # 无展示的 trace 不出现在结果里
|
|
assert rev["t_cap"] == 0.5 # min(¥600, ¥500)/1000,证明钳顶生效(非 0.6)
|
|
finally:
|
|
db.rollback()
|
|
db.close()
|
|
|
|
|
|
def test_revenue_yuan_by_trace_empty() -> None:
|
|
"""空 trace 列表直接返回 {}(避免 IN () 非法)。"""
|
|
db = SessionLocal()
|
|
try:
|
|
assert crud_ecpm.revenue_yuan_by_trace(db, []) == {}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_create_ecpm_record_persists_trace_id() -> None:
|
|
"""create_ecpm_record 落 trace_id。"""
|
|
db = SessionLocal()
|
|
try:
|
|
rec = crud_ecpm.create_ecpm_record(
|
|
db, 1, ad_type="draw", ecpm_raw="150",
|
|
ad_session_id="sess-trace-persist", feed_scene="coupon",
|
|
trace_id="trace-xyz",
|
|
)
|
|
assert rec.trace_id == "trace-xyz"
|
|
finally:
|
|
db.execute(delete(AdEcpmRecord).where(AdEcpmRecord.ad_session_id == "sess-trace-persist"))
|
|
db.commit()
|
|
db.close()
|