docs(design): SVG 尾波补 M4 广告预接线门面图 m4ad-00(三层切换开关+回调 fail-closed 铸币面防护)
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled

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>
This commit is contained in:
lili 2026-07-05 23:44:33 -07:00
parent c6d6d39a2a
commit 8b91f34b13
2 changed files with 210 additions and 1 deletions

View File

@ -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.<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 代码(冲正只做设计)。 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 上级: 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 关联: 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 预接线 · 设计(穿山甲 / 优量汇适配层切真通路) # M4 广告 SDK 预接线 · 设计(穿山甲 / 优量汇适配层切真通路)
## 0 一图看懂 ## 0 一图看懂
![图 · M4 广告 SDK 预接线:三层切换开关与回调 fail-closed 铸币面防护](assets/m4ad-00-三层切换与铸币面防护.svg)
> 门面图并置这条通路的两条主轴:公网回调链上三道 fail-closed 闸(force-mock 熔断 → getStrict 严格命中 → 验签)兜住铸币面,命门是「算钱可降级、验签不可降级」;三层切换开关(代码 / 配置 / 数据)默认全等于 mock、切真是叠加。数值与字段以下方 Mermaid 及后续各节为准。
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph 端侧["端侧 SDK 轨(game-studio,不在本单)"] subgraph 端侧["端侧 SDK 轨(game-studio,不在本单)"]

View File

@ -0,0 +1,205 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1480 998" font-family="-apple-system,'PingFang SC','Microsoft YaHei',Segoe UI,sans-serif">
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#334155"/></marker>
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#dc2626"/></marker>
<marker id="arrG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#16a34a"/></marker>
<style>
.t{fill:#0f172a}.mut{fill:#475569}.sm{font-size:12px}.xs{font-size:11px}.xxs{font-size:10px}
.h1{font-size:24px;font-weight:700}.lbl{font-size:13px;font-weight:600}.box{font-size:15px;font-weight:700;fill:#0f172a}
.bj{font-size:10px;font-weight:700;fill:#fff}
.mono{font-family:'SF Mono',Consolas,Menlo,monospace}
</style>
</defs>
<rect x="0" y="0" width="1480" height="998" fill="#f8fafc"/>
<!-- 标题 -->
<text x="40" y="38" class="t h1">图 · M4 广告 SDK 预接线:三层切换开关 + 回调 fail-closed 铸币面防护</text>
<text x="40" y="60" class="mut sm">把已落地的联盟 SPI 骨架,补成一条从 mock 到真、一键可切、随时可回滚、切完能对账的通路(R3 预接线)。全篇命门——算钱可降级,验签不可降级。</text>
<!-- 状态约定条(三态) -->
<rect x="40" y="72" width="1400" height="60" rx="9" fill="#f1f5f9" stroke="#64748b" stroke-width="1.3"/>
<text x="54" y="94" class="t xs">整图状态(三态)——<tspan fill="#15803d" font-weight="700">绿实线 = 代码已落地</tspan>(getStrict fail-closed / MockAdProvider 覆写回调 / ad 模块 33 单测全绿) · <tspan fill="#2563eb" font-weight="700">蓝实线 = 设计定死待落地</tspan>(冲正模型 / @RefreshScope 热刷新,随 trade 与 Nacos 另单) · <tspan fill="#7c3aed" font-weight="700">紫虚线 = 待官方文档下证</tspan>(联盟签名算法 / ACK / 防重放)。</text>
<text x="54" y="114" class="mut xxs">三层切换开关的默认值一律等于 mock 现状,切真是叠加、不改 mock 计费路径(mock 零回归是硬红线)。回调端点是 @PermitAll 公网入口,验签是它唯一的安全凭据。</text>
<text x="54" y="128" class="mut xxs">徽章:<tspan fill="#dc2626" font-weight="700">红 = fail-closed / 拒发 / 铸币面防护</tspan> · <tspan fill="#16a34a" font-weight="700">绿 = 可降级(算钱)/ 已落地</tspan> · <tspan fill="#334155" font-weight="700">深灰 = 已有骨架(直接复用)</tspan>。</text>
<!-- ===== 区0:四个接入面边界 ===== -->
<text x="40" y="156" class="t lbl">四个接入面的边界(本单只碰服务端回调验签计费一面,其余各归其轨)</text>
<rect x="40" y="164" width="340" height="52" rx="9" fill="#fef2f2" stroke="#dc2626" stroke-width="1.8"/>
<text x="56" y="184" class="t xs" font-weight="700">服务端回调验签计费</text>
<text x="56" y="202" class="mut xxs">ad 模块 · <tspan fill="#dc2626" font-weight="700">本单核心</tspan>(reward/callback)</text>
<rect x="392" y="164" width="340" height="52" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-width="1.6"/>
<text x="408" y="184" class="t xs" font-weight="700">平台 ↔ 联盟收益对账</text>
<text x="408" y="202" class="mut xxs">ad provider + trade · 设计定死、下证落地</text>
<rect x="744" y="164" width="340" height="52" rx="9" fill="#f8fafc" stroke="#64748b" stroke-width="1.5" stroke-dasharray="5 4"/>
<text x="760" y="184" class="t xs" font-weight="700">端侧展示广告</text>
<text x="760" y="202" class="mut xxs">game-studio + WanxiangGameSDK · 另轨,切真需协同</text>
<rect x="1096" y="164" width="344" height="52" rx="9" fill="#f8fafc" stroke="#64748b" stroke-width="1.5" stroke-dasharray="5 4"/>
<text x="1112" y="184" class="t xs" font-weight="700">提现打款渠道(payout)</text>
<text x="1112" y="202" class="mut xxs">trade 模块 · 另轨,与广告切真是两个独立开关</text>
<!-- ===== 区A:回调 fail-closed 闸链 ===== -->
<text x="40" y="242" class="t lbl">回调请求流:公网入站 → 三道 fail-closed 闸 → 计费落台账(任一闸不过即拒,不计费不发奖)</text>
<!-- 端侧 SDK -->
<rect x="40" y="252" width="150" height="120" rx="9" fill="#f8fafc" stroke="#64748b" stroke-width="1.5" stroke-dasharray="5 4"/>
<text x="115" y="280" text-anchor="middle" class="t xs" font-weight="700">端侧 SDK 轨</text>
<text x="115" y="300" text-anchor="middle" class="mut xxs">AdPlugin 拉起</text>
<text x="115" y="316" text-anchor="middle" class="mut xxs">联盟 SDK 展示</text>
<text x="115" y="332" text-anchor="middle" class="mut xxs">激励广告</text>
<text x="115" y="356" text-anchor="middle" class="mut xxs">(不在本单)</text>
<!-- 回调端点 -->
<rect x="210" y="252" width="210" height="120" rx="9" fill="#fff" stroke="#334155" stroke-width="1.5"/>
<text x="222" y="280" class="t xs" font-weight="700">回调入站端点</text>
<text x="222" y="300" class="mut xxs mono">/app-api/ad/reward/callback</text>
<text x="222" y="318" class="mut xxs">@PermitAll + IP 限频</text>
<text x="222" y="338" class="mut xxs">被动接收、快速回执</text>
<text x="222" y="356" class="mut xxs">reportRewardCallback</text>
<!-- fail-closed 闸组 -->
<rect x="440" y="252" width="520" height="120" rx="9" fill="#fff" stroke="#dc2626" stroke-width="2"/>
<text x="452" y="272" class="xs" font-weight="700" fill="#dc2626">fail-closed 闸链 · 铸币面唯一防线(验签路径先熔断、再严格命中、后验签)</text>
<rect x="452" y="280" width="160" height="84" rx="7" fill="#fef2f2" stroke="#dc2626" stroke-width="1.4"/>
<text x="462" y="298" class="xs" font-weight="700" fill="#dc2626">闸① force-mock 熔断</text>
<text x="462" y="315" class="mut xxs">止血:真实联盟位回调</text>
<text x="462" y="330" class="mut xxs">全拒(不碰 calcRevenue)</text>
<text x="462" y="345" class="mut xxs">桩位 mock/callback 不受影响</text>
<text x="462" y="360" class="mut xxs">默认 false,不平时置 true</text>
<rect x="622" y="280" width="160" height="84" rx="7" fill="#fef2f2" stroke="#dc2626" stroke-width="1.4"/>
<text x="632" y="298" class="xs" font-weight="700" fill="#dc2626">闸② <tspan class="mono">getStrict</tspan></text>
<text x="632" y="315" class="mut xxs">精确命中,未注册返 null</text>
<text x="632" y="330" class="mut xxs"><tspan fill="#dc2626" font-weight="700">绝不降级 mock</tspan></text>
<text x="632" y="345" class="mut xxs">回调路径只用 getStrict</text>
<text x="632" y="360" class="mut xxs">null → Service 层拒</text>
<rect x="792" y="280" width="160" height="84" rx="7" fill="#fef2f2" stroke="#dc2626" stroke-width="1.4"/>
<text x="802" y="298" class="xs" font-weight="700" fill="#dc2626">闸③ 验签</text>
<text x="802" y="315" class="mut xxs mono">verifyRewardCallback</text>
<text x="802" y="330" class="mut xxs">密钥 per-provider</text>
<text x="802" y="345" class="mut xxs">mock 恒拒 · callback</text>
<text x="802" y="360" class="mut xxs">空密钥默认拒 + 防重放窗口</text>
<!-- 计费 -->
<rect x="980" y="252" width="230" height="120" rx="9" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.6"/>
<text x="992" y="280" class="t xs" font-weight="700">计费 bill(过闸才走)</text>
<text x="992" y="300" class="mut xxs">① 合规校验</text>
<text x="992" y="318" class="mut xxs">② <tspan class="mono">uk_trace</tspan> 幂等去重</text>
<text x="992" y="336" class="mut xxs">(trace_id, event_type, tenant)</text>
<text x="992" y="354" class="mut xxs">③ 归因 → 算收入 → 落台账</text>
<!-- 对账 -->
<rect x="1230" y="252" width="210" height="120" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-width="1.6"/>
<text x="1242" y="280" class="t xs" font-weight="700">ad ↔ 联盟对账</text>
<text x="1242" y="300" class="mut xxs">不估算入账;以联盟</text>
<text x="1242" y="318" class="mut xxs">T+1 账单实际金额为</text>
<text x="1242" y="336" class="mut xxs">唯一 trade 入账源</text>
<text x="1242" y="356" class="mut xxs">(冲正模型见下 · B2)</text>
<!-- 链箭头 -->
<line x1="190" y1="312" x2="208" y2="312" stroke="#334155" stroke-width="1.8" marker-end="url(#arr)"/>
<line x1="420" y1="312" x2="438" y2="312" stroke="#334155" stroke-width="1.8" marker-end="url(#arr)"/>
<line x1="960" y1="312" x2="978" y2="312" stroke="#16a34a" stroke-width="1.8" marker-end="url(#arrG)"/>
<line x1="1210" y1="312" x2="1228" y2="312" stroke="#334155" stroke-width="1.8" marker-end="url(#arr)"/>
<!-- 拒发红条 -->
<line x1="700" y1="372" x2="700" y2="392" stroke="#dc2626" stroke-width="1.8" marker-end="url(#arrR)"/>
<rect x="440" y="394" width="520" height="40" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.6"/>
<text x="452" y="412" class="xs" font-weight="700" fill="#dc2626">任一闸不过 → 拒:不计费、不发奖、留告警日志</text>
<text x="452" y="428" class="mut xxs mono">PROVIDER_NOT_READY 1-111-002-005(未注册/熔断) · SIGN_INVALID 1-111-002-004(验签失败)</text>
<!-- ===== 区B:两种降级分界(命门) ===== -->
<text x="40" y="464" class="t lbl">命门:两种降级不是一回事——同一个工厂,取实现的两条路径语义相反</text>
<!-- 左:展示计费可降级 -->
<rect x="40" y="474" width="640" height="128" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.7"/>
<text x="56" y="500" class="box" fill="#15803d">展示计费 · 可降级</text>
<rect x="300" y="486" width="52" height="18" rx="5" fill="#16a34a"/><text x="326" y="499" class="bj" text-anchor="middle">宽松</text>
<text x="56" y="524" class="t sm"><tspan class="mono">calcRevenue</tspan> → 工厂 <tspan class="mono">get(provider)</tspan> → 未注册降级 <tspan class="mono">MockAdProvider</tspan></text>
<text x="56" y="548" class="mut xs">还没切真的广告位来了曝光,按 mock 口径算个钱落台账。</text>
<text x="56" y="568" class="mut xs">既不阻断用户看广告,也不产生任何真实结算 —— 宽松且可接受。</text>
<text x="56" y="590" class="mut xs">force-mock 熔断不作用于展示计费,这条现行为保持不变。</text>
<!-- 中:命门竖标 -->
<rect x="694" y="474" width="92" height="128" rx="10" fill="#fef2f2" stroke="#dc2626" stroke-width="1.8"/>
<text x="740" y="512" text-anchor="middle" class="box" fill="#dc2626">命门</text>
<text x="740" y="540" text-anchor="middle" class="xs" fill="#dc2626" font-weight="700">算钱可降</text>
<text x="740" y="558" text-anchor="middle" class="xs" fill="#dc2626" font-weight="700">验签不可降</text>
<text x="740" y="582" text-anchor="middle" class="xxs mut">公网端点</text>
<text x="740" y="596" text-anchor="middle" class="xxs mut">验签唯一凭据</text>
<!-- 右:回调验签不可降级 -->
<rect x="800" y="474" width="640" height="128" rx="10" fill="#fef2f2" stroke="#dc2626" stroke-width="2"/>
<text x="816" y="500" class="box" fill="#dc2626">回调验签 · 不可降级(fail-closed)</text>
<rect x="1300" y="486" width="88" height="18" rx="5" fill="#dc2626"/><text x="1344" y="499" class="bj" text-anchor="middle">代码已落地</text>
<text x="816" y="524" class="t sm"><tspan class="mono">verifyRewardCallback</tspan> → 工厂 <tspan class="mono">getStrict</tspan> → 未注册返 null → Service 拒</text>
<text x="816" y="548" class="mut xs">若用 get 降级 mock、mock 又不覆写回调,就回退到恒真验签 ——</text>
<text x="816" y="568" class="mut xs">任何人对公网端点伪造回调即直通计费、T+1 真结算 = 铸币面。</text>
<text x="816" y="590" class="mut xs">回炉四防护:getStrict 拒 + Service fail-closed + <tspan class="mono">MockAdProvider</tspan> 覆写返 false + 全组合矩阵单测。</text>
<!-- 铸币面三触发路径 注条 -->
<rect x="40" y="612" width="1400" height="42" rx="8" fill="#fffbeb" stroke="#b45309" stroke-width="1.5"/>
<text x="54" y="631" class="xs" font-weight="700" fill="#b45309">铸币面三条触发路径(全被上面四防护堵死):</text>
<text x="330" y="631" class="mut xs">① 数据层领先代码层(admin 把广告位改 csj 而真实类未注入) · ② mock 广告位的公网回调被伪造 · ③ force-mock 熔断把真实位一起降级 mock 时铸币面反而全开。</text>
<text x="54" y="648" class="mut xxs">enum 强校验(AdSlotSaveReqVO.provider 无白名单)是防呆、不是防铸币 —— getStrict 已 fail-closed 兜住,故列 follow-up、不阻断切真。</text>
<!-- ===== 区C:三层切换开关矩阵 ===== -->
<text x="40" y="682" class="t lbl">三层切换开关:默认值一律 = mock 现状,切真是叠加(数据层按广告位粒度,天然灰度)</text>
<rect x="40" y="690" width="1400" height="188" rx="9" fill="#fff" stroke="#334155" stroke-width="1.4"/>
<!-- 表头 -->
<rect x="40" y="690" width="1400" height="30" rx="9" fill="#f1f5f9"/>
<text x="56" y="710" class="xs" font-weight="700">层</text>
<text x="150" y="710" class="xs" font-weight="700">开关</text>
<text x="470" y="710" class="xs" font-weight="700">mock 现状(默认)</text>
<text x="760" y="710" class="xs" font-weight="700">切真取值</text>
<text x="1120" y="710" class="xs" font-weight="700">落点</text>
<line x1="40" y1="720" x2="1440" y2="720" stroke="#cbd5e1" stroke-width="1"/>
<!-- 行1 代码 -->
<rect x="52" y="728" width="42" height="18" rx="5" fill="#334155"/><text x="73" y="741" class="bj" text-anchor="middle">代码</text>
<text x="150" y="741" class="xs">真实 provider 实现类</text>
<text x="470" y="741" class="mut xs">仅 mock/callback 注入</text>
<text x="760" y="741" class="mut xs">注入 CsjAdProvider / GdtAdProvider</text>
<text x="1120" y="741" class="mut xs">下证当天,以 CallbackAdProvider 为模板</text>
<line x1="40" y1="752" x2="1440" y2="752" stroke="#e2e8f0" stroke-width="1"/>
<!-- 行2 配置密钥 -->
<rect x="52" y="760" width="42" height="18" rx="5" fill="#7c3aed"/><text x="73" y="773" class="bj" text-anchor="middle">配置</text>
<text x="150" y="773" class="xs">验签密钥(per-provider)</text>
<text x="470" y="773" class="mut xs">空 / sandbox-passthrough=false</text>
<text x="760" y="773" class="mut xs">配 <tspan class="mono">ad.{csj,gdt}.sign-secret</tspan> 真实密钥</text>
<text x="1120" y="773" class="mut xs">Nacos,双人复核 + 审计</text>
<line x1="40" y1="784" x2="1440" y2="784" stroke="#e2e8f0" stroke-width="1"/>
<!-- 行3 配置 force-mock -->
<rect x="52" y="792" width="42" height="18" rx="5" fill="#b45309"/><text x="73" y="805" class="bj" text-anchor="middle">配置</text>
<text x="150" y="805" class="xs"><tspan class="mono">ad.force-mock</tspan> 全局熔断</text>
<text x="470" y="805" class="mut xs">false</text>
<text x="760" y="805" class="mut xs">保持 false;止血时置 true(只拒真实位回调)</text>
<text x="1120" y="805" class="mut xs">Nacos(本单新增)</text>
<line x1="40" y1="816" x2="1440" y2="816" stroke="#e2e8f0" stroke-width="1"/>
<!-- 行4 配置 防重放 -->
<rect x="52" y="824" width="42" height="18" rx="5" fill="#7c3aed"/><text x="73" y="837" class="bj" text-anchor="middle">配置</text>
<text x="150" y="837" class="xs"><tspan class="mono">callback-replay-window-seconds</tspan></text>
<text x="470" y="837" class="mut xs">预置默认 300 秒(上限 900)</text>
<text x="760" y="837" class="mut xs">按联盟重试间隔校准(下证时)</text>
<text x="1120" y="837" class="mut xs">Nacos(本单新增,owner = ad)</text>
<line x1="40" y1="848" x2="1440" y2="848" stroke="#e2e8f0" stroke-width="1"/>
<!-- 行5 数据 -->
<rect x="52" y="856" width="42" height="18" rx="5" fill="#16a34a"/><text x="73" y="869" class="bj" text-anchor="middle">数据</text>
<text x="150" y="869" class="xs">广告位 provider + providerSlotId</text>
<text x="470" y="869" class="mut xs">provider=mock</text>
<text x="760" y="869" class="mut xs">改 csj/gdt + 填联盟广告位 ID</text>
<text x="1120" y="869" class="mut xs">admin CRUD,可按广告位灰度</text>
<!-- ===== 区D + 区E:冲正 + runbook 底部并排 ===== -->
<!-- 区D 冲正 -->
<rect x="40" y="890" width="690" height="72" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-width="1.6"/>
<text x="56" y="910" class="lbl" fill="#2563eb">冲正模型(B2 · 设计定死,随 trade 线落地)</text>
<text x="56" y="930" class="mut xs">回调不带金额时不估算入账 —— 联盟 T+1 账单实际金额是唯一 trade 入账源。</text>
<text x="56" y="948" class="mut xs"><tspan class="mono">revenue_amount</tspan> 三态:待回填 → 已回填 → 已入账;<tspan class="mono">uk_source</tspan> add-only 幂等成立,账户恒等式不破,消除冲正。</text>
<!-- 区E runbook -->
<rect x="750" y="890" width="690" height="72" rx="9" fill="#f8fafc" stroke="#475569" stroke-width="1.5"/>
<text x="766" y="910" class="lbl">切换 runbook 三阶段(双人复核 + 配置审计贯穿)</text>
<text x="766" y="930" class="mut xs">预置(R3 下证前:Nacos 空值登记 + 对账骨架 + 铸币面单测)→ 切前演练(canary 往返 / force-mock 往返,不碰真钱)</text>
<text x="766" y="948" class="mut xs">→ D-day(写真实类灰度发布 + 双人下密钥 + 逐位放开);超阈即停,回退 = 改回 mock 或 <tspan class="mono">force-mock=true</tspan> 熔断。</text>
<!-- 脚注 -->
<text x="40" y="982" class="mut xs">映射:docs/agent-specs/2026-07-02-M4广告SDK预接线-设计.md(§0 一图看懂 / §1.2 接入面边界 / §2 三层开关与两种降级 / §3.2 冲正模型 / §4 runbook)|三态状态见上方约定条|设计变动须同步本图</text>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB