feat(ad): 记录不发金币的原因(closed_early / too_short)

让广告收益报表能呈现「有展示、没发金币」的原因:
- grant_feed_reward 改整场结算:aborted→closed_early、整场总时长<10s→too_short(均 coin=0)
- 新增 record_reward_noshow + POST /api/v1/ad/reward-noshow(激励视频提前关留痕;
  同 ad_session_id 已 granted 则跳过,防与 S2S 发奖重复)
- feed-reward 加 aborted 入参
- 文档:feed-reward / 两张表 status 取值、新增 ad-reward-noshow、README 索引
- alembic: 合并 ad_revenue_report_cols 与 store_mapping_tb_dl_invalid 双 head(空迁移)

报表复算无需改(非 granted 状态走 else 分支 expected=0 自动归类)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
OuYingJun1024
2026-06-15 22:41:41 +08:00
parent bebd694fb5
commit 3ac58c1670
10 changed files with 245 additions and 23 deletions
+1
View File
@@ -63,6 +63,7 @@
| 34 | `POST /api/v1/ad/test-grant` | Bearer | [详情](./ad-test-grant.md) |
| 35 | `POST /api/v1/ad/ecpm-report` | Bearer | [详情](./ad-ecpm-report.md) |
| 35a | `POST /api/v1/ad/feed-reward` | Bearer | [详情](./ad-feed-reward.md) |
| 35b | `POST /api/v1/ad/reward-noshow` | Bearer | [详情](./ad-reward-noshow.md)(激励视频提前关闭/未发奖留痕,只记原因不发币) |
| **用户资料**(前缀 `/api/v1/user` |||
| 35 | `PATCH /api/v1/user/profile` | Bearer | [详情](./user-profile.md) |
| 36 | `POST /api/v1/user/avatar` | Bearer | [详情](./user-avatar.md) |
+8 -7
View File
@@ -1,6 +1,6 @@
# POST /api/v1/ad/feed-reward — 信息流广告完成后结算金币
点位 2:比价等待 / 领券信息流广告。每展示满 10 秒累计一份奖励,视频完成后一次性入账
点位 2:比价等待 / 领券信息流广告(轮播多条)。**整场比价全程不关广告才发**,金额按整场**总观看时长**折份(每 10 秒 1 份),结束时一次性入账。用户中途 ✕ 关闭则整场不发
## 鉴权
@@ -12,26 +12,27 @@
|---|---|---:|---|
| `client_event_id` | string | 是 | 客户端生成的幂等事件 id,8-64 字符 |
| `ad_session_id` | string\|null | 否 | 客户端生成的一次信息流广告会话 id,用于对账/排查 |
| `ecpm` | string | 是 | 本信息流广告 eCPM(穿山甲 getEcpm 原值),按“分/千次展示”处理(非元) |
| `duration_seconds` | int | 是 | 实际展示/播放秒数 |
| `ecpm` | string | 是 | 本信息流 eCPM 代表值(穿山甲 getEcpm 原值),按“分/千次展示”处理(非元) |
| `duration_seconds` | int | 是 | **整场累计观看秒数**(轮播各条相加) |
| `adn` | string\|null | 否 | 实际投放 ADN(聚合后实际填充的子渠道) |
| `slot_id` | string\|null | 否 | 实际展示代码位(底层 mediation rit) |
| `app_env` | string\|null | 否 | **我们的**应用环境:`prod`(傻瓜比价正式) / `test`(测试应用) |
| `our_code_id` | string\|null | 否 | **我们后台配置的**代码位 ID(104xxx,非底层 rit);供广告收益报表按代码位聚合金币 |
| `aborted` | bool | 否 | 用户中途 ✕ 关闭广告(未走完比价):整场不发,仅记 `closed_early`。默认 `false` |
## 响应
| 字段 | 类型 | 说明 |
|---|---|---|
| `granted` | bool | 本次是否入账;达上限时为 `false` |
| `status` | string | `granted` / `capped` |
| `coin` | int | 本次发放金币 |
| `granted` | bool | 本次是否入账;未发(任一非 granted 状态)时为 `false` |
| `status` | string | `granted`(已发) / `capped`(当日次数超限) / `too_short`(整场总时长<10s 凑不满一份) / `closed_early`(用户中途关闭) |
| `coin` | int | 本次发放金币;非 granted 为 0 |
| `unit_count` | int | 按 10 秒折算出的奖励份数 |
| `daily_limit` | int | 每日信息流展示次数上限,默认 500 |
## 计算口径
- 奖励份数:`duration_seconds // 10`
- 奖励份数:`整场总时长 // 10`
- 单份奖励:`eCPM / 1000 × 因子1(eCPM 档) × 因子2(当天累计份序号) × 10000`,四舍五入为整数金币。
- eCPM 档:`0-100=0.1``101-200=0.3``201-400=0.4``>400=0.6`
- LT 档:第 1 份 `2.0`,第 2 份 `1.5`,第 3 份 `1.3`,第 4-10 份 `1.1`,第 11 份及以后 `1.0`
+41
View File
@@ -0,0 +1,41 @@
# POST /api/v1/ad/reward-noshow — 激励视频提前关闭/未发奖留痕
激励视频**展示了但用户提前关闭/跳过、未触发 S2S 发奖**时,客户端 best-effort 上报一条留痕记录,
让运营后台「广告数据」能呈现「有展示、没发金币」的原因。**不发金币**。
## 鉴权
需要 Bearer token。`user_id` 由 JWT 取,不在 body。
## 请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `ad_session_id` | string | 是 | 本次展示会话 id(与 eCPM 上报、S2S `extra.ad_session_id` 一致)8-64 字符 |
| `watched_seconds` | int | 否 | 关闭前已观看秒数(仅留痕参考,**不入库**)。默认 0 |
| `ecpm` | string\|null | 否 | 本次展示 eCPM(穿山甲 getEcpm 原值,分/千次展示) |
| `adn` | string\|null | 否 | 实际投放 ADN(聚合后实际填充子渠道) |
| `slot_id` | string\|null | 否 | 实际展示代码位(底层 mediation rit) |
| `app_env` | string\|null | 否 | **我们的**应用环境:`prod`(傻瓜比价正式) / `test`(测试应用) |
| `our_code_id` | string\|null | 否 | **我们后台配置的**代码位 ID(104xxx,非底层 rit) |
## 响应
| 字段 | 类型 | 说明 |
|---|---|---|
| `ok` | bool | 是否处理成功(落库或幂等命中) |
| `status` | string | `closed_early`(已留痕) / `granted`(该次其实已发奖,跳过未写) |
## 说明
-`onAdClose` 时若本次**未触发发奖**(无 `onRewardVerify`/`onRewardArrived`)调用。
- **幂等**:写入 `ad_reward_record``trans_id = noreward:{ad_session_id}`(每次展示唯一),重试不重复。
- **防与正常发奖重复**:若同一 `ad_session_id` 已有 `status='granted'` 记录(S2S 已发,说明用户其实看完了),
则跳过不写,返回 `status='granted'`
- best-effort:客户端 fire-and-forget,失败只 log,不影响主流程。
- 信息流的「用户中途关闭」走 [ad-feed-reward](./ad-feed-reward.md) 的 `aborted=true`(记 `closed_early`),不走本接口。
## 数据写入
- `ad_reward_record` 新增一行(`coin=0``status='closed_early'``reward_scene='reward_video'`)。
- **不**写 `coin_account` / `coin_transaction`(不发币)。