docs(wave3): HJ-BUILD-004 review+execution 双 spec(隔离 staging+contractGaps+发布闸门/治理补全)
经 2 个调研编排(7+6 agent,1.16M token)+ 2 轮对抗审查产出: - review(HJ-BUILD-004):两主线 + 已锁决策 D1-D5(守草稿+studio / compliance只Gate / bucket级最小权限 / 单体+同步+mock token / ip不独立建) - execution(HJ-BUILD-004-EXEC):Phase A 跨主线收口(R1-R12)消除审查挑出的 blocker—— publish 唯一化(GAP-1 RespVO 权威,compliance 只交付 Gate seam)、ProjectApi seam 单点收敛、 封禁不破坏状态机(状态分支)、studio 治理登记、V6/V7 全局 G0、真实 API 路径、实测前置门 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8901350247
commit
5be2251498
240
docs/agent-specs/2026-06-08-wave3-staging联调与P0补全-review.md
Normal file
240
docs/agent-specs/2026-06-08-wave3-staging联调与P0补全-review.md
Normal file
@ -0,0 +1,240 @@
|
||||
# Wave3 Review — 隔离 staging 联调 + contractGaps 回填 + 发布闸门/治理补全
|
||||
|
||||
> **HJ-BUILD-004(review 版,结论先行供创始人决策)** |生成 2026-06-08
|
||||
> 上游:HJ-BUILD-003 草稿(studio+compliance+ip)+ Wave1/2 已建脊柱 + 本轮 7-agent 只读调研(INFRA/BACKEND/DOCA/GAPS)+ 2 轮对抗式审查
|
||||
> 定位:本文是 Wave3 **主 review**。HJ-BUILD-003 的模块 MVP 切片方法论(compliance 狠切、ip 薄壳、接入已建模块带回归门、三相编排)**全部继承**;本文补 003 缺的「隔离 staging 联调 + contractGaps 回填」主线,并把范围决策、审查纠正、验收标准收口到一处。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论先行(5 分钟拍板)
|
||||
|
||||
**一句话**:Wave3 把已建的 7 模块脊柱从 mock 推进到「隔离 staging 真实联调」,并补发布闸门与治理底座。两条主线、根子不同,建议并行但解耦。
|
||||
|
||||
1. **主线1(基础设施/联调,方向已验证,方案经加固后可落地)**:在 mini-infra 拉一套与现有服务**物理零交集**的隔离 staging 栈(独立 MySQL/Redis 实例 + 独立网络 172.30/卷/命名空间),回填 4 个 contractGaps,按 12 段闭环分「端到端真跑/需桩」验真。
|
||||
2. **MVP 单体启动只硬依赖 MySQL + Redis**(后端实测 `nacos.config/discovery.enabled=false`)。**Nacos/RocketMQ 非启动前置,本波建议不接线**(容器可拉起备用)。⚠️ 但「不接 RocketMQ 也能启动」**须先在 mini-desktop 实跑验证**(见 §三 前置门 G0)。
|
||||
3. **主线2(业务补全)= 按 Doc A 剩余 P0 补模块。存在范围冲突,须你拍板(§八 D1)**。优先级(有硬依赖不可平铺):**compliance > ip > biz ≈ community**;compliance 卡在 M3 发布关键路径咽喉、集成面最大。
|
||||
4. **对抗式审查重塑了方案(§二)**:3 个 CRITICAL(范围冲突 / 审核归属双写 / member 不在 fork)+ 多个高危(OOM 兜底、MinIO 软隔离、RocketMQ 启动未验、GAP-4 破坏性、Flyway V6/V7 散落)。**这些纠正已并入本文,是它与 synthesis 初稿的本质差异。**
|
||||
5. **Doc A 口径(已核对)**:55 项 P0,已建脊柱覆盖约 30 项;剩余主要在 **studio(7,创作主链路)/compliance(6)/biz(6)/community(5)/pay(1)**。**注意:community/ip/compliance/biz 四模块不补创作主链路(studio)**——这是覆盖盲区,是 D1 决策核心。
|
||||
|
||||
---
|
||||
|
||||
## 二、对抗式审查的关键纠正(必须先看 — 这些改变了执行路径)
|
||||
|
||||
| # | 严重度 | synthesis 初稿的错 | 已核实事实 | 本文采用的纠正 |
|
||||
|---|---|---|---|---|
|
||||
| C1 | CRITICAL | 主线2 平铺 community/ip/compliance/biz、删掉 studio | 已批准草稿 HJ-BUILD-003 + 创始人 2026-06-08 决策 = Wave3=studio+compliance+ip,community/biz→Wave4;studio 承接 7 项创作 P0(最大未覆盖块) | **范围回退为 D1 决策门**:推荐 studio(min)+compliance+ip,community/biz 留 Wave4 |
|
||||
| C2 | CRITICAL | compliance 新建审核状态机,称对 project「切分归属」 | project 已有并已测 `game_review_record`+`reviewProject()`+`ReviewDecisionEnum`+审核队列(9 单测绿) | **compliance MVP 只做锁风门 Gate,project 续拥审核状态机**(§八 D2 选项 B,避免拆除已测代码) |
|
||||
| C3 | CRITICAL | 推荐「解注释 yudao-module-member」启用产品端鉴权 | `yudao-module-member` 源码**不在 fork 内**(裁剪时整体移除,非注释),无源码可解注释 | **产品端 /app-api MVP 走 mock token**(`yudao.security.mock-enable`);引入 member 列独立技术项另评估 |
|
||||
| H1 | 高 | 仅 RocketMQ broker 限 -Xmx | MySQL 默认 innodb buffer pool 按宿主自取、Nacos 未限堆;OOM-killer 不保证只杀新栈 | **全容器 `mem_limit` + MySQL `--innodb-buffer-pool-size=512M` + Nacos `JVM_XMX` + 给现有 infra-* 设负 `oom-score-adj`**(OOM 优先杀新栈,护住现有服务=铁约束) |
|
||||
| H2 | 高 | MinIO「additive 新建 bucket 不影响现有」 | yudao 文件存储由 `infra_file_config` 运行时配置;新建 bucket/key/policy 是对**共享 MinIO 实例**的 IAM 写操作;配错越权读写现有对象 | **二选一**:bucket 级须给**最小权限 policy**(`Resource arn:aws:s3:::game-staging/*`,评审门核验 JSON);或采 §八 D3 物理隔离(新起 game-staging-minio 19000/19001) |
|
||||
| H3 | 高 | 「单体不需 RocketMQ,不启不影响启动」当事实 | `starter-biz-tenant` 传递依赖 rocketmq-spring,`application.yaml` 配了 name-server=127.0.0.1:9876+producer.group;producer 是否启动期连无日志证据 | **降级为「待验证」+ 前置门 G0**:mini-desktop 用 dev profile 指向不存在 name-server 实跑,看是否阻塞 ContextRefreshed;若阻塞则 staging 指向 game-staging-rmq-namesrv 或排除 RocketMQAutoConfiguration |
|
||||
| H4 | 高 | GAP-4 命名统一是「非破坏,老消费者忽略未知字段」 | events.schema.json 的 envelope/user 是 `additionalProperties:false`(严格);snake↔camel 是重命名严格字段 | **GAP-4 按破坏性变更处理**:版本并行或同步改全部生产/消费端(前端 SDK + telemetry @JsonProperty + MQ 落库);不能依赖「忽略未知」 |
|
||||
| H5 | 高 | biz「须核实 bpm fork 保留」 | yudao-module-bpm 在磁盘+根 pom,但 yudao-server pom L98 **被注释未装配**;Flowable 需 act_* 表,建库步骤未覆盖 | biz 若做须先解注释 bpm + 验证 Flowable 建表(更支持 biz 归 Wave4 不触 bpm) |
|
||||
| M | 中 | V6/V7 靠 classpath 合并,「启动后核实」 | 已核实 V1-V5 在 yudao-server、V6/V7 仅在 ad/trade 模块 jar | **开工即把 V6/V7 集中复制到 yudao-server/db/migration**(前置硬门,非事后) |
|
||||
| M | 中 | 回滚 `down -v`;无备份;compose 无 healthcheck;连接串机密未定义;172.30 未全量核验 | — | 全部并入 §三:备份/重建脚本、healthcheck+`depends_on: service_healthy`、机密走 env 占位、起栈前 `docker network` 全量核验 172.30 |
|
||||
|
||||
---
|
||||
|
||||
## 三、主线1:隔离 staging 拉起 + contractGaps 回填
|
||||
|
||||
### 3.1 隔离建栈(mini-infra,物理零交集 + 加固)
|
||||
|
||||
| 容器 | 镜像(占位,须按兼容矩阵二次定版) | 宿主端口 | 内存硬限(H1 加固) |
|
||||
|---|---|---|---|
|
||||
| game-staging-mysql | mysql:8.4.8(对齐现有) | 13306→3306 | `mem_limit=1.5g` + `--innodb-buffer-pool-size=512M` |
|
||||
| game-staging-redis | redis:7-alpine | 16379→6379 | `mem_limit=512m` + `--maxmemory 384mb` |
|
||||
| game-staging-nacos | nacos/nacos-server:v2.x | 18848/19848/19849 | `JVM_XMS=256m JVM_XMX=512m` + `mem_limit=768m` |
|
||||
| game-staging-rmq-namesrv | apache/rocketmq:5.x | 19876 | `mem_limit=512m` |
|
||||
| game-staging-rmq-broker | apache/rocketmq:5.x | 10911/10909/10912 | `-Xmx1g` + `mem_limit=1.5g`;`brokerIP1=100.64.0.8` |
|
||||
|
||||
- **网络**:独立 bridge `game-staging-net` 子网 172.30.0.0/16(起栈前 `docker network ls`+全量 `inspect` 核验含 stopped 网络无占用;查 daemon.json default-address-pool)。不接 infra-shared。
|
||||
- **卷**:`game-staging-*` 具名卷 / `/srv/game-staging/*` 全新目录;绝不挂 `/srv/infra/*`。
|
||||
- **OOM 保护**:给现有 infra-mysql/redis/minio/ragflow 设负 `oom-score-adj`(或给新栈正值),确保 OOM 优先杀新栈。**分步起:先 MySQL+Redis 起且 `free -g` 观察基线 → 再按需起 Nacos/RMQ。**
|
||||
- **healthcheck**:各容器加(mysqladmin ping / redis-cli ping / nacos actuator / namesrv 端口探活);broker `depends_on: {namesrv: condition: service_healthy}`;后端启动前 `wait-for-it` 探 13306/16379。
|
||||
- **MinIO(H2)**:默认 bucket 级隔离须最小权限 policy(仅 `game-staging/*`),bucket 名加随机后缀,留 `mc admin policy info` 证据;或采 D3 物理隔离。
|
||||
- **备份/回滚**:staging 数据定位为「可幂等重建」→ 提供自动化重建脚本(建库+导基表+Flyway);回滚用 `docker compose -p game-staging down`(**不带 -v**,保卷留证据),确认无用再单独 `volume rm`;钱财闭环验通后 `mysqldump` 存 `/srv/game-staging/backup`。
|
||||
|
||||
### 3.2 建库 + 框架基表(启动硬前置)
|
||||
1. 建空库(默认 `ruoyi-vue-pro`;单库,game_* 与 yudao system_*/infra_* 共 schema 前缀区分)。
|
||||
2. **先手动导入 yudao 框架基表** `game-cloud/sql/mysql/*.sql`(system_*/infra_* 含 tenant/user/menu/dict/file_config + 初始租户/管理员),否则多租户/登录/Swagger 不可用;此步使库非空,匹配 Flyway `baseline-on-migrate`。`tenant.enable=true` ⇒ game_* 带 tenant_id,须有效租户。
|
||||
3. **V6/V7 集中复制为前置硬门**(M):开工即把 `V6.0.0`/`V7.0.0` 从 ad/trade 模块复制到 `yudao-server/.../db/migration`,与 contracts/db-schemas diff 一致;构建后 `unzip -l yudao-server.jar | grep migration` 核验 7 个文件在 `BOOT-INF/classes/db/migration`。
|
||||
|
||||
### 3.3 后端接线(新建 application-staging.yaml,不改 dev/local)
|
||||
- `jdbc:mysql://100.64.0.8:13306/ruoyi-vue-pro`(dev 默认 127.0.0.1:3306 必改)
|
||||
- `spring.data.redis host=100.64.0.8 port=16379 password=${STAGING_REDIS_PWD}`(dev 默认公网 400-infra 必改)
|
||||
- 机密走**环境变量占位**(`${STAGING_DB_PWD}` 等),避免明文入库
|
||||
- **no_proxy/nonProxyHosts 放行 100.64.0.0/10**(mihomo 代理占 7890/9090,防 jdbc/redis/nacos 被代理劫持)
|
||||
- nacos/rocketmq:本波 `enabled=false` 保持(除非 D4 选接线)
|
||||
|
||||
### 3.4 启动验证前置门(按序)
|
||||
- **G0(H3,最先)**:mini-desktop 用 dev profile 指向不存在 name-server 实跑 `java -jar`,确认 producer 不阻塞 ContextRefreshed。阻塞则 staging 指 game-staging-rmq-namesrv 或排除 RocketMQAutoConfiguration。
|
||||
- **G1**:在 mini-desktop(15G,避免本机 OOM)`mvn -pl yudao-server -am clean package`(JDK17/Maven3.8.7)。
|
||||
- **G2**:`java -jar --spring.profiles.active=staging`;Flyway 自动建 13+ 张 game_* 表。
|
||||
- **G3 自检**:`/doc.html` 出齐 7 模块接口 + `/actuator/health` db/redis UP + `flyway_schema_history` ≥7 条 success + game_* 表全建。
|
||||
- **G4 鉴权**:管理端 `/admin-api/system/auth/login`;**产品端 /app-api 走 mock token(C3,member 不在 fork)**。
|
||||
|
||||
### 3.5 分段联调验真(12 段)
|
||||
- **端到端真跑(先验)**:(5)审核状态机 /(6)feed 按 zone SQL 出流 /(7)试玩 session /(8)互动 /(9)广告 mock 计费(ecpm/1000,幂等键 trace_id+event_type+tenant_id)。
|
||||
- **钱财闭环(次验,手动触发)**:(10)trade T+1 结算(xxl-job 关闭,手动调 SettlementJob→聚合×0.80→game_trade_income uk 幂等+累加 balance)/(12)提现(≥500 且≤balance,原子冻结→mock 打款)。
|
||||
- **必须桩(最后串)**:(2)Dify(手动 POST dify/callback 驱动状态机)/(3)runtime 编译(直写 build 成功态+伪 package_url)/(4)发布回写 project.game_version(TODO Feign 对接点)/(11)telemetry MQ 聚合(改同步直写绕 MQ)。
|
||||
|
||||
### 3.6 contractGaps 回填(4 个,Phase A 由契约 owner 定稿后再开真实联调)
|
||||
- **GAP-1 发布门禁明细**:project.yaml publish 仅 CommonResultBoolean → 新增 `ProjectPublishRespVO{admitted, gates: GateResultVO[]}`(key/label/pass/code/message)。**兼容性:新增字段而非替换 data 类型**。与 compliance 锁风门 -api 联动设计。
|
||||
- **GAP-2 GamePackage 取包端点**:runtime.yaml 仅返 manifest 元数据,CSP 沙箱 iframe 无法跨域 fetch。二选一:(A 推荐) RuntimePackageRespVO 加 `manifestUrl` + OSS CORS 放行宿主 origin + 宿主 fetch+sha256 注入;或 (B) 新增 `GET /package/{versionId}/full` 返全量 GamePackage。
|
||||
- **GAP-3 ShareMeta 缺 versionId**:ShareMetaRespVO 加 `versionId`(=project.currentVersionId);前端 onPlay 改 `/play/:gameId/:versionId`(路由已预留,无破坏)。**轻量低风险**。
|
||||
- **GAP-4 遥测命名(H4 破坏性)**:events.schema.json(snake) vs telemetry.yaml(camel)。**envelope/user 是 additionalProperties:false,重命名是 breaking**。定 camelCase(改 events.schema.json)须走版本并行或同步改前端 SDK+telemetry @JsonProperty+MQ 落库,迁移完再收紧严格模式。
|
||||
|
||||
---
|
||||
|
||||
## 四、主线2:Doc A 剩余 P0 模块(范围见 §八 D1)
|
||||
|
||||
### 4.1 模块拆解(错误码/Flyway 已对照 contracts/README §4 预留)
|
||||
|
||||
| 模块 | 错误码 | Flyway | P0 | MVP 切片 | 集成面 |
|
||||
|---|---|---|---|---|---|
|
||||
| **compliance** | 109 | V9 | 6(P-LIC-05/P-OPN-01/04/P-ACC-02/03/P-BIZ-11) | **只做锁风门 Gate**(聚合 aigc T-AGC-19 +(可选)ip 原子→pass/review/block)+ 分级 + 封禁降权;**审核状态机仍归 project(C2/D2-B)**;安全硬化 T-CMP-20~38 + 防沉迷/实名 + GDPR 全延后;RBAC 复用 yudao | 最大:焊 project.publish(替 mock 门禁+回归门)/feed(降权) |
|
||||
| **ip** | 108 | V10(薄壳可不占) | 0 | **优先答 openQuestion**:若锁风门用 aigc 风格原子即可,则 ip 本波**不建独立模块**,仅在 compliance 锁风门 -api 预留 ip 扩展点(避免孤儿/为 seam 而 seam) | 仅被 compliance 单向消费 |
|
||||
| **studio**(D1 选 A 时纳入) | 112(须同步登记 README,当前漂移) | V8 | 7(P-CRT-01/02/04/09/10/12/P-TPL-01/03 等创作主链路) | 草稿最小版(任务链调 aigc,MVP 可轮询进度) | 创作侧,调 aigc/project |
|
||||
| **community**(建议 Wave4) | 107 | V11 | 5(P-INC-01/P-NTF-01/02/03/05) | 通知编排+站内信+新人激励;纯事件消费方。**但上游 aigc/compliance/trade 当前不产 MQ 事件(全 TODO),且本波不接 RMQ → 本波跑不通** | 隐性:须改事件源生产侧 |
|
||||
| **biz**(建议 Wave4) | 110 | V12 | 6(P-BIZ-01/02/03/04/08/12) | B 端定制 BPM 闭环;**强依赖 yudao-bpm(当前注释未装配)+ Flowable 建表(建库步骤未覆盖)** | 中:新表+BPM |
|
||||
|
||||
### 4.2 接入已建模块 + 回归门(compliance 专属,最高危)
|
||||
- project.publish T-PRJ-05 当前 Wave1 mock → 改调 compliance 锁风门 -api:**必须带 project 既有 9 单测 + publish 流程回归断言**(Wave2 F5 教训)。
|
||||
- feed 接降权信号(MQ 或同步)。
|
||||
- **审核归属(C2)**:compliance 不接管 project 审核状态机;仅在 publish 链注入 Gate 裁决。由主 agent 在 Phase C 串行做,不挂新建 agent。
|
||||
|
||||
### 4.3 共享资源锁定(开工 Phase A 一次性锁 + 同步 README)
|
||||
- 错误码:community=107/ip=108/compliance=109/biz=110/studio=112(已用 ad=111/trade=106)。**studio=112 与 README §4 当前漂移,须本波同步登记 ErrorCodeConstants+README**。
|
||||
- Flyway:V1-7 已用;studio=V8/compliance=V9/ip=V10(薄壳跳过则不占);community=V11/biz=V12(Wave4)。**一个主版本号一个 owner agent**,主 agent Phase C 校验执行副本与 contracts/ diff 一致。
|
||||
|
||||
---
|
||||
|
||||
## 五、图示
|
||||
|
||||
### 5.1 隔离部署拓扑(与现有 infra 物理零交集)
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph EXIST["mini-infra 现有服务(绝不动,oom-score-adj 负值保护)"]
|
||||
EM[(infra-mysql 3306)]
|
||||
ER[(infra-redis 6379)]
|
||||
EMO[infra-minio 9000/9001]
|
||||
end
|
||||
subgraph STG["game-staging-net 172.30.0.0/16(全新隔离栈,全容器 mem_limit)"]
|
||||
SM[(game-staging-mysql 13306)]
|
||||
SR[(game-staging-redis 16379)]
|
||||
SN[game-staging-nacos 18848 备用]
|
||||
SQ[rmq namesrv19876+broker brokerIP1=100.64.0.8 备用]
|
||||
end
|
||||
subgraph DESK["mini-desktop"]
|
||||
APP[yudao-server.jar profile=staging 7模块脊柱]
|
||||
end
|
||||
APP -->|13306| SM
|
||||
APP -->|16379| SR
|
||||
APP -.可选不接.-> SN
|
||||
APP -.可选不接.-> SQ
|
||||
APP -->|新建bucket=game-staging 最小权限key| EMO
|
||||
EXIST -. "零交集:独立网络/端口/卷/命名空间" .-x STG
|
||||
RES["⚠ 共享面:整机内存9.3G+单磁盘294G → 全容器限内存+oom-score-adj+磁盘水位"]
|
||||
```
|
||||
|
||||
### 5.2 全链路闭环联调时序(12 段)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as 创作者
|
||||
participant ST as studio/aigc
|
||||
participant DF as Dify(桩)
|
||||
participant RT as runtime
|
||||
participant CMP as compliance锁风门
|
||||
participant PRJ as project.publish
|
||||
participant FD as feed
|
||||
participant P as 玩家
|
||||
participant AD as ad(mock)
|
||||
participant TR as trade
|
||||
C->>ST: 1.一句话生成【端到端】
|
||||
ST->>DF: 2.Dify生成【桩:手动callback】
|
||||
DF-->>RT: 3.编译打包【桩:直写build+伪url】
|
||||
RT->>CMP: 锁风门校验(聚合aigc原子)
|
||||
CMP-->>PRJ: 4.Gate裁决 pass/review/block【替mock+回归门】
|
||||
PRJ->>FD: project审核状态机=4 进流(归属仍在project)
|
||||
P->>FD: 5/6.审核后按zone出流【端到端SQL】
|
||||
FD->>RT: 7.点封面试玩 session【端到端】
|
||||
P->>FD: 8.赞/藏/享/举报【端到端幂等】
|
||||
P->>AD: 9.激励视频 mock计费【端到端 ecpm/1000】
|
||||
AD-->>TR: 10.T+1结算【端到端·手动触发×0.80】
|
||||
C->>TR: 12.提现≥500 mock打款【端到端原子冻结】
|
||||
P-->>FD: 11.遥测上报【桩:同步直写绕MQ】
|
||||
```
|
||||
|
||||
### 5.3 模块依赖与 seam
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph BUILT["已建脊柱(接入对象,带回归门)"]
|
||||
PRJ["project 100(审核状态机权威)"]
|
||||
AIGC[aigc 101]
|
||||
FEED[feed 103]
|
||||
end
|
||||
subgraph NEW["Wave3 新建(compliance>ip>studio;community/biz→Wave4)"]
|
||||
CMP["compliance 109/V9:仅锁风门Gate+分级+封禁降权(不接管审核状态机)"]
|
||||
IP["ip 108/V10:薄壳/或并入compliance扩展点(先答openQ)"]
|
||||
STU["studio 112/V8:创作主链路最小版(D1选A纳入)"]
|
||||
end
|
||||
AIGC -->|T-AGC-19风格原子| CMP
|
||||
IP -.可选原子.-> CMP
|
||||
CMP -->|Gate替mock·回归门| PRJ
|
||||
CMP -->|降权信号| FEED
|
||||
STU -->|调生成| AIGC
|
||||
STU -->|建项目| PRJ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、Blast Radius / 风险 / 兼容 / 回滚
|
||||
|
||||
- **主线1 对现有服务**:容器层零影响(独立网络/端口/卷/命名空间,不动现有容器)。**唯一真实间接影响面 = 整机内存(9.3G avail)+单磁盘卷(294G)** → 已用全容器 mem_limit + oom-score-adj + 磁盘水位告警兜底。MinIO 是软隔离面 → 最小权限 policy 或物理隔离。
|
||||
- **主线2 集成面**:compliance 最大(改 project.publish/feed,带回归门);ip 最小(或不建);studio 中(创作侧);community/biz 若做集成面见 §四(biz 依赖 bpm 未装配、community 依赖事件源未产)。
|
||||
- **兼容**:GAP-1 新增字段不替换 data;GAP-4 是 breaking 须版本并行;错误码 107-112 不重叠(studio=112 须同步 README);已合入 Flyway 禁改,回滚写补偿迁移。
|
||||
- **回滚**:staging 栈 `down`(不带 -v 保卷)+ 幂等重建脚本;4 模块未接入前可独立删;compliance 接入 project.publish 后保留 mock 门禁可切换分支。
|
||||
|
||||
---
|
||||
|
||||
## 七、验收标准(AC)
|
||||
|
||||
**主线1**:AC-S1 隔离性(infra-* 容器 inspect 前后 diff=空)|AC-S2 脊柱启动(health db+redis UP、/doc.html 出齐 7 模块、flyway 7 条 success+game_* 全建)|AC-S3 资源安全(落地后 `free -g` 余 ≥4G、无 OOM 日志、现有服务全 healthy)|AC-S4 端到端真跑段(审核/feed/试玩/互动/广告计费 5 段+钱财闭环 2 段幂等正确)|AC-S5 contractGaps 4 个定稿落 contracts/ + 前后端同步。
|
||||
**主线2**:AC-M1 每模块 `mvn -pl <m> -am test` 绿 + 前端 `npm run build` 绿(vue-tsc 假门禁不采信)|AC-M2 P0 逐项可验证(对照 Doc A 编号)|AC-M3 锁风门接入后 project 既有单测全绿+publish 回归断言通过|AC-M4 错误码/Flyway 独占无重叠、执行副本与 contracts/ diff 一致|AC-M5 审核归属:状态机权威在 project、compliance 仅注入 Gate(一次拒绝→重提交验状态一致)。
|
||||
|
||||
---
|
||||
|
||||
## 八、绑定决策(2026-06-09 创始人已锁)
|
||||
|
||||
> **✅ 决策结果(全部采推荐口径)**
|
||||
> - **D1 范围 = 守草稿 + studio**:Wave3 = 主线1(staging+contractGaps 回填) + **studio(创作主链路最小版, 112/V8) + compliance(只做锁风门 Gate, 109/V9) + ip(seam)**;**community(107)/biz(110) 留 Wave4**(本轮调研结论归档作 Wave4 预研)。
|
||||
> - **D2 审核归属 = compliance 只做 Gate**:锁风门 Gate(pass/review/block)+分级+封禁降权;**审核状态机权威仍归 project**,compliance 仅在 publish 链注入 Gate,不拆/不重写 project 已测代码。
|
||||
> - **D3 MinIO = bucket 级 + 最小权限 policy**:复用 infra-minio,新建 bucket(加随机后缀)+仅绑 `game-staging/*` 的最小权限 key,评审门核验 policy JSON。
|
||||
> - **D4 联调形态 = 单体 + 同步直写 + mock token**:只接 MySQL+Redis;telemetry MQ 改同步直写绕过;产品端 /app-api 用 `yudao.security.mock-enable` mock token(member 不在 fork);Nacos/RocketMQ 容器备用不接线。
|
||||
> - **D5 ip = 不独立建模块**:锁风门用 aigc T-AGC-19 风格原子,仅在 compliance 锁风门 -api 预留 ip 扩展点(避免为 seam 而 seam 的孤儿设计);ip 错误码 108/Flyway V10 预留不占用。
|
||||
>
|
||||
> **本波净范围 = 主线1(隔离 staging+4 contractGaps) + 2 个新模块(studio min / compliance Gate-only) + 接入 project.publish 回归门。** 下一步:写 execution 双 spec → 二轮 review → 三相 Workflow build。
|
||||
|
||||
### 原始待确认项(留档)
|
||||
|
||||
> **D1(范围)最关键**:你的「1,2」里 mainline2 = community/ip/compliance/biz,与已批准草稿 + 你 2026-06-08 决策(Wave3=studio+compliance+ip,community/biz→Wave4)冲突,且漏了承接 7 项创作 P0 的 studio。
|
||||
|
||||
- **D1 范围**:A) **守草稿:staging+contractGaps + studio(min)+compliance(Gate)+ip(seam)**,community/biz→Wave4〔推荐〕|B) 你的字面口径:staging+contractGaps + community/ip/compliance/biz(无 studio,需重评 5-6 并行 agent 与内存容量)|C) 本波只做 staging+contractGaps,模块全下一波。
|
||||
- **D2 compliance 审核归属**:A) compliance 接管 project 审核状态机(改黄金模块,高危重写)|B) **compliance 只做锁风门 Gate、project 续拥状态机**〔推荐,避免拆已测代码〕。
|
||||
- **D3 MinIO 隔离**:A) **bucket 级 + 最小权限 policy**〔推荐,省资源〕|B) 物理隔离新起 game-staging-minio(19000/19001)。
|
||||
- **D4 联调形态**:A) **MVP 单体 + 同步直写 + mock token,Nacos/RMQ 容器备用不接线**〔推荐〕|B) 接 Nacos 配置中心|C) 接 RocketMQ 异步链路。
|
||||
- **D5 ip 形态**(依赖 openQuestion):A) **不建独立 ip 模块,锁风门用 aigc 原子 + 预留 ip 扩展点**〔推荐,避免孤儿〕|B) 建 ip 薄壳(确有非 aigc 可覆盖的风格校验时)。
|
||||
|
||||
**开放问题(不阻塞决策,execution 前补)**:telemetry quality_score 公式(待产品/数据侧);跨模块 TODO Feign 对接点(runtime→project/telemetry 回写)本波是否接;staging 库名/初始租户/管理员;SettlementJob 触发手段;Nacos/RMQ 镜像与 spring-cloud-alibaba 2025.0.0.0 兼容矩阵二次定版。
|
||||
|
||||
---
|
||||
|
||||
## 九、执行路径(D1-D5 拍板后)
|
||||
|
||||
1. 按 `wave-needs-review-and-execution-spec-pair` 铁律:本 review 二轮确认 → 写 **execution 双 spec**(到命令粒度:隔离 compose+起栈脚本、建库导表脚本、application-staging.yaml、G0-G4 前置门、模块 Phase A 契约锁/Phase B 并行建/Phase C 接入回归)。
|
||||
2. 三相 Workflow 编排(契约锁→并行建→主 agent 验证门),沿用 Wave1/2 方法论与 6 条教训。
|
||||
3. 全程:重型构建上 mini-desktop、基建只增量上 mini-infra(绝不动现有)、主 agent 独立复跑验证不采信子 agent 自报。
|
||||
|
||||
---
|
||||
|
||||
> 调研原始素材:workflow `wf_7bf2fb26-75d`(7 agent / 576k token,INFRA/BACKEND/DOCA/GAPS + 2 轮对抗审查)。本文已并入两轮审查的全部 CRITICAL/高危纠正。
|
||||
960
docs/agent-specs/2026-06-09-wave3-execution.md
Normal file
960
docs/agent-specs/2026-06-09-wave3-execution.md
Normal file
@ -0,0 +1,960 @@
|
||||
# Wave3 Execution Spec — 隔离 staging 联调 + contractGaps 回填 + compliance/studio 补全
|
||||
|
||||
> **HJ-BUILD-004-EXEC**(execution 版,给子 agent 照建,命令粒度)|生成 2026-06-09
|
||||
> **上游**:review `HJ-BUILD-004`(`docs/agent-specs/2026-06-08-wave3-staging联调与P0补全-review.md`,§八 D1-D5 已锁)+ 6-agent 草案(staging/gaps/compliance/studio)+ 2 轮对抗审查(可执行性 critique[0] + 决策符合性 critique[1])。
|
||||
> **本 spec 已并入全部 blocker 收口(R1-R12)**:草案里已核实、可执行的命令级内容予以保留;凡草案与 R1-R12 冲突,一律以 R1-R12 为准。
|
||||
> **三态标注约定**:【已验证】= 来自代码/契约/README 只读核实;【实测前置】= Phase A/G0 须在内网机实跑后才能定稿的门;【待裁定】= 须主 agent/创始人 Phase A 关闭的开放项。
|
||||
|
||||
---
|
||||
|
||||
## §0 定位与范围
|
||||
|
||||
**一句话**:把已建 7 模块脊柱(project/aigc/runtime/feed/telemetry/ad/trade)从 mock 推进到「隔离 staging 真实联调」,回填 4 个 contractGaps,补 compliance(只做锁风门 Gate)与 studio(创作主链路最小版)两个新模块,并以回归门把 compliance Gate 注入 project.publish。
|
||||
|
||||
**本波净范围(D1-D5 已锁)**:
|
||||
- 主线1-A:mini-infra 隔离 staging 栈(MySQL+Redis,**不接 Nacos/RocketMQ/MinIO 容器**,MinIO 复用 infra-minio)+ 建库导基表 + G0-G4 门 + 12 段联调。
|
||||
- 主线1-B:4 个 contractGaps(GAP-1 publish 结构化、GAP-2 取包、GAP-3 ShareMeta versionId、GAP-4 遥测命名)。
|
||||
- 主线2-A:compliance 模块(**只做 Gate seam**,不建审核状态机表)+ 接入 project.publish。
|
||||
- 主线2-B:studio 模块(创作主链路最小版,治理新登记 112/V8)。
|
||||
|
||||
**明确不做(留 Wave4 / P1)**:community(107)/biz(110) 模块;feed 降权出流屏蔽(P1,本波不生效);studio rig/对白树/六路并发资产;attachments 透传 aigc(本波仅持久化)。
|
||||
|
||||
**锁决策回顾**:D1 守草稿+studio|D2 compliance 只做 Gate、审核状态机权威归 project|D3 MinIO bucket 级最小权限|D4 单体+同步直写+mock token|D5 ip 不独立建模块、仅留 seam。
|
||||
|
||||
---
|
||||
|
||||
## §1 Phase A 跨主线收口(开工前必须完成,主 agent 串行)
|
||||
|
||||
> **为什么必须先收口**:critique[1] 判定 REJECT,根因是 3 条主线并行各自改同一权威资产(project.submitPublish / ProjectApi / V6V7 / 错误码 005),互相覆盖、回归门形同虚设。Phase A 由主 agent 串行把这些「单点」定死,再分发子 agent。
|
||||
|
||||
### 1.1 R1 publish 唯一化(blocker,最高优先)
|
||||
|
||||
【已验证】`ProjectServiceImpl.submitPublish`(L89-108) 现为 `void` + 抛异常;`AppProjectController`(L75-83) `@PostMapping("/{id}/publish")` 硬编码 `return success(true)`;project 现有 9 单测绿。
|
||||
|
||||
**唯一权威形态 = GAP-1 的结构化 `ProjectPublishRespVO`**:
|
||||
- `ProjectPublishRespVO { Boolean admitted; List<GateResultVO> gates; }`
|
||||
- `GateResultVO { String key; String label; Boolean pass; String code; String message; }`
|
||||
- `submitPublish` 签名 `void` → `ProjectPublishRespVO`。
|
||||
- **block → `admitted=false` 不抛异常**,由对应 gate 项(`pass=false` + `code`/`message`)携带 block 原因;硬门禁(适龄/compliance=block)失败时 `admitted=false` 且不改状态机。
|
||||
- compliance 锁风门是 `gates[]` 中的一条 `key=compliance` 的 `GateResultVO`。
|
||||
|
||||
**这是 in-place 破坏性契约变更**(`project.yaml` publish 端点 200 响应 `CommonResultBoolean` → 结构化信封)。publish **只有这一个端点**,改其 `$ref` 即破坏,**不存在「旧 boolean 端点保留」**——删除草案 GAP-1 中"旧 boolean 端点保留/避免破坏既有回归"的错误兼容承诺。前端所有 boolean 消费点(`api/project.ts`/`store/project.ts`/`mock/project.ts`/`Publish.vue`)**同批迁移到 `admitted`,无并存期**。
|
||||
|
||||
**compliance 侧只交付 seam**:`ComplianceGateApi.evaluate(...) → GateVerdict{ verdict:pass/review/block, detail }`(语义等价草案 `ComplianceGateRespDTO`)。**删除 compliance 草案 §4 自有的 submitPublish 改写、独立 `ProjectPublishReqVO` 改动、和它自登记的 `PROJECT_PUBLISH_BLOCKED_BY_GATE` 错误码**。
|
||||
|
||||
**错误码全局只登记一次**:publish-blocked 不再需要独立错误码(block→admitted=false 不抛异常)。若仍保留一个 gate 失败码用于 `GateResultVO.code` 文案,**只在 project `ErrorCodeConstants`(段 100)登记一次**,禁 compliance 重复登记。
|
||||
|
||||
**串行独占纪律**:主 agent 串行独占改 `ProjectServiceImpl.submitPublish` + `AppProjectController` + `project.yaml`(publish 端点)。**禁任何子 agent 并行碰 project.publish**。
|
||||
|
||||
### 1.2 R2 ProjectApi seam 单点收敛(blocker)
|
||||
|
||||
【已验证】`ProjectApi`(`game-module-project-api`)现仅 `getCreatorUserId(gameId)→CommonResult<Long>`(`@GetMapping(PREFIX+"/get-creator-user-id")`,@FeignClient + 同进程 @Primary 解析)。
|
||||
|
||||
本波三方都要改它(compliance 调 reviewProject、studio 要 createProject、GAP-3 要 getCurrentVersionId)。**全部新增方法 Phase A 一次性定稿,主 agent 串行改 `ProjectApi` + `ProjectApiImpl`,带 project 9 单测回归门。禁子 agent 并行改 project-api。**
|
||||
|
||||
**ProjectApi seam 单点定稿表**:
|
||||
|
||||
| 方法签名 | 来源 | 读/写 | 语义 | 归属校验 |
|
||||
|---|---|---|---|---|
|
||||
| `getCreatorUserId(Long gameId) → CommonResult<Long>` | 已有 | 读 | 反查创作者(归因) | 不存在返回 null |
|
||||
| `getCurrentVersionId(Long gameId) → CommonResult<Long>` | GAP-3 (R10) | 读 | 当前生效版本 ID;**无已发布版本返回 null** | 调用方按 isGamePublished 判定 |
|
||||
| `createProject(Long creatorUserId, ...建项目入参) → CommonResult<Long>` | studio (R2) | **写** | studio 草稿移交建游戏,返回 gameId | **写类 RPC 必带显式 creatorUserId 入参**(RPC 无登录上下文),service 层补归属校验,**不裸暴露** |
|
||||
| `forceUnlist(Long gameId) → CommonResult<Boolean>` | R3 备选 | 写 | 受控下架(任意可见态→UNLISTED)+ project 单测回归 | **默认不采用**(见 R3 默认走状态分支);仅当 Phase A 选受控 seam 时才加 |
|
||||
|
||||
> `createProject` 是写类 RPC,跨模块暴露须谨慎:显式 `creatorUserId` 入参 + service 层归属校验,绝不绕过 DataPermission 裸暴露。`forceUnlist` 默认不建(见 R3)。
|
||||
|
||||
### 1.3 R3 compliance 封禁不破坏状态机(blocker,D2)
|
||||
|
||||
【已验证】`resolveReviewTargetStatus`(ProjectServiceImpl L190-195):`UNLIST(3)` 仅当 `currentStatus == PUBLISHED` 才合法,否则抛 `PROJECT_REVIEW_STATUS_INVALID`。封禁对象可能处于 DRAFT/REVIEWING/REJECTED/UNLISTED 任意态。
|
||||
|
||||
**禁止无条件调 `reviewProject(UNLIST=3)`**。MVP 采【**按状态分支**】(默认):
|
||||
- 封禁动作:写 `game_user_ban` 台账(主操作,恒执行)。
|
||||
- 下架动作:**仅当 `game.status == PUBLISHED` 时**才调 `reviewProject(decision=3)` 走 project 权威状态机;**非 PUBLISHED 态只记封禁、不调状态机**(不抛异常、封禁不失败)。
|
||||
|
||||
**备选(标注,默认不采用)**:project-api 加受控 `forceUnlist(gameId)` seam(任意可见态→UNLISTED + project 单测回归)。**默认用状态分支**;若 Phase A 改采 forceUnlist,则随 R2 ProjectApi 收敛一并定稿并补 project 单测。
|
||||
|
||||
### 1.4 R4 studio 治理登记(high,非"修漂移")
|
||||
|
||||
【已验证】README §4 段位表 = `project=100 / aigc=101 / runtime=102 / feed=103 / telemetry=104 / pay=105 / trade=106 / community=107 / ip=108 / compliance=109 / biz=110 / ad=111`,**不含 studio**;CLAUDE.md 13 后端模块定位无 studio(`game-studio` 是产品端**前端仓名**)。
|
||||
|
||||
故新建 studio 后端模块 = **一次治理变更**(非"修 README 漂移")。Phase A 由主 agent 正式登记:
|
||||
- studio = 错误码段 **112** / Flyway **V8**。
|
||||
- 更新 `CLAUDE.md` 模块定位:新增 studio 后端模块条目,**注明"与 game-studio 产品端前端仓区分"**。
|
||||
- 更新 `contracts/README.md` §4 段位表(追加 `studio=112`)+ §1 契约清单/目录树(追加 `studio.yaml` + `V8.0.0__create_game_studio.sql`)+ §2 波次说明(追加 Wave3 studio)。
|
||||
|
||||
**【待裁定】** 这是治理决策,需创始人 Phase A 知会确认。D1 已选"守草稿+studio"方向已定,但**段位 112 登记须显式做**(不得自分配后默认通过)。
|
||||
|
||||
### 1.5 R5 feed 降权 = 本波 P1 不生效(medium)
|
||||
|
||||
【已验证】feed 当前无 `FeedDownweightApi` 入口(仅 `FeedRankDO` 注释提及降权字段,无 API);feed 本波不重建。
|
||||
|
||||
故 compliance 封禁的"降权出流屏蔽" = **P1,本波不生效**。compliance 完成条件**显式标注**:「封禁台账写库 +(若 PUBLISHED)下架;**降权屏蔽 P1 本波不生效**」。**不得用一个默认关的开关掩盖语义残缺**,**不建只有调用方无提供方的 `FeedDownweightApi` 孤儿 seam**(本波 compliance 侧不引入该调用;若保留代码占位须留 TODO 明注,不写默认关开关伪装"已支持")。
|
||||
|
||||
### 1.6 R6 V6/V7 提为全局 G0(medium)
|
||||
|
||||
【已验证】`yudao-server/src/main/resources/db/migration/` 现仅 V1-V5;V6/V7 仅在 `game-module-ad/trade-server` 的 db/migration。
|
||||
|
||||
把"复制 V6/V7 执行副本到 yudao-server"从 compliance/studio 章节**抽出**,作为 **Phase B 开工的全局前置 G0**:主 agent 做一次 + diff-identical 校验 + 构建后 `unzip -l jar` 核验 7 文件。compliance/studio 章节只引用「G0 已完成」,**不重复 cp**。(详见 §6 G0。)
|
||||
|
||||
### 1.7 R8 GAP-2 manifestUrl 来源(Phase A 定,low)
|
||||
|
||||
【待裁定】让 runtime owner 确认 `game_runtime_package` 是否有可派生 manifest 路径的列:
|
||||
- **分支 A(纯派生)**:GAP-2 维持 additive 零 DDL,`manifestUrl` 由 `packageUrl` 同前缀派生(拼 `.../manifest.json`)。
|
||||
- **分支 B(需增列)**:显式升级为 runtime 补偿迁移(新 Flyway 版本,如 `V3.1.0`,纳入 Flyway 唯一性校验),**不藏在 VO 回填里**。
|
||||
|
||||
spec 两分支并列,Phase A 闭合后只走其一。
|
||||
|
||||
### 1.8 R7 studio attachments 持久化-only(medium)
|
||||
|
||||
【已验证】aigc 的 `AigcGenerateReqVO` 无 `attachments` 字段。
|
||||
|
||||
MVP:studio **仅持久化 attachments,不透传 aigc**。`studio.yaml` 的 `StudioGenerateReqVO.attachments` 描述改为「**本波仅持久化,P1 透传 aigc**」。不写"透传"而下游无接收字段。(P1 方案:给 aigc-api 新 DTO 加 attachments,additive,随 AigcApi seam 定。)
|
||||
|
||||
### 1.9 R9 GAP-4 破坏性收口(low)
|
||||
|
||||
【已验证】`events.schema.json` envelope/user 为 `additionalProperties:false`(严格)+ snake_case;telemetry.yaml `EnvelopeReqVO` / `TelemetryEventDO` / 前端 SDK 全 camelCase。
|
||||
|
||||
统一 **camelCase**(改 `events.schema.json`)。**Phase A 评审记录显式写明**:
|
||||
- 利用「无生产数据 + MQ 未实现(`dispatchToMq` TODO)」窗口做一次性破坏收敛。
|
||||
- 豁免 README §2「不兼容变更用新事件名并行消费」规则——**理由:改的是信封(envelope/user)非事件名**,属信封字段命名统一而非单事件演进。
|
||||
- 确认仓外(BI/数据)无 snake 消费者后执行。
|
||||
|
||||
(最小 camelCase 样例 JSON 见 §3 GAP-4,供 `npx ajv-cli` + 必要 `ajv-formats` 校验。)
|
||||
|
||||
### 1.10 实测前置门清单(R11,Phase A/G0 必须实跑,不留运行时再试)
|
||||
|
||||
| # | 门 | 命令(mini-infra 执行,只读核验) | 通过判定 / 兜底 |
|
||||
|---|---|---|---|
|
||||
| P1 | clone 端点 | `git ls-remote http://101.200.34.71:3000/zizi-al/games-development-ai.git` | 【实测前置】本会话已在 mini-desktop 验证 :3000 匿名可用,**但 mini-infra 须复核**;不可达 → SSH/2222 兜底(`ssh://git@101.200.34.71:2222/...`),主 agent 注入 deploy key |
|
||||
| P2 | mysql 镜像 tag | `docker inspect infra-mysql --format '{{.Config.Image}}'` | 取确切 tag 对齐 compose(替换写死的 `8.4.8`;或用浮动 `mysql:8.4`) |
|
||||
| P3 | maven 基础镜像 | `docker pull maven:3.9-eclipse-temurin-17` | 用确认可拉的 `maven:3.9-eclipse-temurin-17`(**非 deprecated 的 `maven:3.8-jdk17`**) |
|
||||
| P4 | MinIO 凭据/端点 | `docker inspect infra-minio` 取真实 root 凭据 + 端口映射 | 填入 `.env`;端点端口须核实(9000 是否映射宿主) |
|
||||
| P5 | MinIO 最小权限 key | 见 §2 A9(**独立用户**,非 root 子账号) | `mc admin user add` → `policy attach` → `svcacct add` |
|
||||
|
||||
```bash
|
||||
# Phase A 实测前置门(mini-infra,只读)
|
||||
ssh mini-infra
|
||||
hostname && docker ps --format '{{.Names}}' | sort # 确认 infra-mysql/redis/minio 在跑
|
||||
# P1 clone 端点复核
|
||||
git ls-remote http://101.200.34.71:3000/zizi-al/games-development-ai.git | head -3 \
|
||||
&& echo "CLONE-HTTP-OK" || echo "CLONE-HTTP-FAIL → 改 SSH/2222 兜底"
|
||||
# P2 mysql tag
|
||||
docker inspect infra-mysql --format '{{.Config.Image}}'
|
||||
# P3 maven 基础镜像可拉性
|
||||
docker pull maven:3.9-eclipse-temurin-17 >/dev/null 2>&1 && echo "MAVEN-IMG-OK" || echo "MAVEN-IMG-FAIL"
|
||||
# P4 MinIO 真实凭据/端点
|
||||
docker inspect infra-minio --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -iE 'MINIO_ROOT|MINIO_ACCESS|MINIO_SECRET'
|
||||
docker inspect infra-minio --format '{{json .NetworkSettings.Ports}}'
|
||||
```
|
||||
|
||||
### 1.11 Phase A 待裁定开放项(汇总,见 §9)
|
||||
|
||||
R8(manifestUrl 派生/增列)、R5(feed 降权 P1 边界确认)、R7(attachments 持久化口径)、R4(studio 112 段位治理确认)、P2(mysql tag)、G0(RMQ 不阻塞实跑结论)。
|
||||
|
||||
---
|
||||
|
||||
## §2 主线1-A:隔离 staging(mini-infra)
|
||||
|
||||
> 范围铁律(D1-D5):只建隔离 staging(MySQL+Redis)联调单体,**不接 Nacos/RocketMQ/MinIO 容器**(MinIO 复用 infra-minio)。所有命令在 `mini-infra`(100.64.0.8) 执行,除非显式标注 `[本机]`(6c6g,有 java17+mvn)。
|
||||
|
||||
### A0. build + run 拓扑
|
||||
|
||||
【已验证】jar 产物名 `yudao-server.jar`(pom `finalName=yudao-server`),主类 `cn.iocoder.yudao.server.YudaoServerApplication`,端口 `48080`;框架基表 `game-cloud/sql/mysql/ruoyi-vue-pro.sql`(48 表 + 租户 id=1 + admin userId=1) + `quartz.sql`;mock token 机制:`mock-enable=true` 且 token 以 `mock-secret`(默认 `test`) 开头 → userId=去前缀,tenantId 取请求头 `tenant-id`;Nacos discovery/config 已 `enabled:false`;Flyway `baseline-on-migrate=true`。
|
||||
|
||||
**推荐方案 A = mini-infra Docker 多阶段构建 + 同网运行**:构建在容器内(`maven:3.9-eclipse-temurin-17`),运行容器接入 `game-staging-net` 与 mysql/redis 容器 DNS 直连,源码匿名 HTTP clone 落 mini-infra。**备选方案 B(兜底)= [本机] 构建 jar + 经 Gitea release/临时分支 HTTP 拉取到 mini-infra 的 `eclipse-temurin:17` 容器 `java -jar`**(jar 不走 scp,被拦);仅当方案 A 构建受阻时启用。
|
||||
|
||||
### A1. 源码 clone(mini-infra)
|
||||
|
||||
```bash
|
||||
ssh mini-infra
|
||||
hostname && docker ps --format '{{.Names}}' | sort
|
||||
# no_proxy 与 NO_PROXY 同给(R12)
|
||||
export no_proxy="100.64.0.0/10,127.0.0.1,localhost,101.200.34.71"
|
||||
export NO_PROXY="$no_proxy"
|
||||
mkdir -p /srv/game-staging/build && cd /srv/game-staging/build
|
||||
# 【实测前置 P1】优先 HTTP;失败回退 SSH/2222(主 agent 注入 deploy key)
|
||||
git clone http://101.200.34.71:3000/zizi-al/games-development-ai.git src \
|
||||
|| git clone ssh://git@101.200.34.71:2222/zizi-al/games-development-ai.git src
|
||||
cd src && git checkout dev/2.0.0 && git log --oneline -1
|
||||
```
|
||||
|
||||
### A2. 起栈前置核验(mini-infra,只读)
|
||||
|
||||
```bash
|
||||
# 1) 网络子网核验:172.30.0.0/16 须空闲(占用则改 172.31 并同步改 compose)
|
||||
for n in $(docker network ls -q); do
|
||||
docker network inspect "$n" --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}}{{end}}'
|
||||
done | sort
|
||||
# 断言:输出中不得出现 172.30.
|
||||
# 2) 端口空闲
|
||||
ss -ltn | grep -E ':13306|:16379' && echo "PORT-OCCUPIED-ABORT" || echo "PORTS-FREE-OK"
|
||||
# 3) 内存基线留档
|
||||
free -g
|
||||
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}'
|
||||
# 4) 保护现有 infra-* 容器(OOM 优先杀新栈)
|
||||
for c in $(docker ps --format '{{.Names}}' | grep -E '^infra-|minio|mysql|redis|postgres|gitea'); do
|
||||
pid=$(docker inspect -f '{{.State.Pid}}' "$c")
|
||||
[ -n "$pid" ] && echo -500 > /proc/$pid/oom_score_adj && echo "protected $c pid=$pid"
|
||||
done
|
||||
```
|
||||
|
||||
### A3. docker-compose.yml(落盘 `/srv/game-staging/docker-compose.yml`)
|
||||
|
||||
> 【实测前置 P2】`image` tag 以 A 章 P2 `docker inspect infra-mysql` 实测为准;下方 `mysql:8.4` 为浮动占位,须对齐。
|
||||
|
||||
```yaml
|
||||
# /srv/game-staging/docker-compose.yml
|
||||
# Wave3 隔离联调栈:仅 MySQL + Redis(D4:Nacos/RMQ 不接,注释备用)
|
||||
name: game-staging
|
||||
|
||||
networks:
|
||||
game-staging-net:
|
||||
name: game-staging-net
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.30.0.0/16 # 起栈前 A2 已核验空闲;占用则改 172.31
|
||||
|
||||
services:
|
||||
game-staging-mysql:
|
||||
image: mysql:8.4 # 【实测前置 P2】对齐 infra-mysql 确切 tag
|
||||
container_name: game-staging-mysql
|
||||
command:
|
||||
- --innodb-buffer-pool-size=512M
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --default-time-zone=+08:00
|
||||
- --max-connections=200
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${STAGING_MYSQL_ROOT_PWD}
|
||||
MYSQL_DATABASE: ruoyi-vue-pro
|
||||
ports:
|
||||
- "13306:3306"
|
||||
volumes:
|
||||
- /srv/game-staging/mysql-data:/var/lib/mysql
|
||||
networks: [game-staging-net]
|
||||
mem_limit: 1536m
|
||||
healthcheck:
|
||||
# 密码插值的 $ 需 .env 用 $$ 转义;R12 硬约束密码仅 [A-Za-z0-9_-] 可免转义
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${STAGING_MYSQL_ROOT_PWD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 40s
|
||||
restart: unless-stopped
|
||||
|
||||
game-staging-redis:
|
||||
image: redis:7-alpine
|
||||
container_name: game-staging-redis
|
||||
command: ["redis-server", "--save", "", "--appendonly", "no", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
|
||||
ports:
|
||||
- "16379:6379"
|
||||
volumes:
|
||||
- /srv/game-staging/redis-data:/data
|
||||
networks: [game-staging-net]
|
||||
mem_limit: 512m
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
|
||||
# ===== 备用(D4:本波不接,保持注释)=====
|
||||
# game-staging-nacos / game-staging-rmq-namesrv —— 本波不起
|
||||
```
|
||||
|
||||
`.env`(同目录,chmod 600,不入库):
|
||||
|
||||
```bash
|
||||
# /srv/game-staging/.env (chmod 600)
|
||||
# 【R12 硬约束】口令仅用 [A-Za-z0-9_-],避开 shell 元字符与 compose $ 插值
|
||||
STAGING_MYSQL_ROOT_PWD=ChangeMe_Staging_2026
|
||||
```
|
||||
|
||||
### A4. 建库 + 导框架基表
|
||||
|
||||
> 跨机时序(R12):先 A3/A4 起栈,再 `[本机]` `nc -zv 100.64.0.8 13306/16379` 通,再做 G0。
|
||||
|
||||
```bash
|
||||
cd /srv/game-staging
|
||||
docker compose --env-file .env up -d game-staging-mysql game-staging-redis
|
||||
# 等 MySQL healthy(条件轮询,非 sleep)
|
||||
until [ "$(docker inspect -f '{{.State.Health.Status}}' game-staging-mysql)" = "healthy" ]; do
|
||||
echo "waiting mysql..."; docker logs --tail 2 game-staging-mysql; sleep 3; done
|
||||
echo "MYSQL-HEALTHY"
|
||||
# 显式 utf8mb4
|
||||
docker exec -i game-staging-mysql mysql -uroot -p"$STAGING_MYSQL_ROOT_PWD" \
|
||||
-e "ALTER DATABASE \`ruoyi-vue-pro\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
# 导框架基表
|
||||
SQLDIR=/srv/game-staging/build/src/game-cloud/sql/mysql
|
||||
docker exec -i game-staging-mysql mysql -uroot -p"$STAGING_MYSQL_ROOT_PWD" ruoyi-vue-pro < $SQLDIR/ruoyi-vue-pro.sql
|
||||
docker exec -i game-staging-mysql mysql -uroot -p"$STAGING_MYSQL_ROOT_PWD" ruoyi-vue-pro < $SQLDIR/quartz.sql
|
||||
# 断言:framework_tables>=48;system_tenant id=1 status=0;system_users id=1 admin status=0
|
||||
docker exec -i game-staging-mysql mysql -uroot -p"$STAGING_MYSQL_ROOT_PWD" ruoyi-vue-pro -e "
|
||||
SELECT COUNT(*) AS framework_tables FROM information_schema.tables WHERE table_schema='ruoyi-vue-pro';
|
||||
SELECT id,name,status FROM system_tenant WHERE id=1;
|
||||
SELECT id,username,status,tenant_id FROM system_users WHERE id=1;"
|
||||
```
|
||||
|
||||
> 跨机可达性预检(`[本机]`,G0 前必跑,区分"卡 DB/Redis"与"卡 RMQ"):
|
||||
> ```bash
|
||||
> nc -zv 100.64.0.8 13306 && nc -zv 100.64.0.8 16379 && echo "STAGING-DB-REDIS-REACHABLE"
|
||||
> ```
|
||||
|
||||
### A5. application-staging.yaml(`[本机]` 新增 `game-cloud/yudao-server/src/main/resources/application-staging.yaml`)
|
||||
|
||||
> 不改 dev/local。机密走 env 占位;容器运行时用 env 覆写 host 为容器 DNS(见 G2)。
|
||||
|
||||
```yaml
|
||||
# application-staging.yaml —— Wave3 隔离联调单体 profile(新增,不动 dev/local)
|
||||
server: { port: 48080 }
|
||||
spring:
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration
|
||||
- org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration
|
||||
# 若 G0 判定 RMQ 阻塞,追加:
|
||||
# - org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration
|
||||
datasource:
|
||||
dynamic:
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://${STAGING_DB_HOST:100.64.0.8}:${STAGING_DB_PORT:13306}/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: ${STAGING_DB_USER:root}
|
||||
password: ${STAGING_DB_PWD}
|
||||
slave:
|
||||
lazy: true
|
||||
url: jdbc:mysql://${STAGING_DB_HOST:100.64.0.8}:${STAGING_DB_PORT:13306}/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: ${STAGING_DB_USER:root}
|
||||
password: ${STAGING_DB_PWD}
|
||||
data:
|
||||
redis:
|
||||
host: ${STAGING_REDIS_HOST:100.64.0.8}
|
||||
port: ${STAGING_REDIS_PORT:16379}
|
||||
database: 0
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
url: jdbc:mysql://${STAGING_DB_HOST:100.64.0.8}:${STAGING_DB_PORT:13306}/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
user: ${STAGING_DB_USER:root}
|
||||
password: ${STAGING_DB_PWD}
|
||||
spring.cloud.nacos.discovery.enabled: false
|
||||
spring.cloud.nacos.config.enabled: false
|
||||
xxl: { job: { enabled: false } }
|
||||
rocketmq:
|
||||
name-server: ${STAGING_RMQ_NAMESRV:127.0.0.1:9876} # D4 不接;G0 验证不阻塞
|
||||
producer: { group: yudao-server_PRODUCER }
|
||||
management:
|
||||
endpoints: { web: { base-path: /actuator, exposure: { include: '*' } } }
|
||||
yudao:
|
||||
security:
|
||||
mock-enable: true # D4:member 不在 fork,用 mock token
|
||||
mock-secret: test # admin: test1+tenant-id:1;产品端: test{memberId}+tenant-id:1
|
||||
captcha: { enable: false }
|
||||
access-log: { enable: false }
|
||||
demo: false
|
||||
tenant: { enable: true } # 须有效租户 id=1(随框架表导入)
|
||||
logging: { file: { name: ${user.home}/logs/yudao-server-staging.log } }
|
||||
debug: false
|
||||
```
|
||||
|
||||
### A6. G0 前置门(RocketMQ 不阻塞启动验证,`[本机]`,R12)
|
||||
|
||||
> 前置(必须先满足,否则误判):① A3/A4 已起栈且 `nc -zv 100.64.0.8 13306/16379` 通;② `[本机]` 已完成 G0 全局复制(§6 G0)并 `mvn -pl yudao-server -am -DskipTests package` 出 jar。
|
||||
|
||||
```bash
|
||||
cd <repo>/game-cloud
|
||||
export STAGING_DB_HOST=100.64.0.8 STAGING_DB_PORT=13306 STAGING_DB_USER=root STAGING_DB_PWD="$STAGING_MYSQL_ROOT_PWD"
|
||||
export STAGING_REDIS_HOST=100.64.0.8 STAGING_REDIS_PORT=16379
|
||||
export STAGING_RMQ_NAMESRV=127.0.0.1:19999 # 故意不存在
|
||||
export no_proxy=100.64.0.0/10,127.0.0.1,localhost NO_PROXY=100.64.0.0/10,127.0.0.1,localhost
|
||||
timeout 180 java -jar -Dspring.profiles.active=staging yudao-server/target/yudao-server.jar 2>&1 | tee /tmp/g0-rmq.log &
|
||||
until grep -qE "Started YudaoServerApplication|APPLICATION FAILED TO START" /tmp/g0-rmq.log; do sleep 5; done
|
||||
grep -E "Started YudaoServerApplication" /tmp/g0-rmq.log && echo "G0-PASS: RMQ 不阻塞启动"
|
||||
```
|
||||
|
||||
**判定(R12,区分卡点)**:
|
||||
- 日志先报 **DB/Redis 连接失败** → 不是 RMQ 问题,回 A4 确认库可达,重跑(不触发兜底)。
|
||||
- **PASS**(打出 Started)→ 放行 G1,staging profile 无需额外处理。
|
||||
- **FAIL 且卡在 RocketMQ** → 兜底1(推荐):`application-staging.yaml` 的 `spring.autoconfigure.exclude` 追加 `org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration`,复跑通过。**【待裁定】** 须创始人确认 telemetry 同步直写绕 MQ 已在 telemetry 模块就位。
|
||||
- 完成后 `kill %1` + 清 `/tmp/g0-rmq.log`。
|
||||
|
||||
### A7. G1 构建(方案 A,mini-infra)
|
||||
|
||||
`mini-infra:/srv/game-staging/build/Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
# 多阶段:maven 构建 → temurin 运行
|
||||
FROM maven:3.9-eclipse-temurin-17 AS build
|
||||
WORKDIR /app
|
||||
COPY src/game-cloud /app/game-cloud
|
||||
WORKDIR /app/game-cloud
|
||||
RUN --mount=type=cache,target=/root/.m2 mvn -q -pl yudao-server -am -DskipTests package
|
||||
FROM eclipse-temurin:17-jre
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/game-cloud/yudao-server/target/yudao-server.jar /app/yudao-server.jar
|
||||
EXPOSE 48080
|
||||
ENTRYPOINT ["java","-Xmx1536m","-Dspring.profiles.active=staging","-jar","/app/yudao-server.jar"]
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /srv/game-staging/build
|
||||
# §6 G0 复制硬门必须先在 src 内完成
|
||||
ls src/game-cloud/yudao-server/src/main/resources/db/migration/V*.sql | wc -l # 必须 7(V1-V7),否则回 G0
|
||||
DOCKER_BUILDKIT=1 docker build --memory 4g -t game-staging-app:wave3 .
|
||||
```
|
||||
|
||||
### A8. G2 运行(容器接入 staging 网,DNS 直连)— R12 命令修正
|
||||
|
||||
```bash
|
||||
docker run -d --name game-staging-app \
|
||||
--network game-staging-net \
|
||||
-p 48080:48080 \
|
||||
--memory 2g \
|
||||
-e STAGING_DB_HOST=game-staging-mysql -e STAGING_DB_PORT=3306 \
|
||||
-e STAGING_DB_USER=root -e STAGING_DB_PWD="$STAGING_MYSQL_ROOT_PWD" \
|
||||
-e STAGING_REDIS_HOST=game-staging-redis -e STAGING_REDIS_PORT=6379 \
|
||||
-e STAGING_RMQ_NAMESRV=127.0.0.1:19999 \
|
||||
-e no_proxy="100.64.0.0/10,127.0.0.1,localhost,game-staging-mysql,game-staging-redis" \
|
||||
-e NO_PROXY="100.64.0.0/10,127.0.0.1,localhost,game-staging-mysql,game-staging-redis" \
|
||||
game-staging-app:wave3
|
||||
until docker logs game-staging-app 2>&1 | grep -qE "Started YudaoServerApplication|APPLICATION FAILED"; do sleep 5; docker logs --tail 3 game-staging-app; done
|
||||
```
|
||||
|
||||
> 【已验证修正】`docker run` 用 `--memory 2g`(**非 `--mem-limit`**,CLI 无此 flag);`no_proxy` 与 `NO_PROXY` 同给(R12)。本节命令与上方逐字一致。
|
||||
|
||||
### A9. G3/G4 自检门(真实 API 路径,R10)
|
||||
|
||||
> **【已验证】全仓 controller 无 `game/` 路径前缀**:AppProjectController `@RequestMapping("/project")` + `/my`(GET 分页)/`/{id}`/`/{id}/publish`;AppFeedController `@RequestMapping("/feed")` + `/stream`/`/zones`/`/zone/stream`/`/interact`/`/share/{gameId}`(**无 list**)。框架自动加 `/app-api`、`/admin-api`。
|
||||
> **铁律**:G3/G4 断言先 dump 真实 paths 再写。禁 grep `game/`、禁 `/feed/list`、禁 `/admin-api/game/project/page`。
|
||||
|
||||
```bash
|
||||
# G3-1 健康
|
||||
curl -s http://127.0.0.1:48080/actuator/health | grep -o '"status":"UP"' && echo HEALTH-UP
|
||||
|
||||
# G3-2 接口文档出齐 7 模块 —— 先 dump 真实 paths(R10)
|
||||
curl -s http://127.0.0.1:48080/v3/api-docs \
|
||||
| python3 -c 'import sys,json;[print(k) for k in json.load(sys.stdin)["paths"]]' \
|
||||
| sort -u | tee /tmp/staging-paths.txt
|
||||
# 断言 7 模块各有真实路径(按真实前缀,非 game/):
|
||||
grep -qE '/app-api/project' /tmp/staging-paths.txt && \
|
||||
grep -qE '/app-api/aigc' /tmp/staging-paths.txt && \
|
||||
grep -qE '/app-api/runtime' /tmp/staging-paths.txt && \
|
||||
grep -qE '/app-api/feed' /tmp/staging-paths.txt && \
|
||||
grep -qE '/app-api/telemetry' /tmp/staging-paths.txt && \
|
||||
grep -qE '/admin-api/ad' /tmp/staging-paths.txt && \
|
||||
grep -qE '/admin-api/trade' /tmp/staging-paths.txt && echo "7-MODULES-OK"
|
||||
# 补 studio/compliance 接入后追加:grep -qE '/app-api/studio' && grep -qE '/admin-api/compliance'
|
||||
|
||||
# G3-3 Flyway 迁移历史 ≥7 + game_* 表 18 张
|
||||
docker exec -i game-staging-mysql mysql -uroot -p"$STAGING_MYSQL_ROOT_PWD" ruoyi-vue-pro -e "
|
||||
SELECT version,success FROM flyway_schema_history WHERE version IS NOT NULL ORDER BY installed_rank;
|
||||
SELECT COUNT(*) AS game_tables FROM information_schema.tables WHERE table_schema='ruoyi-vue-pro' AND table_name LIKE 'game\_%';"
|
||||
# 断言:version 含 1.0.0~7.0.0 且 success=1;game_tables=18(compliance/studio 接入后再加 +6 表)
|
||||
|
||||
# G4 端到端鉴权门(真实路径,R10)
|
||||
# 管理端 mock(admin userId=1 / tenant 1)—— 用真实存在端点
|
||||
curl -s -H "Authorization: Bearer test1" -H "tenant-id: 1" \
|
||||
'http://127.0.0.1:48080/admin-api/project?pageNo=1&pageSize=1' | head -c 300; echo
|
||||
# 产品端 app-api mock —— project/my(GET 分页,非 page)
|
||||
curl -s -H "Authorization: Bearer test1" -H "tenant-id: 1" \
|
||||
'http://127.0.0.1:48080/app-api/project/my?pageNo=1&pageSize=1' | head -c 300; echo
|
||||
# feed 用 stream(无 list)
|
||||
curl -s -H "Authorization: Bearer test1" -H "tenant-id: 1" \
|
||||
'http://127.0.0.1:48080/app-api/feed/stream?pageNo=1&pageSize=1' | head -c 300; echo
|
||||
# 断言:返回 {"code":0,...}(非 401/未登录)。具体路径以 /tmp/staging-paths.txt 实际为准。
|
||||
```
|
||||
|
||||
> 主线1-A 收口:G0 PASS + G3 三项全绿(HEALTH-UP / 7-MODULES-OK / Flyway≥7 且 game 表 18)+ G4 三条 mock 均非未登录。
|
||||
|
||||
### A10. MinIO bucket 级最小权限(D3,复用 infra-minio,R11 独立用户)
|
||||
|
||||
> 【实测前置 P4/P5】root 凭据/端点从 §1 P4 `docker inspect infra-minio` 取真实值;key 用**独立用户**(非 root 子账号)。
|
||||
|
||||
```bash
|
||||
# 0) 配 mc 别名(端点/凭据来自 P4 实测)
|
||||
mc alias set infra http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PWD"
|
||||
mc admin info infra # 只读核验连通
|
||||
# 1) 建带随机后缀 bucket
|
||||
SUFFIX=$(head -c4 /dev/urandom | od -An -tx1 | tr -d ' ')
|
||||
BUCKET="game-staging-${SUFFIX}"; mc mb "infra/${BUCKET}"; echo "BUCKET=${BUCKET}"
|
||||
# 2) 最小权限 policy(仅该 bucket 下 game-staging/* 前缀)
|
||||
cat > /srv/game-staging/minio-policy.json <<JSON
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{ "Effect": "Allow", "Action": ["s3:GetObject","s3:PutObject","s3:DeleteObject"],
|
||||
"Resource": ["arn:aws:s3:::${BUCKET}/game-staging/*"] },
|
||||
{ "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::${BUCKET}"],
|
||||
"Condition": {"StringLike": {"s3:prefix": ["game-staging/*"]}} }
|
||||
]
|
||||
}
|
||||
JSON
|
||||
# >>> 评审门:人工核验 policy JSON 无通配越权(无 *:* / 无跨 bucket)后再继续 <<<
|
||||
mc admin policy create infra game-staging-rw /srv/game-staging/minio-policy.json
|
||||
# 3) 独立最小权限 USER(R11:非 root 子账号)
|
||||
GS_SECRET=$(head -c16 /dev/urandom | od -An -tx1 | tr -d ' ')
|
||||
mc admin user add infra game-staging-app "$GS_SECRET"
|
||||
mc admin policy attach infra game-staging-rw --user game-staging-app
|
||||
mc admin user svcacct add infra game-staging-app # 输出 Access/Secret Key → 仅这对 key 写入 application-staging 文件存储配置(env 占位)
|
||||
```
|
||||
|
||||
> 落地:MinIO key/endpoint/bucket 经 yudao `infra_file_config` 或 env 注入产品端上传链路;本节只交付「bucket+policy+独立 user+key」。
|
||||
|
||||
### A11. 备份 / 重建 / 回滚(R12,down 不带 -v)
|
||||
|
||||
```bash
|
||||
# 幂等重建(保留数据卷)
|
||||
cat > /srv/game-staging/rebuild.sh <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd /srv/game-staging
|
||||
docker compose --env-file .env down # 不带 -v:保留 mysql/redis 数据卷
|
||||
docker rm -f game-staging-app 2>/dev/null || true
|
||||
docker compose --env-file .env up -d game-staging-mysql game-staging-redis
|
||||
until [ "$(docker inspect -f '{{.State.Health.Status}}' game-staging-mysql)" = "healthy" ]; do sleep 3; done
|
||||
echo "REBUILD-OK (数据卷保留)"
|
||||
SH
|
||||
chmod +x /srv/game-staging/rebuild.sh
|
||||
# 钱财闭环验收后备份
|
||||
cat > /srv/game-staging/backup.sh <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source /srv/game-staging/.env
|
||||
TS=$(date +%Y%m%d-%H%M%S); OUT=/srv/game-staging/backup/dump-${TS}.sql
|
||||
mkdir -p /srv/game-staging/backup
|
||||
docker exec game-staging-mysql mysqldump -uroot -p"${STAGING_MYSQL_ROOT_PWD}" \
|
||||
--single-transaction --routines --triggers ruoyi-vue-pro > "${OUT}"
|
||||
gzip "${OUT}" && echo "BACKUP-OK ${OUT}.gz"
|
||||
SH
|
||||
chmod +x /srv/game-staging/backup.sh
|
||||
```
|
||||
|
||||
> 回滚铁律:`down` **不带 `-v`**;`-v` 仅人工显式执行,不写入任何脚本。app 容器无状态可随意 `docker rm -f game-staging-app` 重建。
|
||||
|
||||
### A12. 12 段联调验真
|
||||
|
||||
- **端到端真跑(先验)**:(5)审核状态机 /(6)feed 按 zone SQL 出流 /(7)试玩 session /(8)互动幂等 /(9)广告 mock 计费(ecpm/1000,幂等键 `trace_id+event_type+tenant_id`)。
|
||||
- **钱财闭环(次验,手动触发)**:(10)trade T+1 结算(xxl-job 关,手动调 SettlementJob→×0.80→`game_trade_income` uk 幂等+累加 balance)/(12)提现(≥500 且≤balance,原子冻结→mock 打款)。
|
||||
- **必须桩(最后串)**:(2)Dify(手动 POST dify/callback)/(3)runtime 编译(直写 build 成功态+伪 package_url)/(4)发布回写 project.game_version(TODO Feign 对接点)/(11)telemetry 改同步直写绕 MQ。
|
||||
|
||||
---
|
||||
|
||||
## §3 主线1-B:4 个 contractGaps(命令级)
|
||||
|
||||
> 三相纪律:契约改动属 Phase A 契约锁(过 2 轮 review);后端/前端代码属 Phase B/C。改 `project.publish` 由主 agent 串行(R1),带 project 9 单测 + 4 新断言回归门。
|
||||
|
||||
### GAP-1 发布门禁结构化(in-place 破坏性,R1)
|
||||
|
||||
【已验证】`project.yaml` publish 响应 = `CommonResultBoolean`;`submitPublish` 返回 void;`AppProjectController` 硬编码 `success(true)`;前端 `publish-gates.ts` 纯客户端预检。
|
||||
|
||||
**A. 契约 `contracts/api-schemas/project.yaml`**(publish 端点 200 响应 `$ref` 改结构化信封,**in-place 破坏,非 additive**;删"旧 boolean 端点保留"承诺):
|
||||
|
||||
```yaml
|
||||
# publish 端点 200 响应(in-place 升级,破坏性)
|
||||
schema: { $ref: '#/components/schemas/CommonResultProjectPublish' }
|
||||
# components.schemas 新增三个:
|
||||
CommonResultProjectPublish:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: integer }
|
||||
data: { $ref: '#/components/schemas/ProjectPublishRespVO' }
|
||||
msg: { type: string }
|
||||
ProjectPublishRespVO:
|
||||
type: object
|
||||
required: [admitted, gates]
|
||||
properties:
|
||||
admitted: { type: boolean, description: 'true=已进审核;false=被门禁拦截未进审核(不抛异常)' }
|
||||
gates:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/GateResultVO' }
|
||||
GateResultVO:
|
||||
type: object
|
||||
required: [key, label, pass]
|
||||
properties:
|
||||
key: { type: string, description: 'status/title/cover/version/age/zone/compliance' }
|
||||
label: { type: string }
|
||||
pass: { type: boolean }
|
||||
code: { type: string, description: '失败码(通过为空)' }
|
||||
message: { type: string }
|
||||
```
|
||||
|
||||
**B. 后端(project-server)**:
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| 新增 `controller/app/project/vo/GateResultVO.java` | `String key,label,code,message; Boolean pass;` + `@Data`+`@Schema` |
|
||||
| 新增 `controller/app/project/vo/ProjectPublishRespVO.java` | `Boolean admitted; List<GateResultVO> gates;` |
|
||||
| `ProjectService.java` | `submitPublish` 签名 `void`→`ProjectPublishRespVO`(blast radius 仅 controller+单测,无跨模块 RPC 依赖此方法) |
|
||||
| `ProjectServiceImpl.java`(L89-108) | 校验项改为**收集 gates**(不立即抛);注入 compliance gate(经 `ComplianceGateApi.evaluate` seam,R1)映射为 `key=compliance` 一条;全 pass→`status=1`+`admitted=true`;硬门禁失败→`admitted=false` 不改状态、**不抛异常**。**不动 `reviewProject()`/`game_review_record`/`ReviewDecisionEnum`(D2)** |
|
||||
| `AppProjectController.java`(L78-83) | 返回 `CommonResult<Boolean>`→`CommonResult<ProjectPublishRespVO>` |
|
||||
|
||||
> 主 agent 串行独占改本组(R1)。`ComplianceGateApi` 未就绪时走桩(恒 pass),与 Phase C 接入对齐(开关 `game.publish.compliance-gate-enabled`)。
|
||||
|
||||
**C. 前端 game-studio(同批迁移,无并存期,R1)**:`api/types.ts`(加 `GateResult`/`ProjectPublishResp`)、`api/project.ts`(publish 返回 `Promise<ProjectPublishResp>`)、`store/project.ts`(透传)、`mock/project.ts`(`publishHandler` 返 `{admitted,gates}`)、`Publish.vue`(用 `resp.admitted` 判定 + 渲染服务端 gates)、`publish-gates.ts`(头注改"软提示,权威以服务端 gates 为准")。
|
||||
|
||||
**验证**:`mvn -pl game-module-project/game-module-project-server -am test`(9 旧单测全绿 + 新断言:适龄缺失→admitted=false 且 gate(age).pass=false;全通过→admitted=true+status=1;compliance 桩 block→admitted=false);`cd game-studio && npm run build`。
|
||||
|
||||
### GAP-2 取包端点(manifestUrl,R8 两分支)
|
||||
|
||||
【已验证】`RuntimePackageRespVO` 有 `packageUrl` 无 `manifestUrl`;`SandboxPolicyVO.allowOrigins` 已存在;前端 `GamePlayer.vue resolvePackage`(L99-127) 无条件用 demo 兜底。
|
||||
|
||||
**A. 契约 `runtime.yaml`**:`RuntimePackageRespVO` 加 `manifestUrl`(additive);`SandboxPolicyVO` 描述补 CORS 约束(不改字段)。
|
||||
|
||||
**B. 后端(R8 Phase A 定)**:
|
||||
- **分支 A(派生,零 DDL)**:`RuntimePackageRespVO` 加 `private String manifestUrl;`;`RuntimePackageServiceImpl`/`RuntimeConvert` 由 `packageUrl` 同前缀派生 `.../manifest.json`。
|
||||
- **分支 B(增列)**:若 `game_runtime_package` 无可派生列 → **新 Flyway 补偿迁移**(如 `V3.1.0__add_manifest_path.sql`,纳入唯一性校验),不藏 VO 回填。
|
||||
- MinIO/OSS CORS(D3):staging bucket 设 CORS rule(`AllowedOrigin`=宿主 origin、`AllowedMethod=GET/HEAD`、`ExposeHeader=ETag`),过 D3 评审门。宿主 origin 具体值待联调形态定(见 §9)。
|
||||
|
||||
**C. 前端**:`host/contract.ts` 加 `manifestUrl?`;`GamePlayer.vue resolvePackage` 去无条件 demo 兜底,有 `manifestUrl` 则 `fetch`→按 `checksum` sha256 校验(`crypto.subtle.digest`)→通过返真包,失败才落 demo+`emit('error')`;`mock/runtime.ts` 补 `manifestUrl`。
|
||||
|
||||
**验证**:`mvn -pl game-module-runtime/game-module-runtime-server -am test`(manifestUrl 非空);`npm run build`;联调 `curl -H 'Origin: <宿主源>' -I '<manifestUrl>'` 命中 `Access-Control-Allow-Origin`。
|
||||
|
||||
### GAP-3 ShareMeta versionId(轻量)
|
||||
|
||||
【已验证】`ShareMetaRespVO` 无 versionId;`FeedServiceImpl.getShareMeta`(L154-170) 不读 project 版本;`FeedServiceImpl` 现有 `isGamePublished`。
|
||||
|
||||
**A. 契约 `feed.yaml`**:`ShareMetaRespVO` 加 `versionId`(additive)。
|
||||
|
||||
**B. 后端**:`ShareMetaRespVO.java` 加 `private Long versionId;`;`getShareMeta` —— **仅当 `isGamePublished` 为真时** `setVersionId(projectApi.getCurrentVersionId(gameId).getCheckedData())`,否则留空(critique 修正:null 路径明示;`getCheckedData()` 对 code=0 的 null data 返回 null 不抛)。`ProjectApi.getCurrentVersionId` 由主 agent 在 §1.2 ProjectApi 单点收敛中新增(无已发布版本返回 null)。
|
||||
|
||||
**C. 前端**:`api/types.ts` `ShareMeta` 加 `versionId?`;`Share.vue onPlay` 路由 `/play/${gid}/${meta.value?.versionId ?? ''}`(缺省兜底);`mock/feed.ts` 补 versionId;删契约缺口注释。
|
||||
|
||||
**验证**:`mvn -pl game-module-feed/game-module-feed-server,game-module-project/game-module-project-api -am test`(已发布游戏 versionId 非空);`npm run build`。
|
||||
|
||||
### GAP-4 遥测命名(破坏性收敛 camelCase,R9)
|
||||
|
||||
【已验证】`events.schema.json` envelope/user 为 snake + `additionalProperties:false`;`EnvelopeReqVO`/`TelemetryEventDO`/前端 SDK 全 camelCase;`dispatchToMq` 仍 TODO(无生产数据)。
|
||||
|
||||
**改动(单文件 `contracts/events.schema.json`,代码侧零改)**:envelope/user 键 snake→camel:`schema_version→schemaVersion`、`trace_id→traceId`、`session_id→sessionId`、`user_id→userId`、`anon_id→anonId`、context 内 `game_id→gameId`/`version_id→versionId`/`device_type→deviceType`(`ts` 不变;事件私有 props 如 `duration_ms` 不改);description 标注 snake 已废弃。保持 `additionalProperties:false`。
|
||||
|
||||
**Phase A 评审记录(R9)**:写明利用无生产数据+MQ 未实现窗口一次性破坏收敛、豁免 README §2「新事件名并行」规则(改信封非事件名)、确认仓外无 snake 消费者。
|
||||
|
||||
**最小 camelCase 样例(内联,供 ajv)** —— 落 `/tmp/envelope-sample.json`:
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"ts": 1733740800000,
|
||||
"traceId": "t-abc-123",
|
||||
"sessionId": "s-xyz-789",
|
||||
"user": { "userId": 1, "anonId": "anon-1" },
|
||||
"context": { "gameId": 1024, "versionId": 1, "deviceType": "mobile" },
|
||||
"event": "play_start",
|
||||
"props": { "durationMs": 0 }
|
||||
}
|
||||
```
|
||||
```bash
|
||||
# 工具:npx ajv-cli(pattern 默认可;若用 format 追加 ajv-formats)
|
||||
npx ajv-cli validate -s contracts/events.schema.json -d /tmp/envelope-sample.json
|
||||
# 改前 fail(snake schema 拒 camel)→ 改后 pass
|
||||
```
|
||||
|
||||
**验证**:上述 ajv 改前 fail/改后 pass;`mvn -pl game-module-telemetry/game-module-telemetry-server -am test`(VO/DO 未改,回归零破坏);`npm run build`。**回滚**:还原 events.schema.json 单文件(零代码回滚)。
|
||||
|
||||
---
|
||||
|
||||
## §4 主线2-A:compliance(只 Gate seam,D2)
|
||||
|
||||
> 范围铁律(D2/D5/R1/R3/R5):compliance **只做 Gate**(verdict + 分级 + 封禁台账),**绝不**重写 project 审核状态机;**只交付 `ComplianceGateApi.evaluate` seam 供 GAP-1 注入**,不自改 submitPublish、不自登记 publish-blocked 错误码(R1);封禁按状态分支不破坏状态机(R3);降权 P1 本波不生效、不建孤儿 seam(R5)。错误码段 **109**(README §4 已登记),Flyway **V9**。
|
||||
|
||||
### 1. compliance.yaml 契约
|
||||
|
||||
新建 `contracts/api-schemas/compliance.yaml`,对齐 ad.yaml 头部风格。三类端点 + 一个 RPC seam:
|
||||
|
||||
**1.1 锁风门 Gate(RPC seam,给 project.publish 同进程调,R1 唯一形态)**:
|
||||
```yaml
|
||||
# /rpc-api/compliance —— ComplianceGateApi.evaluate(project.publish 同进程 @Primary 调)
|
||||
components:
|
||||
schemas:
|
||||
ComplianceGateReqDTO:
|
||||
type: object
|
||||
required: [gameId, versionId]
|
||||
properties:
|
||||
gameId: { type: integer, format: int64 }
|
||||
versionId: { type: integer, format: int64 }
|
||||
title: { type: string }
|
||||
summary: { type: string }
|
||||
ageRating: { type: string, description: 'all/8+/12+/16+' }
|
||||
promptHash: { type: string, description: '生成 Prompt 摘要(可空)' }
|
||||
GateVerdict: # = 草案 ComplianceGateRespDTO,R1 命名对齐
|
||||
type: object
|
||||
required: [verdict]
|
||||
properties:
|
||||
verdict: { type: string, enum: [pass, review, block] }
|
||||
rating: { type: string }
|
||||
detail: { type: array, items: { $ref: '#/components/schemas/ComplianceGateItemDTO' } }
|
||||
ComplianceGateItemDTO:
|
||||
type: object
|
||||
properties:
|
||||
atom: { type: string, description: 'textContent/imageContent/ip-reserved' }
|
||||
verdict: { type: string, enum: [pass, review, block] }
|
||||
reason: { type: string }
|
||||
# 接口(RPC):POST /rpc-api/compliance/evaluate → CommonResult<GateVerdict>
|
||||
```
|
||||
> 聚合语义(Service 固化,可单测):`verdict = max(各原子)`,`block > review > pass`。ip 扩展点:聚合预留 `IpComplianceSeam`(默认 pass 桩),Wave4 接 ip 只换实现。
|
||||
|
||||
**1.2 封禁(admin-api,RBAC,R3 状态分支)**:
|
||||
```yaml
|
||||
# POST /admin-api/compliance/ban/create body: BanCreateReqVO{targetType(1游戏/2用户), targetId, banType(1封禁/2降权), reason}
|
||||
# → 落 game_user_ban(主操作,恒执行)
|
||||
# → 下架动作【R3 状态分支】:仅当 targetType=游戏 且 game.status==PUBLISHED 时才调 project reviewProject(decision=3);非 PUBLISHED 态只记封禁、不调状态机、不抛异常
|
||||
# → 降权出流屏蔽:P1 本波不生效(R5,不调 feed,不建 FeedDownweightApi 孤儿 seam)
|
||||
# POST /admin-api/compliance/ban/cancel body:{banId} → 解封(status=0)
|
||||
# GET /admin-api/compliance/ban/page → 封禁台账分页(admin RBAC)
|
||||
```
|
||||
|
||||
**1.3 分级(app-api 只读)**:`GET /app-api/compliance/rating/get?gameId=&versionId= → ContentRatingRespVO{rating,ratedBy,updateTime}`(分级写入仅由 Gate evaluate 时落 `game_content_rating` 与 admin 复核产生)。
|
||||
|
||||
**错误码(compliance.yaml 头部 + ErrorCodeConstants,段 109;publish-blocked 不在此登记,R1)**:
|
||||
```
|
||||
1_109_001_000 COMPLIANCE_GATE_TARGET_NOT_FOUND 锁风门:游戏/版本不存在
|
||||
1_109_002_000 COMPLIANCE_BAN_TARGET_INVALID 封禁:目标类型非法
|
||||
1_109_002_001 COMPLIANCE_BAN_ALREADY_EXISTS 封禁:该目标已被封禁
|
||||
1_109_002_002 COMPLIANCE_BAN_NOT_EXISTS 解封:封禁记录不存在
|
||||
1_109_003_000 COMPLIANCE_RATING_NOT_FOUND 分级:无分级记录
|
||||
```
|
||||
|
||||
### 2. Flyway V9 DDL(不建审核状态机表)
|
||||
|
||||
新建 `contracts/db-schemas/V9.0.0__create_game_compliance.sql`(授权源)+ 复制执行副本(diff-identical)。**三张表**:`game_compliance_gate_result`(lockwind gate result:id/game_id/version_id/verdict/rating/details/trace_id + 审计列+tenant_id,`idx_game_version`)、`game_content_rating`(id/game_id/version_id/rating/rated_by + 审计列+tenant_id,`uk_game_version(game_id,version_id,deleted,tenant_id)`)、`game_user_ban`(id/target_type/target_id/ban_type/status/reason + 审计列+tenant_id,`uk_target_active(target_type,target_id,status,deleted,tenant_id)` + `idx_type_status`)。全表 InnoDB+utf8mb4、显式列、中文列注释、状态机 tinyint 由 Service 校验。**明确不建 audit_record/审核状态机表**(归 project `game_review_record`)。DDL 全文见草案 compliance §2(保留,事实准确)。
|
||||
|
||||
### 3. 模块骨架(克隆 game-module-ad)
|
||||
|
||||
```bash
|
||||
cd /root/games-development-ai/game-cloud
|
||||
mkdir -p game-module-compliance/game-module-compliance-api/src/main/java/cn/wanxiang/game/module/compliance/{api,dto,enums}
|
||||
mkdir -p game-module-compliance/game-module-compliance-server/src/main/java/cn/wanxiang/game/module/compliance/{api,controller/admin/vo,controller/app/vo,service/gate,service/ban,framework/atom,convert,dal/dataobject/gate,dal/dataobject/rating,dal/dataobject/ban,dal/mysql}
|
||||
mkdir -p game-module-compliance/game-module-compliance-server/src/main/resources/db/migration
|
||||
mkdir -p game-module-compliance/game-module-compliance-server/src/test/java/cn/wanxiang/game/module/compliance/service/gate
|
||||
```
|
||||
|
||||
**3 个 pom**(克隆 ad):aggregator + `-api`(yudao-common+springdoc(provided)+validation+openfeign(optional))+ `-server`(本模块-api + **game-module-project-api**(R3 下架仅 PUBLISHED 调 reviewProject)+ env/biz-tenant/security/mybatis/redis/rpc/test)。**不依赖 feed-api**(R5 不建降权孤儿 seam);aigc 风格原子在 compliance 内自包含(aigc 无 T-AGC-19)。
|
||||
|
||||
**-api**:`enums/ApiConstants`(NAME=compliance-server, PREFIX=/rpc-api/compliance)、`enums/ErrorCodeConstants`(段109 五码)、`enums/ComplianceVerdictEnum`(pass/review/block + max)、`api/ComplianceGateApi`(@FeignClient,`@PostMapping(PREFIX+"/evaluate")`→`CommonResult<GateVerdict>`)、`dto/{ComplianceGateReqDTO,GateVerdict,ComplianceGateItemDTO}`。
|
||||
|
||||
**-server**:`api/ComplianceGateApiImpl`(@RestController @Primary,委托 GateService,仿 ProjectApiImpl)、`service/gate/ComplianceGateService(Impl)`(聚合→verdict,落 gate_result + content_rating)、`framework/atom/StyleComplianceAtom`(桩默认 pass+TODO 接 aigc seam)、`framework/atom/IpComplianceSeam`(D5 默认 pass)、`service/ban/UserBanService(Impl)`(**R3 状态分支**:写 ban 台账 + 仅 PUBLISHED 调 reviewProject(3))、controller app/admin + VO + convert + DO×3 + Mapper×3。
|
||||
|
||||
**接 yudao-server(Phase C 主 agent)**:根 `game-cloud/pom.xml` `<modules>` 加 `game-module-compliance`;`yudao-server/pom.xml` 加 `game-module-compliance-server`。【已验证】无需改 `YudaoServerApplication`(scan/@MapperScan 通配 `cn.wanxiang.game.module`) 与 application.yaml(type-aliases 通配)。
|
||||
|
||||
### 4. 接入 project.publish(Phase C 主 agent 串行,R1 唯一注入点)
|
||||
|
||||
compliance **只交付 `ComplianceGateApi.evaluate` seam**;注入逻辑在 GAP-1 的 `ProjectServiceImpl.submitPublish` 内由主 agent 串行写(见 §3 GAP-1 B),映射为 `key=compliance` 一条 GateResultVO:`verdict=block`→该 gate `pass=false`+`admitted=false`(**不抛异常**,R1);`pass/review`→`pass=true` 进 REVIEWING(review 由人工台兜底,状态机权威归 project)。
|
||||
- `project-server/pom.xml` 加 `game-module-compliance-api` 依赖(无 Maven 环:project-server→compliance-api,compliance-server→project-api,四 jar 无环,已核实 ad 同构)。
|
||||
- 开关 `@Value("${game.publish.compliance-gate-enabled:true}")`:false 跳过 Gate(回旧行为,运行期降级,非"双实现")。
|
||||
|
||||
**回归门(R1 唯一实现下,铁律)**:
|
||||
- project 既有 9 单测全绿不改断言(`testSubmitPublish_success` 加 `@Mock ComplianceGateApi`,stub `evaluate` 返 `verdict=pass`,`ReflectionTestUtils.setField(...,"gateEnabled",true)`)。
|
||||
- 4 新 publish 断言(加进 `ProjectServiceImplTest`,不改旧用例):① gate block→`admitted=false` 且 `verify(projectMapper,never()).updateById(...)`;② gate pass→`admitted=true`+status=REVIEWING;③ gate review→进 REVIEWING;④ gateDisabled→`verify(gateApi,never()).evaluate(...)` 且进 REVIEWING。
|
||||
- `reviewProject` 三断言(pass/block/UNLIST 流转)保持原样 = D2 锚点。
|
||||
|
||||
### 5. feed 降权(R5:P1 本波不生效)
|
||||
|
||||
【已验证】feed 无降权入口。**本波 compliance 不调 feed、不建 FeedDownweightApi 孤儿 seam**。compliance 完成条件显式标注「封禁台账写库 +(若 PUBLISHED)下架;**降权屏蔽 P1 本波不生效**」。若保留代码占位,仅留 TODO 明注,不写默认关开关伪装"已支持"。
|
||||
|
||||
### 6. 验证
|
||||
|
||||
```bash
|
||||
cd /root/games-development-ai/game-cloud
|
||||
mvn -f game-module-compliance/pom.xml test # (a) compliance 独立建+单测
|
||||
mvn -f game-module-project/pom.xml test # (b) project 回归(9旧+4新全绿)
|
||||
mvn -pl yudao-server -am compile -f /root/games-development-ai/game-cloud/pom.xml # (c) 全量集成编译
|
||||
diff /root/games-development-ai/contracts/db-schemas/V9.0.0__create_game_compliance.sql \
|
||||
game-module-compliance/game-module-compliance-server/src/main/resources/db/migration/V9.0.0__create_game_compliance.sql # (d) Flyway diff-identical
|
||||
grep -rn "game_review_record" game-module-compliance/ && echo "D2-VIOLATION" || echo "D2-OK(compliance 零写审核状态机表)" # (e) D2 验证
|
||||
```
|
||||
完成条件:(a)(b)(c) BUILD SUCCESS;project 13 单测(9+4)全绿;(d) 无差异;(e) D2-OK。
|
||||
|
||||
### 7. 回滚
|
||||
|
||||
代码:根 pom 删 module + yudao-server pom 删依赖 + 删模块目录 + revert GAP-1 注入。运行期降级:`game.publish.compliance-gate-enabled=false` 回旧行为。DB:V9 已合入则写 V9.0.1 补偿迁移 `DROP TABLE`,禁改 V9.0.0。
|
||||
|
||||
---
|
||||
|
||||
## §5 主线2-B:studio(创作主链路最小版)
|
||||
|
||||
> 错误码段 **112(R4 治理新登记,非"修漂移")** | Flyway **V8** | 包名 `cn.wanxiang.game.module.studio` | 目录 `game-cloud/game-module-studio/`。三相:Phase A 锁 studio.yaml/V8 + 治理登记 + ProjectApi.createProject 硬前置(随 §1.2 收敛)+ aigc-api `AigcApi` seam 预审;Phase B 子 agent 互斥建 `game-module-studio/`(消费 aigc-api/project-api mock);Phase C 主 agent 接 aigc(加 AigcApi/Impl + aigc 回归门)+ 接 yudao-server pom + 前端切换。
|
||||
> 边界(Doc B §71-76):studio = 有状态创作工作台后端,**不跑 LLM(委托 aigc)、不持久化项目版本(交 project)、不做锁风裁决(调 compliance)**。
|
||||
|
||||
### 1. P0 范围(逐项 owner=studio,调 aigc)
|
||||
|
||||
| 草稿 scope | Doc A P0 | studio 落点 | 下游 |
|
||||
|---|---|---|---|
|
||||
| 创作会话 | P-CRT-10 | `game_studio_session`,进工作坊 upsert | — |
|
||||
| 任务链(线性单步 DRAFT→GENERATING→DONE/FAILED) | P-CRT-01/09 | `game_studio_taskchain`,编排调 aigc | aigc submitGenerate |
|
||||
| 单步出整包 | P-CRT-02 | 资产图 `game_studio_asset` 仅落槽位 seam,不六路并发 | aigc |
|
||||
| 附件上下文 | P-CRT-04 | `attachments`(JSON) 随草稿持久化,**本波仅持久化不透传 aigc(R7)** | — |
|
||||
| 草稿移交 project | P-TPL-03 | 调 `ProjectApi.createProject(creatorUserId,...)` 拿 gameId 写回会话(R2 硬前置) | project createProject |
|
||||
| 进度推送(轮询) | P-CRT-12 | `GET /app-api/studio/task/{id}` 透传 aigc progress,不上 SSE(D4) | aigc getTask |
|
||||
| 模板浏览/应用 | P-TPL-01/03 | 透传 aigc `getTemplateList` | aigc getTemplateList |
|
||||
|
||||
> 狠切留 seam 不实现(P1):rig/对白树/可视化批量;资产图仅槽位表+单步调度。
|
||||
|
||||
### 2. studio.yaml 契约(端 /app-api/studio/*,错误码段 112)
|
||||
|
||||
新建 `contracts/api-schemas/studio.yaml`(OpenAPI 3.0.3,CommonResult 信封)。path:`/template/list`(GET 透传 aigc)、`/draft`(POST 建草稿:编排 `ProjectApi.createProject` 拿 gameId + upsert 会话)、`/generate`(POST 一句话生成:调 aigc submitGenerate)、`/task/{id}`(GET 进度轮询)、`/task/{id}/regenerate`(POST 重生成)。schema:`StudioDraftCreateReqVO`/`StudioGenerateReqVO`(其 `attachments` 描述改 **"本波仅持久化,P1 透传 aigc"**,R7)/`StudioSessionRespVO`/`StudioTaskChainRespVO`。`x-feign-contracts`: studio 消费 aigc-api AigcApi + project-api ProjectApi.createProject。契约全文见草案 studio §2(保留,仅改 attachments 描述)。
|
||||
|
||||
**治理登记动作(Phase A 必做,R4)**:`contracts/README.md` §4 追加 `studio=112`、§1 清单/目录树追加 `studio.yaml`+`V8.0.0__create_game_studio.sql`、§2 追加 Wave3 studio;`CLAUDE.md` 模块定位加 studio 后端模块(注明与 game-studio 前端仓区分)。
|
||||
|
||||
### 3. Flyway V8 DDL
|
||||
|
||||
新建 `contracts/db-schemas/V8.0.0__create_game_studio.sql`(授权源)+ 复制执行副本(diff-identical)。三表:`game_studio_session`(id/creator_user_id/game_id/template_id/title/prompt/attachments/status + 审计列+tenant_id,`idx_creator`)、`game_studio_taskchain`(id/session_id/creator_user_id/aigc_task_id/game_id/version_id/status/progress/retry_of + 审计列+tenant_id,`idx_session`+`idx_aigc`)、`game_studio_asset`(id/session_id/asset_type/asset_url/status + 审计列+tenant_id,`idx_session_type`)。InnoDB+utf8mb4、显式列、中文列注释。DDL 全文见草案 studio §3(保留,事实准确)。
|
||||
|
||||
```bash
|
||||
# 复制执行副本到 yudao-server + diff-identical
|
||||
cp contracts/db-schemas/V8.0.0__create_game_studio.sql \
|
||||
game-cloud/yudao-server/src/main/resources/db/migration/V8.0.0__create_game_studio.sql
|
||||
diff contracts/db-schemas/V8.0.0__create_game_studio.sql \
|
||||
game-cloud/yudao-server/src/main/resources/db/migration/V8.0.0__create_game_studio.sql # 须零差异
|
||||
```
|
||||
|
||||
### 4. 模块骨架(克隆 ad/project 黄金模板,Phase B 子 agent 互斥建)
|
||||
|
||||
```bash
|
||||
cd /root/games-development-ai/game-cloud
|
||||
mkdir -p game-module-studio/game-module-studio-api/src/main/java/cn/wanxiang/game/module/studio/{api,dto,enums}
|
||||
mkdir -p game-module-studio/game-module-studio-server/src/main/java/cn/wanxiang/game/module/studio/{controller/app/studio/vo,convert/studio,dal/dataobject/studio,dal/mysql/studio,service/studio}
|
||||
mkdir -p game-module-studio/game-module-studio-server/src/main/resources/db/migration
|
||||
mkdir -p game-module-studio/game-module-studio-server/src/test/java/cn/wanxiang/game/module/studio/service/studio
|
||||
```
|
||||
|
||||
骨架文件(克隆同名文件,改包名→studio、错误码段→112、Feign NAME→studio-server):3 pom(`-server` 依赖 **game-module-aigc-api + game-module-project-api**)、`enums/{ApiConstants(NAME=studio-server,PREFIX=/rpc-api/studio),ErrorCodeConstants(段112),StudioSessionStatusEnum}`、3 DO(继承 TenantBaseDO 对齐 V8 三表)、3 Mapper(显式列无裸 select*)、`StudioService(Impl)`(编排见 §5)、`AppStudioController`(@RequestMapping("/studio"))、`StudioConvert`、`StudioServiceImplTest`(mock aigc-api/project-api)。接 yudao-server pom:`yudao-server/pom.xml` 追加 `game-module-studio-server` 依赖(Phase C 主 agent)。
|
||||
|
||||
### 5. studio→aigc 编排(R2 createProject 硬前置;MVP 轮询非 MQ)
|
||||
|
||||
【已验证】`game-module-aigc-api` 现仅含 enums,**无任何 Feign `*Api` 接口**。故 Phase C 主 agent 串行给 aigc 补 `-api` seam(仿 ProjectApi/AdRevenueApi):
|
||||
```java
|
||||
// game-module-aigc-api/.../api/AigcApi.java(@FeignClient(name="aigc-server"), PREFIX=/rpc-api/aigc)
|
||||
CommonResult<List<AigcTemplateDTO>> getTemplateList();
|
||||
CommonResult<AigcGenerateRespDTO> submitGenerate(AigcGenerateReqDTO req); // 入参带 userId(RPC 无登录上下文)
|
||||
CommonResult<AigcTaskRespDTO> getTask(Long taskId, Long userId);
|
||||
CommonResult<AigcGenerateRespDTO> retryTask(Long taskId, Long userId); // P-CRT-09
|
||||
```
|
||||
`game-module-aigc-server/.../api/AigcApiImpl`(@RestController @Primary 委托既有 `AigcTaskService`);DTO 新建在 aigc-api/dto。**aigc 回归门**:`mvn -pl game-module-aigc/game-module-aigc-server -am test` 既有 9 单测仍绿(只暴露不改 service)。
|
||||
|
||||
**StudioServiceImpl 编排(R2 createProject 是硬前置,不接受 game_id 恒空兜底)**:
|
||||
1. `createDraft`:调 `ProjectApi.createProject(creatorUserId, ...)`(§1.2 已定稿)拿 gameId 回写 `session.game_id`;落 session(status=0)。
|
||||
2. `generate`:校验 session 归属→落 taskchain(status=1)→`AigcApi.submitGenerate`(带 prompt/templateId/gameId/userId)→回填 aigc_task_id→返回 taskChainId。同步直写无 MQ(D4)。**attachments 仅持久化(R7),不入 aigc reqDTO**。
|
||||
3. `getTask`(轮询):`AigcApi.getTask`→透传 progress/status→映射链态(aigc succeeded→链完成回填 version_id;failed/timed_out→链失败)。
|
||||
4. `regenerate`:新建 taskchain(retry_of=原 id),调 `AigcApi.retryTask`。
|
||||
- 失败路径:aigc 超时/异常→链态 3 + 中文日志(traceId 贯穿);归属校验失败→`1_112_000_001`。
|
||||
|
||||
### 6. 前端 game-studio 接线(mock→真实切换)
|
||||
|
||||
新建 `src/api/studio.ts`(贴 studio.yaml)+ `src/mock/studio.ts`(克隆 mock/aigc.ts 进度节奏,`src/mock/index.ts` 注册);`api/types.ts` 加 StudioSession/StudioTaskChain 类型;`store/create.ts` `generate` 改单步调 `studioApi.generate`(删前端"先建 project 再调 aigc"两步)、`pollTask` 改 `studioApi.getTask`;`Create.vue onSubmit` 改调 `studioApi.createDraft+generate`。切真实:`.env` 注入 `VITE_API_BASE` 指 staging 后端 + mock token,不改 `request.ts`。若 studio.yaml 落地缺字段记 openItems 不私造。
|
||||
|
||||
### 7. 错误码登记(段 112)
|
||||
|
||||
```java
|
||||
// game-module-studio-api/.../enums/ErrorCodeConstants.java(克隆 aigc 范式,段 1-112)
|
||||
ErrorCode STUDIO_SESSION_NOT_EXISTS = new ErrorCode(1_112_000_000, "创作会话不存在");
|
||||
ErrorCode STUDIO_SESSION_NOT_OWNER = new ErrorCode(1_112_000_001, "无权操作他人的创作会话");
|
||||
ErrorCode STUDIO_TEMPLATE_REQUIRED = new ErrorCode(1_112_000_002, "玩法模板不能为空");
|
||||
ErrorCode STUDIO_TASKCHAIN_NOT_EXISTS = new ErrorCode(1_112_001_000, "创作任务链不存在");
|
||||
ErrorCode STUDIO_AIGC_DELEGATE_FAIL = new ErrorCode(1_112_002_000, "生成委托失败(aigc 调用异常)");
|
||||
```
|
||||
同步 `contracts/README.md` §4 追加 `studio=112`(R4 治理登记)。
|
||||
|
||||
### 8. 回滚
|
||||
|
||||
代码:studio 全新建,回滚=删未跟踪文件 + 还原 yudao-server/pom.xml studio 依赖 + aigc-api/server 的 AigcApi/Impl + 前端两步直连。DB:V8 未执行前删文件,已执行写 V8.0.1 补偿 `DROP TABLE game_studio_*`。aigc 接入回滚门:AigcApi/Impl 纯暴露层,删除后 aigc 9 单测仍绿。前端:`VITE_API_BASE` 留空回 mock。
|
||||
|
||||
---
|
||||
|
||||
## §6 三相编排
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph PA["Phase A 收口(主 agent 串行,开工前必须完成)"]
|
||||
A1["R1 publish 唯一化:改 ProjectServiceImpl.submitPublish + AppProjectController + project.yaml(in-place 破坏)"]
|
||||
A2["R2 ProjectApi seam 单点定稿:getCurrentVersionId/createProject(+forceUnlist?) 串行改 ProjectApi+Impl + 9 单测回归"]
|
||||
A3["R4 studio 治理登记 112/V8(CLAUDE.md+README §4,创始人确认)"]
|
||||
A4["契约锁:project/runtime/feed/events + compliance/studio.yaml + V8/V9(过 2 轮 review)"]
|
||||
A5["实测前置门 P1-P5(clone/mysql tag/maven 镜像/MinIO 凭据+独立 key)"]
|
||||
end
|
||||
subgraph G0["全局前置 G0(Phase B 开工,主 agent 一次)"]
|
||||
G["R6 复制 V6/V7 → yudao-server/db/migration + diff-identical + 构建后 unzip -l jar 核验 7 文件"]
|
||||
end
|
||||
subgraph PB["Phase B 子 agent 并行建(各写互斥目录)"]
|
||||
B1["compliance:game-module-compliance/(不碰 project.publish,只交付 ComplianceGateApi seam)"]
|
||||
B2["studio:game-module-studio/(消费 aigc-api/project-api mock)"]
|
||||
B3["GAP-1/2/3 后端 VO/Service(各模块互斥);GAP-4 改 events.schema.json"]
|
||||
end
|
||||
subgraph PC["Phase C 主 agent 串行接入 + 回归门"]
|
||||
C1["GAP-1 注入 compliance gate + 改 publish;前端同批迁移 admitted"]
|
||||
C2["aigc 加 AigcApi/Impl(aigc 9 单测回归);接 yudao-server pom"]
|
||||
C3["回归门:project 9 单测 + publish 4 新断言 + grep game_review_record 零命中(验 D2) + Flyway diff-identical"]
|
||||
C4["staging 12 段联调"]
|
||||
end
|
||||
PA --> G0 --> PB --> PC
|
||||
```
|
||||
|
||||
**互斥目录纪律**:compliance 子 agent 只写 `game-module-compliance/`;studio 子 agent 只写 `game-module-studio/`;**两者都不碰 `project.publish`/`ProjectApi`/`AigcApi`/`yudao-server/pom.xml`(全归主 agent Phase A/C 串行)**。
|
||||
|
||||
### G0 全局前置(R6,主 agent 一次)
|
||||
|
||||
```bash
|
||||
cd /root/games-development-ai
|
||||
cp game-cloud/game-module-ad/game-module-ad-server/src/main/resources/db/migration/V6.0.0__create_game_ad.sql \
|
||||
game-cloud/yudao-server/src/main/resources/db/migration/V6.0.0__create_game_ad.sql
|
||||
cp game-cloud/game-module-trade/game-module-trade-server/src/main/resources/db/migration/V7.0.0__create_game_trade.sql \
|
||||
game-cloud/yudao-server/src/main/resources/db/migration/V7.0.0__create_game_trade.sql
|
||||
# diff-identical(授权源 vs 执行副本,零差异)
|
||||
diff contracts/db-schemas/V6.0.0__create_game_ad.sql game-cloud/yudao-server/src/main/resources/db/migration/V6.0.0__create_game_ad.sql
|
||||
diff contracts/db-schemas/V7.0.0__create_game_trade.sql game-cloud/yudao-server/src/main/resources/db/migration/V7.0.0__create_game_trade.sql
|
||||
ls game-cloud/yudao-server/src/main/resources/db/migration/V*.sql | wc -l # 期望 7(V1-V7;V8/V9 由 studio/compliance 各自后补)
|
||||
# 构建后核验 jar 内 7 文件
|
||||
unzip -l game-cloud/yudao-server/target/yudao-server.jar | grep -E 'db/migration/V[0-9]' | wc -l # 期望 7(V8/V9 接入后为 9)
|
||||
```
|
||||
> compliance/studio 章节只引用「G0 已完成」,**不重复 cp V6/V7**(R6)。
|
||||
|
||||
---
|
||||
|
||||
## §7 验收 AC
|
||||
|
||||
| AC | 判定 |
|
||||
|---|---|
|
||||
| AC-1 每模块单测 | `mvn -f game-module-compliance/pom.xml test`、`mvn -f game-module-studio/pom.xml test`、GAP 各模块 `mvn -pl <m> -am test` 全 BUILD SUCCESS |
|
||||
| AC-2 project 回归 | project 13 单测(9 旧不改断言 + 4 新 publish 断言)全绿 |
|
||||
| AC-3 D2 锚点 | `grep -rn game_review_record game-module-compliance/` 零命中;`reviewProject` 三断言原样绿 |
|
||||
| AC-4 staging 门 | G0 PASS + G3(HEALTH-UP/7-MODULES-OK/Flyway≥7 且 game 表数对)+ G4 三条 mock 非未登录(真实路径,R10) |
|
||||
| AC-5 12 段联调 | 端到端 5 段 + 钱财闭环 2 段幂等正确 + 桩 4 段串通 |
|
||||
| AC-6 错误码/Flyway 唯一 | 段 109/112 独占无重叠;V8/V9 唯一;执行副本与 contracts/ diff-identical;publish-blocked 错误码全局仅登记一次(R1) |
|
||||
| AC-7 前端 | `cd game-studio && npm run build` 绿(vue-tsc 假门禁不采信) |
|
||||
|
||||
---
|
||||
|
||||
## §8 回滚
|
||||
|
||||
- **staging**:`docker compose --env-file .env down`(**不带 -v**,保数据卷)+ `rebuild.sh` 幂等重建;app 容器 `docker rm -f game-staging-app` 无状态重建;MinIO 回滚 `mc rb infra/${BUCKET} --force` + `mc admin policy rm` + `mc admin user rm game-staging-app`,infra-minio 主体零影响;oom_score_adj 进程重启即失效无残留。
|
||||
- **配置**:application-staging.yaml/V6V7 复制为新增文件,`git checkout -- game-cloud/yudao-server/src/main/resources/` 还原,不动 dev/local。
|
||||
- **模块**:compliance/studio 未接入前可独立删(根 pom 删 module + yudao-server pom 删依赖 + 删目录)。
|
||||
- **publish**:保留 mock 可切换分支 = 运行期 `game.publish.compliance-gate-enabled=false` 回旧行为(**非"双实现"**,R1:实现唯一,开关切是否调 Gate);代码层 revert GAP-1 注入 commit。
|
||||
- **DB**:Flyway 只增不改;已合入则写 V_x.0.1 补偿 `DROP TABLE`,禁改已合入迁移(校验和敏感)。
|
||||
- **GAP-4**:还原 events.schema.json 单文件(零代码回滚)。
|
||||
|
||||
---
|
||||
|
||||
## §9 开放项 / 待裁定(Phase A 须关闭)
|
||||
|
||||
| 项 | 来源 | 须裁定内容 | 默认/建议 |
|
||||
|---|---|---|---|
|
||||
| O1 studio 段位 112 | R4 | 是否新增 studio 后端模块 + 段位归属 | 创始人确认;D1 方向已定,登记须显式做 |
|
||||
| O2 manifestUrl 来源 | R8 | `game_runtime_package` 能否纯派生 manifest 路径 | 能→零 DDL;不能→runtime 补偿迁移 |
|
||||
| O3 feed 降权边界 | R5 | 降权出流屏蔽本波是否做 | **P1 不生效**(不建孤儿 seam),完成条件显式标注 |
|
||||
| O4 attachments 透传 | R7 | studio attachments 是否透传 aigc | **本波仅持久化**;P1 给 aigc-api 加 DTO 字段 |
|
||||
| O5 G0 RMQ 结论 | A6 | RMQ 不可达是否阻塞启动 | 实跑定;FAIL→兜底1 排除 RocketMQAutoConfiguration(须确认 telemetry 同步直写就位) |
|
||||
| O6 mysql 镜像 tag | P2 | infra-mysql 确切 tag | `docker inspect infra-mysql` 取真值对齐 compose |
|
||||
| O7 clone 端点 | P1 | mini-infra :3000 匿名是否可达 | 复核;不可达→SSH/2222+deploy key |
|
||||
| O8 MinIO 凭据/端点 | P4 | infra-minio root 凭据/端口映射 | `docker inspect infra-minio` 取真值入 .env |
|
||||
| O9 GAP-2 CORS 宿主 origin | GAP-2 | game-studio dev 端口 | 待联调形态定稿后填 CORS rule(过 D3 评审门) |
|
||||
| O10 events.schema.json 外部消费者 | R9 | 仓外 BI/数据是否按 snake 取数 | 确认无后做破坏性统一 |
|
||||
| O11 ProjectApi.createProject 入参集 | R2 | 建项目最小必要入参 | Phase A 随 ProjectApi 收敛定稿,带 creatorUserId + 归属校验 |
|
||||
| O12 R3 下架联动方式 | R3 | 状态分支 vs forceUnlist seam | **默认状态分支**;改 forceUnlist 须补 project 单测 |
|
||||
|
||||
---
|
||||
|
||||
> **自检(写完只读核对)**:无 `game/` 路径断言残留(G3/G4 全用真实 `/app-api/*`、`/admin-api/*`,先 dump paths);无 publish 双实现(compliance 只交付 evaluate seam,注入唯一在 GAP-1);无无条件 reviewProject(UNLIST)(R3 状态分支,仅 PUBLISHED 调);V6/V7 在 §6 G0 唯一复制(compliance/studio 不重复);ProjectApi seam 集中在 §1.2;docker 用 `--memory`(非 `--mem-limit`)。
|
||||
Loading…
x
Reference in New Issue
Block a user