2026-06-11 创始人拍板:①假设值设硬红线(真账单前一律【假设·待测】、不作预测口径、不对外锁价)②gross语义现定schema形状(game_ad_revenue 存G总消耗+N渠道净额+渠道扣率快照,分成从N算、对账用G),实际值留真实化波③对账归属推迟到支付真实化波。三条均不阻塞→W4埋点review spec解锁,待排期开execution(③④独立小波)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
39 KiB
W4 单位经济埋点 · Review 版 Spec(HJ-W4-METRICS-001)
2026-06-11 起草 | 维护:主 agent | review 版:结论先行、供人决策、降认知负荷、不写可执行代码 上游依据:
docs/mvp/单位经济敏感性模型.md(下称【模型】,R3 拍板后唯一口径)§7 测量计划 关联件:newapi 计费 review v2(2026-06-11-newapi计费平面集成-review.md)/ Wave4 community-biz execution(V14 预占登记)/ events.schema.json 契约 #5
0. 结论先行(一段话)
本波 = 给 B2 数据回路补「单位经济测量(measurement)」能力,让【模型】§1/§3/§5 的【假设·待测】数字逐项变【实测】,覆盖四类指标:① LLM token 计费 ② 素材生成成本 ③ 广告产值 ④ 创作者应得。
范围边界(已收敛,不再浮动):本波 = ③④ 后端按游戏埋点的独立小波(不是仅交付 spec、也不推迟到 M4;recon 已证③④数据源就位、改动链清晰可做,见 §8.0 S1 范围裁决已收敛)。范围只到「埋点 measurement + 按游戏 curl/DB 读出」,不含 M4 支付——不做真实记账/真实支付/对账重建/控成本/IP 真实分账(继承 M4 边界与 newapi review v2 推迟裁定)。
核心取舍三条:(a) per-game 粒度分两档落地——③④ 走后端、game_id 在 ad 侧天然就位故做真 per-game,①成本侧 new-api logs 无 game_id 列故MVP 接受 per-batch 时间窗均摊(不强行透传 game_id 进网关),②素材无源则降级为「不建桶、只登记口径」;(b) ③④落点已定向(不再二选一)——③走 ad 侧加聚合查询(game_id+idx_game 已就位,不落 telemetry 列)、④走 trade 侧补 game_id 贯通(ad.yaml契约+DTO+列+透传四步链 + admin group by),均在收益侧自有表,零跨模块写 seam;明确否决「往 telemetry game_telemetry_game_stat 加 ad/income 聚合列」路径(telemetry-api 仅暴露 3 枚举、无 Stat 写 API,insertOrAccumulate 是冻结签名 8 列硬编码、trade pom 不依赖 telemetry,「加列」实为新建跨模块 Feign 写 seam,非加列,且会把恰一次保证从 ad/trade 的 uk_trace/uk_source 错位到 telemetry 的 uk_event_id,详见 §4.1);(c) 复用优先于新建——③④复用 ad/trade 现表与现幂等键,仅④的 game_trade_income.game_id 占 V14 一次 ALTER,不新建独立宽表(维度=游戏×日,现表已兼容)。
成本/单位口径说明(避免跨平面误读):收益侧(ad/trade)DB 全程用分(BIGINT 整数),成本侧(编排器 newapi_cost.py/report.py)全程用元(浮点)——两者分属两个平面、两个精度域,对账时须显式换算,禁止把「元」成本与「分」产值同列求差(详见 §6.3)。
本波不碰任何业务代码,仅交付本 review 版 spec,下一步进 execution 版才落地③④后端埋点。
1. 背景与目标
1.1 B2 回路现状(已 e2e 真实,但有四类指标缺口)
B2 数据回路当前是 HTTP 请求线程内同步事务实现(MQ 消费者 TelemetryEventConsumer 是空壳,真实链路全在 EventIngestServiceImpl.ingestBatch 同步 @Transactional 内),事件 → quality_score → feed 重排已端到端接通、有单测 + e2e 证据。成本侧另在编排器平面:newapi_cost.py 读 new-api logs.quota 权威成本已落(薄片①),与后端 telemetry/trade 收益侧是两个分离平面。
链路形态澄清(消除契约描述与实现的张力):
contracts/events.schema.json:5与V5.0.0建表头注描述的「→ MQ 异步入库 → qualityScore → feed」是目标态(M5 余项),当前实现为同步事务写(见上)。本波不改契约/建表的链路描述,execution 一律以代码现状(同步写)为准,勿以契约描述误判链路形态。
1.2 四类指标缺口盘点(带证据)
| 指标 | 权威源现状 | 缺口 | 证据 |
|---|---|---|---|
| ① LLM token 计费 | 已落地(编排器计量 + new-api logs.quota 权威 + 报告 §8 渲染;实测权威 ¥0.031/款 来自模型 §5 merge-prod-20 批实测) | 批级总量、真实数据待下次批跑;per-game 仅「批级总量÷acceptCount」近似 | llm_client.py:166-197/newapi_cost.py:136-141/report.py:316;模型:48,63 |
| ② 素材生成成本 | 零数据 | ComfyUI 未部署,当前生成管线 assets 恒空集合,无文生图 |
AigcGenerateExecutor.java:396(setAssets(emptyList()));模型:46,61 |
| ③ 广告产值 | 数据源已落、按游戏可聚合(ad 侧 game_ad_revenue 有 game_id + idx_game) |
缺按游戏粒度的聚合查询/报表落点;反作弊仅幂等键+IP 限流桩 | V6.0.0__create_game_ad.sql:54-77;模型:25,61 |
| ④ 创作者应得 | 分账流水已落(game_trade_income 有 net/share_rate 快照) |
trade 侧零 game_id 引用(seam DTO 丢弃 game_id)→ 按游戏归集断裂;IP 叠加分账未实现(代码恒 80% 平铺) | V7.0.0__create_game_trade.sql:59-79;AdRevenueRespDTO.java:17-40;SettlementServiceImpl.java:43-44 |
1.3 本波补什么
一句话:把③④从「全平台日期聚合」升到「按游戏(game_id × 日)可 curl/DB 取到」,把①的口径锚定为「per-batch 均摊(MVP)+ 待网关账单替换假设单价」,把②降级为「登记口径、接文生图时再建桶」。产出 = 用实测列替换【模型】§1/§3/§5 的【假设·待测】标注(模型:61)。
2. 非目标(明确不做)
继承 M4 变现真实化边界 + newapi review v2 推迟裁定,本波显式不做:
- 真实记账 / 真实支付 / 真实打款:trade 结算链当前是 mock provider(
MockAdProvider单次收入=ecpmFloor/1000),本波不接真实广告联盟、不接微信支付、不做提现打款真实化。 - per-creator 用户开通 + 支付→配额同步 + 订阅/充值 + UI 购买流:按 newapi review v2 推迟到支付通道真实化后(阻塞 ICP 7-20 工作日 + 微信进件 1-3 周,3-5 周内无法 e2e;订阅定价未定)。
- 对账重建:渠道结算单 T+1/月结对账、三方对账(微信 trade_no ↔ 平台订单 ↔ new-api top_ups.trade_no)属支付真实化波,本波不做。
- 素材 GPU 成本桶(无源则降级):ComfyUI 未部署、
assets恒空 = 零生成零成本,本波不建素材成本桶,仅登记「接文生图/GPU 渲染时再补独立桶」口径(new-api logs 不覆盖 GPU,除非图片模型也走 new-api 通道)。 - 控成本 / 生成定价锁价售卖:生成成本可忽略——实测权威 ¥0.031/款(【模型】§5 merge-prod-20 批实测:20 创意/57 调用/19 accept,¥0.59 总/¥0.0103 次);newapi review §0 另给 ¥0.009/款(单次 602 quota 估算,非初稿 ¥0.02-0.04,评审 F 已否定高估 3-5×)——两数来源不同(0.009=单次 quota 推算,0.031=批实测),非连续区间两端,execution 引数以模型 §5 实测 ¥0.031/款为准。计量的 MVP 价值是「成本归属/基建预演」非「控成本」;new-api QUOTA_PER_UNIT/FX 是假设值,只供成本台账,不得用于对外锁价(硬边界,见 §6/§8.1)。
- MQ 异步化(M5 余项):当前同步写已 e2e 闭环,MQ 化前置依赖 mini-infra 补 RocketMQ/Nacos(当前缺),默认不纳入本波(见 §5 权衡,留待裁决)。
- IP 叠加分账真实落账:IP 素材标记字段在 ad/trade/project 现表全缺,「该游戏是否用 IP + IP 方是谁」数据源完全缺失,本波不建 IP 分账流水,仅在 share_rate 快照位为口径演进留位(见 §4.4)。
3. 推荐方案(四类指标:数据源 → 挂点 → per-game 落库)
3.1 数据血缘总图(四类指标如何汇到 per-game 载体)
flowchart TB
subgraph GEN["生成平面(编排器 / aigc,成本侧)"]
LLM["LLM 调用<br/>设计1+对抗1+修复≤1+裁判1 ≈4±1次"]
NAPI[("new-api PG logs<br/>type=2 消费日志<br/>sum(quota) 含真实倍率")]
CMFY["ComfyUI 文生图<br/>(未部署·assets 恒空)"]
LLM -->|model×token_name 聚合| NAPI
CMFY -.->|零数据·本波不建桶| X1["②素材成本=降级登记"]
end
subgraph PLAY["消费平面(telemetry / ad / trade,收益侧)"]
IMP["ad_impression / ad_reward<br/>@PermitAll + 幂等键 + IP限流"]
ADREV[("game_ad_revenue<br/>★有 game_id + idx_game<br/>revenue_amount(分)")]
SETTLE["SettlementJob T+1<br/>recordIncome 逐笔入账"]
TRDINC[("game_trade_income<br/>net=gross×share_rate<br/>⚠无 game_id 列")]
IMP -->|bill 计费| ADREV
ADREV -->|getUnsettledRevenue| SETTLE
SETTLE -->|分账后流水| TRDINC
end
subgraph SINK["per-game 读出口(本波裁决:收益侧自有表·零跨模块写)"]
Q3["③ ad 侧 group by game_id<br/>(零建表·idx_game 已就位)"]
Q4["④ game_trade_income 加 game_id 列<br/>(V14 ALTER) + admin group by"]
end
NAPI -.->|①per-batch 均摊<br/>无 game_id 列·MVP 不透传| REPORT["报告 §8 / 成本台账<br/>estCostYuan÷acceptCount"]
ADREV ==>|③广告产值<br/>ad 侧天然 per-game| Q3
TRDINC ==>|④创作者应得<br/>补 game_id 贯通| Q4
style ADREV stroke:#2a2,stroke-width:2px
style Q3 stroke:#22a,stroke-width:2px
style Q4 stroke:#22a,stroke-width:2px
style X1 stroke:#a22,stroke-dasharray:4
读图要点:双线箭头(==>)= 本波要打通的 per-game 真链路(③④,均在收益侧自有表);虚线(-.->)= 本波接受降级/不建桶(①均摊、②无源)。绿框
game_ad_revenue= 唯一天然 per-game 就位的源;③只读聚合不建表、④补 game_id 列;否决往 telemetrygame_telemetry_game_stat加 ad/income 列(理由见 §4.1)。
3.2 指标① LLM token 计费 —— 维持现状 + 锚定 per-batch 均摊口径
- 数据源:new-api PG
logs(type=2,sum(quota)含真实模型倍率)为权威;llm_client.py累加 usage 为 fallback/交叉校验。 - 挂点:已就位,无需新挂点(
newapi_cost.py只读、零生成路径依赖、零回归)。 - per-game 落库裁决:MVP 接受 per-batch 时间窗均摊(
report.py:316estCostYuan÷acceptCount),不强行把 game_id/trace_id 透传进 new-api 网关(new-api logs 无 game_id/version_id/trace_id 列,要 per-game 需在生成调用编码 token_name 或时间戳夹逼单 task,复杂度高、价值低——成本本身可忽略)。本波仅显式声明此为 MVP 近似口径,真 per-game 归属留作「后续可细化」(模型:63)。 - 次薄片(可选):按
--platform-token标签分「平台铺量 vs 创作者自产」成本桶(机制已在newapi_cost.py:111-172预埋,需给 agent-loop 批跑挂独立 token_name 标签才生效);按创作者 token-name 归属拿「每创作者成本」,绕开 per-creator 建号阻塞。
3.3 指标② 素材生成成本 —— 降级登记,不建桶
- 数据源现状:零。Tier1 自研 Canvas 用代码渲染、无需文生图,
AigcGenerateExecutor.java:396assets恒空。 - 裁决:本波不建素材成本桶。仅在 spec/模型登记口径:「接文生图/GPU 渲染时再建独立成本桶;若图片模型走 new-api 通道则可纳入同一 logs.quota(type=2),否则需独立 GPU 计量源」。避免后补的同时不引入空表。
3.4 指标③ 广告产值 —— ad 侧天然 per-game,加聚合 + 看板维度
- 数据源:
game_ad_revenue(计费级台账,game_id/creator_user_id/event_type/revenue_amount(分)/stat_date/trace_id,已建idx_game(game_id,stat_date))。收入口径=reward 的 impression 不计收入(防双算)、eCPM 取广告位ecpm_floor、归因projectApi.getCreatorUserId(gameId)、幂等键(traceId,event_type,tenant_id)命中 uk_trace。 - 挂点:ad 侧已是按游戏可聚合的天然落点,无需改 ad 表结构,只需加「按 game_id × stat_date 聚合查询 + 看板维度」。
- per-game 落库裁决(已定向·非二选一):③直接在 ad 侧加聚合查询 + admin 报表 group by game_id(零冗余、读在 ad 模块、改动面最小),不往
game_telemetry_game_stat加列(避开新建 telemetry 写 seam,理由见 §4.1)。归因creator_user_id在计费时已反查定格落库快照(V6 注释「由 game_id 反查后落库定格」),聚合直读已定格的 game_id/creator_user_id,与运行时 project 状态解耦——被删/下架游戏的历史收入仍按定格快照可聚合、不漏账、不在聚合期重新反查 projectApi。
3.5 指标④ 创作者应得 —— 补 game_id 贯通 trade 侧(最小改动链)
- 数据源:
game_trade_income(分账后流水,net=gross×share_rate、share_rate 快照定格、source_ref=game_ad_revenue.id对账锚、trace_id全链路)。 - 断裂点:跨模块 seam DTO
AdRevenueRespDTO只有 5 字段(id/creatorUserId/revenueAmount/statDate/traceId),game_id 不在其中 → trade 整个模块零 game_id 引用、game_trade_income无 game_id 列。 - 裁决:要让④按游戏归集,走最小改动链四步(缺一即断):
- 契约
ad.yaml:x-feign-contracts.operations[getUnsettledRevenue].output.itemFields加gameId: Long(ad.yaml:389-394 是该 DTO 字段的授权源,DTO 注释「字段严格对齐契约·不多不少」,先改契约再改 DTO,否则契约与代码漂移)。 AdRevenueRespDTO加gameId(与上一步契约同步;新增字段不算 breaking,但触碰已合入契约须通知 ad/trade stakeholder)。game_trade_income加game_id列(V14 ALTER,裸 BIGINT 业务列无 FK;V14 两副本字节一致同 §4.4,禁第三副本入 trade 单模块——V7 残留三副本是反面模板不可仿照,详见 §4.4)。recordIncome透传 gameId(注意现签名为 7 参,加 gameId 后须同步改调用方 settle)。
- 契约
- 挂点:照搬 Wave4
notifyIncomeChanged范式——聚合/透传一律只在 firstRecord 真实入账分支内执行(命中 uk_source 的幂等回查分支 firstRecord=false 不得累加/重计,否则同 source_ref 重放会虚高)、失败不中断结算循环仅 log.error、系统身份withSystemIdentity注入 LoginUser(id=0) + finally clearContext 防 NOT NULL 拒绝。④落 trade 自有表 = 同事务安全、零跨模块写,否决任何「telemetry 回写卷入分账事务」。
3.6 指标①③④ 与看板的关系
A1 看板(平台产值/创作者应得)落点 = 扩 TradeAdminController.getRevenueReport(现仅按 settle_date 区间聚合 Σgross/Σnet)加 game_id/creator group by 维度(③读 ad 侧聚合、④读 trade game_trade_income.game_id)。因③④已定向落在 ad/trade 自有表(不落 telemetry,见 §4.1),看板读口走 ad/trade 的 admin 端而非 telemetry game-stat 读口。本波只到「按游戏可 curl/DB 取到」,看板 UI 渲染属 A1 看板波。
4. 数据模型取舍
4.1 ★核心裁决:③④落点为何选「收益侧自有表」而非「telemetry 加列」
现状:V14 当前未占用(实测 ls V14* exit=2);V12/V13(community/biz)已双副本落盘、Wave4 已收口(contracts/db-schemas/ + yudao-server/.../db/migration/ 各两副本),故 V14 是已收口后的下一空位、无并行撞号风险。Wave4 execution §0.2:409 登记「V14 预留给 W4 埋点加列(本波不写,仅占号)」。
flowchart LR
Q{"③④聚合落点?"}
Q -->|★选定·收益侧自有表| A["③ ad 侧加聚合查询(零建表)<br/>④ game_trade_income 加 game_id 列(V14 ALTER)<br/>+ admin group by game_id"]
Q -->|❌否决·telemetry 加列| B["往 game_telemetry_game_stat<br/>加 ad/income 列"]
A --> A1["优点:复用 ad/trade 现幂等键<br/>(uk_trace / uk_source)<br/>同事务安全·零跨模块写 seam<br/>仅④占 V14 一次 ALTER"]
B --> B1["❌真实成本=新建跨模块 Feign 写 seam:<br/>telemetry-api 仅 3 枚举无 Stat 写 API<br/>insertOrAccumulate 冻结签名 8 列硬编码<br/>trade pom 不依赖 telemetry<br/>且恰一次保证从 uk_trace/uk_source<br/>错位到 telemetry uk_event_id"]
style A stroke:#2a2,stroke-width:2px
style B stroke:#a22,stroke-width:2px,stroke-dasharray:4
裁决依据(为何否决 telemetry 加列、为何不新建宽表):
- 否决「加列到
game_telemetry_game_stat」:该路径表面是「加列」、实为新建跨模块写 seam——game-module-telemetry-api仅暴露 3 个枚举文件(TelemetryEventEnum/ErrorCodeConstants/EventProcessStatusEnum),无任何 Stat 写 API;game_telemetry_game_stat的唯一写通道insertOrAccumulate是冻结签名的 8 列硬编码INSERT...ON DUPLICATE KEY UPDATE(数据回路小波·修2,只含 6 计数器);game-module-trade-serverpom 仅依赖 ad-api + community-api、不依赖 telemetry。要让 ad/trade 金额写进 telemetry 表,须「新增 telemetry-api 写 seam(新跨模块 Feign 契约 + @Primary 实现)+ 扩冻结的 insertOrAccumulate」或「trade 直写 telemetry 表破模块边界」,均非『加列』。更关键:telemetry 聚合的恰一次保证来自入库层uk_event_id(GameStatMapper:72 注释明证「聚合层纯加法、恰一次由 uk_event_id 挡重放」),而 ad/trade 的去重轨道是uk_trace/uk_source——把金额累加进 telemetry 表时,uk_game_date只保证「同游戏同日一行」、绝不保证「同一笔不重复累加」,防线只能来自上游 firstBilling/firstRecord 判定(见 §6.1)。故 telemetry 加列既增跨模块成本、又埋幂等错位雷,裁定否决。 - ③ = ad 侧加聚合查询:
game_ad_revenue已有game_id+idx_game(game_id,stat_date),直接 group by 即可,零建表、零 V14。 - ④ = trade 侧补 game_id 贯通 + 占 V14 一次 ALTER:
game_trade_income加game_id列(裸 BIGINT、无 FK),落在 trade 自有表 = 同事务安全、零跨模块写,配合 §3.5 四步链贯通 game_id。 - 不新建独立宽表:W4 维度 = 游戏×日,ad/trade 现表已兼容;新建宽表只引冗余 + 独立一致性维护,无收益。
4.2 ④ game_id 加列(ALTER)落地铁律
④ 的 game_trade_income.game_id 加列复用 V10(本仓唯一 ALTER ADD COLUMN 先例)两条铁律:
- 新增列若进唯一键必须 NOT NULL(V10
event_id VARCHAR(64) NOT NULL DEFAULT '',头注守门①「MySQL 唯一键允许多 NULL → 可空则幂等失效」)。本波 game_id 不进唯一键(uk_source 不含 game_id),但仍给NOT NULL DEFAULT 0兼容存量行。 - 加列须给 DEFAULT 兼容存量行(存量 trade_income 行 game_id 回填 0 或 later 反查补,execution 定)。
- ★ALTER 放宽业务列凡涉及写路径必须复核审计列/系统身份(engineering-conventions §1.2:42 红线,V11 漏 creator/updater 即此雷)。④ game_id 由
recordIncome/settle现有写路径透传填充、写路径已在withSystemIdentity范式内,不引新写点。注:因③④已否决「telemetry 加列」(见 §4.1),本波无 telemetry 表回写,故不存在「trade 结算任务跨模块回写 telemetry」的系统身份/事务复核问题。
4.3 按游戏粒度的聚合策略:同步写 or MQ 异步
- 现状:B2 回路是同步写(
EventIngestServiceImpl.ingestBatch在 HTTP 事务内完成聚合 + 远程回灌 feed),MQ 消费者空壳。 - 裁决倾向:本波沿用同步写(与现有 B2 一致、零基建依赖)。③广告产值=ad 侧只读聚合查询(group by game_id),无写累加、无幂等风险;④创作者应得=
recordIncome透传 gameId 后写入game_trade_income.game_id,透传一律只在 firstRecord=true 真实入账分支(命中 uk_source 的回查分支不写),按游戏归集由 admin 侧 group by game_id 读出。MQ 异步化留待 §5 权衡裁决(前置缺 RocketMQ/Nacos)。
4.4 契约扩展(三处,缺一即断)
flowchart LR
subgraph C["W4 埋点契约扩展面"]
DB["DB #2 Flyway<br/>V14 两副本字节一致<br/>(contracts/db-schemas/ + yudao-server/.../db/migration/)<br/>禁第三副本入单模块"]
EC["错误码段<br/>挂 telemetry→复用 104<br/>挂 ad→复用 111<br/>挂 trade→复用 106<br/>★无独立新段"]
EV["events.schema.json<br/>★仅当新增遥测事件才动<br/>+ 同步 TelemetryEventEnum 枚举"]
end
DB -.常规收口.-> SEQ["V14 单序列资源<br/>(V12/V13 已落·无并行撞号)<br/>两副本字节一致 + 先 push 再 pull"]
EV -.本波不动.-> RED["③④不新增事件名<br/>→ events.schema.json/枚举零改动<br/>(红线本波不适用)"]
style RED stroke:#2a2,stroke-width:2px
- DB #2 Flyway V14:命名
V{x.y.z}__{desc}.sql;两副本字节一致(contracts/db-schemas/ 授权源 + yudao-server/.../db/migration/ 唯一执行副本,md5 全等);绝不放单模块 -server/db/migration/(同版本多 classpath jar 触发 Flyway 重复校验失败,V11 守门①根因);已合入禁改、回滚写新补偿迁移 V14.0.1 DROP。V12/V13(community/biz)已双副本落盘、Wave4 已收口,V14 为无争议的下一空位、无并行撞号——只需常规单序列收口纪律(两副本字节一致 + 先 push 再 pull)。★防呆:V14 ALTER 的是game_trade_income,但 V14 文件禁止放game-module-trade-server/src/main/resources/db/migration/——本仓 V7 残留第三副本(trade 单模块 src,与两授权副本 md5 全等 dd5bb43…)是反面模板、不可仿照,照它布局会正好踩中 Flyway 重复校验雷。 - 错误码段:③在 ad 复用 111 / ④在 trade 复用 106,W4 埋点无独立新段;新增子码在对应
-api的ErrorCodeConstants.java按new ErrorCode(1_106_00X_***,"中文")登记(③只读聚合通常无需新错误码)。 - events.schema.json(本波结论:不动):③④仅在 ad/trade 侧加聚合查询/加 game_id 列、不新增遥测事件名,故 events.schema.json 与
TelemetryEventEnum本波零改动。补确证(review 阶段已结案,非 execution 待办):枚举已含AD_IMPRESSION/AD_REWARD/INCOME_SETTLED(TelemetryEventEnum.java:45-47),events.schema.json eventRegistry 已登记ad_impression/ad_reward/income_settled(:66-68),契约↔枚举两处已双向就位。仅当未来确需新增事件名才回到双处同步红线(conventions §1.2:43:只改契约不改枚举 = 事件静默 rejected,accepted:0 但信封 code:0,鉴权波 user_login e2e 实证)。 - tenant_id 单租户口径统一表述(避免实现 agent 混淆):DB 列
tenant_id BIGINT DEFAULT 0(V5/V12/V13 列默认);运行期系统身份/聚合任务注入的 LoginUsertenantId=1L(随 execution §8.2 范本,tenant.enable=false)。两者分属 DB 列默认与运行期注入两层、不矛盾,execution 须显式声明。
4.5 全仓零 FOREIGN KEY 硬范式
所有跨表关联靠业务列 + 应用层维护,DB 层不建外键(yudao 一贯范式,14 个迁移文件零 FK 命中)。④若加 game_trade_income.game_id = 裸 BIGINT 业务列,逻辑关联 game_project.id,无 FK。归属隔离靠 Mapper 显式 creator_user_id/user_id WHERE 谓词 + getLoginUserId()(token 解析非前端入参),非 @DataPermission 框架(黄金模块零此注解)——W4 客户侧端点同此口径,勿假定框架已隔离。
5. 关键权衡
| 权衡维度 | 选项 A | 选项 B | 本 spec 倾向 |
|---|---|---|---|
| 同步埋点 vs 异步聚合 | 同步写(现状 B2,零基建依赖,事务内完成) | MQ 异步(快速 ACK + 削峰,前置缺 RocketMQ/Nacos) | A 同步写(与现状一致、本波不引基建;MQ 化作 M5 余项留裁) |
| 埋点精度 vs 性能 | 每条 engagement 事件同步触发 feed rank UPDATE(现状,高频写放大) | 按 gameId 节流/批量回灌 | 维持现状(同步写每 play_end 打一次 upsertRank),性能优化留作观察项,热门游戏 rank 写放大若实测成瓶颈再做 |
| 成本源权威性 | new-api logs.quota 实测(含真实倍率) | 客户端估算 fallback(假设单价 2.0) | A 权威 + B fallback 交叉校验(已落;权威为准,client 降 fallback) |
| per-game 粒度(①成本) | per-batch 时间窗均摊(已落,无 game_id 列) | 真 per-game(需透传 game_id 进网关) | A 均摊(成本可忽略、透传复杂度高价值低,MVP 接受近似,显式标注) |
| per-game 粒度(③④收益) | 收益侧自有表(③ ad 侧只读聚合 + ④ trade 加 game_id 列) | 往 telemetry game_telemetry_game_stat 加列 / 新建独立宽表 | A 收益侧自有表(零跨模块写、复用 uk_trace/uk_source 幂等;telemetry 加列实为新建跨模块写 seam + 幂等错位,宽表引冗余——均否决,见 §4.1) |
| eCPM/人均展示档 | 锁单值 | 保留三档敏感性扫描(15/30/60) | B 三档扫描(外部核查仅从业者区间 20-80 低置信,须保留敏感性 + 显式声明「非收入预测」,模型:4 性质声明) |
6. Blast Radius / 风险 / 兼容
6.1 挂点触及模块 vs Wave4 已挂 community notify
flowchart TB
W4["W4 埋点挂点"]
W4 --> AD["ad: 加 game_id×stat_date 聚合查询<br/>(只读·已有 game_id·零写累加)"]
W4 --> TRD["trade: recordIncome 透传 gameId<br/>+ game_trade_income 加 game_id 列(V14)<br/>+ admin group by"]
W4 --> ORC["编排器: newapi_cost.py<br/>已落·零改动"]
WAVE4["Wave4 已挂: settle firstRecord<br/>→ communityNotifyApi.notifyIncomeChanged"]
TRD -.同一挂点区.-> WAVE4
WAVE4 --> CHK{"撞点检查<br/>(execution 阶段必验)"}
CHK --> OK["同进程 @Primary 本地调用<br/>同 withSystemIdentity 范式<br/>失败不中断结算循环<br/>→ 可并存·照搬范式"]
style WAVE4 stroke:#a82,stroke-width:2px
- 关键风险:④的 trade 挂点与 Wave4 已挂的
communityNotifyApi.notifyIncomeChanged在同一SettlementServiceImpl.settlefirstRecord 分支区。两者均为「结算任务触发的写库/通知埋点」,范式相同可并存(同进程 @Primary、withSystemIdentity注入 LoginUser(id=0)、失败仅 log.error 不中断循环)。execution 阶段须验:加 gameId 透传不破坏recordIncome既有 uk_source 幂等、不破坏 notifyIncomeChanged 触发条件。 - ★firstRecord/firstBilling 守门(防重复计数硬约束):③④的写/透传一律只在首次真实入账分支执行——③若在 ad 侧累加(本波③走只读聚合查询、无累加,此约束主要针对未来;若改累加须落 firstBilling=true),④的 game_id 透传只在 firstRecord=true 分支(命中 uk_source 的回查分支 firstRecord=false 不得写/累加)。
AdRevenueServiceImpl.bill命中 uk_trace 走幂等回查 returnAdBillingResult(existing, false)——任何挂在 bill 主路径而非 firstBilling 分支内的累加,会在重复上报时虚高 sum(gross)。验收项6须含「同 trace_id/source_ref 重放后按游戏 sum 不变」的 staging 实测(非仅单测 mock)。 - 事务边界(已定向·无跨模块写):
recordIncome是 @Transactional(写流水 + 累加账户同事务),④的 game_id 落在 trade 自有表game_trade_income(同事务、同进程、无跨模块远程调用),天然事务安全。因③④已否决 telemetry 加列(§4.1),本波不存在「telemetry 表回写卷入分账事务」的两难——明确否决任何把 telemetry 写操作放进 trade 事务的方案;④若未来需异步外写,照 Wave4notifyIncomeChanged事务内 try-catch 吞异常范式(提交后补写失败仅 log、不回滚已发的钱)。 - 幂等(三轨道不可混淆):③复用 ad
uk_trace(trace_id,event_type,tenant_id);④复用 tradeuk_source(source,source_ref,tenant_id)(均含 tenant_id 三列,单租户 tenant_id=1L 恒定下等价,但实现 agent 写去重谓词须按三列、勿按二列假设);①成本侧由 new-api logs 去重。注意:telemetry 的uk_event_id(聚合纯加法的恰一次来源,GameStatMapper:72)与 ad/trade 的 uk_trace/uk_source 是两条不相干的去重轨道——③④的恰一次只能靠上游 firstBilling/firstRecord 守门 + ad/trade 自身幂等键,与 telemetry uk_event_id/uk_game_date 无关(这也是否决 telemetry 加列的幂等理由,见 §4.1)。
6.2 兼容性红线
- DTO + 契约改动(成对):
AdRevenueRespDTO加 gameId = 新增字段不算 breaking(README §三),但必须同步改契约授权源contracts/api-schemas/ad.yaml的x-feign-contracts.operations[getUnsettledRevenue].output.itemFields(DTO 注释「严格对齐契约·不多不少」,只改 DTO 不改 ad.yaml = 契约与代码漂移)——故④的契约面是「ad.yaml itemFields + DTO + V14 列 + 透传」四点,缺一即断;触碰已合入契约须通知 ad/trade 双方 stakeholder。 - Flyway V14:已合入禁改;V12/V13 已落盘、Wave4 已收口,V14 为无并行撞号的下一空位,常规单序列收口(两副本字节一致 + 先 push 再 pull),且禁第三副本入 trade 单模块(V7 残留三副本是反例,见 §4.4)。
- 量纲红线:telemetry quality_score(0-100 运营质量分)≠ aigc 生成质量分(0-1);③④收益维度落在 ad/trade 自有表、不与 quality_score 同表混算(这也是③④不落 telemetry 的附带好处)。
- 三空列与本波无关(不强绑):
game_telemetry_game_stat现有三空列(favorite_count/report_count/load_fail_count,列+DO+VO 全就位但insertOrAccumulate不累加)是 B2 既有缺口——与本波单位经济埋点完全无关,明确排除在本波 DoD 之外(本波③④不碰 telemetry 表,更不会「反正要 ALTER 这张表」顺手补这三列;三空列累加若做属独立改动,留作 open question §8.2#9)。
6.3 对账口径硬约束(单位 / 取整 / 日期键 / mock 不变式)
execution 落③④对账时必须钉死以下四条,否则 per-game 聚合会误判失败或埋真实化漂移雷:
- 单位双平面(禁同列求差):收益侧(ad/trade)DB 全程分(BIGINT 整数);成本侧(编排器
report.py/newapi_cost.py)全程元(浮点,estCostYuan/yuan_per_quota)。两者两种单位、两个精度域——跨平面对照须显式换算并标注,禁止把「元」成本与「分」产值直接同列求差;若未来成本入后端台账须先定「成本也落分」口径再迁。 - 逐笔向下取整,禁用聚合反推:链路有两次独立 floor——
MockAdProvider.calcRevenue整除ecpmFloor/1000(2999 分→2 分)、SettlementServiceImpl算 net 又setScale(0,RoundingMode.DOWN)。逐笔 floor 之和 < 总额 floor,故 Σnet ≠ (Σgross)×0.80,偏差随该游戏笔数线性放大。验收项3 的对账以「逐笔」为权威(单笔 net==floor(gross×rate)、单笔 gross==floor(ecpm/1000)),禁用「总额×0.80」反推;若必须聚合对账须显式声明容差 = 该游戏笔数 ×(0,1) 分量级。 - 日期键统一上游 stat_date:③④按游戏×日聚合统一以
game_ad_revenue.stat_date(= tradesettle_date透传,V7 注释二者相等)为日期键,非 create_time。验收项3 须含「③ Σgross[game,stat_date] 与 ④ Σgross[game,settle_date] 同游戏同日相等」断言,锁死 T+1/SettlementJob重跑补结算下的日期一致性。 - mock 不变式(真实化阻塞前置):登记硬不变式——「
game_ad_revenue.revenue_amount在 MVP/mock 下 ≡ 实收净额 N(渠道扣率=0,故 G==N),net=gross×0.80 暂自洽」。真实接联盟波必须先确定 revenue_amount 存 G 还是 N:若存 G(总消耗,未扣渠道 40%),则 trade 分账须先 ×(1-渠道扣率) 再 ×creator-share(先 ×0.6 再 ×0.8),否则违反【模型】R3(N=0.6G、平台恒 0.12G),且已落game_trade_income历史快照不可回算 → 静默口径漂移。此条为支付真实化波的阻塞前置(见 §8.2#5/#6)。
7. 验收标准
每类指标须可由 curl/DB 实证「按游戏取到」,且不破坏 B2 现有回路:
| # | 验收项 | 验证方法 |
|---|---|---|
| 1 | ①成本 per-batch 口径显式标注 | 报告 §8 渲染含「单游戏成本估算 ≈ ¥X(per-batch 均摊·MVP 近似·未计素材 GPU)」+ new-api 权威成本可 newapi_cost.py --batch 跑出(已落,回归不破) |
| 2 | ③广告产值按游戏取到 | curl /admin-api/... 或 DB select game_id, sum(revenue_amount) from game_ad_revenue where stat_date=? group by game_id 返非空、与单游戏 impression/reward 台账对账一致;直读已定格 game_id/creator_user_id,被删/下架游戏的历史收入仍可聚合不漏账(不在聚合期重新反查 projectApi) |
| 3 | ④创作者应得按游戏取到 | game_id 贯通后 DB 可 select game_id, sum(net_amount) from game_trade_income where settle_date=? group by game_id;对账以逐笔为权威(单笔 net==floor(gross×share_rate)、单笔 gross==floor(ecpm/1000),trace_id/source_ref 双锚),禁用「Σnet==Σgross×0.80」反推(两次 floor 致聚合不等,§6.3);日期键统一上游 stat_date;③Σgross[game,statDate] 与 ④Σgross[game,settleDate] 同游戏同日相等 |
| 4 | ②素材成本口径登记 | spec/模型登记「素材桶降级、接文生图再建」,无空表引入 |
| 5 | B2 回路不破坏 | 既有单测全绿(EventIngestServiceImplTest 等);staging e2e:play_end → quality_score → feed 重排链路不变;曝光中性裁决不破(impression 零增量不建全零行);telemetry 表零改动(③④不落 telemetry) |
| 6 | 幂等(重放不重计) | ③④staging 重放同 trace_id/event/source_ref 后按游戏 sum 不变(非仅单测 mock);④ game_id 透传只在 firstRecord=true 分支、命中 uk_source 回查分支不写;uk_trace/uk_source(均含 tenant_id 三列)守门 |
| 7 | 系统身份/审计列 | ④ game_id 经现有 recordIncome/settle 写路径透传、写路径已在 withSystemIdentity 范式内,creator/updater 落 '0'(系统身份),staging 实测不撞 NOT NULL(单测 mock 测不到,须 staging 实证);本波无 telemetry 跨模块回写故无新写点系统身份雷 |
| 8 | 敏感性声明 | eCPM 三档(15/30/60)保留扫描 + 对外/对内数字保留【假设·待测】标注,不作预测口径 |
8. 待确认项(已收敛裁决 + 仍待创始人/排期拍的口径)
8.0 本 review 版已收敛的裁决(不再浮动,进 execution 不需再问)
定稿时把原「★待裁」中纯设计/范围裁决按 plan 倾向固化,使 execution 拿到闭合范围。
| # | 原待裁项 | 收敛结论 | 落点 |
|---|---|---|---|
| S1 | ③④埋点归属(并入 M4 / 独立小波) | 本波 = ③④后端按游戏埋点的独立小波(数据源已落、改动链清晰可做,不推迟 M4) | §0 范围边界 |
| S2 | ③④落库形态(telemetry 加列 vs 收益侧自有表 vs 宽表) | ③ ad 侧只读聚合 + ④ trade 加 game_id 列;否决 telemetry 加列(新建跨模块写 seam + 幂等错位)、否决新建宽表(引冗余) | §4.1 |
| S3 | events.schema.json / 枚举是否动 | 本波不动(③④不新增事件名,枚举+契约已双处就位) | §4.4 |
| S4 | V14 收口动作 | 常规单序列收口(两副本字节一致 + 先 push 再 pull);V12/V13 已落、无并行撞号;禁第三副本入 trade 单模块 | §4.4/§6.2 |
| S5 | ①成本 per-game 关联键 | 接受 per-batch 均摊(成本可忽略、透传复杂度高价值低,显式标注 MVP 近似) | §3.2/§5 |
| S6 | quality_score 分母口径 | 与本波解耦(B2 既有缺口,单独裁决,本波不碰) | — |
| S7 | 三空聚合列是否顺带补 | 与本波解耦、排除在 DoD 外(本波不碰 telemetry 表) | §6.2 |
| S8 | MQ 异步化是否纳入本波 | 不纳入(基建 RocketMQ/Nacos 未就位,留 M5) | §5 |
| S9 | IP 叠加分账 | 本波不建(IP 标识数据源 ad/trade/project 全缺,仅 share_rate 快照位留演进位) | §2#7 |
8.1 仍须创始人/排期拍的口径(仅 3 条,均为口径/真实化路线,非本波实现裁决)
✅ 创始人拍板 2026-06-11(全采纳):① 设硬红线——
PRICE_PER_MTOKEN_YUAN/QUOTA_PER_UNIT/USD_CNY三假设值拿到 new-api 真实账单/实配前一律保留【假设·待测】、不作预测口径、不对外锁价;② 现定 schema 形状——game_ad_revenue真实化后存 G(总消耗)+ N(渠道实收净额)+ 渠道扣率快照,分成从 N 算、对账用 G(实际值留真实化波,防历史不可回算);③ 对账归属推迟到支付真实化波。三条均不阻塞 execution——W4 埋点 review spec 自此解锁,待排期开 execution(③④独立小波)。
-
【★假设值红线】token 单价/换算三个假设值替换路径绑 A1 LLM 实名充值闸门:
PRICE_PER_MTOKEN_YUAN=2.0(llm_client.py:43 假设占位)、QUOTA_PER_UNIT=500000(newapi_cost.py:34 编译默认非库内实配)、USD_CNY=7.2(假设 FX)。需确认:拿到 new-api 网关真实账单/QUOTA_PER_UNIT 实配前,对外/对内数字是否一律保留【假设·待测】标注、不得作为预测口径、不得用于对外锁价(建议写成硬边界)。 -
【★真实化路线·gross 语义 + 分成深度】合并裁决:代码现状 net=gross×0.80 平铺(无渠道扣 40%、无 IP 叠加、无平台恒 20% 显式建模),与【模型】R3(渠道扣40%→净额N→创作者80%N→IP15-25%N 从创作者份额出→平台恒20%N)有分叉。本波裁定仅埋点观测(mock 链 revenue_amount 既是 G 也是 N、渠道扣率=0 暂自洽,见 §6.3 mock 不变式);待创始人/排期拍的是真实接联盟波的前置:
game_ad_revenue.revenue_amount真实化后存 G(总消耗)还是 N(渠道实收净额)——决定 trade 的 ×0.8 是否需先 ×0.6(若存 G 则 trade 须先 ×(1-渠道扣率) 再 ×creator-share,否则违反 R3 且历史快照不可回算)。字段命名/口径须在真实化波开工前定义防漂移。 -
【★对账归属·排期】③④与渠道结算单对账归属:本波③④到「平台内按游戏可 curl/DB 取到」即止;与渠道结算单 T+1/月结对账(外部账单 ↔ 平台台账)属支付真实化波,待排期定何时做、谁做。
注:原 §8.1#1/#2(落点二选一 / 范围归属)与原 §8.2#4/#8/#9/#10/#11(成本键 / 分母 / 三空列 / MQ / V14 收口)已在 §8.0 收敛,不再作为开放裁决;§8.2#5/#6(分成深度 / gross 语义)合并为本节第 2 条。