Compare commits

..

2 Commits

Author SHA1 Message Date
linkeyu beadce31ed fix(ad): 服务端强制不足一秒曝光收益为零 (#150)
## 改动
- eCPM 上报新增可选 exposure_ms,兼容旧客户端
- exposure_ms < 1000 时保留展示记录并强制有效 eCPM 为 0
- 失败领券任务允许保留短曝光零收益 trace,后台显示 0 而不是未填充
- 其他失败后的迟到曝光仍按原规则解绑 trace

## 验证
- 相关 pytest:10 passed
- Ruff:通过
- compileall:通过

依赖:先合并 Server #149。

---------

Co-authored-by: unknown <798648091@qq.com>
Reviewed-on: #150
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-21 13:53:10 +08:00
guke f39467ec08 docs(welfare): 15天不活跃清零金币/现金 设计文档(spec) (#151)
对齐前端首页可见事件home_visible

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #151
2026-07-21 13:52:40 +08:00
4 changed files with 67 additions and 6 deletions
+7 -3
View File
@@ -281,7 +281,10 @@ def ecpm_report(payload: EcpmReportIn, user: CurrentUser, db: DbSession) -> Ecpm
丢一两条不影响业务(穿山甲后台报表是结算权威)。eCPM 与发奖(S2S)是两条独立流,不逐条关联。 丢一两条不影响业务(穿山甲后台报表是结算权威)。eCPM 与发奖(S2S)是两条独立流,不逐条关联。
""" """
attributed_trace_id = crud_ecpm.attributable_trace_id( attributed_trace_id = crud_ecpm.attributable_trace_id(
db, feed_scene=payload.feed_scene, trace_id=payload.trace_id db,
feed_scene=payload.feed_scene,
trace_id=payload.trace_id,
exposure_ms=payload.exposure_ms,
) )
if payload.trace_id and attributed_trace_id is None: if payload.trace_id and attributed_trace_id is None:
logger.info( logger.info(
@@ -296,11 +299,12 @@ def ecpm_report(payload: EcpmReportIn, user: CurrentUser, db: DbSession) -> Ecpm
feed_scene=payload.feed_scene, feed_scene=payload.feed_scene,
trace_id=attributed_trace_id, trace_id=attributed_trace_id,
app_env=payload.app_env, our_code_id=payload.our_code_id, app_env=payload.app_env, our_code_id=payload.our_code_id,
exposure_ms=payload.exposure_ms,
) )
logger.info( logger.info(
"ad ecpm report user_id=%d type=%s scene=%s session=%s ecpm=%s adn=%s slot=%s app=%s code=%s", "ad ecpm report user_id=%d type=%s scene=%s session=%s ecpm=%s exposure_ms=%s adn=%s slot=%s app=%s code=%s",
user.id, payload.ad_type, payload.feed_scene, payload.ad_session_id, payload.ecpm, user.id, payload.ad_type, payload.feed_scene, payload.ad_session_id, payload.ecpm,
payload.adn, payload.slot_id, payload.app_env, payload.our_code_id, payload.exposure_ms, payload.adn, payload.slot_id, payload.app_env, payload.our_code_id,
) )
return EcpmReportOut(ok=True) return EcpmReportOut(ok=True)
+22 -3
View File
@@ -15,9 +15,22 @@ from app.core.rewards import cn_today
from app.models.ad_ecpm import AdEcpmRecord from app.models.ad_ecpm import AdEcpmRecord
from app.models.coupon_state import CouponSession from app.models.coupon_state import CouponSession
MIN_REVENUE_EXPOSURE_MS = 1000
def effective_ecpm_raw(ecpm_raw: str, exposure_ms: int | None) -> str:
"""曝光不足一秒时保留展示记录,但把该条有效 eCPM 归零。"""
if exposure_ms is not None and exposure_ms < MIN_REVENUE_EXPOSURE_MS:
return "0"
return ecpm_raw
def attributable_trace_id( def attributable_trace_id(
db: Session, *, feed_scene: str | None, trace_id: str | None db: Session,
*,
feed_scene: str | None,
trace_id: str | None,
exposure_ms: int | None = None,
) -> str | None: ) -> str | None:
"""返回广告展示允许归属的业务 trace。 """返回广告展示允许归属的业务 trace。
@@ -31,7 +44,12 @@ def attributable_trace_id(
session_status = db.execute( session_status = db.execute(
select(CouponSession.status).where(CouponSession.trace_id == trace_id) select(CouponSession.status).where(CouponSession.trace_id == trace_id)
).scalar_one_or_none() ).scalar_one_or_none()
return None if session_status in {"failed", "abandoned"} else trace_id if session_status not in {"failed", "abandoned"}:
return trace_id
# 已真实上墙但不足一秒的曝光要在终态明细中明确显示 0,而不是被误判成“未填充”。
if exposure_ms is not None and exposure_ms < MIN_REVENUE_EXPOSURE_MS:
return trace_id
return None
def create_ecpm_record( def create_ecpm_record(
@@ -47,6 +65,7 @@ def create_ecpm_record(
trace_id: str | None = None, trace_id: str | None = None,
app_env: str | None = None, app_env: str | None = None,
our_code_id: str | None = None, our_code_id: str | None = None,
exposure_ms: int | None = None,
) -> AdEcpmRecord: ) -> AdEcpmRecord:
"""落一条 eCPM 上报记录。report_date 用北京时间当天,供按天聚合。 """落一条 eCPM 上报记录。report_date 用北京时间当天,供按天聚合。
@@ -67,7 +86,7 @@ def create_ecpm_record(
trace_id=trace_id, trace_id=trace_id,
app_env=app_env, app_env=app_env,
our_code_id=our_code_id, our_code_id=our_code_id,
ecpm_raw=ecpm_raw, ecpm_raw=effective_ecpm_raw(ecpm_raw, exposure_ms),
report_date=cn_today().isoformat(), report_date=cn_today().isoformat(),
) )
db.add(rec) db.add(rec)
+6
View File
@@ -69,6 +69,12 @@ class EcpmReportIn(BaseModel):
description="本次比价/领券 trace_id(信息流场景带上):把这条展示收益归属到对应比价/领券," description="本次比价/领券 trace_id(信息流场景带上):把这条展示收益归属到对应比价/领券,"
"供领券数据/比价记录看板聚合本场广告收益;激励视频/福利为空", "供领券数据/比价记录看板聚合本场广告收益;激励视频/福利为空",
) )
exposure_ms: int | None = Field(
None,
ge=0,
le=86_400_000,
description="本条广告真实在屏曝光毫秒数;小于 1000ms 时收益强制按 0 计算。旧客户端不传则保持原口径",
)
app_env: str | None = Field( app_env: str | None = Field(
None, max_length=16, description="我们的穿山甲应用环境:prod(傻瓜比价正式) / test(测试应用)" None, max_length=16, description="我们的穿山甲应用环境:prod(傻瓜比价正式) / test(测试应用)"
) )
+32
View File
@@ -58,6 +58,32 @@ def test_revenue_yuan_by_trace_empty() -> None:
db.close() db.close()
def test_short_exposure_keeps_record_with_zero_revenue() -> None:
"""不足一秒仍落展示记录,以便后台显示 0 而不是未填充。"""
db = SessionLocal()
try:
rec = crud_ecpm.create_ecpm_record(
db, 1, ad_type="draw", ecpm_raw="350",
ad_session_id="sess-short-exposure", feed_scene="coupon",
trace_id="trace-short-exposure", exposure_ms=999,
)
assert rec.ecpm_raw == "0"
assert crud_ecpm.revenue_yuan_by_trace(db, ["trace-short-exposure"]) == {
"trace-short-exposure": 0.0
}
finally:
db.execute(delete(AdEcpmRecord).where(
AdEcpmRecord.ad_session_id == "sess-short-exposure"
))
db.commit()
db.close()
def test_one_second_exposure_keeps_original_ecpm() -> None:
assert crud_ecpm.effective_ecpm_raw("350", 1000) == "350"
assert crud_ecpm.effective_ecpm_raw("350", None) == "350"
def test_terminal_coupon_trace_is_not_attributable_to_late_impression() -> None: def test_terminal_coupon_trace_is_not_attributable_to_late_impression() -> None:
"""领券失败或被放弃后才到达的广告展示保留收益记录,但不再关联死亡 trace。""" """领券失败或被放弃后才到达的广告展示保留收益记录,但不再关联死亡 trace。"""
db = SessionLocal() db = SessionLocal()
@@ -80,6 +106,12 @@ def test_terminal_coupon_trace_is_not_attributable_to_late_impression() -> None:
assert crud_ecpm.attributable_trace_id( assert crud_ecpm.attributable_trace_id(
db, feed_scene="coupon", trace_id="abandoned-before-ad" db, feed_scene="coupon", trace_id="abandoned-before-ad"
) is None ) is None
assert crud_ecpm.attributable_trace_id(
db, feed_scene="coupon", trace_id="failed-before-ad", exposure_ms=999
) == "failed-before-ad"
assert crud_ecpm.attributable_trace_id(
db, feed_scene="coupon", trace_id="abandoned-before-ad", exposure_ms=999
) == "abandoned-before-ad"
assert crud_ecpm.attributable_trace_id( assert crud_ecpm.attributable_trace_id(
db, feed_scene="comparison", trace_id="failed-before-ad" db, feed_scene="comparison", trace_id="failed-before-ad"
) == "failed-before-ad" ) == "failed-before-ad"