games-development-ai/docs/agent-specs/2026-07-02-M4广告SDK预接线-设计.md
2026-07-02 20:07:17 -07:00

11 KiB

date, topic, status, sot-impact, 上级, 关联, 图清单
date topic status sot-impact 上级 关联 图清单
2026-07-02 M4 广告 SDK 预接线 草稿 · 待 Codex+Opus 双评审 → 通过后进 R3 实现 不新建 canonical topic(R3 工单实现设计,归属既有 SoT「变现端到端」)。修订触点 = 变现端到端 §6(切换红线由「待落地」落成 runbook)、§8(切真接线点细化);落地时新增 Nacos 配置约定(per-provider 密钥命名空间 + ad.force-mock 熔断);不改 ad.yaml API 契约、不改 mock 路径代码。 docs/mvp/可行性方案16周-波次映射.md game-cloud/game-module-ad/game-module-ad-server/.../framework/provider/{AdProvider,AdProviderFactory,MockAdProvider,CallbackAdProvider,AdRewardCallbackContext}.java · controller/app/AdController.java:94-102 · service/revenue/AdRevenueServiceImpl.java:101-210 · contracts/api-schemas/ad.yaml · contracts/ad-slot.schema.json · docs/architecture/运营/变现端到端.md §6/§8 @ 4d220e77
图1 接入面边界与三层切换开关(mermaid);SVG 门面图收口时按 atlas 补

M4 广告 SDK 预接线 · 设计(穿山甲 / 优量汇适配层切真通路)

0 一图看懂

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 限频"]
    F["AdProviderFactory.get(provider)<br/>未注册→降级 mock;force-mock→全局回退"]
    V["provider.verifyRewardCallback<br/>验签(密钥 per-provider)"]
    BILL["bill():合规→uk_trace 幂等→归因→算收入→落台账"]
    REC["ad↔联盟对账(本单新增设计)<br/>估算入账 vs 联盟账单,T+1 冲正"]
  end
  SDK -. 用户看完,联盟服务端回调 .-> CB
  CB --> F --> V -->|通过| BILL
  BILL --> REC
  V -->|失败 1-111-002-004| X["不计费不发奖 + 告警"]
  classDef out fill:#eef,stroke:#88a;
  classDef new fill:#fff7e6,stroke:#d90;
  class 端侧 out; class REC new;

三层切换开关(默认值一律 = mock 现状,切真是叠加、不改 mock 路径):代码层 = 注入真实 CsjAdProvider/GdtAdProvider 实现类;配置层 = Nacos 的 per-provider 验签密钥 + ad.force-mock 熔断;数据层 = 广告位 game_ad_slot.provider 字段 + providerSlotId

1 意图与边界

1.1 意图

把已经落地的广告联盟 SPI 骨架,补成一条「从 mock 到真、一键可切、随时可回滚、切完能对账」的完整通路,让穿山甲(csj)/优量汇(gdt)资质下证当天,以最小动作切真而不动 mock。R3 是预接线——下证前能做的全部就位,把真正等文档的部分压缩到最小。

预接线并非从零。当前已就位、本单直接复用的骨架:

  • SPI 与工厂:AdProvider(getProvider/calcRevenue/verifyReward/verifyRewardCallback)+ AdProviderFactory 按 provider 路由、未注册降级 mock。
  • 回调验签桩: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)
  • 广告位契约:provider 枚举已含 csj/gdt/mock/callback;providerSlotId(联盟真实广告位 ID)provider≠mock 时必填、admin 录入、恒不下发前端(前端不持有联盟广告位 ID,由服务端代理)。

因此真实联盟与 mock 的差别,收敛到一个 provider 实现类里的验签算法收益口径;端点、幂等、归因、限频、安全红线全部共用。

1.2 边界(四个接入面的归属)

接入面 归属 本单
端侧展示广告(showRewarded/showInterstitial,拉起联盟 SDK) game-studio + WanxiangGameSDK 否(另轨,切真时需协同)
服务端激励回调验签计费(reward/callback) ad 模块 是 · 核心
平台↔联盟收益对账(真实 eCPM/结算额) ad 模块 provider 是 · 设计,下证落地
提现打款渠道(payout) trade 模块 否(与广告切真是两个独立开关)

不做:不写联盟真实签名算法(强依赖官方文档,提前写即编造);不动 mock 路径任何代码;不接端侧 SDK。

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(本单新增)
数据 广告位 provider + providerSlotId provider=mock 改为 csj/gdt + 填联盟广告位 ID admin CRUD,可按广告位灰度

数据层按广告位粒度,天然支持灰度:先把一个低流量广告位切 csj 验证,再逐位放开,不需要额外的全局灰度旋钮。

2.2 「下证当天可切」= 预置一切、当天只补一个类

下证当天唯一的代码增量,是照着 CallbackAdProvider 写出真实 provider 类、填入联盟签名算法。其余全是已预置好的配置与数据操作:配密钥、改广告位 provider 字段。这就是「藏 flag 后当天可切」的实指——flag(密钥项、熔断开关、provider 字段)在 R3 全部预埋,平时走 mock,当天翻开即真。

2.3 R3 落地的设计增量(不依赖联盟文档的部分)

  • per-provider 密钥命名空间:现仅一个 ad.callback.sign-secret,真实需 csj/gdt 各自独立密钥。约定 ad.<provider>.sign-secret(下证前以空值占位登记进 Nacos,纳入配置基线),避免下证当天临时新造配置项。具体密钥格式/字段名待官方文档核实
  • ad.force-mock 全局熔断:AdProviderFactory.get() 前置一个 @Value("${ad.force-mock:false}") 判断,为 true 时对任意 provider 一律返回 MockAdProvider。默认 false 即当前行为,mock/callback 广告位不受影响;联盟故障或对账严重不平时,一个开关把全平台广告回退 mock 止血。
  • 真实 provider 的 fail-safe 语义:真实类在签名算法填入前,verifyRewardCallback 必须默认返回 false(继承 P2-2 精神),绝不因「类已注入但算法未就绪」静默放行铸币。
  • 收益口径先锁毛/净:revenue_amount(分)在 mock 与真实之间口径必须一致(变现端到端 §6 红线)。R3 锁定其为毛额;真实收益以联盟为准,回调若未携带实际金额则先按 ecpmFloor 估算入账、T+1 对账时以联盟账单冲正差额。联盟是否在回调中回传实际金额待官方文档核实

3 失败模式(红线:外部交互必处理超时 / 重试 / 幂等 / 验签)

失败模式 已落地 R3 补
回调超时 / 丢失 平台被动接收、快速响应;联盟侧超时自行重推,uk_trace 保证重推安全 回调端点验签与重活解耦、快速回执防联盟误判超时;回调彻底丢失的兜底 = 平台主动对账拉取补记(依赖联盟查询接口,待文档)
验签失败 AD_REWARD_CALLBACK_SIGN_INVALID(1-111-002-004),不计费不发奖 + warn 日志 失败计数告警(密钥错配 / 疑似伪造);启用 timestamp 时间窗口防重放(窗口大小待联盟重试间隔文档)
重复回调幂等 uk_trace(trace_id, event_type) 去重,重复只计一次、只发奖一次 沿用,无需新增
对账不平 ad↔trade 有 source_ref + trace_id 锚点链 新增 ad↔联盟对账面:平台估算入账 vs 联盟实际结算,T+1 差异检测 + 告警 + 以联盟账单为准的冲正口径

平台对联盟回调是被动入站,超时与重试的主动方是联盟;平台的责任是幂等接收 + 快速回执 + 验签。真正的平台主动外呼(拉联盟收益报表对账)才需自己处理超时 / 重试 / 降级,那部分随对账接口一并接入。

4 切换 runbook 骨架

前置(R3 预置,下证前完成):per-provider 密钥项空值登记进 Nacos;ad.force-mock 开关登记(默认 false);对账 job 骨架 + 差异告警;切换 checklist;单测加固(见 §5)。

下证当天(D-day):

  1. CallbackAdProvider 写真实 provider 类、填联盟签名算法 + 单测,灰度发布(唯一代码增量)。
  2. 双人复核下配置:填 ad.{csj,gdt}.sign-secret 真实密钥,留配置审计(谁 / 何时 / 从什么改成什么)。
  3. admin 选一个低流量广告位:provider 改 csj/gdt + 填 providerSlotId(灰度起点)。
  4. 端侧 SDK 灰度接入该广告位(端侧另轨,需协同)。
  5. 验证:真回调验签通过→计费落台账→T+1 对账平;失败率、验签失败告警在阈内。
  6. 全量:逐位把其余广告位切真。
  7. 回滚:异常时 admin 把广告位 provider 改回 mock/callback;大面积异常直接 ad.force-mock=true 全局熔断。

双人复核 + 配置审计贯穿步骤 2、3,把变现端到端 §6 那条「待落地」的操作红线,落成这份可执行清单。

5 验证

  • mock 零回归(硬红线):provider=mock 路径代码零改动;现有 ad 模块单测(含 CallbackAdProvider 7 例)全绿;ad.force-mock 默认 false 时,工厂对 mock/callback 广告位的返回逐一比对切换前后一致。
  • 新增单测:force-mock=true 时任意 provider 返回 mock;per-provider 密钥装配正确;真实 provider fail-safe(算法未填时 verifyRewardCallback 返回 false);对账差异检测命中人造差额。
  • 切换演练:sandbox(provider=callback + sandbox-passthrough)跑通「回调→验签→计费→对账」全链,作为真实化前的形态验证;此演练不碰真钱、不需联盟资质。

6 待官方文档核实清单(不编造,下证时逐项落实)

  • 穿山甲(csj)/ 优量汇(gdt)各自的服务端激励回调:端点方向、请求参数名(交易号 / 签名 / 时间戳 / nonce 等)、签名算法(md5 / sha256 / hmac)、密钥获取与格式、防重放窗口、重试与超时判定策略。
  • 联盟实际收益 / eCPM 数据来源:是否随回调携带实际金额、是否需平台主动拉报表接口、结算口径(毛 / 净 / 税)、对账文件格式与周期。
  • 端侧 SDK(game-studio 另轨):穿山甲 / 优量汇在目标运行环境的接入方式,以及 providerSlotId 在端侧取位与服务端代理之间的分工。