fix(广告发奖): eCPM 过低单份金币四舍五入成 0 时兜底发 1,避免看了广告被记 too_short 零发 #205

Merged
guke merged 2 commits from fix/ad-reward-min-one-coin into main 2026-07-31 11:18:02 +08:00
Member

背景 / 问题

线上 record 4667:用户在比价等候期看满一条低 eCPM(¥0.43 CPM)的信息流广告(≥10s),
但单份金币按公式 0.43/1000 × 0.1(档位因子) × 1.0(LT因子) × 10000 = 0.43,四舍五入成 0
grant_feed_rewardunit_cap=0coin<=0 → 记 too_short 零发。

结果:用户看满了广告却什么都没拿到,还被标成"时长不足"。

改动

app/core/rewards.pycalculate_ad_reward_coin(发奖与后台审计对账的唯一口径):

  • 有真实正 eCPM 时,单份金币 floor 到 1(max(0, …)max(1, …))。
  • eCPM 缺失 / 为 0 / 非法(没有真实广告价值)时提前 return 0 —— 不凭空铸币、不破坏 ecpm_missing 语义。
    关键设计
    if ecpm_yuan <= 0: return 0 是防铸币防线:没它的话 reward_video 路径 ecpm="0" 会被 floor 成 1。
    不影响防刷:上限仍由 AD_ECPM_MAX_FEN(¥500 CPM)钳顶;LT 因子最低 1.0、从不归零,限流靠每日 500 次上限。
    正常量级 eCPM 本就 ≥1,下限不改变其取值(纯回归保护)。
## 背景 / 问题 线上 record 4667:用户在比价等候期看满一条低 eCPM(¥0.43 CPM)的信息流广告(≥10s), 但单份金币按公式 `0.43/1000 × 0.1(档位因子) × 1.0(LT因子) × 10000 = 0.43`,四舍五入成 **0**。 在 `grant_feed_reward` 里 `unit_cap=0` → `coin<=0` → 记 `too_short` 零发。 结果:**用户看满了广告却什么都没拿到**,还被标成"时长不足"。 ## 改动 `app/core/rewards.py` 的 `calculate_ad_reward_coin`(发奖与后台审计对账的**唯一口径**): - 有真实正 eCPM 时,单份金币 floor 到 1(`max(0, …)` → `max(1, …)`)。 - eCPM 缺失 / 为 0 / 非法(没有真实广告价值)时提前 `return 0` —— 不凭空铸币、不破坏 `ecpm_missing` 语义。 关键设计 if ecpm_yuan <= 0: return 0 是防铸币防线:没它的话 reward_video 路径 ecpm="0" 会被 floor 成 1。 不影响防刷:上限仍由 AD_ECPM_MAX_FEN(¥500 CPM)钳顶;LT 因子最低 1.0、从不归零,限流靠每日 500 次上限。 正常量级 eCPM 本就 ≥1,下限不改变其取值(纯回归保护)。
guke added 1 commit 2026-07-30 17:55:30 +08:00
Author
Member

🤖 review-pr 深审结论

🟢 可合并 · 置信度 0.92 —— 金钱发奖修复正确、防铸币防线到位、测试充分(33 用例全过)。

改动calculate_ad_reward_coin(发奖 + 审计对账唯一口径

  • 新增 if ecpm_yuan <= 0: return 0(在 max 之前)——防铸币防线。
  • max(0, …)max(1, …)——有真实正 eCPM 时单份至少 1 金币。

正确性核对(合并后)

  • 顺序正确:先 ecpm<=0 → return 0,再对正 eCPM max(1)。故低正 eCPM(0.43 四舍五入成 0)→ 1;eCPM 缺失/0/非法 → 0(不凭空铸币,保护 reward_video 回退 ecpm="0" 路径)。
  • 唯一口径:发奖(ad_feed_reward/ad_reward)与后台审计对账(ad_audit)同调此函数 → 改一处两边同源,对账仍自洽。
  • 上限 AD_ECPM_MAX_FEN 钳顶不变,防刷不受影响。
  • 铁律1:main 未改 rewards.py

构建/测试(实跑 .venv / py3.12 合并后)

  • test_ad_reward_coin_floor.py 4 passed(低正→1 / 零·缺失·非法→0 / 正常回归 / 端到端 grant_feed_reward granted+coin=1)
  • test_ad_reward.py 29 passed(现有发奖回归,正常 eCPM 不受下限影响)

提示(info,不阻塞):历史 too_short(发 0)记录若纳入审计对账,新口径复算 expected=1 会显示「应发 1 / 实发 0」差异——属历史遗留(本就发错),非本 PR 引入;若审计只对 granted 记录则无影响。

风险 🟢

🤖 review-pr 自动深审 @guke

## 🤖 review-pr 深审结论 🟢 **可合并** · 置信度 0.92 —— 金钱发奖修复正确、防铸币防线到位、测试充分(33 用例全过)。 **改动**:`calculate_ad_reward_coin`(发奖 + 审计对账**唯一口径**) - 新增 `if ecpm_yuan <= 0: return 0`(在 max 之前)——防铸币防线。 - `max(0, …)` → `max(1, …)`——有真实正 eCPM 时单份至少 1 金币。 **正确性核对(合并后)** - 顺序正确:先 `ecpm<=0 → return 0`,再对正 eCPM `max(1)`。故低正 eCPM(0.43 四舍五入成 0)→ 1;eCPM 缺失/0/非法 → 0(不凭空铸币,保护 `reward_video` 回退 `ecpm="0"` 路径)。 - 唯一口径:发奖(`ad_feed_reward`/`ad_reward`)与后台审计对账(`ad_audit`)同调此函数 → 改一处两边同源,对账仍自洽。 - 上限 `AD_ECPM_MAX_FEN` 钳顶不变,防刷不受影响。 - 铁律1:main 未改 `rewards.py`。 **构建/测试(实跑 .venv / py3.12 合并后)** - ✅ `test_ad_reward_coin_floor.py` **4 passed**(低正→1 / 零·缺失·非法→0 / 正常回归 / 端到端 `grant_feed_reward` granted+coin=1) - ✅ `test_ad_reward.py` **29 passed**(现有发奖回归,正常 eCPM 不受下限影响) **提示(info,不阻塞)**:历史 `too_short`(发 0)记录若纳入审计对账,新口径复算 `expected=1` 会显示「应发 1 / 实发 0」差异——属历史遗留(本就发错),非本 PR 引入;若审计只对 `granted` 记录则无影响。 风险 🟢。 — 🤖 review-pr 自动深审 @guke
guke added 1 commit 2026-07-31 11:17:56 +08:00
guke merged commit eeecb5faf0 into main 2026-07-31 11:18:02 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: WonderableAI/shaguabijia-app-server#205