fix(wave3): 补回被 .gitignore build/ 吞的 runtime build 包源码(克隆即崩,后端版)

staging fresh clone 编译失败暴露:根 .gitignore 的 build/ 规则(为产物设计)又静默吞掉
后端 cn.wanxiang.game.module.runtime 的 build 包 5 个源码(RuntimeBuildDO/Mapper/Service/
ServiceImpl/Test,编译任务域)——与此前 game-admin 同根因。本地有盘上文件故 reactor 编译/单测
绿,但推送的仓库缺这 5 文件,任何 fresh clone 都编译不过。
.gitignore 已加通用豁免 !**/src/**/build/(src 下名为 build 的一律是源码非产物),根治该类复发。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-09 03:23:42 +00:00
parent 18e263b4c7
commit 2ae0c6f267
6 changed files with 347 additions and 7 deletions

17
.gitignore vendored
View File

@ -17,15 +17,18 @@ coverage/
.DS_Store
.gstack/
# ── game-adminyudao 前端 fork作用域例外 ──
# yudao 约定将 build/vite 插件配置源码)与 .env*(前端构建配置,非密钥)纳入版本库;
# 上面为 Java 产物/密钥设计的笼统 build/、.env、.env.* 规则会误吞它们,
# 导致克隆后缺源码vite.config 解析 ./build/vite 失败)无法构建。故在 game-admin 子目录内豁免,
# 不影响后端模块的 build/ 产物忽略。
# ── 例外 1源码树src/)下名为 build 的目录是合法源码,绝非构建产物 ──
# 第 5 行笼统的 build/(为构建产物设计)会误吞 Java/前端源码中名为 build 的包/目录,例如:
# 后端 cn.wanxiang.game.module.runtime.{dal.dataobject,dal.mysql,service}.build编译任务域4 文件)、
# 前端 game-admin/src/views/infra/build。
# 构建产物绝不落在 src/ 下,故一律豁免 src/ 下的 build/,避免克隆/CI 缺源码而编译失败。
!**/src/**/build/
!**/src/**/build/**
# ── 例外 2game-adminyudao 前端 fork模块根级例外 ──
# yudao 约定将「模块根」build/vite 插件配置源码,不在 src 下,故需单独豁免)与 .env*(前端构建配置,非密钥)纳入版本库。
!game-admin/build/
!game-admin/build/**
!game-admin/src/views/infra/build/
!game-admin/src/views/infra/build/**
!game-admin/.env
!game-admin/.env.dev
!game-admin/.env.local

View File

@ -0,0 +1,78 @@
package cn.wanxiang.game.module.runtime.dal.dataobject.build;
import cn.wanxiang.game.module.runtime.enums.BuildStatusEnum;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 编译任务 DO对应表 game_runtime_buildGameConfig版本化运行包异步幂等任务
*
* 继承 {@link TenantBaseDO}自动携带 creator/create_time/updater/update_time/deleted + tenant_id 审计/租户列
* 状态机合法性由 RuntimeBuildService 校验DO 层只承载数据
* 编译消费该 version GamePackage(#4) GameConfig 静态校验(T-RT-16)/体积门禁(T-RT-08)/打包(T-RT-02)/版本化上传(T-RT-03)这些为外部对接点
*
* @author 绘境AI
*/
@TableName("game_runtime_build")
@KeySequence("game_runtime_build_seq") // Oracle/PostgreSQL 等主键自增用MySQL 可忽略
@Data
@EqualsAndHashCode(callSuper = true)
public class RuntimeBuildDO extends TenantBaseDO {
/**
* 编译任务 IDbuildId
*/
private Long id;
/**
* 游戏 IDproject.game_project.id
*/
private Long gameId;
/**
* 版本 IDproject.game_version.id = GamePackage.versionId
*/
private Long versionId;
/**
* 玩法模板 ID决定 Runtime 容器 / 校验规则
*/
private String templateId;
/**
* 编译状态机
*
* 枚举 {@link BuildStatusEnum}0排队 1编译中 2成功 3失败
*/
private Integer status;
/**
* 产出 GamePackagemanifestOSS/CDN URL成功时回写对齐 #4 manifest失败为空
*/
private String packageUrl;
/**
* 包总字节体积门禁 T-RT-0810MB首屏2MB 由编译期单独校验
*/
private Long bundleSize;
/**
* 整包 sha256hex#4 manifest.checksum完整性校验
*/
private String checksum;
/**
* 链路追踪 trace_id贯穿 生成编译加载运行T-RT-28
*/
private String traceId;
/**
* 失败错误码 1-102-002-001 GameConfig 校验失败 / 1-102-002-002 体积超限成功为空
*/
private String failCode;
/**
* 失败原因回填运营成功为空
*/
private String failReason;
/**
* 编译完成时间成功/失败均记
*/
private LocalDateTime finishTime;
}

View File

@ -0,0 +1,39 @@
package cn.wanxiang.game.module.runtime.dal.mysql.build;
import cn.wanxiang.game.module.runtime.dal.dataobject.build.RuntimeBuildDO;
import cn.wanxiang.game.module.runtime.enums.BuildStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import org.apache.ibatis.annotations.Mapper;
import java.util.Arrays;
/**
* 编译任务 Mapper
*
* @author 绘境AI
*/
@Mapper
public interface RuntimeBuildMapper extends BaseMapperX<RuntimeBuildDO> {
/**
* 查同版本可复用的编译任务进行中=排队/编译中 已成功用于编译幂等
*
* 命中 idx_version_status 索引取最新一条
*
* @param versionId 版本 ID
* @return 可复用任务无则 null
*/
default RuntimeBuildDO selectReusableByVersionId(Long versionId) {
return selectOne(new LambdaQueryWrapperX<RuntimeBuildDO>()
.eq(RuntimeBuildDO::getVersionId, versionId)
// 排队/编译中/成功 三态可复用失败不复用需重编
.in(RuntimeBuildDO::getStatus, Arrays.asList(
BuildStatusEnum.QUEUING.getStatus(),
BuildStatusEnum.BUILDING.getStatus(),
BuildStatusEnum.SUCCESS.getStatus()))
.orderByDesc(RuntimeBuildDO::getId)
.last("LIMIT 1"));
}
}

View File

@ -0,0 +1,35 @@
package cn.wanxiang.game.module.runtime.service.build;
import cn.wanxiang.game.module.runtime.controller.admin.runtime.vo.CompileReqVO;
import cn.wanxiang.game.module.runtime.controller.admin.runtime.vo.CompileRespVO;
import cn.wanxiang.game.module.runtime.dal.dataobject.build.RuntimeBuildDO;
/**
* 编译任务 Service 接口
*
* 承载编译幂等同版本进行中/已成功任务复用编译状态机
* 真正的编译执行GameConfig 校验 / 体积门禁 / 打包 / 版本化上传为外部对接点骨架不实现
*
* @author 绘境AI
*/
public interface RuntimeBuildService {
/**
* 触发编译GameConfig版本化运行包消费 aigc 产物 #4
*
* 异步幂等同一 versionId 重复触发命中进行中/已成功任务返回同一 buildIdforce=true 时强制重编
*
* @param reqVO 版本 ID + force
* @return 编译任务 ID + 是否复用
*/
CompileRespVO triggerCompile(CompileReqVO reqVO);
/**
* 查询编译任务发布编排/运营轮询
*
* @param buildId 编译任务 ID
* @return 编译任务 DO
*/
RuntimeBuildDO getBuild(Long buildId);
}

View File

@ -0,0 +1,74 @@
package cn.wanxiang.game.module.runtime.service.build;
import cn.wanxiang.game.module.runtime.controller.admin.runtime.vo.CompileReqVO;
import cn.wanxiang.game.module.runtime.controller.admin.runtime.vo.CompileRespVO;
import cn.wanxiang.game.module.runtime.dal.dataobject.build.RuntimeBuildDO;
import cn.wanxiang.game.module.runtime.dal.mysql.build.RuntimeBuildMapper;
import cn.wanxiang.game.module.runtime.enums.BuildStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import static cn.wanxiang.game.module.runtime.enums.ErrorCodeConstants.RUNTIME_BUILD_NOT_EXISTS;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 编译任务 Service 实现
*
* 业务规则编译幂等 / 状态机在此承载可单测可追溯
* 真正的编译执行拉取 GamePackage #4 GameConfig 校验 体积门禁 打包 版本化上传 回写运行包
* 为外部对接点骨架阶段只落排队任务并返回 buildId不驱动执行引擎
*
* @author 绘境AI
*/
@Slf4j
@Service
public class RuntimeBuildServiceImpl implements RuntimeBuildService {
@Resource
private RuntimeBuildMapper runtimeBuildMapper;
@Override
public CompileRespVO triggerCompile(CompileReqVO reqVO) {
CompileRespVO resp = new CompileRespVO();
// 幂等force=false 命中同版本进行中/已成功任务则复用避免重复编译对齐契约 #1 异步幂等语义
boolean force = Boolean.TRUE.equals(reqVO.getForce());
if (!force) {
RuntimeBuildDO reusable = runtimeBuildMapper.selectReusableByVersionId(reqVO.getVersionId());
if (reusable != null) {
log.info("[triggerCompile] 命中可复用编译任务,复用 buildId={}, versionId={}",
reusable.getId(), reqVO.getVersionId());
resp.setBuildId(reusable.getId());
resp.setReused(true);
return resp;
}
}
// 落一条排队任务status=0 排队
RuntimeBuildDO build = new RuntimeBuildDO();
build.setVersionId(reqVO.getVersionId());
build.setStatus(BuildStatusEnum.QUEUING.getStatus());
build.setBundleSize(0L);
// TODO 对接点编译执行引擎此处仅落排队任务未真正触发编译待编译执行引擎异步 Job/MQ 消费者就绪后
// 1) 读取该 versionId GamePackage(#4)回填 gameId/templateId/traceId
// 2) 执行 GameConfig 静态校验(T-RT-16)/体积门禁(T-RT-08超限抛 RUNTIME_BUILD_BUNDLE_SIZE_EXCEED)
// 3) Manifest 生成与打包(T-RT-02)资源版本化上传 OSS/CDN(T-RT-03)
// 4) 成功置 status=2 并回写 package_url/checksum/bundle_size
// 同时 upsert game_runtime_packagestatus=0 预览就绪+ 按决策5 回写 project.game_version 三字段失败置 status=3 回填 fail_code/fail_reason
runtimeBuildMapper.insert(build);
log.info("[triggerCompile] 新建编译任务 buildId={}, versionId={}(骨架仅排队,待执行引擎驱动)",
build.getId(), reqVO.getVersionId());
resp.setBuildId(build.getId());
resp.setReused(false);
return resp;
}
@Override
public RuntimeBuildDO getBuild(Long buildId) {
RuntimeBuildDO build = runtimeBuildMapper.selectById(buildId);
if (build == null) {
throw exception(RUNTIME_BUILD_NOT_EXISTS);
}
return build;
}
}

View File

@ -0,0 +1,111 @@
package cn.wanxiang.game.module.runtime.service.build;
import cn.wanxiang.game.module.runtime.controller.admin.runtime.vo.CompileReqVO;
import cn.wanxiang.game.module.runtime.controller.admin.runtime.vo.CompileRespVO;
import cn.wanxiang.game.module.runtime.dal.dataobject.build.RuntimeBuildDO;
import cn.wanxiang.game.module.runtime.dal.mysql.build.RuntimeBuildMapper;
import cn.wanxiang.game.module.runtime.enums.BuildStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static cn.wanxiang.game.module.runtime.enums.ErrorCodeConstants.RUNTIME_BUILD_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* {@link RuntimeBuildServiceImpl} 单元测试 Mockito不依赖 DB
*
* 覆盖核心业务规则守卫编译幂等命中复用新建排队任务编译任务不存在
*
* @author 绘境AI
*/
class RuntimeBuildServiceImplTest extends BaseMockitoUnitTest {
@InjectMocks
private RuntimeBuildServiceImpl runtimeBuildService;
@Mock
private RuntimeBuildMapper runtimeBuildMapper;
// ============================== triggerCompile 幂等 ==============================
@Test
void testTriggerCompile_reuseInProgress() {
// 命中同版本可复用任务进行中/已成功 复用同一 buildId不新建
RuntimeBuildDO reusable = new RuntimeBuildDO();
reusable.setId(555L);
reusable.setVersionId(2048L);
reusable.setStatus(BuildStatusEnum.BUILDING.getStatus());
when(runtimeBuildMapper.selectReusableByVersionId(2048L)).thenReturn(reusable);
CompileReqVO reqVO = compileReq(2048L, false);
CompileRespVO resp = runtimeBuildService.triggerCompile(reqVO);
assertEquals(555L, resp.getBuildId());
assertTrue(resp.getReused());
// 复用不应再 insert带类型消歧
verify(runtimeBuildMapper, never()).insert(any(RuntimeBuildDO.class));
}
@Test
void testTriggerCompile_newQueuing() {
// 无可复用任务 新建排队任务status=0 排队
when(runtimeBuildMapper.selectReusableByVersionId(2048L)).thenReturn(null);
doAnswer(inv -> {
RuntimeBuildDO d = inv.getArgument(0);
d.setId(777L);
return 1;
}).when(runtimeBuildMapper).insert(any(RuntimeBuildDO.class));
CompileRespVO resp = runtimeBuildService.triggerCompile(compileReq(2048L, false));
assertEquals(777L, resp.getBuildId());
assertFalse(resp.getReused());
ArgumentCaptor<RuntimeBuildDO> captor = ArgumentCaptor.forClass(RuntimeBuildDO.class);
verify(runtimeBuildMapper).insert(captor.capture());
assertEquals(BuildStatusEnum.QUEUING.getStatus(), captor.getValue().getStatus()); // 初始排队
assertEquals(2048L, captor.getValue().getVersionId());
}
@Test
void testTriggerCompile_forceSkipsReuse() {
// force=true 强制重编跳过复用查询直接新建
doAnswer(inv -> {
RuntimeBuildDO d = inv.getArgument(0);
d.setId(888L);
return 1;
}).when(runtimeBuildMapper).insert(any(RuntimeBuildDO.class));
CompileRespVO resp = runtimeBuildService.triggerCompile(compileReq(2048L, true));
assertEquals(888L, resp.getBuildId());
assertFalse(resp.getReused());
// force 不查复用
verify(runtimeBuildMapper, never()).selectReusableByVersionId(any());
}
// ============================== getBuild ==============================
@Test
void testGetBuild_notExists() {
when(runtimeBuildMapper.selectById(1L)).thenReturn(null);
ServiceException ex = assertThrows(ServiceException.class,
() -> runtimeBuildService.getBuild(1L));
assertEquals(RUNTIME_BUILD_NOT_EXISTS.getCode(), ex.getCode());
}
// ============================== 测试夹具 ==============================
private static CompileReqVO compileReq(Long versionId, boolean force) {
CompileReqVO reqVO = new CompileReqVO();
reqVO.setVersionId(versionId);
reqVO.setForce(force);
return reqVO;
}
}