docs(agent-specs): M4 广告SDK预接线设计段成稿(R3)——双评审已回判回炉,先落基线版便于修订对照

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-02 20:07:17 -07:00
parent 74c3f72998
commit 93b51aad74

View File

@ -0,0 +1,125 @@
---
date: 2026-07-02
topic: M4 广告 SDK 预接线
status: 草稿 · 待 Codex+Opus 双评审 → 通过后进 R3 实现
sot-impact: 不新建 canonical topic(R3 工单实现设计,归属既有 SoT「变现端到端」)。修订触点 = 变现端到端 §6(切换红线由「待落地」落成 runbook)、§8(切真接线点细化);落地时新增 Nacos 配置约定(per-provider 密钥命名空间 + ad.force-mock 熔断);不改 ad.yaml API 契约、不改 mock 路径代码。
上级: docs/mvp/可行性方案16周-波次映射.md
关联: game-cloud/game-module-ad/game-module-ad-server/.../framework/provider/{AdProvider,AdProviderFactory,MockAdProvider,CallbackAdProvider,AdRewardCallbackContext}.java · controller/app/AdController.java:94-102 · service/revenue/AdRevenueServiceImpl.java:101-210 · contracts/api-schemas/ad.yaml · contracts/ad-slot.schema.json · docs/architecture/运营/变现端到端.md §6/§8 @ 4d220e77
图清单: [图1 接入面边界与三层切换开关(mermaid);SVG 门面图收口时按 atlas 补]
---
# M4 广告 SDK 预接线 · 设计(穿山甲 / 优量汇适配层切真通路)
## 0 一图看懂
```mermaid
flowchart TB
subgraph 端侧["端侧 SDK 轨(game-studio,不在本单)"]
SDK["WanxiangGameSDK · AdPlugin<br/>showRewarded / showInterstitial<br/>拉起联盟 SDK 展示广告"]
end
subgraph 服务端["服务端轨 · ad 模块(本单核心)"]
CB["/app-api/ad/reward/callback<br/>@PermitAll + IP 限频"]
F["AdProviderFactory.get(provider)<br/>未注册→降级 mock;force-mock→全局回退"]
V["provider.verifyRewardCallback<br/>验签(密钥 per-provider)"]
BILL["bill():合规→uk_trace 幂等→归因→算收入→落台账"]
REC["ad↔联盟对账(本单新增设计)<br/>估算入账 vs 联盟账单,T+1 冲正"]
end
SDK -. 用户看完,联盟服务端回调 .-> CB
CB --> F --> V -->|通过| BILL
BILL --> REC
V -->|失败 1-111-002-004| X["不计费不发奖 + 告警"]
classDef out fill:#eef,stroke:#88a;
classDef new fill:#fff7e6,stroke:#d90;
class 端侧 out; class REC new;
```
三层切换开关(默认值一律 = mock 现状,切真是叠加、不改 mock 路径):**代码层** = 注入真实 `CsjAdProvider`/`GdtAdProvider` 实现类;**配置层** = Nacos 的 per-provider 验签密钥 + `ad.force-mock` 熔断;**数据层** = 广告位 `game_ad_slot.provider` 字段 + `providerSlotId`
## 1 意图与边界
### 1.1 意图
把已经落地的广告联盟 SPI 骨架,补成一条「从 mock 到真、一键可切、随时可回滚、切完能对账」的完整通路,让穿山甲(csj)/优量汇(gdt)资质下证当天,以最小动作切真而不动 mock。R3 是**预接线**——下证前能做的全部就位,把真正等文档的部分压缩到最小。
预接线并非从零。当前已就位、本单直接复用的骨架:
- **SPI 与工厂**:`AdProvider`(`getProvider`/`calcRevenue`/`verifyReward`/`verifyRewardCallback`)+ `AdProviderFactory` 按 provider 路由、未注册降级 mock。
- **回调验签桩**:`CallbackAdProvider`(provider=callback)已实现服务端回调的完整形态——MD5 验签骨架、`ad.callback.sign-secret` 密钥开关、`ad.callback.sandbox-passthrough` 桩直通开关,以及 P2-2 安全红线(空密钥默认拒绝,防 `@PermitAll` 公网端点被伪造回调铸币),配 7 个单测。
- **入站端点与计费**:`/app-api/ad/reward/callback`(`@PermitAll` + IP 限频),`reportRewardCallback` 走「取广告位→验签→按 reward 计费」,计费幂等键 `uk_trace(trace_id, event_type, tenant)`
- **广告位契约**:`provider` 枚举已含 csj/gdt/mock/callback;`providerSlotId`(联盟真实广告位 ID)provider≠mock 时必填、admin 录入、**恒不下发前端**(前端不持有联盟广告位 ID,由服务端代理)。
因此真实联盟与 mock 的差别,收敛到一个 provider 实现类里的**验签算法**与**收益口径**;端点、幂等、归因、限频、安全红线全部共用。
### 1.2 边界(四个接入面的归属)
| 接入面 | 归属 | 本单 |
|---|---|---|
| 端侧展示广告(showRewarded/showInterstitial,拉起联盟 SDK) | game-studio + WanxiangGameSDK | 否(另轨,切真时需协同) |
| 服务端激励回调验签计费(reward/callback) | ad 模块 | **是 · 核心** |
| 平台↔联盟收益对账(真实 eCPM/结算额) | ad 模块 provider | **是 · 设计,下证落地** |
| 提现打款渠道(payout) | trade 模块 | 否(与广告切真是两个独立开关) |
**不做**:不写联盟真实签名算法(强依赖官方文档,提前写即编造);不动 mock 路径任何代码;不接端侧 SDK。
## 2 切换开关设计
### 2.1 三层开关矩阵
| 层 | 开关 | mock 现状(默认) | 切真取值 | 落点 |
|---|---|---|---|---|
| 代码 | 真实 provider 实现类 | 仅 mock/callback 注入 | 注入 CsjAdProvider/GdtAdProvider | 新增类(下证当天,以 CallbackAdProvider 为模板) |
| 配置 | 验签密钥(per-provider) | 空 / sandbox-passthrough=false | 配 `ad.{csj,gdt}.sign-secret` 真实密钥 | Nacos,双人复核 + 审计 |
| 配置 | `ad.force-mock` 全局熔断 | false | 保持 false;止血时置 true | Nacos(本单新增) |
| 数据 | 广告位 `provider` + `providerSlotId` | provider=mock | 改为 csj/gdt + 填联盟广告位 ID | admin CRUD,可按广告位灰度 |
数据层按广告位粒度,天然支持灰度:先把一个低流量广告位切 csj 验证,再逐位放开,不需要额外的全局灰度旋钮。
### 2.2 「下证当天可切」= 预置一切、当天只补一个类
下证当天唯一的代码增量,是照着 `CallbackAdProvider` 写出真实 provider 类、填入联盟签名算法。其余全是**已预置好的配置与数据操作**:配密钥、改广告位 provider 字段。这就是「藏 flag 后当天可切」的实指——flag(密钥项、熔断开关、provider 字段)在 R3 全部预埋,平时走 mock,当天翻开即真。
### 2.3 R3 落地的设计增量(不依赖联盟文档的部分)
- **per-provider 密钥命名空间**:现仅一个 `ad.callback.sign-secret`,真实需 csj/gdt 各自独立密钥。约定 `ad.<provider>.sign-secret`(下证前以空值占位登记进 Nacos,纳入配置基线),避免下证当天临时新造配置项。**具体密钥格式/字段名待官方文档核实**。
- **`ad.force-mock` 全局熔断**:`AdProviderFactory.get()` 前置一个 `@Value("${ad.force-mock:false}")` 判断,为 true 时对任意 provider 一律返回 `MockAdProvider`。默认 false 即当前行为,mock/callback 广告位不受影响;联盟故障或对账严重不平时,一个开关把全平台广告回退 mock 止血。
- **真实 provider 的 fail-safe 语义**:真实类在签名算法填入前,`verifyRewardCallback` 必须默认返回 false(继承 P2-2 精神),绝不因「类已注入但算法未就绪」静默放行铸币。
- **收益口径先锁毛/净**:`revenue_amount`(分)在 mock 与真实之间口径必须一致(变现端到端 §6 红线)。R3 锁定其为**毛额**;真实收益以联盟为准,回调若未携带实际金额则先按 `ecpmFloor` 估算入账、T+1 对账时以联盟账单冲正差额。**联盟是否在回调中回传实际金额待官方文档核实**。
## 3 失败模式(红线:外部交互必处理超时 / 重试 / 幂等 / 验签)
| 失败模式 | 已落地 | R3 补 |
|---|---|---|
| 回调超时 / 丢失 | 平台被动接收、快速响应;联盟侧超时自行重推,`uk_trace` 保证重推安全 | 回调端点验签与重活解耦、快速回执防联盟误判超时;回调彻底丢失的兜底 = 平台主动对账拉取补记(依赖联盟查询接口,待文档) |
| 验签失败 | `AD_REWARD_CALLBACK_SIGN_INVALID`(1-111-002-004),不计费不发奖 + warn 日志 | 失败计数告警(密钥错配 / 疑似伪造);启用 `timestamp` 时间窗口防重放(窗口大小待联盟重试间隔文档) |
| 重复回调幂等 | `uk_trace(trace_id, event_type)` 去重,重复只计一次、只发奖一次 | 沿用,无需新增 |
| 对账不平 | ad↔trade 有 `source_ref` + `trace_id` 锚点链 | 新增 ad↔联盟对账面:平台估算入账 vs 联盟实际结算,T+1 差异检测 + 告警 + 以联盟账单为准的冲正口径 |
平台对联盟回调是**被动入站**,超时与重试的主动方是联盟;平台的责任是幂等接收 + 快速回执 + 验签。真正的平台主动外呼(拉联盟收益报表对账)才需自己处理超时 / 重试 / 降级,那部分随对账接口一并接入。
## 4 切换 runbook 骨架
**前置(R3 预置,下证前完成)**:per-provider 密钥项空值登记进 Nacos;`ad.force-mock` 开关登记(默认 false);对账 job 骨架 + 差异告警;切换 checklist;单测加固(见 §5)。
**下证当天(D-day)**:
1. 照 `CallbackAdProvider` 写真实 provider 类、填联盟签名算法 + 单测,灰度发布(唯一代码增量)。
2. **双人复核**下配置:填 `ad.{csj,gdt}.sign-secret` 真实密钥,留配置审计(谁 / 何时 / 从什么改成什么)。
3. admin 选一个低流量广告位:`provider` 改 csj/gdt + 填 `providerSlotId`(灰度起点)。
4. 端侧 SDK 灰度接入该广告位(端侧另轨,需协同)。
5. 验证:真回调验签通过→计费落台账→T+1 对账平;失败率、验签失败告警在阈内。
6. 全量:逐位把其余广告位切真。
7. **回滚**:异常时 admin 把广告位 `provider` 改回 mock/callback;大面积异常直接 `ad.force-mock=true` 全局熔断。
双人复核 + 配置审计贯穿步骤 2、3,把变现端到端 §6 那条「待落地」的操作红线,落成这份可执行清单。
## 5 验证
- **mock 零回归(硬红线)**:provider=mock 路径代码零改动;现有 ad 模块单测(含 `CallbackAdProvider` 7 例)全绿;`ad.force-mock` 默认 false 时,工厂对 mock/callback 广告位的返回逐一比对切换前后一致。
- **新增单测**:`force-mock=true` 时任意 provider 返回 mock;per-provider 密钥装配正确;真实 provider fail-safe(算法未填时 `verifyRewardCallback` 返回 false);对账差异检测命中人造差额。
- **切换演练**:sandbox(provider=callback + sandbox-passthrough)跑通「回调→验签→计费→对账」全链,作为真实化前的形态验证;此演练不碰真钱、不需联盟资质。
## 6 待官方文档核实清单(不编造,下证时逐项落实)
- 穿山甲(csj)/ 优量汇(gdt)各自的服务端激励回调:端点方向、请求参数名(交易号 / 签名 / 时间戳 / nonce 等)、签名算法(md5 / sha256 / hmac)、密钥获取与格式、防重放窗口、重试与超时判定策略。
- 联盟实际收益 / eCPM 数据来源:是否随回调携带实际金额、是否需平台主动拉报表接口、结算口径(毛 / 净 / 税)、对账文件格式与周期。
- 端侧 SDK(game-studio 另轨):穿山甲 / 优量汇在目标运行环境的接入方式,以及 `providerSlotId` 在端侧取位与服务端代理之间的分工。