- B0 产出:化解 autoplan 5 个 critical,主 agent Opus 子代理独立核验真实代码(file:line) - Codex 首轮发现 11 项(4C+5H+2M)全采纳,推翻 v1 的 C5 幂等简化(恢复 eventId+uk_event_id) - Codex 第二轮确认轮:Go,11 项 spec 层全闭合、无新设计矛盾,留 4 个 B1 实现守门 - D-PUB 已拍板 α(审核通过即自动发布);完成线=数据回路 A/B 可证 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
21 KiB
王蓝莓小卖部黄金闭环 — 执行 spec(HJ-EXEC-006 · v2 经 Codex 复审硬化)
文档号:HJ-EXEC-006 | 版本:v2 | 日期:2026-06-09 | 分支:dev/2.0.0 | 基线:dev/1.0.0 上游:设计
王蓝莓小卖部黄金闭环-design.md(v2)、路线下一阶段路线-plan.md(B 轨)。 执行视图:MVP作战清单.md的 B0→B1→B2→B3。状态以MVP进度总账.md为准。 双模型评审:主 agent Opus 子代理独立核验真实代码(§3 file:line)+ Codex Eng 二审(4C+5H+2M 全采纳,对账见 §9)。v1→v2 关键反转:C5 恢复 eventId(v1 简化被证不成立)。
0. 目标与范围边界
目标:1 条垂直闭环做真,完成线 = 数据回路 A/B 可证(B 上报有效事件后排到 A 前 + 关 mock 仍通过 + 重放不重复计数)。
范围内:通用 clicker-plus 数据驱动 runtime + 确定性 PackageFactory;发布编排 审核通过→PUBLISHED→写 feed;最小数据回路(同步写 telemetry→算 quality_score→回灌 feed rank);小卖部皮肤 + ≥10 参数化种子。
非目标(YAGNI):真实 Dify/LLM(用模板/兜底)、真实 OSS(package 存 DB,退场契约 §3.4)、MQ producer/consumer/DLQ(同步写替代 §3.5)、创作者自助/定时发布(auto-publish §1)、真实鉴权/多租户(mock token + tenant_id=1)、3D/Cocos、为单品类扩 runtime(经营=配置皮肤)。
1. 决策点 D-PUB(✅ 已拍板 α,2026-06-09):审核通过即自动发布
Codex 二审指出 α 会改 APPROVED 常驻语义(admin UI 把 APPROVED 当可下架态,
game-admin/src/views/wanxiang/review/index.vue:88)。用户已拍板 α(2026-06-09);连带改动(本节下方)纳入 B1 实现。
现状:submitPublish(创作者)已声明发布意图→项目进 REVIEWING(1);reviewProject(APPROVE) 止于 APPROVED(2)(ProjectServiceImpl.java:291);APPROVED→PUBLISHED 无代码。
推荐 α(审核通过即发布):人工 reviewProject(APPROVE) 在同一本地事务内:记 APPROVE 审核记录 → 项目状态直接 REVIEWING(1)→PUBLISHED(4)(跳过常驻 APPROVED)→ 编排发布(§3.2)。
- α 连带改动(Codex M10):①
ProjectStatusEnum注释标注 auto-publish 下 APPROVED(2) 仅作审核记录值、项目态不停留;②contracts/api-schemas/project.yaml审核响应说明;③ adminreview/index.vue的状态筛选/「可下架」操作改挂PUBLISHED(4)(UNLIST 作用于已发布);④ 单测更新。 - compliance gate 与 α 的关系(Codex M10 澄清):
submitPublish的锁风门 verdict——block→不写库(admitted=false);review/pass→进REVIEWING(1)。compliance「pass」不自动发布,只是放进人工审核队列;只有人工 APPROVE 才触发 α 自动发布。
若选 β(审核通过→APPROVED 常驻,再单独「发布」动作):§3.2 触发点从 reviewProject(APPROVE) 改为独立 publishProject 端点,§1 连带改动里 ③ 反向(APPROVED 保留可下架),其余 §3 不变。localized。
2. 前置条件(开工即查,不补则静默假成功 —— Eng 双审实证)
后端/环境:
- 契约裁决:以
contracts/*.yaml/-apiJava 接口为准;不破坏契约、不改已合入 Flyway V1-V9,允许新增 V10 + 最小后端实现。 - staging:
BASE=http://100.64.0.7:48080,单体在 mini-desktop 隔离容器。重型构建/启动一律 mini-desktop,勿本机。
前端联调(Codex C3/C4/H8 —— 不补则试玩取包失败退 demo、like/share 漏采):
3. 环境变量真名 VITE_API_BASE(game-studio/src/api/request.ts:35);mock 中间件加门控(VITE_API_BASE 非空即关),启动打印 baseURL+mock 状态。
4. host 取包统一走 src/api/runtime.ts(经 request.ts),删除/改造裸 axios 同源取包(src/host/runtimeApi.ts:37);Play/Preview 显式注入 fetchPackage(Play.vue:196 现未注入);request 拦截器补 tenant-id:1 + token=test1。
5. fetchAndVerifyManifest 的相对 manifestUrl resolve 到 API base(inject.ts:174 现裸 fetch 同源)。
6. Play.vue 的 onTelemetry 白名单原样转发契约事件名(Play.vue:112 现把所有事件改写成 game_impression,吞掉 like/share);Feed 互动补有效 traceId(Feed.vue:151,168)。
7. sendBeacon 写死同源 /app-api(telemetry/index.ts:24-25,142,204)→ 改走 VITE_API_BASE 或常规 axios 通道;所有 staging 种子行统一 tenant_id=1。
8. 前端补 eventId(UUID)+ 真 traceId(buildEnvelope 现无 eventId、traceId 默认 '',telemetry/index.ts:101)。
3. 5 个 critical 的确定性解法(现状 file:line → 解法 → 契约/文件 → AC)
3.1 C1 — 「可见态」精确定义 + feed 读路径 enforce(Codex H9)
现状(已核验):ProjectStatusEnum DRAFT0/REVIEWING1/APPROVED2/REJECTED3/PUBLISHED4/UNLISTED5/BANNED6(game-module-project-api/.../ProjectStatusEnum.java:21-27);game_project.current_version_id 在(V1.0.0:24);game_runtime_package.status 0/1已发布/2(PackageStatusEnum);feed 默认查 zone_id=0(FeedServiceImpl.java:47)。Codex H9:feed 读只过 rank.zone/status/sort(FeedRankMapper.java:29),发布门禁 isGamePublished() 默认 true(FeedServiceImpl.java:285)——写错一条 rank 就出流。
解法 —— 可见态四条件谓词 + 读写双侧 enforce:
可见态 = project.status==PUBLISHED(4) AND project.current_version_id==versionId
AND game_runtime_package(versionId).status==PUBLISHED(1) AND game_feed_rank(game_id,zone_id).status==1
- 写侧:
FeedApi.upsertRank写status=1前校验前三条件(不满足拒写)。 - 读侧:去掉
isGamePublished()默认放行;buildStream对 rank 结果批查 project 二次过滤(或 join),漏发/降级游戏不出流。
AC:仅走完 §3.2 编排的游戏进 feed;手工写错的 rank 行不出流。
3.2 C2 — 发布编排(核心;本地事务硬约束 Codex H5/H6)
现状(已核验):reviewProject APPROVE 落 APPROVED(2)、全仓无 setStatus(PUBLISHED);submitPublish 止于 REVIEWING + compliance gate 已注入(ProjectServiceImpl.java:140);feed-api 无 FeedApi、getZones() 返空;可复用孤儿端点 RuntimePackageServiceImpl.publishPackage(:57-82)翻包 0→1,但 version 回写是 TODO(:77)、不写 feed、无调用方。Codex H5:跨模块 -api 是 @FeignClient 风格(ProjectApi.java:22),本地事务仅靠 @Primary 本地实现(ProjectApiImpl.java:21)。
解法 —— 新增 PublishOrchestrationService(project-server),α 在 reviewProject(APPROVE) 同一 @Transactional 内串联:
- 记 APPROVE 审核记录。
- 校验
game_runtime_package(currentVersionId).status==0预览就绪,否则抛PROJECT_PUBLISH_PACKAGE_NOT_READY(新错误码),事务回滚、审核不落。 RuntimePackageApi.publish(currentVersionId):包 0→1(复用publishPackage逻辑)。game_version(currentVersionId).status=3已发布由本编排显式写(不依赖 runtime TODO,Codex H6)。project.status=PUBLISHED(4)(首个 PUBLISHED 写入点)。FeedApi.upsertRank(...):写game_feed_rank(game_id, zone_id=launch_zone_id||0, version_id, quality_score=0, boost=0, sort_score=0, status=1, pinned=0)(发布基线 upsert,与 §3.5 算分 upsert 分流)。
事务原子硬约束(Codex H5,B1 必守):FeedApiImpl/RuntimePackageApiImpl 必须为 @RestController @Primary 本地委托 Service 的实现;PublishOrchestrationService 注入这些本地 bean;事务内禁止 Feign/HTTP/@Async/REQUIRES_NEW。加故障注入测试:feed upsert 抛错 → project/package/version 全回滚。
契约/文件:新增 game-module-feed-api:FeedApi+FeedRankUpsertReqDTO(含 mode:PUBLISH_BASELINE / QUALITY_REFRESH 分流,Codex M11);game-module-runtime-api:RuntimePackageApi.publish(versionId);project-server 加依赖 feed-api/runtime-api;新增 FeedApiImpl/RuntimePackageApiImpl(@Primary 本地);改 ProjectServiceImpl.reviewProject APPROVE 分支调编排。
AC:人工 APPROVE 后 /app-api/feed/stream(zone 0)含该游戏,顺序由 sort_score 真实驱动。
3.3 C3 — version 创建 + 完成回填闭环(Codex H7)
现状(已核验):game_version 表在(V1.0.0:44-61),全仓无 insert;AigcTaskDO.versionId 字段在(:43)靠未实现 difyCallback 回填;studio 轮询完成态直接取 task.getVersionId()(StudioServiceImpl.java:181)——只在 PackageFactory 插 version 不够。
解法 —— 落包步骤建版本 + 回填 aigc 任务,按 taskId 幂等:PackageFactory 成功落包后:① ProjectVersionApi.createForPackage(...) 创建/复用 game_version(status=2预览就绪);② 写 game_runtime_package(status=0);③ AigcTaskService.completeWithVersion(taskId, versionId) 回填 game_aigc_task.version_id + status=succeeded → studio 轮询据此拿到 versionId。current_version_id 仍由创作者 submitPublish(versionId) 写入。
契约/文件:新增 project-server GameVersionDO/GameVersionMapper/GameVersionService + ProjectVersionApi.createForPackage(-api);aigc 新增 AigcTaskService.completeWithVersion(幂等)。不改 V1 表结构。
AC:每个 clicker-plus 包对应真实 game_version 行;studio 轮询能拿到 versionId;重复回填幂等。
3.4 C4 — package 落地 + manifest 原样服务(Codex C4)
现状(已核验):game_runtime_package 无 package_json 列,manifestUrl 由 package_url 纯派生(RuntimeConvert.java:44、runtime.yaml:223);前端 fetchAndVerifyManifest 对原始字节算 sha256、checksum 缺省则跳过(inject.ts:169-186);当前最大 Flyway=V9。
解法(MVP 无 OSS,包存 DB,端点原样服务,抽象隔离):
- Flyway V10:
ALTER TABLE game_runtime_package ADD COLUMN package_json LONGTEXT NULL COMMENT 'MVP 临时整包;M3 接 OSS 后下线(退场契约)'。三处同步(contracts/db-schemas、模块、yudao-server)。 - 新增
PackageStore接口(runtime-server):putManifest(versionId,json)/getManifest(versionId):String;MVP impl=DB(读写 package_json),M3 换 OSS impl 调用方不变(退场契约)。 - 新增端点
GET /app-api/runtime/package/{versionId}/manifest:返回原始 JSON 文本,不包CommonResult、不 parse/re-serialize、不加换行(Codex C4);checksum=sha256(该响应文本),PackageFactory 写入时按相同字节计算并存game_runtime_package.checksum。 - 前端:相对
manifestUrlresolve 到 API base(§2.5);取包统一走 request(§2.4)。
AC:GamePlayer 取真包→manifest 来自 DB→sha256 校验通过→runtime 加载试玩,不退 demo 兜底(staging 须真包)。
3.5 C5 — eventId 幂等(v1 简化被推翻,恢复 design v2)+ process_status(Codex C1/C2)
现状(已核验):game_telemetry_event 唯一键 uk_dedup(trace_id,event,ts)(V5.0.0:38)、无 eventId;信封无 eventId、事件名 game_play_start/game_play_end(props duration_ms,completed);ingest 仅 dispatchToMq() 空壳不落库(EventIngestServiceImpl.java:120-124);process_status 语义 0待聚合/1已聚合/2失败(V5.0.0:13,30);前端无 eventId、traceId 默认 ''。
解法(Codex C1:恢复 eventId;C2:定义 process_status 事务):
- 恢复
eventId(每事件实例 UUID,幂等真身):events.schema.json+telemetry.yaml信封加eventId(必填);Flyway V10 给game_telemetry_event加event_id VARCHAR(64)+ 以uk_event_id(event_id)替换uk_dedup(UUID 全局唯一,彻底解决"同毫秒同名误吞 + 重放重计数");前端buildEnvelope生成eventId=crypto.randomUUID()+ 真traceId(每 play 会话一个)。 - 同步写本地事务(替代 MQ):
ingestBatch在一个@Transactional内:逐信封INSERT(uk_event_id 幂等)→仅"真插入"(affected>0)才累加game_telemetry_game_stat(按(game_id,stat_date)upsert)→ 算quality_score→FeedApi.upsertRank(mode=QUALITY_REFRESH)回灌 → 把该事件process_status=1已聚合;任一步失败整体回滚(事件不计)。MQ producer/consumer 只留契约+TODO。
quality_score 口径:clamp(60×完玩率+20×like率+20×share率,0,100),完玩率=completed_count/play_end_count,写 game_telemetry_game_stat.quality_score(DECIMAL5,2);回灌 game_feed_rank.quality_score(DECIMAL10,4)=当日 stat(精度转换),sort_score=quality_score+boost。
AC:B 上报 game_play_start+game_play_end(duration_ms,completed=true)+like+share(带 eventId+真 traceId)→ game_telemetry_event 有原始行 → stat.quality_score 变大 → rank.sort_score 同步变大;重放同批次(同 eventId)不重复计数。
4. 三批次执行(涉及模块/文件/步骤/失败路径)
4.1 批次①契约/状态机校正(B1)
落 §2 全部前置项(含 host 取包统一走 request、Play.onTelemetry 白名单、eventId/traceId);落 §3.1 可见态读写 enforce、§3.2 发布编排(@Primary 本地事务 + 故障注入测试)、§3.3 version 创建+回填、§3.4 V10+PackageStore+manifest 端点、§3.5 V10 eventId+同步写。
- 失败路径:包未就绪→拒发布并回滚;compliance block→不写库;feed 本体回填查不到 project→空态;编排任一步失败→全回滚。
- done:前端真连 staging;submit→人工 APPROVE→项目 PUBLISHED(4)+包 1+版本 3+feed rank(status=1);feed/stream 读到它。
4.2 批次②最小数据闭环(B2)← 护城河种子,A/B AC 在此锁定
PackageFactory(aigc 桩→确定性产 clicker-plus 包)→ 落包(建 version+runtime_package+PackageStore 存 package_json,回填 aigc 任务)→ §4.1 发布编排入 feed → 通用 clicker-plus runtime 试玩。
- runtime 必须真吐
game_play_start/game_play_end(duration_ms,completed)/like/share(带 eventId),经 Play.onTelemetry 白名单原样上报 → §3.5 同步写 → 算分 → 回灌。 - 失败路径:取包失败→不静默退 demo 兜底掩盖(demo 兜底仅 mock 链路)。
- done = 数据回路 A/B 验收协议通过(§6)。
4.3 批次③小卖部皮肤 + ≥10 种子(B3,可降级)
clicker-plus 配置出小卖部皮(点击售卖+极简库存)+ ≥10 参数化种子;撞 <15KB 预算先退纯 clicker-plus,数据回路 AC 不动。done:feed 有内容池(≥10)。
5. 接口/数据契约清单(contract-first,先改后写 —— 补全 Codex M11)
| 契约/产物 | 动作 | 位置 |
|---|---|---|
FeedApi + FeedRankUpsertReqDTO(含 mode:PUBLISH_BASELINE/QUALITY_REFRESH) + FeedApiImpl(@Primary 本地) |
新增 | game-module-feed-{api,server} |
RuntimePackageApi.publish(versionId) + RuntimePackageApiImpl(@Primary 本地) |
新增 | game-module-runtime-{api,server} |
ProjectVersionApi.createForPackage + GameVersionDO/Mapper/Service |
新增 | game-module-project-{api,server} |
AigcTaskService.completeWithVersion(taskId,versionId)(幂等) |
新增 | game-module-aigc-server |
PublishOrchestrationService |
新增 | game-module-project-server |
| pom 依赖:project-server→feed-api/runtime-api;telemetry-server→feed-api | 新增 | 各 server pom.xml |
错误码 PROJECT_PUBLISH_PACKAGE_NOT_READY(1-100 段) |
新增 | project-api ErrorCodeConstants |
V10:game_runtime_package.package_json LONGTEXT + game_telemetry_event.event_id+uk_event_id(替换 uk_dedup) |
新增(三处同步) | contracts/db-schemas/V10.0.0__*.sql + 模块 + yudao-server |
events.schema.json / telemetry.yaml 信封加 eventId(必填) |
改 | contracts/ |
runtime.yaml 新增 /manifest 端点(原样 JSON)+ manifestUrl 语义说明 |
改 | contracts/api-schemas/runtime.yaml |
| Mapper 自定义 upsert SQL(stat、feed_rank 两套 mode) | 新增 | 各 server Mapper |
project.yaml α 审核响应说明 + admin review/index.vue 状态筛选/操作 |
改 | contracts + game-admin |
game-package.schema.json |
不改(clicker-plus gameConfig 子 schema 作 templates/ 后续批) | — |
B1 实现守门(第二轮 Codex 确认轮残留,必守):①
event_id VARCHAR(64) NOT NULL再加uk_event_id(MySQL 唯一键允许多 NULL,必须非空,否则幂等失效);② V10 只能有一个被 Flyway 实际执行的副本(yudao-server/.../db/migration/),勿在多个 classpath jar 放同版本 V10(一个 V10 文件同时ALTERruntime+telemetry 两表 OK);③FeedRankUpsertReqDTO.mode=QUALITY_REFRESH只更 quality_score/sort_score,保留 boost/pinned/status;④ 新增-api包不得反向依赖-server或互嵌对方 DTO(防 Maven 成环;已核实现有方向 server→对方 api 无环)。
6. 验证(A/B 协议 + 通用判定 + 负路径)
A/B 协议(可执行):备同 zone 同 boost 的 A/B 两个已发布游戏(tenant_id=1)→记初始 feed 顺序→向 B 上报有效 game_play_start+game_play_end(duration_ms,completed=true)+like+share(带 eventId+真 traceId)→断言 game_telemetry_event 有原始行、stat.quality_score 变大、feed_rank.sort_score 同步变大→再请求 feed B 排到 A 前→重放同批次(同 eventId)不重复计数→关 mock 仍通过。
通用判定:不只看 200,须 code==0+结构符 yaml+非 mock 特征值;主 agent 独立复跑,不采信子代理自报。
负路径:错 token→401;停后端→不白屏;空数据→空态。故障注入:feed upsert 抛错→发布事务全回滚(§3.2)。
数据清理:固定 traceId/eventId 前缀 + 固定 gameId 清单 + cleanup SQL,验证前后表快照。
7. 回滚(4 独立单元,各自 commit)
① 前端联调(env/mock/token/tenant/beacon/取包统一/onTelemetry/eventId)|② 发布编排(PublishOrchestration+FeedApi/RuntimePackageApi @Primary+GameVersion+ProjectVersionApi+V10 package_json+PackageStore+manifest 端点)|③ telemetry 同步写+算分+回灌+V10 event_id |④ clicker-plus runtime+种子脚本。
8. 完成条件(B0→B3)
§6 A/B 协议通过(关 mock)= 本轮完成线;4 链路烟测登记总账;缺口 T+1 修复机制(标 owner/严重度/是否阻断/最晚日)。
9. 决策审计 + 双模型对账
| 项 | v1 | Codex 二审 | v2 处置 |
|---|---|---|---|
| C1 可见态 | 四条件谓词 | H9:读路径未 enforce | 读写双侧 enforce + 去 isGamePublished 默认放行 |
| C2 编排 | 复用孤儿端点 + @Transactional | H5:仅 @Primary 本地实现成立;H6:version 由 project 写 | @Primary 硬约束 + 故障注入测试 + version 显式写 |
| C3 version | 落包建版本 | H7:缺 aigc 回填闭环(studio 依赖) | + completeWithVersion 回填,taskId 幂等 |
| C4 package | V10 package_json + PackageStore | C4:manifest 端点须原样 JSON + 前端 baseURL resolve | 端点不包 CommonResult + 前端 resolve + 取包统一走 request |
| C5 幂等 | 砍 eventId 复用 (trace_id,event,ts) | C1:不成立(误吞/重计);C2:process_status 未定义 | 恢复 eventId+uk_event_id(推翻 v1)+ 同步写本地事务定义 process_status |
| 前端联调 | 6 前置项 | C3:host 取包绕 request;H8:Play 吞 like/share | + 取包统一走 request + onTelemetry 白名单 + Feed traceId |
| D-PUB | α 待点头 | M10:改 APPROVED 语义,admin 连带 | 已拍板 α(2026-06-09)+ 连带改 admin/enum/project.yaml/单测 |
| 契约清单 | §5 | M11:缺实现类/错误码/pom/upsert 分流 | §5 补全 |
评审轮次:① 主 agent Opus 子代理独立核验真实代码(5 critical 全实,C2/C5 有简化空间);② Codex 首轮(11 项 4C+5H+2M,全采纳,推翻 v1 C5 简化);③ Codex 第二轮确认轮(2026-06-09):Go,11 项 spec 层全闭合、无新设计矛盾,残留 4 守门见 §5 末。