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:
parent
18e263b4c7
commit
2ae0c6f267
17
.gitignore
vendored
17
.gitignore
vendored
@ -17,15 +17,18 @@ coverage/
|
||||
.DS_Store
|
||||
.gstack/
|
||||
|
||||
# ── game-admin(yudao 前端 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/**
|
||||
|
||||
# ── 例外 2:game-admin(yudao 前端 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
|
||||
|
||||
@ -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_build,GameConfig→版本化运行包,异步幂等任务)
|
||||
*
|
||||
* 继承 {@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 {
|
||||
|
||||
/**
|
||||
* 编译任务 ID(buildId)
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 游戏 ID(project.game_project.id)
|
||||
*/
|
||||
private Long gameId;
|
||||
/**
|
||||
* 版本 ID(project.game_version.id = GamePackage.versionId)
|
||||
*/
|
||||
private Long versionId;
|
||||
/**
|
||||
* 玩法模板 ID(决定 Runtime 容器 / 校验规则)
|
||||
*/
|
||||
private String templateId;
|
||||
/**
|
||||
* 编译状态机
|
||||
*
|
||||
* 枚举 {@link BuildStatusEnum}:0排队 1编译中 2成功 3失败
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 产出 GamePackage(manifest)OSS/CDN URL(成功时回写,对齐 #4 manifest;失败为空)
|
||||
*/
|
||||
private String packageUrl;
|
||||
/**
|
||||
* 包总字节(体积门禁 T-RT-08:≤10MB;首屏≤2MB 由编译期单独校验)
|
||||
*/
|
||||
private Long bundleSize;
|
||||
/**
|
||||
* 整包 sha256(hex,#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;
|
||||
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 重复触发命中进行中/已成功任务返回同一 buildId(force=true 时强制重编)。
|
||||
*
|
||||
* @param reqVO 版本 ID + force
|
||||
* @return 编译任务 ID + 是否复用
|
||||
*/
|
||||
CompileRespVO triggerCompile(CompileReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 查询编译任务(发布编排/运营轮询)
|
||||
*
|
||||
* @param buildId 编译任务 ID
|
||||
* @return 编译任务 DO
|
||||
*/
|
||||
RuntimeBuildDO getBuild(Long buildId);
|
||||
|
||||
}
|
||||
@ -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_package(status=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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user