按 _recall/创始人判定.md(创始人 2026-06-22 逐条判定),6 域并行 additive 落档, 每处标来源、人读散文、涉契约者给 contract-first 待落地清单: - 产品:商业定位补竞品现任者/IP 关键人风险/增长冷启动/验收北极星;护城河话术补 腰尾部 IP/真资产 reframe/自有吉祥物红线;Doc A 就地改格(P-PUB-01→P1、 P-OPS-01→P0-lite,ACC 进 P0↔PUB 出 P0 保住 55 锚)+ P-CRT-02 注记每类 workflow 生成/rig 留 premium 远期/补 P-id - 前端:README §9 前端域承载(admin 审核台四页+观测三入口/C 端收益页溯源条 AIGC 标识/43 口径引用/移动端适配/Debug 插件) - 后端:数据模型 §7 待落地缺口(026 game_id/029 留存/030 锁风 JSON/031 信用台账/ 绘豆)、鉴权与权限 032 封号级联+018 短信防护、契约总览 Pact/Registry 远期 - 生成引擎:029 硬门 A-E 回填验收门+OpenGame对照/031 Phase 路线/033 创作期回灌/ 035 反锁死/036 harness 护城河/037 整图串行/001 对话式/002 透传/007 绘豆预算 - 运维:观测体系 §3.7 网关健康巡检、README §5/6/7 job 调度面/flag 纪律/数据可靠性 - 运营:渠道发行广告运行时红线/审核台事中召回/变现端到端资金切换护栏/变现与单位经济 月增速+eCPM 口径统一+成本漏算+成本假设待测 contract-first 待落地项均标"勿当已建"。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
26 KiB
变现端到端
这是什么:绘境AI 三条变现线的工程钱流地图——钱从玩家的一次广告、一笔订阅、一单定制出发,经过哪些表、哪些后台 job、哪些跨模块接缝、哪些回调,最终落到创作者钱包或平台营收。 给谁看:变现链路的后端工程师、运营,以及要接真实支付与广告渠道的对接人。 怎么读:先看 §1 那张全景图认清三条线和两套钱包,再按线深入——广告分成线最长(§2),订阅(§3)、B 端(§4)各一段;§5 是控制平面(谁在什么时候推动钱流),§6–§7 是改钱流代码时要对照的不变量与对账锚点,§8 是现状边界与接线点。 和变现与单位经济的分工:那份算账——分账公式、eCPM 敏感性、回本 DAU、这门生意成不成立;本份讲钱具体怎么流——经哪些表、哪些服务、哪些 job 与回调。引用代码处都带
file:line锚点,以dev/2.0.0分支为准。
1. 一张图看懂:三线钱流与两套钱包
广告分成、订阅会员、B 端定制三条线,钱的进法、经过的表与服务、终点各不相同,成熟度也不同。
flowchart LR
subgraph AD线["广告分成线 · 已建"]
A1["玩家看广告<br/>ad 计费"] --> A2["T+1 结算<br/>SettlementJob"]
A2 --> A3["入账<br/>game_trade_income"]
end
subgraph SUB线["订阅会员线 · 入账已建/收单未建"]
S1["admin 赋订阅<br/>(自助购买未接 pay)"] --> S2["幂等账本<br/>subscription_grant"]
end
subgraph BIZ线["B 端定制线 · 仅状态机"]
Z1["询单 → 报价<br/>→ 进度 → 验收"] --> Z2["签署<br/>(收款/分账留 M4)"]
end
A3 --> WALLET["创作者收益钱包<br/>game_trade_account"]
S1 --> WALLET
WALLET --> WD["提现 → 异步打款<br/>(真渠道 mock)"]
A3 --> REPORT["平台营收报表<br/>trade/report/revenue"]
classDef done fill:#e6ffe6,stroke:#3a3;
classDef stub fill:#fff7e6,stroke:#d90;
classDef none fill:#fee,stroke:#c33;
class AD线 done;
class SUB线,BIZ线 stub;
class WD stub;
三条线在创作者收益钱包 game_trade_account 上汇合,平台那一侧另有一张营收报表。当前真实终点的成熟度要分清:广告计费、T+1 分账、入账、订阅赋权这几段都已落地并真跑;提现走到打款时,真实微信/支付宝渠道还没接,终点是 mock;B 端的收款与分账整段留在 M4,现在只走到状态机加线下签署兜底标记。
模块分工是这样切的:广告分成线跨两个模块,ad 管"造钱 + 台账"、trade 管"分账 + 钱包 + 提现";订阅会员线由 trade 独管(V21 三表);B 端定制线由 biz 独管(V13 四表)。
一处必须先纠正的事实:没有 game-module-pay 这个业务模块
读旧文档容易踩一个坑——把 pay 当成一个已经接好的"game 业务模块"。代码里不是这样:
- 现有的是框架自带的 huijing-module-pay(命名空间
com.wanxiang.huijing.module.pay),它在后端聚合 pom 里挂载(game-cloud/pom.xml:20),提供两个能力 API:PayTransferApi(企业付款转账)和PayWalletApi(消费钱包)。 - trade 模块并不依赖它——grep 过
trade-server与trade-api的 pom,零module-pay依赖。trade 自己定义了打款的抽象接口,并没有走 huijing-module-pay。 - 真实的微信/支付宝打款渠道(
wxpay/alipay)还没接线。当前钱流走到打款这一步,终点是 mock。
所以本档凡提到"打款",真实终点都是 mock;真渠道的接线点在哪、零改业务码怎么切到真实渠道,在 §8 讲清。
2. 广告分成线:从一次曝光到创作者提现
这是最长的一条线,跨 ad 和 trade 两个模块,中间隔着一道 Feign 接缝和一个 T+1 的结算 job。
2.1 广告造钱(ad 模块)
ad 模块管的是"把一次广告事件变成一条创作者收入台账"。错误码段 1-111,建表脚本 V6.0.0__create_game_ad.sql。
SDK 先拉广告位再上报事件。拉取走 GET /ad/slot/list-enabled(AdController.java:65),上报走三个 @PermitAll 的计费端点:POST /app-api/ad/report/impression、/report/reward、/reward/callback(AdController.java:74/84/94)。这三个端点对匿名玩家放行,是 2026-06-10 鉴权波的明确裁决——曝光不能因为玩家没登录就丢掉。
计费的核心在 AdRevenueServiceImpl.bill()(AdRevenueServiceImpl.java:139-210),五步走:
sequenceDiagram
participant SDK as Game SDK(玩家端)
participant C as AdController
participant S as AdRevenueServiceImpl.bill()
participant CK as AdComplianceChecker
participant P as ProjectApi
participant DB as game_ad_revenue
SDK->>C: report/impression · report/reward
C->>S: bill(traceId, eventType, gameId, ...)
S->>S: ① 查广告位 enabled
S->>CK: ② 合规校验(未成年 blockMinor / 单会话 maxPerSession)
CK-->>S: 超限即不计费
S->>DB: ③ 幂等查 uk_trace(traceId, eventType)
S->>P: ④ getCreatorUserId(gameId) 归因到游戏作者
S->>S: ⑤ mock 算收入 calcRevenue(ecpm)
S->>DB: ⑥ 落 game_ad_revenue(settle_status=0)
几个要点钉清楚:合规校验(AdRevenueServiceImpl.java:147-153)在未成年 blockMinor 命中或单会话超 maxPerSession 时直接不计费;收入归因靠 ProjectApi.getCreatorUserId(gameId) 把这笔收入挂到游戏作者头上(:163);激励视频(rewarded)类型的曝光不计收入,只有 reward 事件计(:172);最终落库时 settle_status 置 0(待结算),审计列硬兜底成字符串 "0",防匿名玩家撞 NOT NULL(:196)。
台账表 game_ad_revenue(V6.0.0__create_game_ad.sql:54-77)有几处设计值得记住:金额单位是"分",BIGINT;计费幂等键是 uk_trace(trace_id, event_type, tenant),所以同一个 trace 的 impression 与 reward 各落一条、互不覆盖(:73);settle_status 由 0 翻到 1 是 trade 结算后回标的;idx_settle_date 供 trade 扫未结算的行。
广告渠道是可插拔的 SPI:AdProvider + AdProviderFactory,现有 MockAdProvider 和 CallbackAdProvider 两个实现。这条线的降级口径是宽松的——没注册真实渠道时降级到 mock 可以接受,代价只是少算些收入,不影响主链路。(打款那条线的降级口径正相反,见 §2.4。)
admin 侧有两个控制器:AdSlotController 管广告位 CRUD(RBAC ad:slot:*),AdRevenueController 查台账(/ad/revenue/page,ad:revenue:query)。前端 game-admin/src/views/wanxiang/adSlot/ 有广告位的 CRUD 视图。
2.2 ad→trade 分账接缝(Feign)
ad 算完台账,钱还在 ad 的库里;要变成创作者钱包余额,得跨到 trade。这道接缝的契约是 AdRevenueApi(在 ad-api 包,AdRevenueApi.java:46/61),两个方法:getUnsettledRevenue(statDate) 拉某天未结算的台账,markSettled(revenueIds) 把入账成功的行回标。
MVP 是单体,这道 Feign 接缝由 AdRevenueApiImpl(@RestController @Primary)在同进程内就地解析——跨模块只在编译期依赖对方的 -api 包,运行期不走真实网络。@Primary 让单体优先用本地实现而非 Feign 代理。
2.3 T+1 结算
把昨天的广告台账结算成创作者收入,由 XXL-Job 触发。job 名 tradeSettlementJob,带 @TenantJob(按租户跑),入口 SettlementJob.java:39。默认每日凌晨结算昨天(T+1);它接受 yyyy-MM-dd 参数,传一个历史日期就能补结算或重跑某天,靠下游的 uk_source 幂等保证不会重复发钱。
编排逻辑在 SettlementServiceImpl.settle()(SettlementServiceImpl.java:64-115):
flowchart TB
J["tradeSettlementJob<br/>param=昨天 或 指定日"] --> F["Feign getUnsettledRevenue(statDate)<br/>拉未结算台账"]
F --> L{逐条}
L --> R["recordIncome<br/>source=AD · source_ref=ad_revenue.id<br/>gross · creatorShare · traceId"]
R --> N["首次入账才发 community 通知<br/>notifyIncomeChanged(失败不中断)"]
N --> L
L -->|本批入账成功的 id| M["markSettled(ids) 回标 0→1"]
M -.->|回标失败| NEXT["下一轮 job 补偿"]
逐条调 recordIncome,source=AD、source_ref 记成 ad_revenue.id、带上 gross/creatorShare/traceId。分账比例走 Nacos 的 trade.creator-share,默认 0.80(@Value,:43)。这里有一条关键的补偿语义——先入账,后回标:只有入账成功的那些 id 才回标(:110),回标本身要是失败了,下一轮 job 会把它们当成"仍未结算"再拉一次,靠 uk_source 挡住重复入账。只有首次入账才发 community 的收益变动通知 notifyIncomeChanged(:99),通知失败不中断整个循环。
入账的原语是 IncomeServiceImpl.recordIncome()(IncomeServiceImpl.java:43-78):@Transactional 把"写流水 + 账户余额原子增"放进同一个事务;幂等靠 uk_source 先查后插,再用 DuplicateKeyException 兜底并发;calcNet 用 BigDecimal.setScale(0, DOWN) 向下取整,防浮点(:89)。
2.4 提现 + 打款异步化
创作者点提现到钱真正打出去,是一个跨"申请 / 审核 / 发起 / 终态"四个阶段的状态机。提现表 game_trade_withdraw(V7.0.0__create_game_trade.sql:93-116,后续 V14.0.0__m4_trade_withdraw_transfer_ref.sql 加列 transfer_ref),状态机 5 态,幂等键 uk_biz_no,金额单位"分"。
stateDiagram-v2
[*] --> 待审_0: applyWithdraw<br/>freeze(balance→frozen)
待审_0 --> 打款中_1: 审核通过<br/>approveToPaying CAS 0→1
待审_0 --> 驳回_3: 审核驳回<br/>refundFrozen
打款中_1 --> 已打款_2: 回调成功<br/>settlePaid
打款中_1 --> 打款失败_4: 回调失败<br/>refundFrozen
打款失败_4 --> [*]
已打款_2 --> [*]
驳回_3 --> [*]
申请走 app:POST /app-api/trade/withdraw/apply(TradeController.java:88)。WithdrawServiceImpl.applyWithdraw()(:90-133)先校门槛——≥ Nacos trade.withdraw-min,默认 500 分(满 5 元,:68),再用 bizNo 幂等先查,落单,然后 freeze 把 balance 原子挪进 frozen(带充足校验,余额不够就整事务回滚、不留脏单)。
审核走 admin:POST /admin-api/trade/withdraw/audit(TradeAdminController:86,RBAC trade:withdraw:audit),真实渠道回调入口是 /withdraw/payout-notify(:95)。前端 game-admin/src/views/wanxiang/withdraw/index.vue 有"提现审核"Tab,带 approve/reject 操作。
这一段最硬的红线是"发起 ≠ 终态"的异步化(WithdrawServiceImpl.java:44-50,135-274):审核事务内只做 CAS 0→1(approveToPaying),绝不在审核事务里直接置成"已打款";事务提交之后,才发起真正的转账 initiatePayout(这一步不进事务,:213);资金的终态由 handlePayoutNotify 回调驱动——成功就 CAS 1→2 并 settlePaid,失败就 CAS 1→4 并 refundFrozen(:241-274)。mock 渠道是同步直通的,发起后本进程直接把回调驱动一遍,但仍然走 CAS,不抄近路直接置 2。
打款渠道也是 SPI:PayoutClient + PayoutClientFactory(PayoutClientFactory.java)。它的降级口径和广告渠道正好相反——配置了某个渠道却找不到实现时,fail-fast 抛 TRADE_PAYOUT_CHANNEL_UNAVAILABLE,严禁静默降级到 mock 发假钱(:78-87)。原因很直白:广告渠道缺失只是少算收入,打款渠道缺失若降级 mock 就是真把假钱当成功发出去。当前只有 MockPayoutClient 一个实现(grep 证),没有 wxpay/alipay 实现;PayoutClient.java:14/52 的注释写明真渠道应委托 huijing-module-pay 的 PayTransferApi.create/getTransfer,但还没接线。
打款发起后可能卡在"打款中"——网关超时、回调丢失都会让单子停在状态 1。兜底是对账补偿 job WithdrawPayoutCompensateJob(XXL-Job tradeWithdrawPayoutCompensateJob,默认卡单阈值 15 分钟、单批 100):扫 status=1 的超时单,按单子自己记的 withdraw.channel(不是当前 Nacos 配的渠道)取对应 client 查终态、补驱动(compensateStuckPayouts,WithdrawServiceImpl.java:277-312)。按单子血统的渠道而非当前配置取 client,是为了让换了渠道配置之后,历史卡单仍能找回原渠道对账。
2.5 打赏现金线
打赏和广告共用 trade 的入账原语,但分账比例不同。RewardPayoutServiceImpl.payoutPendingCashRewards()(RewardPayoutServiceImpl.java:51-91)从 community 拉待发的现金奖励,调 recordIncome 入账,source=TIP、shareRate=1.0——打赏全额给创作者、平台不分成,然后 markRewardGranted 把 community 那边 CAS 0→1 回写。同样是"先入账后回写"的补偿语义。触发器是 RewardPayoutJob。
3. 订阅会员线:admin 赋订阅到 C 端查态
订阅完全在 trade 内部,建表 V21.0.0__create_game_trade_subscription.sql,错误码 1-106-006。它现在只接通了"admin 手动赋订阅"这一半,自助购买那一半要等真实支付。
三张表各管一段:
game_trade_grant——admin 赋余额的流水,幂等键uk_biz_no。game_trade_subscription——一个用户一行(uk_user),plan取 1 月/2 季/3 年,有效性以expire_time为权威。game_trade_subscription_grant——每笔赋订阅落一行的幂等账本(uk_biz_no)。这张表是 P0 修复加进来的(V21.0.0__create_game_trade_subscription.sql:82-114):订阅主行里只有一个last_grant_biz_no字段,它只能挡住"紧挨着的上一次"重放,挡不住任意历史bizNo的重放;独立账本才能挡住任意历史 bizNo 重复赋权。
赋订阅的钱流入口在 admin:POST /admin-api/trade/subscription/grant(TradeAdminController:184,RBAC trade:subscription:grant),赋余额另有 /account/grant(:173)。SubscriptionServiceImpl.grantSubscription()(:54-140)先写幂等账本,再 upsert 续期:
flowchart LR
G["admin grant<br/>bizNo · plan"] --> L["写幂等账本<br/>subscription_grant(uk_biz_no)"]
L --> U["upsert 续期<br/>newExpire = max(now, 旧expire) + 时长"]
U --> C["renewByCas 防并发 lost-update"]
C --> Q["C 端查态<br/>effectiveStatus = expire_time > now"]
续期的算法是 newExpire = max(now, 旧expire) + 时长(:177-209),所以续期会把没用完的时长叠加上去、不丢;renewByCas 防并发的 lost-update。赋余额走 AccountServiceImpl.grant()(:84-133):写 grant 流水 + balance/total_income 原子增,在同一事务里。
C 端只读查态:GET /app-api/trade/subscription/mine(TradeController:103)。没有 cron 去物化"过期"这件事,effectiveStatus 按 expire_time > now 实时回算(SubscriptionServiceImpl.java:34-36)——少维护一个定时任务,代价是查的时候算一下。
边界要说清:真实支付收单、自助购买订阅是日历闸门约束的事,还没做;当前订阅只能 admin 手动赋,source 标 admin_grant。
4. B 端定制线:询单到交付(钱在 M4)
B 端由 biz 独管,建表 V13.0.0__create_game_biz.sql,错误码 1-110。这一波只走业务状态机,完全不碰钱——收款、在线签章、分账整段留在 M4。
四张表:game_biz_lead(询单主单,单线性五态)、game_biz_quote(报价,只展示不收款)、game_biz_progress(进度工单时间线)、game_biz_acceptance(试玩→反馈→确认三态,带 signed_offline 线下签兜底)。主单的状态机是一条单线:
stateDiagram-v2
[*] --> 待跟进_0
待跟进_0 --> 已报价_1: 报价
已报价_1 --> 制作中_2: 接单
制作中_2 --> 待验收_3: 提交验收
待验收_3 --> 已交付_4: 确认
已交付_4 --> [*]
admin 侧 AdminBizLeadController(/admin-api/biz/*)管 leads 队列、指派 assign、报价 quotes、推进 advance、线下签 sign-offline;app 侧 AppBizLeadController(/app-api/biz/*)给客户提单、查我的订单 leads/my、看进度/模板、反馈 feedback、确认 confirm。状态机的合法迁移由 BizLeadServiceImpl 在写前校验。
资金流边界写在建表脚本和配置里(V13.0.0__create_game_biz.sql:6、biz.yaml:17):本波只写 biz 自有的四张表,不写 pay、不写 trade;报价不等于可支付的订单,签署(在线电子签章)、收款(对应工作项 T-BIZ-08)、分账全留 M4。现在唯一能标的是 signed_offline——线下签了合同就打个兜底标记。前端 game-admin 还没有 biz/lead 的视图(find 证零)。
5. 控制平面:谁推动钱流
前面几节是"钱从哪流到哪"的数据视角。但钱不会自己流——广告台账躺在库里不会自动变成钱包余额,打款发起了不等于钱到账。推动这些状态前进的是三个后台 job、一道单体内就地解析的 Feign 接缝、和打款那条"发起 ≠ 终态"的双路驱动。
flowchart TB
subgraph JOBS["三个 XXL-Job(@TenantJob)"]
J1["tradeSettlementJob<br/>每日凌晨 · param=昨天/指定日<br/>T+1 分账"]
J2["tradeWithdrawPayoutCompensateJob<br/>卡单阈值 15min · 单批 100<br/>对账补偿"]
J3["RewardPayoutJob<br/>打赏发放"]
end
J1 -->|Feign getUnsettled/markSettled| SEAM["AdRevenueApi 接缝<br/>单体 @Primary 就地解析"]
J1 --> INC["recordIncome 入账"]
J3 --> INC
PAYOUT["打款发起 initiatePayout<br/>(发起 ≠ 终态)"]
CB["回调 handlePayoutNotify<br/>成功 1→2 / 失败 1→4"]
PAYOUT -.->|回调到| CB
J2 -.->|回调丢失时补驱动| CB
三个 job 的调度与 param 约定:tradeSettlementJob 每日凌晨跑、默认结算昨天,传 yyyy-MM-dd 可补结算/重跑;tradeWithdrawPayoutCompensateJob 周期扫 status=1 超过 15 分钟的卡单、单批最多 100;RewardPayoutJob 驱动打赏现金发放。三个都带 @TenantJob,按租户维度跑。
**Feign 接缝的单体解析:**ad→trade 的 getUnsettledRevenue/markSettled 在单体里由 @Primary 的本地实现就地解析,不走真实网络;拆成微服务后换成真 Feign 调用,调用方代码不用改。
**打款的双路驱动终态:**打款只负责"发起",钱的终态由两条路推动——正常路是渠道回调 handlePayoutNotify,异常路(回调丢失、网关超时)是补偿 job 兜底重新查终态。两条路都通过 CAS 改状态,所以重复驱动是安全的:已经从 1 翻走的单子,CAS 会失败、不会二次发钱。
6. 资金一致性不变量(代码锚点版)
变现与单位经济§5 把资金一致性红线讲成了文字;改钱流代码时要对照的是它们钉在哪个方法、哪个 CAS、哪条事务边界上。
账户恒等式。game_trade_account(V7.0.0__create_game_trade.sql:32-47)是物化汇总账户,恒等式 balance + frozen + total_withdraw = total_income 必须永远成立;uk_user 保证一个创作者一个账户;余额的增减是行级原子操作,带充足校验防超扣。维持这条恒等式的是 AccountServiceImpl 的四个原子动作 addIncome/freeze/settlePaid/refundFrozen——它们有一条红线注释(AccountServiceImpl.java:148/160):资金动作更新 0 行就抛错,让外层事务回滚,绝不允许"状态已置、资金未动"的半态 commit。
四条红线钉到代码:
- 发起 ≠ 终态——审核事务内只 CAS 0→1(
approveToPaying,WithdrawServiceImpl.java),终态留给回调/补偿,不在审核里置 2。 - 打款 fail-fast 不降级 mock——
PayoutClientFactory(:78-87)找不到配置渠道的实现时抛TRADE_PAYOUT_CHANNEL_UNAVAILABLE,绝不静默发假钱。 - 逐笔向下取整,禁总额反推——
recordIncome用calcNet(BigDecimal.setScale(0, DOWN),IncomeServiceImpl.java:89)逐笔算净额并向下取整;对账以逐笔为权威(单笔net == floor(gross × rate)),不能拿总gross × 0.8反推总额。 - mock 不变量先定毛/净——接真渠道之前,要先定
revenue_amount这一列存的是毛额还是净额,口径一旦定下,mock 和真渠道必须一致,否则切渠道时账面会错位。
七个资金动作的幂等键(可直接复用变现与单位经济§2 那张表,本档补上 source_ref 与 CAS 列):
| 资金动作 | 表 | 幂等键 | source_ref / CAS |
|---|---|---|---|
| 广告计费落台账 | game_ad_revenue |
uk_trace(trace_id, event_type, tenant) |
— |
| 广告分账入账 | game_trade_income |
uk_source(source, source_ref, tenant) |
source_ref = ad_revenue.id |
| 打赏入账 | game_trade_income |
uk_source |
source=TIP,source_ref = reward id |
| 提现申请冻结 | game_trade_withdraw |
uk_biz_no |
freeze 充足校验 |
| 提现审核 | game_trade_withdraw |
CAS | approveToPaying 0→1 |
| 打款终态 | game_trade_withdraw |
CAS | 回调/补偿 1→2 或 1→4 |
| admin 赋订阅 | game_trade_subscription_grant |
uk_biz_no |
renewByCas 续期 |
资金渠道切换要双人复核 + 配置审计(操作红线 · 待落地)。(创始人 2026-06-22 历史回收判定捡回)上面四条红线管的是代码逻辑——钱在哪个事务、哪个 CAS、哪条边界上动;这一条管的是人去改配置的那个动作本身,是流程规矩不是代码规则。变现链上有两个"切真"的开关:广告 provider 从 MockAdProvider 切到真实联盟(CsjAdProvider/GdtAdProvider)、打款 channel 从 MockPayoutClient 切到真实微信/支付宝(Nacos trade.payout-channel 设为 wxpay)。这两个开关一翻,系统就开始动真钱——配置填错的后果是直接的资损:渠道选错或密钥配错,可能让本该 fail-fast 的打款变成"以为切了真渠道、其实在发 mock 假钱",或者把真实联盟的回调对到错的归因上。
资金渠道是整条变现链风险最高的开关,绝不能允许单人改一行配置就切真。要钉死两条操作约束:① 双人复核——切换 provider/payout-channel 的配置变更必须经第二人复核确认后才生效,不允许单人直接改 Nacos/配置中心就上线;② 配置审计——每一次渠道切换都要留下"谁、什么时候、把哪个开关从什么改成什么"的审计记录,可追溯、可回查。这条和变现与单位经济§4.3"拿到真实账单前不对外锁价"是一类纪律——都是真钱面前的操作护栏,约束的是人的操作而非系统的逻辑。
它落地依赖配置变更的管控面,不是改业务码:双人复核挂在配置中心(Nacos)或运维变更流程的审批环节上,配置审计取配置中心自带的变更历史或单独记一条运维操作台账。待落地——当前 §8 标"打款执行=mock 桩",这两个切真开关都还没真正翻过,但红线要在切真之前就立好,别等手滑改错配置酿成资损才补。
7. 对账锚点链
钱跨表、跨模块流的时候,要能从任意一段反查到上下游。端到端的对账锚点串成一条链:
flowchart LR
AR["game_ad_revenue<br/>id · trace_id · stat_date"] -->|source_ref = id| TI["game_trade_income<br/>source_ref · trace_id"]
TI -->|余额变动| ACC["game_trade_account<br/>恒等式校验"]
ACC --> WD["game_trade_withdraw<br/>biz_no · transfer_ref"]
TI -.->|分析镜像 income_settled| TM["telemetry 埋点<br/>(不替代落账)"]
四个锚点各管一段对账:game_ad_revenue.id 经 source_ref 钉到 game_trade_income,是"台账→入账"的对账键;trace_id 全链透传,从计费一路带到入账;transfer_ref 是打款回调与渠道流水的对账键(V14 加的列);stat_date 是日期键,统一取上游的 stat_date,不要在下游另算一个日期,否则跨日的单子会对不上。
埋点和落账要分清主次:contracts/events.schema.json:68 里的 income_settled 事件携带 amount/source,但它只是 telemetry 的分析镜像,不替代 game_trade_income 落账——对账永远以数据库落账为权威,埋点丢了不影响钱,落账错了才影响钱。
8. 现状边界与接线点(纯钱流工程口径)
哪些已经真跑、哪些是桩、哪些还没接,以及每项的接线点在哪。这张表是钱流工程视角的;经济口径的待办在变现与单位经济§6。
| 能力 | 状态 | 接线点 |
|---|---|---|
| 广告计费落台账 | ✅ 已 real | AdRevenueServiceImpl.bill() |
| T+1 分账入账 | ✅ 已 real | SettlementServiceImpl.settle() |
| 提现申请 + 冻结 + 审核 CAS | ✅ 已 real | WithdrawServiceImpl |
| 打赏现金发放 | ✅ 已 real | RewardPayoutServiceImpl |
| admin 赋余额 / 赋订阅 | ✅ 已 real | AccountServiceImpl.grant / SubscriptionServiceImpl |
| 打款执行 | 🟡 mock 桩 | 实现 PayoutClient 注入(委托 PayTransferApi.create/getTransfer),fail-fast 已就位,补真渠道实现即切 |
| 消费钱包内购扣款 | ❌ 未接 | huijing-module-pay 的 PayWalletApi 未接入 game 业务;接 pay 后走 reduceWalletBalance |
| 自助购买订阅 | ❌ 未接 | 需接真实支付收单 + 支付回调驱动 grantSubscription,替代 admin 手动赋 |
| B 端收款 / 分账 | ❌ 未接(M4) | biz 仅状态机,收款(T-BIZ-08)/在线签章/分账全留 M4,现仅 signed_offline 兜底 |
| 按游戏粒度 game_id 透传 | 🟡 部分 | 归因已到创作者(getCreatorUserId),game_id 维度的端到端透传待补全四步链 |
**真渠道的零改业务码切法:**打款这条线已经把抽象做对了——业务码只调 PayoutClient 接口和 PayoutClientFactory,具体渠道是 SPI。切到真实微信/支付宝,只要实现一个真 PayoutClient(内部委托 huijing-module-pay 的 PayTransferApi)并注册到 factory,WithdrawServiceImpl 的审核/发起/回调三段代码一行不改。fail-fast 这条红线反而帮了忙——没接真渠道时它硬拦,杜绝了"以为切了真渠道、其实还在发 mock"的隐患。
测试覆盖现状(find 计数):trade 9 个 Test、ad 3 个、biz 1 个。
9. 关键指针
- 账怎么算、这门生意成不成立 → 变现与单位经济(R3 分账公式、eCPM 敏感性、回本 DAU、成本台账口径)。
- 二清风险、持牌分账/银行存管/灵活用工三通道选型、提现实名税务闸门 → 合规闸门。
- 各合规闸门的当前状态(活数据) → A1 闸门看板。
- 接缝事实源(ad↔trade seam、资金边界口径) →
contracts/api-schemas/{ad,trade,biz}.yaml、contracts/ad-slot.schema.json(广告位第 7 契约)、contracts/sdk-interface.d.ts(AdPlugin.showRewarded/showInterstitial,:48-52)、contracts/events.schema.json(income_settled仅分析镜像,:68)。 - 建表脚本与版本(版本号以实时核对迁移目录为准) →
game-cloud/**/resources/db/migration/(ad=V6 / trade=V7+V14+V21 / biz=V13)。