--- 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..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 一图看懂 ![图 · M4 广告 SDK 预接线:三层切换开关与回调 fail-closed 铸币面防护](assets/m4ad-00-三层切换与铸币面防护.svg) > 门面图并置这条通路的两条主轴:公网回调链上三道 fail-closed 闸(force-mock 熔断 → getStrict 严格命中 → 验签)兜住铸币面,命门是「算钱可降级、验签不可降级」;三层切换开关(代码 / 配置 / 数据)默认全等于 mock、切真是叠加。数值与字段以下方 Mermaid 及后续各节为准。 ```mermaid flowchart TB subgraph 端侧["端侧 SDK 轨(game-studio,不在本单)"] SDK["WanxiangGameSDK · AdPlugin
showRewarded / showInterstitial
拉起联盟 SDK 展示广告"] end subgraph 服务端["服务端轨 · ad 模块(本单核心)"] CB["/app-api/ad/reward/callback
@PermitAll + IP 限频"] FUSE{"force-mock 熔断?
真实联盟位(非 mock/callback)"} STRICT["getStrict(provider)
严格命中,未注册返 null 即拒
绝不降级 mock"] V["provider.verifyRewardCallback
验签(密钥 per-provider)
mock 恒拒 · callback 空密钥拒"] BILL["bill():合规→uk_trace 幂等→归因→算收入→落台账"] REC["ad↔联盟对账
不估算入账,以联盟 T+1 账单
实际金额为唯一 trade 入账源"] end SDK -. 用户看完,联盟服务端回调 .-> CB CB --> FUSE FUSE -->|真实位 + 熔断| X["拒:不计费不发奖 + 告警
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..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..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..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 闸、无新增真实计费断言、放量与停止阈值、回退演练。