feat(B1): 黄金闭环契约先行基座 — 接口/DTO/V10/契约 yaml/错误码/pom

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 <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-09 13:50:30 +00:00
parent 867c4523ae
commit e670eaf476
16 changed files with 456 additions and 6 deletions

View File

@ -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 无 OSSmanifest 整包存 game_runtime_package.package_json由 PackageStore 的 DB impl 读出原样返回M3 接 OSS 后换 OSS impl调用方/契约不变,退场契约)。
取包门禁与 GET /app-api/runtime/package/{versionId} 同源scene=preview 放行 status∈{0,1} 且校验版本 ownerscene=play 仅放行 status=1无就绪运行包返回 1-102-001-001。
parameters:
- { name: versionId, in: path, required: true, schema: { type: integer, format: int64 }, description: 版本 IDproject.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不包 CommonResultsha256(响应文本)==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: 'GamePackagemanifestOSS/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.5M3 接 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' }

View File

@ -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 次仍失败转 DLQtelemetry-event-dlq由 T-TEL-19 数据质量监控告警
sideEffects:
- '落 game_telemetry_event原始事件幂等'

View File

@ -0,0 +1,37 @@
-- =============================================================================
-- 契约 #2 DB 迁移 | 主题王蓝莓小卖部黄金闭环HJ-EXEC-006 v2§3.4 C4 + §3.5 C5| ownerB1 契约先行基座
-- 文件V10.0.0__golden_loop_publish_telemetry.sqlFlyway只新增/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 C4PackageStore 的 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 全局唯一,同信封重复投递只落一条';

View File

@ -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_iduk_event_id 唯一键),重放同 eventId 不重复计数。", "type": "string" },
"sessionId": { "description": "会话 ID一次游戏流消费/一次创作会话)", "type": "string" },
"user": {
"description": "用户标识。匿名玩家用 anonId登录后带 userId。",

View File

@ -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 两轮评审 GoD-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 有内容池,不是两款游戏赛马`

View File

@ -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 服务 - 游戏流排序 APIfeed 对外写排序行契约黄金闭环新增
*
* 用途两个上游写入点经本 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 -apicn.wanxiang.game.module.feed.api/.dto禁止上游依赖 feed -server避免编译耦合守门-api 不反依赖 -server
* 事务原子硬约束§3.2 Codex H5MVP 单体内由 {@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<Boolean> upsertRank(@RequestBody FeedRankUpsertReqDTO req);
}

View File

@ -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 C2mode=PUBLISH_BASELINE 发布基线/激活排序行status=1
* 2. telemetry 算分回灌telemetry-server ingestBatch§3.5 C5mode=QUALITY_REFRESH 仅刷新 quality_score/sort_score保留 boost/pinned/status
*
* 可见态写侧 enforce§3.1 C1mode=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 {
/**
* 游戏项目 IDgame_feed_rank.game_id zoneId 组成排序行业务键
*/
private Long gameId;
/**
* 版本 IDgame_feed_rank.version_id发布基线写当前生效版本回灌时透传对账
*/
private Long versionId;
/**
* 专区 IDgame_feed_rank.zone_id默认 0 主流发布编排取 launch_zone_id 0
*/
private Long zoneId;
/**
* 运营质量分game_feed_rank.quality_scoreDECIMAL10,4
* mode=QUALITY_REFRESH 时来自 telemetry 当日 game_telemetry_game_stat.quality_scoreDECIMAL5,210,4 精度转换
* mode=PUBLISH_BASELINE 时为发布基线 0
*/
private Double qualityScore;
/**
* 运营加权因子game_feed_rank.boostmode=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.status1=在流可见 / 0=不在流
* mode=PUBLISH_BASELINE 1发布即入流mode=QUALITY_REFRESH 不覆盖守门保留 status仅刷分
*/
private Integer status;
/**
* 是否置顶game_feed_rank.pinned1=置顶 / 0=
* mode=PUBLISH_BASELINE 0mode=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;
}
}

View File

@ -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";
}

View File

@ -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 服务 - 游戏版本 APIproject 对外建版本契约黄金闭环新增 §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_versionstatus=2 预览就绪 req.genTaskId 幂等同任务复用已建版本
* 不写 current_version_id仍由创作者 submitPublish 写入本方法只建版本不动当前生效版本
*
* @param req 建版本入参gameId/genTaskId/packageUrl 可空/bundleSize/checksum
* @return 新建或复用的版本 IDCommonResult 包裹
*/
@PostMapping(PREFIX + "/version/create-for-package")
@Operation(summary = "落包建版本(创建/复用 game_version返回 versionId")
CommonResult<Long> createForPackage(@RequestBody ProjectVersionCreateForPackageReqDTO req);
}

View File

@ -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 {
/**
* 所属游戏 IDgame_version.game_id必填
*/
private Long gameId;
/**
* 生成任务 IDgame_version.gen_task_idaigc taskId幂等键 genTaskId 复用已建版本不重复建
*/
private String genTaskId;
/**
* 运行包 URLgame_version.package_urlMVP OSS 包存 DB退场契约 §3.4故可空为空落空串
*/
private String packageUrl;
/**
* 包总字节game_version.bundle_size编译期门禁PackageFactory 据落包大小填
*/
private Long bundleSize;
/**
* 整包 sha256game_version.checksumhex manifest 端点原样响应字节同源计算§3.4 C4
*/
private String checksum;
}

View File

@ -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, "当前版本运行包未就绪,不可发布");
}

View File

@ -38,6 +38,20 @@
<version>${revision}</version>
</dependency>
<!-- 依赖 runtime 的 -api发布编排翻包 0→1PublishOrchestrationService → RuntimePackageApi.publish§3.2 C2只依赖对方 -api 不依赖 -server守门④无环 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>game-module-runtime-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 依赖 feed 的 -api发布编排写发布基线排序行PublishOrchestrationService → FeedApi.upsertRank mode=PUBLISH_BASELINE§3.2 C2只依赖对方 -api 不依赖 -server守门④无环 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>game-module-feed-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件:数据权限(创作者只见自己数据)+ 多租户DO 继承 TenantBaseDO -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>

View File

@ -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 服务 - 运行包发布 APIruntime 对外发布翻包契约黄金闭环新增
*
* 用途发布编排project-server PublishOrchestrationService§3.2 C2把指定版本的运行包翻包 01已发布
* 使该版本 scene=play 可被玩家试玩取包门禁权威判定字段 = game_runtime_package.status PackageStatusEnum
* 复用既有 {@code RuntimePackageServiceImpl.publishPackage} 翻包逻辑version.status=3 由发布编排显式写不依赖 runtimeCodex H6
*
* 设计纪律跨模块只依赖 runtime -api禁止上游依赖 runtime -server避免编译耦合守门-api 不反依赖 -server
* 事务原子硬约束§3.2 Codex H5MVP 单体内由 {@code RuntimePackageApiImpl}(@RestController @Primary) 就地解析委托本地 ServiceB1 后续步骤新增
* 发布编排注入本地 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 已发布发布编排 01 单跳
*
* 委托 {@code RuntimePackageService.publishPackage(versionId)}校验该版本运行包存在且为 0 预览就绪 status=1已发布
* 幂等/校验语义在 Service 承载运行包不存在或未就绪由 Service 1-102-001-001RUNTIME_PACKAGE_NOT_READY
* 同步事务语义§3.2发布编排在本地 @Transactional 内调用本方法抛错则发布事务整体回滚project/package/version/feed 全回滚
*
* @param versionId 版本 IDproject.game_version.id定位待发布运行包
* @return 操作是否成功CommonResult 包裹校验失败由 Service 抛对应错误码
*/
@PostMapping(PREFIX + "/publish")
@Operation(summary = "发布运行包(翻包 0→1 已发布)")
@Parameter(name = "versionId", description = "版本 ID", required = true, example = "1024")
CommonResult<Boolean> publish(@RequestParam("versionId") Long versionId);
}

View File

@ -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} 把运行包翻 01已发布
*
* @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";
}

View File

@ -31,6 +31,13 @@
<version>${revision}</version>
</dependency>
<!-- 依赖 feed 的 -api算分回灌排序行ingestBatch → FeedApi.upsertRank mode=QUALITY_REFRESH§3.5 C5只依赖对方 -api 不依赖 -server守门④无环 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>game-module-feed-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件:数据权限 + 多租户DO 继承 TenantBaseDO -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>

View File

@ -0,0 +1,37 @@
-- =============================================================================
-- 契约 #2 DB 迁移 | 主题王蓝莓小卖部黄金闭环HJ-EXEC-006 v2§3.4 C4 + §3.5 C5| ownerB1 契约先行基座
-- 文件V10.0.0__golden_loop_publish_telemetry.sqlFlyway只新增/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 C4PackageStore 的 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 全局唯一,同信封重复投递只落一条';