Compare commits

..

1 Commits

Author SHA1 Message Date
marco 2e91c9f72f ecpm保底是1 2026-08-01 02:57:45 +08:00
2 changed files with 56 additions and 15 deletions
+8 -4
View File
@@ -234,13 +234,17 @@ def calculate_ad_reward_coin(ecpm: str | int | float | None, count_after_this: i
会铸天量金币;钳在这唯一入口,feed 与 reward_video 回退客户端 eCPM 的路径都护住,且阈值高于
所有真实值,不影响正规发奖。
下限(2026-07):有真实正 eCPM 时单份至少 1 金币——低 eCPM 单份收益四舍五入成 0 时兜底为 1,
避免用户看了广告却因数值太小被记 too_short 零发。eCPM 缺失/为 0/非法(没有真实广告价值)仍返 0,
不凭空铸币、不破坏 ecpm_missing 语义;防刷上限仍由 AD_ECPM_MAX_FEN 钳顶把守。
下限(2026-08,产品口径「看了就保底 1」):**任何输入**都至少 1 金币,与前端展示公式
FeedRewardFormula.singleUnitCoin 完全对齐(那边注释:"无论 eCPM 是否为空、非法或非正数,
单条广告最低都发 1 金币,不能出现 +0")。此前 eCPM 缺失/为 0 返 0,造成两端不一致:
小球显示 +1、后端信息流记 too_short 零发;激励视频侧 "0" 字符串还是 truthy、绕过
ecpm_missing 的 `if not ecpm_raw` 判定,落成 granted 0 币且白占当日额度/LT 计数。
防刷影响:伪造 eCPM≤0 每天至多多骗 每日上限×1 金币(500 金币=0.05 元),量级可控;
天价伪造仍由 AD_ECPM_MAX_FEN 钳顶把守。
"""
ecpm_yuan = min(parse_ecpm_yuan(ecpm), AD_ECPM_MAX_FEN / 100.0)
if ecpm_yuan <= 0:
return 0
return 1 # 保底:缺失/为 0/非法也发 1(镜像前端 validEcpmFen 判非法 → 直接返 1)
yuan = (ecpm_yuan / 1000.0) * ad_ecpm_factor(ecpm_yuan) * ad_lt_factor(count_after_this)
return max(1, round(yuan * COIN_PER_YUAN))
+48 -11
View File
@@ -1,8 +1,13 @@
"""发奖公式下限:有真实正 eCPM 时最低发 1 金币,eCPM 缺失/为 0 仍发 0
"""发奖公式下限:看了就保底 1 金币,与前端展示公式完全对齐
针对「eCPM 过低时公式四舍五入成 0 金币 → 被记 too_short 不发」的问题:只要广告有真实
正 eCPM,单份金币至少 1(不再 0);但 eCPM 缺失/为 0/非法(没有真实广告价值)仍发 0,
不凭空铸币、不破坏 ecpm_missing 语义。公式是发奖与后台审计对账的唯一口径,改这一处两边同源
产品口径(2026-08「看了就保底 1」):`calculate_ad_reward_coin` 对**任何输入**都至少返回 1,
镜像客户端 FeedRewardFormula.singleUnitCoin(那边:eCPM 空/非法/非正数都返 1)。此前 eCPM
缺失/为 0 返 0,与前端小球显示的 +1 不一致,且信息流侧把这类看满一份的广告记成 too_short 零发
公式是发奖与后台审计对账的唯一口径,改这一处两边同源。
注:激励视频(S2S 回调)路径在公式之前还有一道 `if not ecpm_raw` 早退——**完全没上报 eCPM**
的回调仍记 ecpm_missing 零发(见 test_ad_reward.test_callback_without_ecpm_records_exception),
那是"回调缺字段"的数据完整性闸,区别于"广告如实上报 eCPM=0"(=看了真广告 → 保底 1)。
"""
from __future__ import annotations
@@ -10,19 +15,23 @@ from app.core.rewards import calculate_ad_reward_coin
def test_low_positive_ecpm_floors_to_one_coin() -> None:
"""真实但极低的 eCPM 原本四舍五入成 0 金币,现在兜底为 1(线上 record 4667 的 43 分场景)。"""
"""真实但极低的 eCPM 原本四舍五入成 0 金币,兜底为 1(线上 record 4667 的 43 分场景)。"""
# 43 分 = ¥0.43 CPM,因子1=0.1,重度用户 LT 第 69 条=1.0 → 0.43/1000×0.1×1.0×10000=0.43 → 旧口径 round=0
assert calculate_ad_reward_coin("43", 69) == 1
# 更低的 5 分同理:算出来 <0.5,旧口径也是 0
assert calculate_ad_reward_coin("5", 11) == 1
def test_zero_or_missing_ecpm_stays_zero() -> None:
"""eCPM 缺失 / 为 0 / 非法(没有真实广告价值)不兜底,仍发 0。"""
assert calculate_ad_reward_coin("0", 1) == 0
assert calculate_ad_reward_coin(None, 1) == 0
assert calculate_ad_reward_coin("", 1) == 0
assert calculate_ad_reward_coin("abc", 1) == 0
def test_zero_or_missing_ecpm_also_floors_to_one() -> None:
"""eCPM 为 0 / 缺失 / 非法都保底 1(2026-08「看了就保底 1」,与前端 FeedRewardFormula 对齐)。
尤其 "0"(广告如实上报零价值)此前返 0,导致小球显示 +1、后端信息流记 too_short 零发的
前后端不一致 —— 现在两端都 1。
"""
assert calculate_ad_reward_coin("0", 1) == 1
assert calculate_ad_reward_coin(None, 1) == 1
assert calculate_ad_reward_coin("", 1) == 1
assert calculate_ad_reward_coin("abc", 1) == 1
def test_normal_ecpm_value_unchanged() -> None:
@@ -57,3 +66,31 @@ def test_feed_reward_low_ecpm_grants_one_coin_instead_of_too_short() -> None:
assert rec.coin == 1
finally:
db.close()
def test_feed_reward_zero_ecpm_grants_one_coin() -> None:
"""端到端:eCPM 如实上报 0(用户反馈的真实广告返回 eCPM=0 场景),看满一份也保底 1、
状态 granted —— 与前端小球显示的 +1 一致,不再前显示后零发。"""
from app.db.session import SessionLocal
from app.models.user import User
from app.repositories.ad_feed_reward import grant_feed_reward
db = SessionLocal()
try:
user = User(phone="19900000044", username="feedzero44", register_channel="sms")
db.add(user)
db.commit()
db.refresh(user)
rec = grant_feed_reward(
db, user.id,
client_event_id="feed-zero-ecpm-0001",
ecpm="0", # 广告如实上报 eCPM=0
duration_seconds=15, # 看满一份
ad_type="draw",
feed_scene="comparison",
)
assert rec.status == "granted"
assert rec.coin == 1
finally:
db.close()