diff --git a/docs/agent-specs/2026-07-02-M4广告SDK预接线-设计.md b/docs/agent-specs/2026-07-02-M4广告SDK预接线-设计.md index fe6d5be0..41d49082 100644 --- a/docs/agent-specs/2026-07-02-M4广告SDK预接线-设计.md +++ b/docs/agent-specs/2026-07-02-M4广告SDK预接线-设计.md @@ -5,13 +5,17 @@ status: 回炉修订 · 双评审 BLOCKER/MAJOR 已吃(回调铸币面 fail-clos 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 -图清单: [图1 接入面边界与三层切换开关(mermaid);SVG 门面图收口时按 atlas 补] +图清单: [图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,不在本单)"] diff --git a/docs/agent-specs/assets/m4ad-00-三层切换与铸币面防护.svg b/docs/agent-specs/assets/m4ad-00-三层切换与铸币面防护.svg new file mode 100644 index 00000000..ebae43a7 --- /dev/null +++ b/docs/agent-specs/assets/m4ad-00-三层切换与铸币面防护.svg @@ -0,0 +1,205 @@ + + + + + + + + + + + 图 · M4 广告 SDK 预接线:三层切换开关 + 回调 fail-closed 铸币面防护 + 把已落地的联盟 SPI 骨架,补成一条从 mock 到真、一键可切、随时可回滚、切完能对账的通路(R3 预接线)。全篇命门——算钱可降级,验签不可降级。 + + + + 整图状态(三态)——绿实线 = 代码已落地(getStrict fail-closed / MockAdProvider 覆写回调 / ad 模块 33 单测全绿) · 蓝实线 = 设计定死待落地(冲正模型 / @RefreshScope 热刷新,随 trade 与 Nacos 另单) · 紫虚线 = 待官方文档下证(联盟签名算法 / ACK / 防重放)。 + 三层切换开关的默认值一律等于 mock 现状,切真是叠加、不改 mock 计费路径(mock 零回归是硬红线)。回调端点是 @PermitAll 公网入口,验签是它唯一的安全凭据。 + 徽章:红 = fail-closed / 拒发 / 铸币面防护 · 绿 = 可降级(算钱)/ 已落地 · 深灰 = 已有骨架(直接复用)。 + + + 四个接入面的边界(本单只碰服务端回调验签计费一面,其余各归其轨) + + 服务端回调验签计费 + ad 模块 · 本单核心(reward/callback) + + 平台 ↔ 联盟收益对账 + ad provider + trade · 设计定死、下证落地 + + 端侧展示广告 + game-studio + WanxiangGameSDK · 另轨,切真需协同 + + 提现打款渠道(payout) + trade 模块 · 另轨,与广告切真是两个独立开关 + + + 回调请求流:公网入站 → 三道 fail-closed 闸 → 计费落台账(任一闸不过即拒,不计费不发奖) + + + + 端侧 SDK 轨 + AdPlugin 拉起 + 联盟 SDK 展示 + 激励广告 + (不在本单) + + + + 回调入站端点 + /app-api/ad/reward/callback + @PermitAll + IP 限频 + 被动接收、快速回执 + reportRewardCallback + + + + fail-closed 闸链 · 铸币面唯一防线(验签路径先熔断、再严格命中、后验签) + + 闸① force-mock 熔断 + 止血:真实联盟位回调 + 全拒(不碰 calcRevenue) + 桩位 mock/callback 不受影响 + 默认 false,不平时置 true + + 闸② getStrict + 精确命中,未注册返 null + 绝不降级 mock + 回调路径只用 getStrict + null → Service 层拒 + + 闸③ 验签 + verifyRewardCallback + 密钥 per-provider + mock 恒拒 · callback + 空密钥默认拒 + 防重放窗口 + + + + 计费 bill(过闸才走) + ① 合规校验 + ② uk_trace 幂等去重 + (trace_id, event_type, tenant) + ③ 归因 → 算收入 → 落台账 + + + + ad ↔ 联盟对账 + 不估算入账;以联盟 + T+1 账单实际金额为 + 唯一 trade 入账源 + (冲正模型见下 · B2) + + + + + + + + + + + 任一闸不过 → 拒:不计费、不发奖、留告警日志 + PROVIDER_NOT_READY 1-111-002-005(未注册/熔断) · SIGN_INVALID 1-111-002-004(验签失败) + + + 命门:两种降级不是一回事——同一个工厂,取实现的两条路径语义相反 + + + + 展示计费 · 可降级 + 宽松 + calcRevenue → 工厂 get(provider) → 未注册降级 MockAdProvider + 还没切真的广告位来了曝光,按 mock 口径算个钱落台账。 + 既不阻断用户看广告,也不产生任何真实结算 —— 宽松且可接受。 + force-mock 熔断不作用于展示计费,这条现行为保持不变。 + + + + 命门 + 算钱可降 + 验签不可降 + 公网端点 + 验签唯一凭据 + + + + 回调验签 · 不可降级(fail-closed) + 代码已落地 + verifyRewardCallback → 工厂 getStrict → 未注册返 null → Service 拒 + 若用 get 降级 mock、mock 又不覆写回调,就回退到恒真验签 —— + 任何人对公网端点伪造回调即直通计费、T+1 真结算 = 铸币面。 + 回炉四防护:getStrict 拒 + Service fail-closed + MockAdProvider 覆写返 false + 全组合矩阵单测。 + + + + 铸币面三条触发路径(全被上面四防护堵死): + ① 数据层领先代码层(admin 把广告位改 csj 而真实类未注入) · ② mock 广告位的公网回调被伪造 · ③ force-mock 熔断把真实位一起降级 mock 时铸币面反而全开。 + enum 强校验(AdSlotSaveReqVO.provider 无白名单)是防呆、不是防铸币 —— getStrict 已 fail-closed 兜住,故列 follow-up、不阻断切真。 + + + 三层切换开关:默认值一律 = mock 现状,切真是叠加(数据层按广告位粒度,天然灰度) + + + + 层 + 开关 + 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(本单新增) + + + 配置 + callback-replay-window-seconds + 预置默认 300 秒(上限 900) + 按联盟重试间隔校准(下证时) + Nacos(本单新增,owner = ad) + + + 数据 + 广告位 provider + providerSlotId + provider=mock + 改 csj/gdt + 填联盟广告位 ID + admin CRUD,可按广告位灰度 + + + + + 冲正模型(B2 · 设计定死,随 trade 线落地) + 回调不带金额时不估算入账 —— 联盟 T+1 账单实际金额是唯一 trade 入账源。 + revenue_amount 三态:待回填 → 已回填 → 已入账;uk_source add-only 幂等成立,账户恒等式不破,消除冲正。 + + + + 切换 runbook 三阶段(双人复核 + 配置审计贯穿) + 预置(R3 下证前:Nacos 空值登记 + 对账骨架 + 铸币面单测)→ 切前演练(canary 往返 / force-mock 往返,不碰真钱) + → D-day(写真实类灰度发布 + 双人下密钥 + 逐位放开);超阈即停,回退 = 改回 mock 或 force-mock=true 熔断。 + + + 映射:docs/agent-specs/2026-07-02-M4广告SDK预接线-设计.md(§0 一图看懂 / §1.2 接入面边界 / §2 三层开关与两种降级 / §3.2 冲正模型 / §4 runbook)|三态状态见上方约定条|设计变动须同步本图 +