feat(ad): eCPM 上报接口 + 冷却策略解耦纯函数 + savings JSONB 跨库修复 (#8)
- 新增 POST /api/v1/ad/ecpm-report:激励视频展示后客户端上报本次 eCPM,落 ad_ecpm_record 做内部收益统计(model/repository/schema/alembic 迁移 + 接口文档) - 看广告冷却策略抽到 app/core/ad_cooldown.py 纯函数;ad_reward.today_status 只取数据,换策略只改这一处 - savings.dishes 改 JSON().with_variant(JSONB,"postgresql"):SQLite 无 visit_JSONB 会让 create_all 编译崩,修复后测试套件恢复 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: OuYingJun1024 <1034284404@qq.com> Reviewed-on: #8 Co-authored-by: ouzhou <ouzhou@wonderable.ai> Co-committed-by: ouzhou <ouzhou@wonderable.ai>
This commit was merged in pull request #8.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""看激励视频冷却策略 —— 与发奖记录查询解耦的纯计算。
|
||||
|
||||
当前策略:**每 N 次一轮,看满一轮后强制冷却若干秒**(N / 秒数 取自 [rewards] 常量)。
|
||||
[repositories.ad_reward.today_status] 只负责取数据(今日 granted 的 created_at 列表),
|
||||
把"本轮已看几次 + 冷却到几点"的策略判断委托到这里。
|
||||
|
||||
⚠️ 这是临时策略,后续要调。换策略(间隔式 / 每日配额式 / 指数退避 …)**只改本文件**,
|
||||
repository 不碰——这就是把它独立出来的目的。保持 [compute_cooldown] 签名稳定即可。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.core.rewards import VIDEO_ROUND_COOLDOWN_SECONDS, VIDEO_ROUND_REQUIRED_COUNT
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CooldownState:
|
||||
"""冷却策略的输出。"""
|
||||
|
||||
round_count: int # 本轮已看次数 0..N-1(展示用)
|
||||
cooldown_until: datetime | None # 本轮冷却结束时间(UTC);None = 不在冷却
|
||||
|
||||
|
||||
def compute_cooldown(
|
||||
granted_times_desc: list[datetime],
|
||||
now: datetime,
|
||||
*,
|
||||
round_size: int = VIDEO_ROUND_REQUIRED_COUNT,
|
||||
cooldown_seconds: int = VIDEO_ROUND_COOLDOWN_SECONDS,
|
||||
) -> CooldownState:
|
||||
"""按"每 round_size 次一轮、看满一轮后冷却 cooldown_seconds 秒"算本轮进度 + 冷却结束时间。
|
||||
|
||||
:param granted_times_desc: 今日 status=granted 记录的 created_at,**按时间倒序**(最新在前)。
|
||||
:param now: 当前时间(UTC,带 tzinfo),用于判断冷却是否已过。
|
||||
:param round_size / cooldown_seconds: 策略参数,默认取 rewards 常量,可注入便于测试/调参。
|
||||
|
||||
纯函数,不碰 DB。冷却派生算法:把今日 granted 倒序,跳过当前未完成轮的 round_count 条,
|
||||
下一条即"上一轮最后一次"的时间,+ cooldown_seconds 仍 > now 则在冷却中。
|
||||
SQLite 上 created_at 可能是 naive,按 UTC 解读再比较。
|
||||
"""
|
||||
used = len(granted_times_desc)
|
||||
round_count = used % round_size
|
||||
cooldown_until: datetime | None = None
|
||||
if used >= round_size:
|
||||
# round_count 必 < round_size <= used,索引合法
|
||||
last_round_end = granted_times_desc[round_count]
|
||||
if last_round_end.tzinfo is None:
|
||||
last_round_end = last_round_end.replace(tzinfo=timezone.utc)
|
||||
cd_end = last_round_end + timedelta(seconds=cooldown_seconds)
|
||||
if cd_end > now:
|
||||
cooldown_until = cd_end
|
||||
return CooldownState(round_count=round_count, cooldown_until=cooldown_until)
|
||||
+16
-3
@@ -58,13 +58,26 @@ TASK_REWARDS: dict[str, int] = {
|
||||
}
|
||||
|
||||
|
||||
# ===== 比价战绩里程碑(累计成功比价 N 次,逐档解锁领金币)=====
|
||||
# 第 1→6 次的金币奖励(1-based:第 N 次比价解锁第 N 档)。值沿用客户端原型档位。
|
||||
# 数据源是 comparison_record 里 status='success' 的条数;每档领一次,
|
||||
# comparison_milestone_claim 去重(仿一次性任务)。要调档位/金额直接改这里。
|
||||
RECORD_MILESTONES: tuple[int, ...] = (120, 180, 300, 500, 800, 1200)
|
||||
RECORD_MILESTONE_COUNT: int = len(RECORD_MILESTONES)
|
||||
|
||||
|
||||
def record_milestone_reward(milestone: int) -> int:
|
||||
"""第 milestone 档(1..RECORD_MILESTONE_COUNT)的金币。越界抛 IndexError。"""
|
||||
return RECORD_MILESTONES[milestone - 1]
|
||||
|
||||
|
||||
# ===== 看激励视频发金币(穿山甲 S2S 服务端回调发奖)=====
|
||||
# 看完一个激励视频发的金币(100 金币 ≈¥0.01,汇率 10000 金币=1 元)。
|
||||
# 看完一个激励视频发的金币(666 金币 ≈¥0.0666,汇率 10000 金币=1 元)。
|
||||
# 作用:① 回调缺/坏 reward_amount 时的回退值;② 客户端进度接口展示的"单次预告金币";
|
||||
# ③ test-grant 本地联调的发奖额。
|
||||
# 真实发放以穿山甲回调带回的 reward_amount 为准(见 resolve_ad_reward_coin),后台应把
|
||||
# 代码位"奖励数量"配成与本值一致(=100),保证"广告内展示 / 进度预告 / 实际到账"三者一致。
|
||||
AD_REWARD_COIN: int = 100
|
||||
# 代码位"奖励数量"配成与本值一致(=666),保证"广告内展示 / 进度预告 / 实际到账"三者一致。
|
||||
AD_REWARD_COIN: int = 666
|
||||
# 单次发奖金币上限:夹紧穿山甲回调里异常的 reward_amount(如后台多打一个 0),防刷爆余额。
|
||||
MAX_AD_REWARD_COIN: int = 1000
|
||||
# 每用户每日发奖次数上限,防刷 + 控成本。
|
||||
|
||||
Reference in New Issue
Block a user