From e670eaf476251a77f4ae5eb3111ec2e9bd91dd65 Mon Sep 17 00:00:00 2001 From: zizi Date: Tue, 9 Jun 2026 13:50:30 +0000 Subject: [PATCH] =?UTF-8?q?feat(B1):=20=E9=BB=84=E9=87=91=E9=97=AD?= =?UTF-8?q?=E7=8E=AF=E5=A5=91=E7=BA=A6=E5=85=88=E8=A1=8C=E5=9F=BA=E5=BA=A7?= =?UTF-8?q?=20=E2=80=94=20=E6=8E=A5=E5=8F=A3/DTO/V10/=E5=A5=91=E7=BA=A6=20?= =?UTF-8?q?yaml/=E9=94=99=E8=AF=AF=E7=A0=81/pom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit contract-first 基座(B1 step①,编译验证待 mini-desktop,与 step② 合并后编译): - feed-api: FeedApi.upsertRank + FeedRankUpsertReqDTO(UpsertMode 分流) + ApiConstants - runtime-api: RuntimePackageApi.publish + ApiConstants - project-api: ProjectVersionApi.createForPackage + DTO + 错误码 PROJECT_PUBLISH_PACKAGE_NOT_READY - V10: game_runtime_package.package_json + game_telemetry_event.event_id/uk_event_id(替换 uk_dedup);仅 contracts 源 + yudao-server 单一执行副本(守门②) - 契约 yaml: events/telemetry 信封加 eventId + runtime /manifest 端点(原样 JSON 不包 CommonResult) - pom: project-server→feed/runtime-api、telemetry-server→feed-api(守门④无环已核) 主 agent 独立核验: V10 双副本一致 / uk_dedup 索引名 / 错误码无碰撞 / 守门①②④ / telemetry x-mq 幂等口径同步改 eventId 避免契约自相矛盾 Co-Authored-By: Claude Opus 4.8 --- contracts/api-schemas/runtime.yaml | 24 ++++- contracts/api-schemas/telemetry.yaml | 8 +- ...V10.0.0__golden_loop_publish_telemetry.sql | 37 ++++++++ contracts/events.schema.json | 3 +- docs/mvp/MVP作战清单.md | 2 +- .../game/module/feed/api/FeedApi.java | 46 ++++++++++ .../module/feed/dto/FeedRankUpsertReqDTO.java | 87 +++++++++++++++++++ .../game/module/feed/enums/ApiConstants.java | 32 +++++++ .../module/project/api/ProjectVersionApi.java | 43 +++++++++ .../ProjectVersionCreateForPackageReqDTO.java | 42 +++++++++ .../project/enums/ErrorCodeConstants.java | 2 + .../game-module-project-server/pom.xml | 14 +++ .../module/runtime/api/RuntimePackageApi.java | 46 ++++++++++ .../module/runtime/enums/ApiConstants.java | 32 +++++++ .../game-module-telemetry-server/pom.xml | 7 ++ ...V10.0.0__golden_loop_publish_telemetry.sql | 37 ++++++++ 16 files changed, 456 insertions(+), 6 deletions(-) create mode 100644 contracts/db-schemas/V10.0.0__golden_loop_publish_telemetry.sql create mode 100644 game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/api/FeedApi.java create mode 100644 game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/dto/FeedRankUpsertReqDTO.java create mode 100644 game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/enums/ApiConstants.java create mode 100644 game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/api/ProjectVersionApi.java create mode 100644 game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/dto/ProjectVersionCreateForPackageReqDTO.java create mode 100644 game-cloud/game-module-runtime/game-module-runtime-api/src/main/java/cn/wanxiang/game/module/runtime/api/RuntimePackageApi.java create mode 100644 game-cloud/game-module-runtime/game-module-runtime-api/src/main/java/cn/wanxiang/game/module/runtime/enums/ApiConstants.java create mode 100644 game-cloud/yudao-server/src/main/resources/db/migration/V10.0.0__golden_loop_publish_telemetry.sql diff --git a/contracts/api-schemas/runtime.yaml b/contracts/api-schemas/runtime.yaml index 960b1d2e..285414e2 100644 --- a/contracts/api-schemas/runtime.yaml +++ b/contracts/api-schemas/runtime.yaml @@ -44,6 +44,28 @@ paths: application/json: schema: { $ref: '#/components/schemas/CommonResultRuntimePackage' } + /app-api/runtime/package/{versionId}/manifest: + get: + tags: [app-runtime] + summary: 取版本运行包 manifest 原始 JSON(宿主 fetch 此端点取 manifest 并做 sha256 完整性校验) + description: > + 【§3.4 C4 关键约束】返回 manifest 的原始 JSON 文本,**不包 CommonResult、不 parse/re-serialize、不加换行**—— + 宿主对该响应文本原始字节算 sha256,须与 RuntimePackageRespVO.checksum 严格一致(PackageFactory 写入时按相同字节计算并存 game_runtime_package.checksum), + 任何包裹/重序列化都会破坏字节一致性导致校验失败。 + MVP 无 OSS:manifest 整包存 game_runtime_package.package_json,由 PackageStore 的 DB impl 读出原样返回(M3 接 OSS 后换 OSS impl,调用方/契约不变,退场契约)。 + 取包门禁与 GET /app-api/runtime/package/{versionId} 同源:scene=preview 放行 status∈{0,1} 且校验版本 owner;scene=play 仅放行 status=1;无就绪运行包返回 1-102-001-001。 + parameters: + - { name: versionId, in: path, required: true, schema: { type: integer, format: int64 }, description: 版本 ID(project.game_version.id) } + - { name: scene, in: query, required: false, schema: { type: string, enum: [preview, play], default: play }, description: 'preview=创作者预览未发布版本 / play=玩家试玩已发布版本' } + responses: + '200': + description: manifest 原始 JSON 文本(application/json,不包 CommonResult;sha256(响应文本)==checksum) + content: + application/json: + schema: + type: string + description: 'GamePackage.manifest 的原始 JSON 字符串(原样字节,宿主据此算 sha256 校验后注入运行容器,对齐 #4 manifest 段)' + /app-api/runtime/session/start: post: tags: [app-runtime] @@ -220,7 +242,7 @@ components: versionId: { type: integer, format: int64, description: 版本 ID } templateId: { type: string, description: 玩法模板 ID(宿主据此选 Runtime 容器) } packageUrl: { type: string, description: 'GamePackage(manifest)OSS/CDN URL,按 /games/{gameId}/versions/{versionId}/ 版本化(#4)' } - manifestUrl: { type: string, description: 'GAP-2 分支A(纯派生,零 DDL):由 packageUrl 同前缀派生 .../manifest.json;宿主先 fetch 并按 checksum 做 sha256 完整性校验后再注入' } + manifestUrl: { type: string, description: '§3.4 C4:指向 GET /app-api/runtime/package/{versionId}/manifest 端点(MVP 包存 DB,端点原样服务 manifest JSON)。宿主先 fetch 此端点取 manifest,对响应原始文本算 sha256 与 checksum 严格比对通过后再注入运行容器(相对 URL 须 resolve 到 API base,见 §2.5);M3 接 OSS 后改指向 OSS/CDN 版本化 URL(退场契约)' } entry: { type: string, description: 入口文件相对路径(#4 manifest.entry) } runtimeVersion: { type: string, description: '目标 WanxiangGameSDK / Canvas Runtime 版本(semver,#4 manifest.runtimeVersion)' } preloadPolicy: { type: string, enum: [eager, lazy], description: '预加载策略(#4 manifest.preloadPolicy)' } diff --git a/contracts/api-schemas/telemetry.yaml b/contracts/api-schemas/telemetry.yaml index 3959969d..e3380619 100644 --- a/contracts/api-schemas/telemetry.yaml +++ b/contracts/api-schemas/telemetry.yaml @@ -130,12 +130,13 @@ components: EnvelopeReqVO: type: object description: 单条事件信封;字段语义与约束以 #5 events.schema.json 为单一事实源,此处仅做 OpenAPI 入参映射。 - required: [event, schemaVersion, ts, traceId] + required: [event, schemaVersion, ts, traceId, eventId] properties: event: { type: string, description: '事件名(须在 #5 eventRegistry 中登记,否则消费侧丢弃并计 rejected)' } schemaVersion: { type: string, pattern: '^v[0-9]+$', description: '事件 Schema 版本,如 v1/v2(对应 #5 schema_version)' } ts: { type: integer, format: int64, description: '事件发生时间戳(毫秒)' } traceId: { type: string, description: '全链路追踪 ID,贯穿生成→编译→加载→运行→上报(对应 #5 trace_id)' } + eventId: { type: string, description: '事件实例 UUID(必填,每事件实例唯一,幂等真身,对应 #5 eventId / game_telemetry_event.event_id 的 uk_event_id 唯一键)。前端 crypto.randomUUID() 生成;重放同 eventId 不重复计数' } sessionId: { type: string, description: '会话 ID(一次游戏流消费/一次创作会话,对应 #5 session_id);来源=runtime sessionId(int64) 转 string,作 session 维度对账键' } user: type: object @@ -202,7 +203,8 @@ components: description: >- telemetry 摄取与消费之间的 RocketMQ 对接点契约。生产者=上报通道(/events/batch、/perf/beacon), 消费者=本模块异步消费组(落 game_telemetry_event + 增量聚合 game_telemetry_game_stat + 回灌 feed/project)。 - 幂等键=信封 (traceId + event + ts),对应原始表 uk_dedup 唯一约束,重复消息消费侧落库幂等(INSERT IGNORE 语义)。 + 幂等键=信封 eventId(事件实例 UUID),对应原始表 uk_event_id 唯一约束,重复消息消费侧落库幂等(INSERT IGNORE 语义)。 + (历史口径 (traceId,event,ts)/uk_dedup 已于 V10 被 uk_event_id 取代——同毫秒同名会误吞、重放会重计数,见 V10 迁移与 HJ-EXEC-006 §3.5 C5。) producer: topic: telemetry-event tag: 'event | perf' @@ -213,7 +215,7 @@ components: consumer: group: telemetry-event-consumer concurrency: 建议并发消费(无序);聚合写入按 gameId 行级原子累加避免覆盖 - idempotency: 以 (traceId, event, ts) 为幂等键,命中 game_telemetry_event.uk_dedup 即跳过,保证至少一次投递下不重复聚合 + idempotency: 以 eventId(事件实例 UUID)为幂等键,命中 game_telemetry_event.uk_event_id 即跳过,保证至少一次投递下不重复聚合(V10 起;原 uk_dedup 已下线) retry: 默认重试 ≤16 次仍失败转 DLQ(telemetry-event-dlq),由 T-TEL-19 数据质量监控告警 sideEffects: - '落 game_telemetry_event(原始事件,幂等)' diff --git a/contracts/db-schemas/V10.0.0__golden_loop_publish_telemetry.sql b/contracts/db-schemas/V10.0.0__golden_loop_publish_telemetry.sql new file mode 100644 index 00000000..22e6fdaf --- /dev/null +++ b/contracts/db-schemas/V10.0.0__golden_loop_publish_telemetry.sql @@ -0,0 +1,37 @@ +-- ============================================================================= +-- 契约 #2 DB 迁移 | 主题:王蓝莓小卖部黄金闭环(HJ-EXEC-006 v2,§3.4 C4 + §3.5 C5)| owner:B1 契约先行基座 +-- 文件:V10.0.0__golden_loop_publish_telemetry.sql(Flyway,只新增/ALTER;已合入禁止修改,回滚写新补偿迁移 V10.0.1) +-- 内容:黄金闭环跨 runtime + telemetry 两表的最小 DDL(三条 ALTER): +-- 1. game_runtime_package 加 package_json LONGTEXT —— MVP 无 OSS,整包存 DB(退场契约:M3 接 OSS 后下线); +-- 2. game_telemetry_event 加 event_id VARCHAR(64) NOT NULL —— 客户端生成事件实例 UUID,幂等真身; +-- 3. game_telemetry_event 以 uk_event_id(event_id) 替换 uk_dedup(trace_id,event,ts) —— UUID 全局唯一, +-- 彻底解决「同毫秒同名误吞 + 重放重计数」(V5 表当前为空,DROP/ADD 唯一键安全)。 +-- 守门①(B1 必守):event_id 必须 NOT NULL —— MySQL 唯一键允许多 NULL,若可空则同批多 NULL 行不互斥、幂等失效。 +-- 守门②(B1 必守):本 V10 跨 runtime+telemetry 两表,只放仓根 contracts/db-schemas/(源)+ yudao-server 执行目录(唯一执行副本), +-- 不放任何单模块 -server/db/migration/(避免同版本 V10 出现在多个 classpath jar 触发 Flyway 重复校验失败)。 +-- 约定:只 ALTER 既有表、不新建表;显式列注释(中文);状态/幂等语义见对应 -api 枚举与 telemetry.yaml/events.schema.json。 +-- ============================================================================= + +-- ----------------------------------------------------------------------------- +-- 1) runtime:运行包整包 JSON 落 DB(§3.4 C4,PackageStore 的 DB impl 读写本列) +-- 退场契约:M3 接 OSS 后本列下线,PackageStore 换 OSS impl,调用方不变。 +-- ----------------------------------------------------------------------------- +ALTER TABLE `game_runtime_package` + ADD COLUMN `package_json` LONGTEXT NULL COMMENT 'MVP 临时整包;M3 接 OSS 后下线(退场契约)'; + +-- ----------------------------------------------------------------------------- +-- 2) telemetry:事件实例 UUID(§3.5 C5,幂等真身) +-- 守门①:NOT NULL —— 唯一键允许多 NULL,必须非空否则幂等失效。DEFAULT '' 兼容存量行(V5 表当前为空,仅为 DDL 安全兜底)。 +-- 前端 buildEnvelope 生成 event_id=crypto.randomUUID(),逐事件实例全局唯一。 +-- ----------------------------------------------------------------------------- +ALTER TABLE `game_telemetry_event` + ADD COLUMN `event_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '客户端生成事件实例 UUID,幂等真身'; + +-- ----------------------------------------------------------------------------- +-- 3) telemetry:以 uk_event_id 替换 uk_dedup(§3.5 C5) +-- 原 uk_dedup(trace_id,event,ts) 同毫秒同名会误吞、重放会重计数;改用 event_id 全局唯一键, +-- ingestBatch 据「真插入(affected>0)」判定幂等。V5 表当前为空,DROP/ADD 唯一键安全。 +-- ----------------------------------------------------------------------------- +ALTER TABLE `game_telemetry_event` + DROP INDEX `uk_dedup`, + ADD UNIQUE KEY `uk_event_id` (`event_id`) COMMENT '事件实例幂等键:event_id 全局唯一,同信封重复投递只落一条'; diff --git a/contracts/events.schema.json b/contracts/events.schema.json index e8c874f0..c0a85820 100644 --- a/contracts/events.schema.json +++ b/contracts/events.schema.json @@ -10,13 +10,14 @@ "envelope": { "description": "事件信封(所有事件共用)。批量上报时为 { batch: Envelope[] }。", "type": "object", - "required": ["event", "schemaVersion", "ts", "traceId"], + "required": ["event", "schemaVersion", "ts", "traceId", "eventId"], "additionalProperties": false, "properties": { "event": { "description": "事件名(见 eventRegistry 键)", "type": "string" }, "schemaVersion": { "description": "事件 Schema 版本,如 v1 / v2", "type": "string", "pattern": "^v[0-9]+$" }, "ts": { "description": "事件发生时间戳(毫秒)", "type": "integer" }, "traceId": { "description": "全链路追踪 ID,贯穿生成→编译→加载→运行→上报", "type": "string" }, + "eventId": { "description": "事件实例 UUID(每事件实例唯一,幂等真身)。前端 crypto.randomUUID() 生成;服务端落 game_telemetry_event.event_id(uk_event_id 唯一键),重放同 eventId 不重复计数。", "type": "string" }, "sessionId": { "description": "会话 ID(一次游戏流消费/一次创作会话)", "type": "string" }, "user": { "description": "用户标识。匿名玩家用 anonId;登录后带 userId。", diff --git a/docs/mvp/MVP作战清单.md b/docs/mvp/MVP作战清单.md index cf61147d..37830679 100644 --- a/docs/mvp/MVP作战清单.md +++ b/docs/mvp/MVP作战清单.md @@ -28,7 +28,7 @@ ## B 轨〔🤖 我〕黄金闭环(吸收 autoplan 未结终审) -- [ ] **B0 锁定执行 spec**:化解 autoplan 挖出的 5 个 critical —— 精确定义"可见态"(project PUBLISHED + package READY + versionId + rank.status=1) · 发布编排 `APPROVED→PUBLISHED` · version 创建链 · `package_json` 多文件契约 · `eventId` 幂等契约。**含一项需你点头的架构决策:审核通过是否自动发布到 PUBLISHED。** `done=execution spec 过自审 + 你点头` +- [x] **B0 锁定执行 spec**(✅ 2026-06-09 v2 定稿 `867c452`,主 agent 独立核验 + Codex 两轮评审 Go,D-PUB=α 已拍板):化解 autoplan 挖出的 5 个 critical —— 精确定义"可见态"(project PUBLISHED + package READY + versionId + rank.status=1) · 发布编排 `APPROVED→PUBLISHED` · version 创建链 · `package_json` 多文件契约 · `eventId` 幂等契约。**含一项需你点头的架构决策:审核通过是否自动发布到 PUBLISHED。** `done=execution spec 过自审 + 你点头` - [ ] **B1 批次①契约/状态机校正**(含 6 个开工前置项:`VITE_API_BASE` / mock 门控 / token=test1+tenant-id / 本机 vite dev 连 staging / 契约裁决 / beacon 走 baseURL)。`done=前端真连 staging、发布能进 PUBLISHED、feed 读 rank` - [ ] **B2 批次②最小数据闭环** ← **护城河种子真实起步**:PackageFactory→落包→发布入 feed→试玩→事件落库→quality_score→feed 排序。`done=A/B 验收协议通过(见 §0 验收门)` - [ ] **B3 批次③小卖部皮肤 + ≥10 参数化种子内容**(可降级)。`done=feed 有内容池,不是两款游戏赛马` diff --git a/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/api/FeedApi.java b/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/api/FeedApi.java new file mode 100644 index 00000000..af121c81 --- /dev/null +++ b/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/api/FeedApi.java @@ -0,0 +1,46 @@ +package cn.wanxiang.game.module.feed.api; + +import cn.wanxiang.game.module.feed.dto.FeedRankUpsertReqDTO; +import cn.wanxiang.game.module.feed.enums.ApiConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * RPC 服务 - 游戏流排序 API(feed 对外写排序行契约,黄金闭环新增) + * + * 用途:两个上游写入点经本 seam 写 game_feed_rank(由 {@link FeedRankUpsertReqDTO.UpsertMode} 分流): + * 1. 发布编排(project-server,§3.2 C2):发布成功后 mode=PUBLISH_BASELINE 写发布基线,游戏入流; + * 2. telemetry 算分回灌(telemetry-server,§3.5 C5):聚合后 mode=QUALITY_REFRESH 刷 quality_score/sort_score,回灌排序信号。 + * + * 设计纪律:跨模块只依赖 feed 的 -api(cn.wanxiang.game.module.feed.api/.dto),禁止上游依赖 feed 的 -server,避免编译耦合(守门④:-api 不反依赖 -server)。 + * 事务原子硬约束(§3.2 Codex H5):MVP 单体内由 {@code FeedApiImpl}(@RestController @Primary) 就地解析委托 FeedService 本地实现(B1 后续步骤新增), + * 发布编排注入本地 bean 并在同一 @Transactional 内调用,事务内禁止 Feign/HTTP/@Async/REQUIRES_NEW;拆微服务后按 {@link ApiConstants#NAME} 走真实 Feign。 + * + * @author 造梦AI + */ +@FeignClient(name = ApiConstants.NAME) +@Tag(name = "RPC 服务 - 游戏流排序") +public interface FeedApi { + + String PREFIX = ApiConstants.PREFIX; + + /** + * upsert 游戏流排序行(发布基线 / 算分回灌两路,由 req.mode 分流) + * + * 委托 FeedService 按 mode 选不同 upsert SQL(守门③): + * - PUBLISH_BASELINE:写 status=1 前须校验可见态前三条件(§3.1 C1 写侧 enforce),不满足拒写;写全字段,游戏入流。 + * - QUALITY_REFRESH:仅更 quality_score/sort_score,保留 boost/pinned/status(不覆盖运营加权与在流态)。 + * 同步事务语义(§3.5):调用方在本地 @Transactional 内调用,本方法抛错则上游整体回滚(发布编排/算分回灌均要求原子性)。 + * + * @param req 排序行 upsert 入参(gameId/versionId/zoneId/qualityScore/boost/sortScore/status/pinned + mode) + * @return 操作是否成功(CommonResult 包裹;业务校验失败由 FeedService 抛对应错误码) + */ + @PostMapping(PREFIX + "/upsert-rank") + @Operation(summary = "upsert 游戏流排序行(发布基线/算分回灌两路)") + CommonResult upsertRank(@RequestBody FeedRankUpsertReqDTO req); + +} diff --git a/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/dto/FeedRankUpsertReqDTO.java b/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/dto/FeedRankUpsertReqDTO.java new file mode 100644 index 00000000..583e26a9 --- /dev/null +++ b/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/dto/FeedRankUpsertReqDTO.java @@ -0,0 +1,87 @@ +package cn.wanxiang.game.module.feed.dto; + +import lombok.Data; + +/** + * 游戏流排序行 upsert 跨模块入参(→ feed,对应表 game_feed_rank 的写入镜像) + * + * 两个上游写入点共用本 DTO,由 {@link UpsertMode} 分流(守门③:两种 mode 写入字段集不同,不可混淆): + * 1. 发布编排(project-server PublishOrchestrationService,§3.2 C2):mode=PUBLISH_BASELINE —— 发布基线,建/激活排序行(status=1); + * 2. telemetry 算分回灌(telemetry-server ingestBatch,§3.5 C5):mode=QUALITY_REFRESH —— 仅刷新 quality_score/sort_score,保留 boost/pinned/status。 + * + * 可见态写侧 enforce(§3.1 C1):mode=PUBLISH_BASELINE 写 status=1 前须校验「project.status==PUBLISHED(4) + * AND project.current_version_id==versionId AND runtime_package(versionId).status==1」三条件,不满足拒写(具体校验在 FeedService 承载,本 DTO 仅承载数据)。 + * + * @author 造梦AI + */ +@Data +public class FeedRankUpsertReqDTO { + + /** + * 游戏项目 ID(game_feed_rank.game_id;与 zoneId 组成排序行业务键) + */ + private Long gameId; + + /** + * 版本 ID(game_feed_rank.version_id;发布基线写当前生效版本,回灌时透传对账) + */ + private Long versionId; + + /** + * 专区 ID(game_feed_rank.zone_id;默认 0 主流,发布编排取 launch_zone_id 或 0) + */ + private Long zoneId; + + /** + * 运营质量分(game_feed_rank.quality_score,DECIMAL10,4); + * mode=QUALITY_REFRESH 时来自 telemetry 当日 game_telemetry_game_stat.quality_score(DECIMAL5,2→10,4 精度转换); + * mode=PUBLISH_BASELINE 时为发布基线 0。 + */ + private Double qualityScore; + + /** + * 运营加权因子(game_feed_rank.boost);mode=PUBLISH_BASELINE 发布基线 0; + * mode=QUALITY_REFRESH 不覆盖本字段(守门③:保留运营既有 boost),由 feed 侧读现存值参与 sort_score 计算。 + */ + private Double boost; + + /** + * 综合排序分(game_feed_rank.sort_score)= quality_score + boost;游戏流降序排序的真实驱动字段。 + */ + private Double sortScore; + + /** + * 排序行状态(game_feed_rank.status,1=在流可见 / 0=不在流); + * mode=PUBLISH_BASELINE 写 1(发布即入流);mode=QUALITY_REFRESH 不覆盖(守门③:保留 status,仅刷分)。 + */ + private Integer status; + + /** + * 是否置顶(game_feed_rank.pinned,1=置顶 / 0=否); + * mode=PUBLISH_BASELINE 写 0;mode=QUALITY_REFRESH 不覆盖(守门③:保留运营既有 pinned)。 + */ + private Integer pinned; + + /** + * upsert 模式(分流两个写入点的字段集,必填) + */ + private UpsertMode mode; + + /** + * upsert 模式枚举(Codex M11 分流;守门③) + * + * 两种模式写入 game_feed_rank 的字段集不同,由 feed 侧据此选不同的 upsert SQL: + * - PUBLISH_BASELINE:发布编排写发布基线,落 game_id/version_id/zone_id/quality_score=0/boost=0/sort_score=0/status=1/pinned=0; + * - QUALITY_REFRESH:算分回灌只更 quality_score 与 sort_score,保留 boost/pinned/status(不被回灌覆盖)。 + */ + public enum UpsertMode { + + /** 发布基线:发布编排建/激活排序行(写全字段,status=1 入流) */ + PUBLISH_BASELINE, + + /** 质量刷新:telemetry 算分回灌(仅刷 quality_score/sort_score,保留 boost/pinned/status) */ + QUALITY_REFRESH; + + } + +} diff --git a/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/enums/ApiConstants.java b/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/enums/ApiConstants.java new file mode 100644 index 00000000..07ca7c25 --- /dev/null +++ b/game-cloud/game-module-feed/game-module-feed-api/src/main/java/cn/wanxiang/game/module/feed/enums/ApiConstants.java @@ -0,0 +1,32 @@ +package cn.wanxiang.game.module.feed.enums; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; + +/** + * feed 模块 API 相关枚举(Feign 服务名 + RPC 前缀) + * + * 仿 yudao system / project 模块的 {@code ApiConstants} 风格。MVP 为单体(spring.application.name=yudao-server,所有模块编入一个 JAR), + * 同进程内 Feign 调用由本模块 {@code FeedApiImpl}(@RestController @Primary) 就地解析(B1 后续步骤新增);拆微服务后该 NAME 即对应 feed 服务名。 + * + * 用途:发布编排(project-server)与 telemetry 算分回灌均经 {@code FeedApi.upsertRank} 写 game_feed_rank。 + * + * @author 造梦AI + */ +public class ApiConstants { + + /** + * 服务名(拆微服务后需与 feed 服务的 spring.application.name 一致;MVP 单体下仅作 Feign 客户端名占位) + */ + public static final String NAME = "feed-server"; + + /** + * RPC API 前缀(/rpc-api/feed) + */ + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/feed"; + + /** + * 接口版本 + */ + public static final String VERSION = "1.0.0"; + +} diff --git a/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/api/ProjectVersionApi.java b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/api/ProjectVersionApi.java new file mode 100644 index 00000000..31b3dc52 --- /dev/null +++ b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/api/ProjectVersionApi.java @@ -0,0 +1,43 @@ +package cn.wanxiang.game.module.project.api; + +import cn.wanxiang.game.module.project.dto.ProjectVersionCreateForPackageReqDTO; +import cn.wanxiang.game.module.project.enums.ApiConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * RPC 服务 - 游戏版本 API(project 对外建版本契约,黄金闭环新增 §3.3 C3) + * + * 用途:PackageFactory 成功落包后委托本 seam 创建/复用 game_version(落包建版本闭环步骤①), + * 产出 versionId 供后续写 game_runtime_package、回填 aigc 任务(completeWithVersion)、最终供 studio 轮询取用。 + * version 权威归 project(与 game_version 表归属一致):版本由 project 写,runtime 不建版本(Codex H6/H7)。 + * + * 设计纪律:跨模块只依赖 project 的 -api,禁止上游依赖 project 的 -server,避免编译耦合(守门④:-api 不反依赖 -server)。 + * 复用 project 既有 {@link ApiConstants}(NAME=project-server,与 ProjectApi 同服务);MVP 单体内由 @Primary 本地实现就地解析(B1 后续步骤新增)。 + * + * @author 造梦AI + */ +@FeignClient(name = ApiConstants.NAME) +@Tag(name = "RPC 服务 - 游戏版本") +public interface ProjectVersionApi { + + String PREFIX = ApiConstants.PREFIX; + + /** + * 落包建版本(PackageFactory 落包后创建/复用 game_version,返回 versionId) + * + * 委托 {@code GameVersionService}:创建 game_version(status=2 预览就绪);按 req.genTaskId 幂等(同任务复用已建版本)。 + * 不写 current_version_id(仍由创作者 submitPublish 写入);本方法只建版本,不动当前生效版本。 + * + * @param req 建版本入参(gameId/genTaskId/packageUrl 可空/bundleSize/checksum) + * @return 新建或复用的版本 ID(CommonResult 包裹) + */ + @PostMapping(PREFIX + "/version/create-for-package") + @Operation(summary = "落包建版本(创建/复用 game_version,返回 versionId)") + CommonResult createForPackage(@RequestBody ProjectVersionCreateForPackageReqDTO req); + +} diff --git a/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/dto/ProjectVersionCreateForPackageReqDTO.java b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/dto/ProjectVersionCreateForPackageReqDTO.java new file mode 100644 index 00000000..6463022a --- /dev/null +++ b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/dto/ProjectVersionCreateForPackageReqDTO.java @@ -0,0 +1,42 @@ +package cn.wanxiang.game.module.project.dto; + +import lombok.Data; + +/** + * 落包建版本跨模块入参(PackageFactory → project,对应表 game_version 的写入镜像) + * + * 用途:PackageFactory 成功落包后调 {@code ProjectVersionApi.createForPackage} 创建/复用 game_version(§3.3 C3 闭环步骤①)。 + * 创建态:version.status=2(预览就绪);按 genTaskId 幂等(重复落包复用同一 game_version,避免重复建版本)。 + * current_version_id 不在此写:仍由创作者 submitPublish(versionId) 写入(本 DTO 只建版本、不动当前生效版本)。 + * + * @author 造梦AI + */ +@Data +public class ProjectVersionCreateForPackageReqDTO { + + /** + * 所属游戏 ID(game_version.game_id;必填) + */ + private Long gameId; + + /** + * 生成任务 ID(game_version.gen_task_id,aigc taskId);幂等键:同 genTaskId 复用已建版本,不重复建 + */ + private String genTaskId; + + /** + * 运行包 URL(game_version.package_url);MVP 无 OSS 包存 DB(退场契约 §3.4),故可空(为空落空串) + */ + private String packageUrl; + + /** + * 包总字节(game_version.bundle_size;编译期门禁,PackageFactory 据落包大小填) + */ + private Long bundleSize; + + /** + * 整包 sha256(game_version.checksum,hex);与 manifest 端点原样响应字节同源计算(§3.4 C4) + */ + private String checksum; + +} diff --git a/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/enums/ErrorCodeConstants.java b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/enums/ErrorCodeConstants.java index 29c0aec4..918293db 100644 --- a/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/enums/ErrorCodeConstants.java +++ b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/enums/ErrorCodeConstants.java @@ -23,5 +23,7 @@ public interface ErrorCodeConstants { ErrorCode PROJECT_PUBLISH_AGE_RATING_REQUIRED = new ErrorCode(1_100_000_003, "发布前必须填写适龄提示"); /** 审核态非法:仅审核中(status=1)的项目可被审核决策 */ ErrorCode PROJECT_REVIEW_STATUS_INVALID = new ErrorCode(1_100_000_004, "项目当前状态不可审核"); + /** 发布编排前置校验:当前生效版本无就绪运行包(status=0 预览就绪),不可发布(§3.2 C2,校验失败回滚事务、审核不落) */ + ErrorCode PROJECT_PUBLISH_PACKAGE_NOT_READY = new ErrorCode(1_100_000_005, "当前版本运行包未就绪,不可发布"); } diff --git a/game-cloud/game-module-project/game-module-project-server/pom.xml b/game-cloud/game-module-project/game-module-project-server/pom.xml index 1d7678f1..c2039fd3 100644 --- a/game-cloud/game-module-project/game-module-project-server/pom.xml +++ b/game-cloud/game-module-project/game-module-project-server/pom.xml @@ -38,6 +38,20 @@ ${revision} + + + cn.iocoder.cloud + game-module-runtime-api + ${revision} + + + + + cn.iocoder.cloud + game-module-feed-api + ${revision} + + cn.iocoder.cloud diff --git a/game-cloud/game-module-runtime/game-module-runtime-api/src/main/java/cn/wanxiang/game/module/runtime/api/RuntimePackageApi.java b/game-cloud/game-module-runtime/game-module-runtime-api/src/main/java/cn/wanxiang/game/module/runtime/api/RuntimePackageApi.java new file mode 100644 index 00000000..83c88496 --- /dev/null +++ b/game-cloud/game-module-runtime/game-module-runtime-api/src/main/java/cn/wanxiang/game/module/runtime/api/RuntimePackageApi.java @@ -0,0 +1,46 @@ +package cn.wanxiang.game.module.runtime.api; + +import cn.wanxiang.game.module.runtime.enums.ApiConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * RPC 服务 - 运行包发布 API(runtime 对外发布翻包契约,黄金闭环新增) + * + * 用途:发布编排(project-server PublishOrchestrationService,§3.2 C2)把指定版本的运行包翻包 0→1(已发布), + * 使该版本 scene=play 可被玩家试玩(取包门禁权威判定字段 = game_runtime_package.status,见 PackageStatusEnum)。 + * 复用既有 {@code RuntimePackageServiceImpl.publishPackage} 翻包逻辑;version.status=3 由发布编排显式写(不依赖 runtime,Codex H6)。 + * + * 设计纪律:跨模块只依赖 runtime 的 -api,禁止上游依赖 runtime 的 -server,避免编译耦合(守门④:-api 不反依赖 -server)。 + * 事务原子硬约束(§3.2 Codex H5):MVP 单体内由 {@code RuntimePackageApiImpl}(@RestController @Primary) 就地解析委托本地 Service(B1 后续步骤新增), + * 发布编排注入本地 bean 并在同一 @Transactional 内调用,事务内禁止 Feign/HTTP/@Async/REQUIRES_NEW;拆微服务后按 {@link ApiConstants#NAME} 走真实 Feign。 + * + * @author 造梦AI + */ +@FeignClient(name = ApiConstants.NAME) +@Tag(name = "RPC 服务 - 运行包发布") +public interface RuntimePackageApi { + + String PREFIX = ApiConstants.PREFIX; + + /** + * 发布运行包(翻包 0 预览就绪 → 1 已发布;发布编排 0→1 单跳) + * + * 委托 {@code RuntimePackageService.publishPackage(versionId)}:校验该版本运行包存在且为 0 预览就绪,置 status=1(已发布)。 + * 幂等/校验语义在 Service 承载:运行包不存在或未就绪由 Service 抛 1-102-001-001(RUNTIME_PACKAGE_NOT_READY)。 + * 同步事务语义(§3.2):发布编排在本地 @Transactional 内调用,本方法抛错则发布事务整体回滚(project/package/version/feed 全回滚)。 + * + * @param versionId 版本 ID(project.game_version.id;定位待发布运行包) + * @return 操作是否成功(CommonResult 包裹;校验失败由 Service 抛对应错误码) + */ + @PostMapping(PREFIX + "/publish") + @Operation(summary = "发布运行包(翻包 0→1 已发布)") + @Parameter(name = "versionId", description = "版本 ID", required = true, example = "1024") + CommonResult publish(@RequestParam("versionId") Long versionId); + +} diff --git a/game-cloud/game-module-runtime/game-module-runtime-api/src/main/java/cn/wanxiang/game/module/runtime/enums/ApiConstants.java b/game-cloud/game-module-runtime/game-module-runtime-api/src/main/java/cn/wanxiang/game/module/runtime/enums/ApiConstants.java new file mode 100644 index 00000000..8b0bbeac --- /dev/null +++ b/game-cloud/game-module-runtime/game-module-runtime-api/src/main/java/cn/wanxiang/game/module/runtime/enums/ApiConstants.java @@ -0,0 +1,32 @@ +package cn.wanxiang.game.module.runtime.enums; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; + +/** + * runtime 模块 API 相关枚举(Feign 服务名 + RPC 前缀) + * + * 仿 yudao system / project 模块的 {@code ApiConstants} 风格。MVP 为单体(spring.application.name=yudao-server,所有模块编入一个 JAR), + * 同进程内 Feign 调用由本模块 {@code RuntimePackageApiImpl}(@RestController @Primary) 就地解析(B1 后续步骤新增);拆微服务后该 NAME 即对应 runtime 服务名。 + * + * 用途:发布编排(project-server)经 {@code RuntimePackageApi.publish} 把运行包翻 0→1(已发布)。 + * + * @author 造梦AI + */ +public class ApiConstants { + + /** + * 服务名(拆微服务后需与 runtime 服务的 spring.application.name 一致;MVP 单体下仅作 Feign 客户端名占位) + */ + public static final String NAME = "runtime-server"; + + /** + * RPC API 前缀(/rpc-api/runtime) + */ + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/runtime"; + + /** + * 接口版本 + */ + public static final String VERSION = "1.0.0"; + +} diff --git a/game-cloud/game-module-telemetry/game-module-telemetry-server/pom.xml b/game-cloud/game-module-telemetry/game-module-telemetry-server/pom.xml index 4e6ccfb3..bc3f1faa 100644 --- a/game-cloud/game-module-telemetry/game-module-telemetry-server/pom.xml +++ b/game-cloud/game-module-telemetry/game-module-telemetry-server/pom.xml @@ -31,6 +31,13 @@ ${revision} + + + cn.iocoder.cloud + game-module-feed-api + ${revision} + + cn.iocoder.cloud diff --git a/game-cloud/yudao-server/src/main/resources/db/migration/V10.0.0__golden_loop_publish_telemetry.sql b/game-cloud/yudao-server/src/main/resources/db/migration/V10.0.0__golden_loop_publish_telemetry.sql new file mode 100644 index 00000000..22e6fdaf --- /dev/null +++ b/game-cloud/yudao-server/src/main/resources/db/migration/V10.0.0__golden_loop_publish_telemetry.sql @@ -0,0 +1,37 @@ +-- ============================================================================= +-- 契约 #2 DB 迁移 | 主题:王蓝莓小卖部黄金闭环(HJ-EXEC-006 v2,§3.4 C4 + §3.5 C5)| owner:B1 契约先行基座 +-- 文件:V10.0.0__golden_loop_publish_telemetry.sql(Flyway,只新增/ALTER;已合入禁止修改,回滚写新补偿迁移 V10.0.1) +-- 内容:黄金闭环跨 runtime + telemetry 两表的最小 DDL(三条 ALTER): +-- 1. game_runtime_package 加 package_json LONGTEXT —— MVP 无 OSS,整包存 DB(退场契约:M3 接 OSS 后下线); +-- 2. game_telemetry_event 加 event_id VARCHAR(64) NOT NULL —— 客户端生成事件实例 UUID,幂等真身; +-- 3. game_telemetry_event 以 uk_event_id(event_id) 替换 uk_dedup(trace_id,event,ts) —— UUID 全局唯一, +-- 彻底解决「同毫秒同名误吞 + 重放重计数」(V5 表当前为空,DROP/ADD 唯一键安全)。 +-- 守门①(B1 必守):event_id 必须 NOT NULL —— MySQL 唯一键允许多 NULL,若可空则同批多 NULL 行不互斥、幂等失效。 +-- 守门②(B1 必守):本 V10 跨 runtime+telemetry 两表,只放仓根 contracts/db-schemas/(源)+ yudao-server 执行目录(唯一执行副本), +-- 不放任何单模块 -server/db/migration/(避免同版本 V10 出现在多个 classpath jar 触发 Flyway 重复校验失败)。 +-- 约定:只 ALTER 既有表、不新建表;显式列注释(中文);状态/幂等语义见对应 -api 枚举与 telemetry.yaml/events.schema.json。 +-- ============================================================================= + +-- ----------------------------------------------------------------------------- +-- 1) runtime:运行包整包 JSON 落 DB(§3.4 C4,PackageStore 的 DB impl 读写本列) +-- 退场契约:M3 接 OSS 后本列下线,PackageStore 换 OSS impl,调用方不变。 +-- ----------------------------------------------------------------------------- +ALTER TABLE `game_runtime_package` + ADD COLUMN `package_json` LONGTEXT NULL COMMENT 'MVP 临时整包;M3 接 OSS 后下线(退场契约)'; + +-- ----------------------------------------------------------------------------- +-- 2) telemetry:事件实例 UUID(§3.5 C5,幂等真身) +-- 守门①:NOT NULL —— 唯一键允许多 NULL,必须非空否则幂等失效。DEFAULT '' 兼容存量行(V5 表当前为空,仅为 DDL 安全兜底)。 +-- 前端 buildEnvelope 生成 event_id=crypto.randomUUID(),逐事件实例全局唯一。 +-- ----------------------------------------------------------------------------- +ALTER TABLE `game_telemetry_event` + ADD COLUMN `event_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '客户端生成事件实例 UUID,幂等真身'; + +-- ----------------------------------------------------------------------------- +-- 3) telemetry:以 uk_event_id 替换 uk_dedup(§3.5 C5) +-- 原 uk_dedup(trace_id,event,ts) 同毫秒同名会误吞、重放会重计数;改用 event_id 全局唯一键, +-- ingestBatch 据「真插入(affected>0)」判定幂等。V5 表当前为空,DROP/ADD 唯一键安全。 +-- ----------------------------------------------------------------------------- +ALTER TABLE `game_telemetry_event` + DROP INDEX `uk_dedup`, + ADD UNIQUE KEY `uk_event_id` (`event_id`) COMMENT '事件实例幂等键:event_id 全局唯一,同信封重复投递只落一条';