W-DSGN 尾波实际残项唯一 1 张(R3/M4 门面图),补齐并接入 M4 设计档 §0。 主控终审:qlmanage 亲眼渲染验证,修两处单行文本溢出(顶部状态条 tspan 超框 裁字「防重放」→ 精简正文他处已凸显的细节;页脚映射行冗余「状态」段与顶部 约定条重复 → 删段),两处修后重渲染确认完整落框。docs-gate 七检全绿。 作战清单点名的其余尾波图(plan①顶图/quality-01·02/rethink-00/cfgctl2-01~04) 经核对早已提交,无需重画。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
206 lines
26 KiB
Markdown
206 lines
26 KiB
Markdown
---
|
||
date: 2026-07-02
|
||
topic: M4 广告 SDK 预接线
|
||
status: 回炉修订 · 双评审 BLOCKER/MAJOR 已吃(回调铸币面 fail-closed 代码已落 + 冲正/熔断/契约在档定死)· 待复评
|
||
sot-impact: 不新建 canonical topic(R3 工单实现设计,归属既有 SoT「变现端到端」)。修订触点 = 变现端到端 §6(切换红线由「待落地」落成 runbook)、§8(切真接线点细化);落地新增 Nacos 配置约定(per-provider 密钥命名空间 + ad.force-mock 熔断 + ad.<provider>.callback-replay-window-seconds 防重放窗口);新增 ad 段错误码 1-111-002-005(AD_REWARD_CALLBACK_PROVIDER_NOT_READY);force-mock 语义 = 回调路径拒真实联盟位、不作用于 calcRevenue。契约漂移登记:ad-slot.schema.json 的 callback 枚举已于 R4(bc0f486b)补齐、基线已含;AdSlotSaveReqVO.provider 仍无 enum 强校验(R3 不补,理由见 §1.1)。不改 ad.yaml、不改 mock 计费路径、不改 trade 代码(冲正只做设计)。
|
||
上级: docs/mvp/可行性方案16周-波次映射.md
|
||
关联: game-cloud/game-module-ad/game-module-ad-server/.../framework/provider/{AdProvider,AdProviderFactory(get/getStrict),MockAdProvider,CallbackAdProvider,AdRewardCallbackContext}.java · service/revenue/AdRevenueServiceImpl.java:reportRewardCallback · controller/app/AdController.java:94-102 · game-module-ad-api/.../enums/ErrorCodeConstants.java(SIGN_INVALID / PROVIDER_NOT_READY) · contracts/api-schemas/ad.yaml · contracts/ad-slot.schema.json · docs/architecture/运营/变现端到端.md §6/§8 @ 4d220e77
|
||
图清单: [图0 三层切换与铸币面防护(SVG 门面图,assets/m4ad-00-三层切换与铸币面防护.svg,已补);图1 接入面边界与三层切换开关(mermaid)]
|
||
---
|
||
|
||
# M4 广告 SDK 预接线 · 设计(穿山甲 / 优量汇适配层切真通路)
|
||
|
||
## 0 一图看懂
|
||
|
||

|
||
|
||
> 门面图并置这条通路的两条主轴:公网回调链上三道 fail-closed 闸(force-mock 熔断 → getStrict 严格命中 → 验签)兜住铸币面,命门是「算钱可降级、验签不可降级」;三层切换开关(代码 / 配置 / 数据)默认全等于 mock、切真是叠加。数值与字段以下方 Mermaid 及后续各节为准。
|
||
|
||
```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 限频"]
|
||
FUSE{"force-mock 熔断?<br/>真实联盟位(非 mock/callback)"}
|
||
STRICT["getStrict(provider)<br/>严格命中,未注册返 null 即拒<br/>绝不降级 mock"]
|
||
V["provider.verifyRewardCallback<br/>验签(密钥 per-provider)<br/>mock 恒拒 · callback 空密钥拒"]
|
||
BILL["bill():合规→uk_trace 幂等→归因→算收入→落台账"]
|
||
REC["ad↔联盟对账<br/>不估算入账,以联盟 T+1 账单<br/>实际金额为唯一 trade 入账源"]
|
||
end
|
||
SDK -. 用户看完,联盟服务端回调 .-> CB
|
||
CB --> FUSE
|
||
FUSE -->|真实位 + 熔断| X["拒:不计费不发奖 + 告警<br/>PROVIDER_NOT_READY / SIGN_INVALID"]
|
||
FUSE -->|放行| STRICT
|
||
STRICT -->|null 未注册| X
|
||
STRICT -->|命中真实实现| V
|
||
V -->|通过| BILL
|
||
V -->|失败| X
|
||
BILL --> REC
|
||
classDef out fill:#eef,stroke:#88a;
|
||
classDef new fill:#fff7e6,stroke:#d90;
|
||
class 端侧 out; class REC,FUSE,STRICT new;
|
||
```
|
||
|
||
三层切换开关,默认值一律等于 mock 现状,切真是叠加、不改 mock 计费路径。**代码层**是注入真实 `CsjAdProvider`/`GdtAdProvider` 实现类;**配置层**是 Nacos 的 per-provider 验签密钥、`ad.force-mock` 熔断和防重放窗口;**数据层**是广告位 `game_ad_slot.provider` 字段加 `providerSlotId`。这三层里藏着一条容易被忽略的安全分界:算钱可以降级,验签不能降级。回调端点是 `@PermitAll` 的公网入口,验签是它唯一的安全凭据,所以任何一层"没切到位"时,回调都必须拒绝而不是放行——这条 fail-closed 原则贯穿全篇,是本次回炉的核心。
|
||
|
||
## 1 意图与边界
|
||
|
||
### 1.1 意图
|
||
|
||
把已经落地的广告联盟 SPI 骨架,补成一条从 mock 到真、一键可切、随时可回滚、切完能对账的通路,让穿山甲(csj)和优量汇(gdt)资质下证当天以最小动作切真而不动 mock。R3 是预接线:下证前能做的全部就位,把真正等官方文档的部分压到最小。
|
||
|
||
预接线并非从零起步。当前已就位、本单直接复用的骨架有四块:
|
||
|
||
- **SPI 与工厂**:`AdProvider` 定义 `getProvider`/`calcRevenue`/`verifyReward`/`verifyRewardCallback` 四个方法;`AdProviderFactory` 按 provider 路由。工厂有两条取实现的路径,语义不同,见 §2.2——展示计费用宽松的 `get`(未注册降级 mock),回调验签用严格的 `getStrict`(未注册返 null、由 Service 拒绝)。
|
||
- **回调验签桩**:`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)`。
|
||
- **广告位契约**:契约 #7 `ad-slot.schema.json` 的 `provider` 枚举已含 `csj/gdt/mock/callback`(R4 commit `bc0f486b` 补齐,本基线已含),`api-schemas/ad.yaml` 同步含 callback。`providerSlotId`(联盟真实广告位 ID)在 provider≠mock 时必填、admin 录入、恒不下发前端(前端不持有联盟广告位 ID,由服务端代理)。
|
||
|
||
有一处要如实说明:admin 入参 `AdSlotSaveReqVO.provider` 目前是 `@NotBlank String`,没有 enum 白名单强校验,所谓"枚举"只存在于契约 JSON Schema 和 ad.yaml,不在 Java 入参层。R3 不补这道 enum 校验,理由是回调路径已经对非法或未注册的 provider 做了 fail-closed:`getStrict` 命中不到就拒,伪造一个 `provider=csjj` 顶多让展示计费降级 mock 按 mock 口径算钱,不产生真实结算、不构成铸币面。也就是说 enum 强校验是防呆、不是防铸币,列为 follow-up 不阻断切真。
|
||
|
||
真实联盟与 mock 的差别,在**验签算法**这一维上确实收敛到一个 provider 实现类里;端点、幂等、归因、限频、安全红线全部共用。但**收益口径的对账**是另一回事——它牵涉 trade 侧结算,不在 provider 类内闭合,这一段单独在 §3 定死。
|
||
|
||
### 1.2 边界(四个接入面的归属)
|
||
|
||
| 接入面 | 归属 | 本单 |
|
||
|---|---|---|
|
||
| 端侧展示广告(showRewarded/showInterstitial,拉起联盟 SDK) | game-studio + WanxiangGameSDK | 否(另轨,切真时需协同) |
|
||
| 服务端激励回调验签计费(reward/callback) | ad 模块 | **是 · 核心** |
|
||
| 平台↔联盟收益对账(真实 eCPM/结算额) | ad 模块 provider + trade 结算 | **是 · 设计,下证落地** |
|
||
| 提现打款渠道(payout) | trade 模块 | 否(与广告切真是两个独立开关) |
|
||
|
||
不做的事:不写联盟真实签名算法(强依赖官方文档,提前写就是编造);不动 mock 计费路径任何代码;不接端侧 SDK;不改 trade 代码(冲正模型在 §3 定死,落地随 trade 结算线另单)。
|
||
|
||
## 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(本单新增) |
|
||
| 配置 | `ad.<provider>.callback-replay-window-seconds` 防重放窗口 | 预置默认值 | 按联盟重试间隔校准 | Nacos(本单新增,见 §3) |
|
||
| 数据 | 广告位 `provider` + `providerSlotId` | provider=mock | 改为 csj/gdt + 填联盟广告位 ID | admin CRUD,可按广告位灰度 |
|
||
|
||
数据层按广告位粒度,天然支持灰度:先把一个低流量广告位切 csj 验证,再逐位放开,不需要额外的全局灰度旋钮。
|
||
|
||
### 2.2 两种降级不是一回事:算钱可降级,验签不可降级(B1 铸币面)
|
||
|
||
工厂原本只有一个 `get(provider)`:未注册就降级到 mock。这个降级用在**展示计费**(`calcRevenue`)上是对的——一个还没切真的广告位来了曝光,按 mock 口径算个钱落台账,既不阻断用户看广告,也不产生任何真实结算,是宽松且可接受的。
|
||
|
||
但同一个降级套到**回调验签**(`verifyRewardCallback`)上就是灾难。回调端点公网可达,验签是唯一凭据。如果 `provider=csj` 而真实类还没注入,`get("csj")` 降级到 `MockAdProvider`,而 mock 若不覆写 `verifyRewardCallback` 就会继承接口 default、回退到恒真的 `verifyReward()`——于是任何人对着这个公网端点伪造一条回调,验签直通,计费落台账、T+1 真结算。这就是双评审判定回炉的铸币面。它有三条触发路径:数据层领先代码层(admin 把广告位改成 csj 而真实类未注入)、mock 广告位的公网回调被伪造、以及 `force-mock` 熔断把所有真实位一起降级 mock 时铸币面反而全开。
|
||
|
||
回炉后的做法是把两种降级彻底分开,并让验签这条 fail-closed:
|
||
|
||
1. **工厂增设 `getStrict(provider)`**:精确命中,未注册返回 `null`,绝不降级 mock。回调路径只用 `getStrict`;展示计费继续用 `get`。二者语义相反,注释和方法名都写死这条分界。
|
||
2. **Service 层 fail-closed**:`reportRewardCallback` 拿到 `null` 就抛 `AD_REWARD_CALLBACK_PROVIDER_NOT_READY`(新增错误码 1-111-002-005),不计费不发奖、留告警日志。
|
||
3. **`MockAdProvider` 覆写 `verifyRewardCallback` 返回 false**:mock 广告位本就不接联盟服务端回调,堵死这条不影响它的曝光计费和前端直通发奖(`verifyReward` 与 `calcRevenue` 不变),与"mock 零回归"前提不冲突。这是纵深防御的第一层——即便某条路径把 mock 当成了验签实现,也拒。
|
||
4. **回调铸币面全组合单测**:`provider∈{mock, callback(空密钥/直通), csj(未注册)} × force-mock{off,on}` 的行为矩阵,断言任何组合都打不开铸币面,已随代码落地(见 §5)。
|
||
|
||
### 2.3 `ad.force-mock` 熔断:只拒真实位回调,不碰验签之外的东西
|
||
|
||
`ad.force-mock` 是联盟故障或对账严重不平时的止血开关。它的语义在回炉后收窄并写死,避免重蹈"熔断放大铸币面"的覆辙:
|
||
|
||
- **force-mock=true 时,回调路径拒绝一切真实联盟位(即 provider 不属于内置桩集合 `{mock, callback}`)的回调**,抛 `PROVIDER_NOT_READY`、不计费不发奖。这才是止血的本义——熔断期干脆不接可能有问题的联盟回调,而不是"照收但换个口径算钱"。
|
||
- **熔断不作用于展示计费(`calcRevenue`)**。展示不涉铸币;真实位在未注册时的展示计费本来就走 `get` 降级 mock 算钱,是现行为,force-mock 不需要、也不去改它。
|
||
- **内置桩位(mock/callback)不受熔断影响**,继续走各自的验签语义:mock 恒拒回调,callback 按 secret 严格校验或空密钥默认拒。桩位不涉真实联盟结算,不是止血对象。
|
||
|
||
这条语义和代码一致:验签路径先过熔断判定,再走 `getStrict`,`force-mock` 永远不会把请求引到降级 mock 上。即便熔断开关因为热刷新未生效(见 §2.4)一时没能置上,铸币面也已经由 `getStrict` 的 fail-closed 和 mock/callback 各自的默认拒兜住——`force-mock` 是额外的主动熔断,不是唯一防线。
|
||
|
||
### 2.4 熔断与密钥的热生效:@RefreshScope 方案与当前的重启兜底(M1)
|
||
|
||
`ad.force-mock` 和各 `ad.*.sign-secret` 目前都用普通 `@Value` 承载。普通 `@Value` 在 Bean 初始化时一次性绑定,Nacos 改了配置不会自动刷新,要重启进程才生效。这与"一键实时熔断"和"配密钥即切真"的预期有落差:熔断要快,重启慢且有损;切密钥若不重启,验签仍读旧的空值。
|
||
|
||
按"生产稳定优先于开发期简单"的原则,正确方向是给这两类配置的载体 Bean 加 `@RefreshScope`(或 `@NacosValue(autoRefreshed=true)`),让 Nacos 推送后热生效。落地时倾向把 `force-mock` 与 per-provider 密钥抽到一个独立的 `@RefreshScope` 配置 Bean,而不是直接给核心计费 Service 加注解——`@RefreshScope` 会把 Bean 变成每次刷新重建的代理,加在计费 Service 这种带事务和多依赖注入的核心 Bean 上有未验证的代理副作用,独立配置 Bean 更干净、影响面可控。
|
||
|
||
这一步在 R3 标记为 follow-up,原因是本机没有 Nacos 环境,热刷新是否真的生效验证不了,不能靠"加了注解"就宣称可用。在热刷新落地并验证之前,runbook 按"改 force-mock 或密钥 = 滚动重启"操作(见 §4),并明确重启窗口的资损敞口:滚动重启期间新旧实例并存,旧实例仍读旧配置,若正处在"刚配好真实密钥、想立刻切真"的窗口,旧实例会以旧空密钥拒掉真实回调(fail-closed,不铸币、只是丢几条真实发奖),不会反向铸币。这个敞口方向安全,可接受。
|
||
|
||
## 3 失败模式(红线:外部交互必处理超时 / 重试 / 幂等 / 验签)
|
||
|
||
| 失败模式 | 已落地 | R3 补 |
|
||
|---|---|---|
|
||
| 回调超时 / 丢失 | 平台被动接收、快速响应;联盟侧超时自行重推,`uk_trace` 保证重推安全 | 回调端点验签与重活解耦、快速回执防联盟误判超时;回调彻底丢失的兜底 = 平台主动对账拉取补记(依赖联盟查询接口,待文档) |
|
||
| 验签失败 | `AD_REWARD_CALLBACK_SIGN_INVALID`(1-111-002-004),不计费不发奖 + warn 日志 | provider 未就绪单列 `AD_REWARD_CALLBACK_PROVIDER_NOT_READY`(1-111-002-005),便于告警区分"签名错"与"provider 未注册/熔断";失败计数告警;防重放窗口见下 |
|
||
| 重复回调幂等 | `uk_trace(trace_id, event_type)` 去重,重复只计一次、只发奖一次 | 沿用;与防重放窗口分工见下 |
|
||
| 对账不平 | ad↔trade 有 `source_ref` + `trace_id` 锚点链 | 冲正模型定死(见下):不估算入账,以联盟账单实际金额为唯一 trade 入账源 |
|
||
|
||
平台对联盟回调是被动入站,超时与重试的主动方是联盟;平台的责任是幂等接收、快速回执、验签。真正的平台主动外呼(拉联盟收益报表对账)才需要自己处理超时、重试、降级,那部分随对账接口一并接入。
|
||
|
||
### 3.1 防重放:窗口配置项预置(不编造联盟数值,但预置项目侧框架)
|
||
|
||
`AdRewardCallbackContext` 已带 `timestamp` 字段,但当前验签没用它。R3 预置一个项目侧的防重放窗口配置,框架先立住,联盟侧的具体数值下证时再校准:
|
||
|
||
- **配置项**:`ad.<provider>.callback-replay-window-seconds`,owner = ad 模块(WS5)。
|
||
- **默认值 300 秒、上限 900 秒**:默认给一个不至于误杀联盟正常重推的宽度,上限封住"配得过大等于没防"。这两个数是项目侧的保守缺省,联盟真实重试间隔以官方文档为准校准。
|
||
- **timestamp 缺失的 fail-safe**:真实 provider 要求回调必带 `timestamp`,缺失即拒(视为不合规回调);桩 callback 的 timestamp 可选(sandbox 自测无联盟时间戳)。这条写进真实 provider 的验签实现里,默认从严。
|
||
|
||
要点是三道防线分工,不能相互替代:`uk_trace` 防的是同一 `trace_id` 的重复计费(重推安全);防重放窗口防的是陈旧报文重放,并把服务端需要永久去重的窗口收窄到有限时间;而 `timestamp` 只有被纳入联盟 `sign` 才真正防篡改——否则攻击者改了 timestamp 绕过窗口,签名照样通过。桩 `CallbackAdProvider` 的 `sign=md5(traceId|transId|secret)` 目前不含 timestamp,真实联盟 sign 的组成(是否含 timestamp/nonce)是 §5 待核实项,直接决定 timestamp 防重放到底成不成立。
|
||
|
||
### 3.2 冲正模型定死:不估算入账,以联盟账单为唯一入账源(B2)
|
||
|
||
初稿写"回调若未携带实际金额则先按 `ecpmFloor` 估算入账、T+1 对账时以联盟账单冲正差额",双评审判为孤儿设计——冲正差额无处落账。根因在于 trade 侧入账是 add-only 的:`recordIncome` 幂等键 `uk_source(source, source_ref=ad_revenue.id)`,同一 `source_ref` 入过账再来就 `return false`。于是估算入账之后想冲正,复用同键会被幂等挡死;另造一笔负向分录走 `addIncome(负值)`,而 `AccountMapper.addIncome` 没有充足校验,会把 `balance` 扣成负、`total_income` 下调,可钱可能已经 `freeze` 甚至提现,账户恒等式 `balance + frozen + total_withdraw = total_income` 左边就大于右边,账破了;若改走带充足校验的扣减,余额不足直接抛错回滚,冲正卡死。三条路都撞资损或破账红线。
|
||
|
||
回炉采评审首选,从根上消除冲正:**回调不带实际金额时不估算入账,联盟 T+1 账单的实际金额是唯一的 trade 入账源。**具体地:
|
||
|
||
- 回调验签通过后,ad 侧照常落 `game_ad_revenue` 台账、记录发奖这件事的发生(`uk_trace` 幂等、归因照旧),但当 provider 是真实联盟且回调不含实际金额时,这笔的 `revenue_amount` 是"待联盟账单回填"状态,**不驱动 trade 入账**(结算 job 跳过"金额待定"的笔)。
|
||
- T+1 联盟对账单到账,用实际金额回填 `revenue_amount`,这时才驱动 trade 一次性入账,`source_ref` 仍是这条 `ad_revenue.id`。因为每个 `source_ref` 只入账一次、且入的就是实际金额,`uk_source` 的 add-only 幂等天然成立,恒等式的 `total_income` 从不被事后下调,不存在"钱已提现却要回收"的场景。
|
||
- mock 与 callback 桩不受影响:它们的 `calcRevenue` 给的是确定值(`ecpm/1000`)、回调即带确定金额,照现状即时入账,没有"金额待定"。所以这条只对真实联盟的"回调不带金额"生效,mock 计费路径零回归。
|
||
|
||
`revenue_amount` 的口径仍锁毛额(变现端到端 §6 红线),回填的是联盟结算的毛额;分账与回标沿用现有 `settle_status 0→1` 与 creator 聚合链路,只是入账时点从"回调即入"推迟到"账单回填后入"。对账查询以 `game_ad_revenue` 的"待回填 / 已回填 / 已入账"三态 + `trace_id`/`source_ref` 锚点链为准。
|
||
|
||
这一段是设计,落地牵涉 ad 结算 job 与 trade 入账触发时点的改动,随对账接口和 trade 结算线另单实现,本单不动 trade 代码。
|
||
|
||
## 4 切换 runbook 骨架
|
||
|
||
**前置(R3 预置,下证前完成)**:per-provider 密钥项空值登记进 Nacos;`ad.force-mock` 开关登记(默认 false);`ad.<provider>.callback-replay-window-seconds` 登记(默认 300);对账 job 骨架 + 差异告警;切换 checklist;回调铸币面 fail-closed 单测已随代码落地(见 §5)。
|
||
|
||
**切前演练(不碰真钱、不需联盟资质,证明开关和防线可用)**:
|
||
|
||
1. **canary provider 往返**:选一个 canary 广告位,provider 从 mock 改 callback、再改回 mock,确认改到"未注册的真实位名"(如 `csj`)时回调被 `PROVIDER_NOT_READY` 拒,而不是降级 mock 放行——这是 fail-closed 的现场验证。
|
||
2. **force-mock 开关往返**:`false → true → false`,确认置 true 时真实位回调被熔断拒、mock/callback 位不受影响,置回 false 后恢复。
|
||
3. **热刷新 / 重启确认**:在当前普通 `@Value` 下,确认改 force-mock 或密钥后按滚动重启生效(热刷新 follow-up 落地后改为确认热刷新已生效)。
|
||
4. **无新增真实计费断言**:演练全程不产生真实联盟台账(全走 mock/callback 桩),核对 `game_ad_revenue` 无真实 provider 的入账。
|
||
|
||
**下证当天(D-day)**:
|
||
|
||
1. 照 `CallbackAdProvider` 写真实 provider 类、填联盟签名算法与防重放 fail-safe + 单测,灰度发布(唯一代码增量)。真实类的 `verifyRewardCallback` 在签名算法填入前默认返回 false(继承 P2-2 精神),绝不因"类已注入但算法未就绪"静默放行。
|
||
2. **双人复核**下配置:填 `ad.{csj,gdt}.sign-secret` 真实密钥,留配置审计(谁、何时、从什么改成什么)。改配置后按 §2.4 确认生效(当前 = 滚动重启)。
|
||
3. **切 provider 前的 fail-closed 闸**:admin 改广告位 provider 之前,先确认工厂确实命中了真实类、而不是降级 mock(切前演练第 1 步的现场复核),命中真实类才继续。
|
||
4. admin 选一个低流量广告位:`provider` 改 csj/gdt + 填 `providerSlotId`(灰度起点)。
|
||
5. 端侧 SDK 灰度接入该广告位(端侧另轨,需协同)。
|
||
6. 验证:真回调验签通过 → 落台账(待账单回填,不即时入账)→ T+1 账单回填后 trade 入账、对账平;验签失败率、`PROVIDER_NOT_READY` 计数、告警在阈内。
|
||
7. **放量与停止阈值**:逐位放开;验签失败率或对账差异超过预设阈值即停止放量。异常升级时的回退:admin 把广告位 `provider` 改回 mock/callback;大面积异常直接 `ad.force-mock=true` 全局熔断真实位回调。
|
||
|
||
双人复核 + 配置审计贯穿步骤 2–4,把变现端到端 §6 那条"待落地"的操作红线,落成这份可执行清单。
|
||
|
||
## 5 验证
|
||
|
||
- **mock 零回归(硬红线)**:provider=mock 计费路径代码零改动;现有 ad 模块单测全绿(`CallbackAdProviderTest` 7 例、`AdSlotServiceImplTest` 6 例未改仍绿,`AdRevenueServiceImplTest` 原有计费/幂等/合规用例仍绿);`ad.force-mock` 默认 false 时,mock/callback 广告位的回调与计费行为与改造前一致。
|
||
- **回调铸币面 fail-closed 矩阵(已落地)**:`provider∈{mock, callback(空密钥/直通), csj(未注册)} × force-mock{off,on}` 的回调行为矩阵——mock 位恒拒(`SIGN_INVALID`)、callback 空密钥默认拒、csj 未注册拒(`PROVIDER_NOT_READY`)、force-mock 熔断拒真实位;并各配一格正路径(callback 显式直通 + force-mock 两态)证明改造不误伤桩位正常计费。合计 33 个 ad 模块单测全绿。
|
||
- **真实 provider fail-safe 单测的时序**:"真实类签名算法未填时 `verifyRewardCallback` 返回 false"这条单测在 R3 预置期没有对象(真实类 D-day 才写),属正常时序错位,随 D-day 步骤 1 的真实类单测一起补,不在 R3 清单里勾选。
|
||
- **切换演练**:sandbox(provider=callback + sandbox-passthrough)跑通「回调 → 验签 → 计费」全链,作为真实化前的形态验证;此演练不碰真钱、不需联盟资质。
|
||
|
||
## 6 待官方文档核实清单(不编造,下证时逐项落实)
|
||
|
||
- 穿山甲(csj)/ 优量汇(gdt)各自的服务端激励回调:端点方向、请求参数名(交易号 / 签名 / 时间戳 / nonce 等)、签名算法(md5 / sha256 / hmac)、密钥获取与格式、防重放窗口的联盟重试间隔、超时判定策略。
|
||
- **回调成功应答协议**:联盟判定"平台已成功接收"要求的 HTTP 状态码、响应 body、content-type;失败时联盟的重推规则与次数;据此才能把"快速回执防误判超时"实现成可验证的应答。
|
||
- **回调来源边界**:联盟回调的出口 IP / 网段白名单,或在无固定 IP 时的替代来源校验方式(验签是主凭据,来源白名单是纵深防御的补充)。
|
||
- **nonce 契约触点**:若联盟用 nonce 防重放,回调 VO(`AdRewardCallbackReqVO`)与 `ad.yaml` 需新增 nonce 字段——这是一个契约扩展点,下证时按联盟实际字段决定是否补,补则走 contract-first。
|
||
- 联盟实际收益 / eCPM 数据来源:是否随回调携带实际金额(直接决定 §3.2 是否触发"待账单回填")、是否需平台主动拉报表接口、结算口径(毛 / 净 / 税)、对账文件格式与周期。
|
||
- 端侧 SDK(game-studio 另轨):穿山甲 / 优量汇在目标运行环境的接入方式,以及 `providerSlotId` 在端侧取位与服务端代理之间的分工。
|
||
|
||
## 7 回炉修订记录(供复评核对)
|
||
|
||
本轮相对初稿的实质变更,以及已落代码的位置:
|
||
|
||
- **B1 铸币面(代码已落)**:`AdProviderFactory` 增 `getStrict`(未注册返 null、不降级);`MockAdProvider` 覆写 `verifyRewardCallback` 返 false;`AdRevenueServiceImpl.reportRewardCallback` 改用 `getStrict` 并加 `PROVIDER_NOT_READY` fail-closed;新增错误码 `AD_REWARD_CALLBACK_PROVIDER_NOT_READY`(1-111-002-005);展示计费的 `get` 降级路径保持不变。全组合矩阵单测 8 格随代码落地,ad 模块 33 单测全绿。
|
||
- **B2 冲正模型(档内定死)**:采首选"不估算入账、以联盟 T+1 账单实际金额为唯一 trade 入账源",消除冲正,论证了 `uk_source` add-only 幂等与账户恒等式为何成立;落地随 trade 结算线另单,本单不动 trade 代码。
|
||
- **M1 热刷新(方案入档 + 代码 follow-up)**:`@RefreshScope` 载体 Bean 方案写入 §2.4,本机无 Nacos 验证不了故标 follow-up,runbook 给滚动重启兜底与资损敞口(方向安全)。
|
||
- **M2 契约断言(改真话)**:§1.1 更正为 ad-slot.schema.json 的 callback 已于 R4 `bc0f486b` 补齐、基线已含;如实写明 `AdSlotSaveReqVO.provider` 无 enum 强校验且 R3 不补的理由。
|
||
- **force-mock 语义(收窄定死)**:§2.3 改为"回调拒真实位、不碰 calcRevenue、不影响内置桩",与代码一致,废弃初稿"工厂 get 全局回退 mock"的旧语义。
|
||
- **防重放 / ACK / 来源 IP(预置与补清单)**:§3.1 预置 `callback-replay-window-seconds` 的 owner/默认 300/上限 900/timestamp 缺失 fail-safe,并辨析 uk_trace 与 timestamp 分工;§6 补回调 ACK 协议、来源 IP 白名单、nonce 契约触点。
|
||
- **runbook(切前演练与放量阈值)**:§4 增 canary provider 往返、force-mock 往返、切前 fail-closed 闸、无新增真实计费断言、放量与停止阈值、回退演练。
|