games-development-ai/docs/agent-specs/2026-07-02-M4广告SDK预接线-设计.md
lili 8b91f34b13
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
docs(design): SVG 尾波补 M4 广告预接线门面图 m4ad-00(三层切换开关+回调 fail-closed 铸币面防护)
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>
2026-07-05 23:44:33 -07:00

26 KiB
Raw Permalink Blame History

date, topic, status, sot-impact, 上级, 关联, 图清单
date topic status sot-impact 上级 关联 图清单
2026-07-02 M4 广告 SDK 预接线 回炉修订 · 双评审 BLOCKER/MAJOR 已吃(回调铸币面 fail-closed 代码已落 + 冲正/熔断/契约在档定死)· 待复评 不新建 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 一图看懂

图 · M4 广告 SDK 预接线:三层切换开关与回调 fail-closed 铸币面防护

门面图并置这条通路的两条主轴:公网回调链上三道 fail-closed 闸(force-mock 熔断 → getStrict 严格命中 → 验签)兜住铸币面,命门是「算钱可降级、验签不可降级」;三层切换开关(代码 / 配置 / 数据)默认全等于 mock、切真是叠加。数值与字段以下方 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.jsonprovider 枚举已含 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 广告位本就不接联盟服务端回调,堵死这条不影响它的曝光计费和前端直通发奖(verifyRewardcalcRevenue 不变),与"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 绕过窗口,签名照样通过。桩 CallbackAdProvidersign=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 全局熔断真实位回调。

双人复核 + 配置审计贯穿步骤 24,把变现端到端 §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 铸币面(代码已落):AdProviderFactorygetStrict(未注册返 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 闸、无新增真实计费断言、放量与停止阈值、回退演练。