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:
parent
867c4523ae
commit
e670eaf476
@ -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)' }
|
||||
|
||||
@ -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(原始事件,幂等)'
|
||||
|
||||
@ -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 全局唯一,同信封重复投递只落一条';
|
||||
@ -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。",
|
||||
|
||||
@ -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 有内容池,不是两款游戏赛马`
|
||||
|
||||
@ -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<Boolean> upsertRank(@RequestBody FeedRankUpsertReqDTO req);
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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";
|
||||
|
||||
}
|
||||
@ -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<Long> createForPackage(@RequestBody ProjectVersionCreateForPackageReqDTO req);
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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, "当前版本运行包未就绪,不可发布");
|
||||
|
||||
}
|
||||
|
||||
@ -38,6 +38,20 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 依赖 runtime 的 -api:发布编排翻包 0→1(PublishOrchestrationService → 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>
|
||||
|
||||
@ -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<Boolean> publish(@RequestParam("versionId") Long versionId);
|
||||
|
||||
}
|
||||
@ -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";
|
||||
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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 全局唯一,同信封重复投递只落一条';
|
||||
Loading…
x
Reference in New Issue
Block a user