feat(project): bind runtime artifacts to versions
This commit is contained in:
parent
9fb669c4fa
commit
f7d0942ab6
@ -1,10 +1,12 @@
|
|||||||
package com.wanxiang.huijing.game.module.project.api;
|
package com.wanxiang.huijing.game.module.project.api;
|
||||||
|
|
||||||
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionBindArtifactReqDTO;
|
||||||
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionCreateForPackageReqDTO;
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionCreateForPackageReqDTO;
|
||||||
import com.wanxiang.huijing.game.module.project.enums.ApiConstants;
|
import com.wanxiang.huijing.game.module.project.enums.ApiConstants;
|
||||||
import com.wanxiang.huijing.framework.common.pojo.CommonResult;
|
import com.wanxiang.huijing.framework.common.pojo.CommonResult;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.cloud.openfeign.FeignClient;
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@ -40,4 +42,17 @@ public interface ProjectVersionApi {
|
|||||||
@Operation(summary = "落包建版本(创建/复用 game_version,返回 versionId)")
|
@Operation(summary = "落包建版本(创建/复用 game_version,返回 versionId)")
|
||||||
CommonResult<Long> createForPackage(@RequestBody ProjectVersionCreateForPackageReqDTO req);
|
CommonResult<Long> createForPackage(@RequestBody ProjectVersionCreateForPackageReqDTO req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 runtime 已持久化的权威产物摘要绑定到版本。
|
||||||
|
*
|
||||||
|
* <p>MVP 单体内由 callback 的本地事务调用;落包、版本摘要回写、任务完成任一步失败都会整体回滚。
|
||||||
|
* 审核门继续只读取 game_version.checksum,不接受调用方临时摘要或放宽格式。
|
||||||
|
*
|
||||||
|
* @param req 版本 ID、权威 SHA-256 和包字节数
|
||||||
|
* @return true 表示版本已完成绑定
|
||||||
|
*/
|
||||||
|
@PostMapping(PREFIX + "/version/bind-runtime-artifact")
|
||||||
|
@Operation(summary = "绑定版本运行产物摘要")
|
||||||
|
CommonResult<Boolean> bindRuntimeArtifact(@RequestBody @Valid ProjectVersionBindArtifactReqDTO req);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.wanxiang.huijing.game.module.project.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行包产物绑定入参。
|
||||||
|
*
|
||||||
|
* <p>callback 在取得 versionId 后才能生成最终 GamePackage 摘要;runtime 成功持久化该权威产物后,
|
||||||
|
* 通过本契约把同一摘要和字节数同步到 game_version,供 project 审核门绑定发布产物。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ProjectVersionBindArtifactReqDTO {
|
||||||
|
|
||||||
|
/** 已由 callback 创建或幂等复用的版本 ID。 */
|
||||||
|
@NotNull(message = "versionId 不能为空")
|
||||||
|
private Long versionId;
|
||||||
|
|
||||||
|
/** runtime 已持久化的 GamePackage 原文 SHA-256,小写 64 位十六进制。 */
|
||||||
|
@NotBlank(message = "checksum 不能为空")
|
||||||
|
@Pattern(regexp = "^[a-f0-9]{64}$", message = "checksum 必须为 64 位小写十六进制 sha256")
|
||||||
|
private String checksum;
|
||||||
|
|
||||||
|
/** runtime 已持久化的 GamePackage 字节数。 */
|
||||||
|
@NotNull(message = "bundleSize 不能为空")
|
||||||
|
@Positive(message = "bundleSize 必须大于 0")
|
||||||
|
private Long bundleSize;
|
||||||
|
}
|
||||||
@ -29,5 +29,7 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PROJECT_REVIEW_VERSION_INVALID = new ErrorCode(1_100_000_006, "审核版本与项目当前版本不一致");
|
ErrorCode PROJECT_REVIEW_VERSION_INVALID = new ErrorCode(1_100_000_006, "审核版本与项目当前版本不一致");
|
||||||
/** 审核批准版本缺少合法的小写 SHA-256 产物摘要。 */
|
/** 审核批准版本缺少合法的小写 SHA-256 产物摘要。 */
|
||||||
ErrorCode PROJECT_REVIEW_ARTIFACT_HASH_INVALID = new ErrorCode(1_100_000_007, "审核版本产物摘要无效");
|
ErrorCode PROJECT_REVIEW_ARTIFACT_HASH_INVALID = new ErrorCode(1_100_000_007, "审核版本产物摘要无效");
|
||||||
|
/** callback 绑定的运行产物摘要非法,或目标版本不存在。 */
|
||||||
|
ErrorCode PROJECT_VERSION_ARTIFACT_BIND_FAILED = new ErrorCode(1_100_000_008, "版本运行产物摘要绑定失败");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.wanxiang.huijing.game.module.project.api;
|
package com.wanxiang.huijing.game.module.project.api;
|
||||||
|
|
||||||
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionBindArtifactReqDTO;
|
||||||
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionCreateForPackageReqDTO;
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionCreateForPackageReqDTO;
|
||||||
import com.wanxiang.huijing.game.module.project.service.version.GameVersionService;
|
import com.wanxiang.huijing.game.module.project.service.version.GameVersionService;
|
||||||
import com.wanxiang.huijing.framework.common.pojo.CommonResult;
|
import com.wanxiang.huijing.framework.common.pojo.CommonResult;
|
||||||
@ -32,4 +33,11 @@ public class ProjectVersionApiImpl implements ProjectVersionApi {
|
|||||||
return success(gameVersionService.createForPackage(req));
|
return success(gameVersionService.createForPackage(req));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonResult<Boolean> bindRuntimeArtifact(ProjectVersionBindArtifactReqDTO req) {
|
||||||
|
// 同进程调用加入 callback 既有事务;服务异常直接向上冒泡,确保落包与版本摘要不会半成功。
|
||||||
|
gameVersionService.bindRuntimeArtifact(req);
|
||||||
|
return success(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.wanxiang.huijing.game.module.project.service.version;
|
package com.wanxiang.huijing.game.module.project.service.version;
|
||||||
|
|
||||||
import com.wanxiang.huijing.game.module.project.dal.dataobject.version.GameVersionDO;
|
import com.wanxiang.huijing.game.module.project.dal.dataobject.version.GameVersionDO;
|
||||||
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionBindArtifactReqDTO;
|
||||||
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionCreateForPackageReqDTO;
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionCreateForPackageReqDTO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,6 +25,13 @@ public interface GameVersionService {
|
|||||||
*/
|
*/
|
||||||
Long createForPackage(ProjectVersionCreateForPackageReqDTO req);
|
Long createForPackage(ProjectVersionCreateForPackageReqDTO req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 runtime 成功持久化的权威摘要同步到版本行。
|
||||||
|
*
|
||||||
|
* @param req 版本 ID、权威 SHA-256 和包字节数
|
||||||
|
*/
|
||||||
|
void bindRuntimeArtifact(ProjectVersionBindArtifactReqDTO req);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取版本 DO(发布编排取当前生效版本以做版本态流转)
|
* 取版本 DO(发布编排取当前生效版本以做版本态流转)
|
||||||
*
|
*
|
||||||
|
|||||||
@ -4,12 +4,18 @@ import com.wanxiang.huijing.game.module.project.dal.dataobject.project.ProjectDO
|
|||||||
import com.wanxiang.huijing.game.module.project.dal.dataobject.version.GameVersionDO;
|
import com.wanxiang.huijing.game.module.project.dal.dataobject.version.GameVersionDO;
|
||||||
import com.wanxiang.huijing.game.module.project.dal.mysql.project.ProjectMapper;
|
import com.wanxiang.huijing.game.module.project.dal.mysql.project.ProjectMapper;
|
||||||
import com.wanxiang.huijing.game.module.project.dal.mysql.version.GameVersionMapper;
|
import com.wanxiang.huijing.game.module.project.dal.mysql.version.GameVersionMapper;
|
||||||
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionBindArtifactReqDTO;
|
||||||
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionCreateForPackageReqDTO;
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionCreateForPackageReqDTO;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static com.wanxiang.huijing.game.module.project.enums.ErrorCodeConstants.PROJECT_VERSION_ARTIFACT_BIND_FAILED;
|
||||||
|
import static com.wanxiang.huijing.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏版本 Service 实现(黄金闭环 §3.3 C3 新增)
|
* 游戏版本 Service 实现(黄金闭环 §3.3 C3 新增)
|
||||||
*
|
*
|
||||||
@ -27,6 +33,8 @@ public class GameVersionServiceImpl implements GameVersionService {
|
|||||||
private static final int STATUS_PUBLISHED = 3;
|
private static final int STATUS_PUBLISHED = 3;
|
||||||
/** 落包建版本默认版本号(MVP 单版本场景;多版本递增留后续) */
|
/** 落包建版本默认版本号(MVP 单版本场景;多版本递增留后续) */
|
||||||
private static final int DEFAULT_VERSION_NO = 1;
|
private static final int DEFAULT_VERSION_NO = 1;
|
||||||
|
/** 审核门认可的权威产物摘要格式,与 runtime 落包契约保持一致。 */
|
||||||
|
private static final Pattern SHA256_PATTERN = Pattern.compile("^[a-f0-9]{64}$");
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private GameVersionMapper gameVersionMapper;
|
private GameVersionMapper gameVersionMapper;
|
||||||
@ -69,6 +77,29 @@ public class GameVersionServiceImpl implements GameVersionService {
|
|||||||
return version.getId();
|
return version.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bindRuntimeArtifact(ProjectVersionBindArtifactReqDTO req) {
|
||||||
|
// API 校验可能因同进程直接调用方式而被绕过,服务可信边界再次校验,禁止非法摘要进入审核门。
|
||||||
|
if (req == null || req.getVersionId() == null || req.getBundleSize() == null || req.getBundleSize() <= 0
|
||||||
|
|| !StringUtils.hasText(req.getChecksum()) || !SHA256_PATTERN.matcher(req.getChecksum()).matches()) {
|
||||||
|
log.warn("[bindRuntimeArtifact] 拒绝非法版本产物绑定 versionId={}, checksum={}, bundleSize={}",
|
||||||
|
req == null ? null : req.getVersionId(), req == null ? null : req.getChecksum(),
|
||||||
|
req == null ? null : req.getBundleSize());
|
||||||
|
throw exception(PROJECT_VERSION_ARTIFACT_BIND_FAILED);
|
||||||
|
}
|
||||||
|
// 仅更新审核所需的产物元数据;状态和其它版本字段继续由原有状态机负责。
|
||||||
|
GameVersionDO update = new GameVersionDO();
|
||||||
|
update.setId(req.getVersionId());
|
||||||
|
update.setChecksum(req.getChecksum());
|
||||||
|
update.setBundleSize(req.getBundleSize());
|
||||||
|
if (gameVersionMapper.updateById(update) != 1) {
|
||||||
|
log.error("[bindRuntimeArtifact] 目标版本不存在或更新异常 versionId={}", req.getVersionId());
|
||||||
|
throw exception(PROJECT_VERSION_ARTIFACT_BIND_FAILED);
|
||||||
|
}
|
||||||
|
log.info("[bindRuntimeArtifact] 版本已绑定 runtime 权威产物 versionId={}, checksum={}, bundleSize={}",
|
||||||
|
req.getVersionId(), req.getChecksum(), req.getBundleSize());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 回写项目「当前生效版本」ID(修 HJ-FE-WALK-001 Bug A)
|
* 回写项目「当前生效版本」ID(修 HJ-FE-WALK-001 Bug A)
|
||||||
*
|
*
|
||||||
|
|||||||
@ -0,0 +1,83 @@
|
|||||||
|
package com.wanxiang.huijing.game.module.project.service.version;
|
||||||
|
|
||||||
|
import com.wanxiang.huijing.framework.common.exception.ServiceException;
|
||||||
|
import com.wanxiang.huijing.framework.test.core.ut.BaseMockitoUnitTest;
|
||||||
|
import com.wanxiang.huijing.game.module.project.dal.dataobject.version.GameVersionDO;
|
||||||
|
import com.wanxiang.huijing.game.module.project.dal.mysql.project.ProjectMapper;
|
||||||
|
import com.wanxiang.huijing.game.module.project.dal.mysql.version.GameVersionMapper;
|
||||||
|
import com.wanxiang.huijing.game.module.project.dto.ProjectVersionBindArtifactReqDTO;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
|
||||||
|
import static com.wanxiang.huijing.game.module.project.enums.ErrorCodeConstants.PROJECT_VERSION_ARTIFACT_BIND_FAILED;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link GameVersionServiceImpl} 运行产物绑定测试。
|
||||||
|
*
|
||||||
|
* <p>callback 先取得 versionId,再由 runtime 持久化权威摘要;本服务负责把同一摘要写回 game_version,
|
||||||
|
* 供 {@code ProjectServiceImpl.reviewProject} 的既有 64hex 审核门读取。测试只覆盖该直接相关写入边界。
|
||||||
|
*/
|
||||||
|
class GameVersionServiceImplTest extends BaseMockitoUnitTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private GameVersionServiceImpl gameVersionService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GameVersionMapper gameVersionMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ProjectMapper projectMapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindRuntimeArtifact_updatesAuthoritativeChecksumForApproveGate() {
|
||||||
|
ProjectVersionBindArtifactReqDTO req = artifactReq("a".repeat(64));
|
||||||
|
when(gameVersionMapper.updateById(any(GameVersionDO.class))).thenReturn(1);
|
||||||
|
|
||||||
|
gameVersionService.bindRuntimeArtifact(req);
|
||||||
|
|
||||||
|
ArgumentCaptor<GameVersionDO> captor = ArgumentCaptor.forClass(GameVersionDO.class);
|
||||||
|
verify(gameVersionMapper).updateById(captor.capture());
|
||||||
|
assertEquals(2048L, captor.getValue().getId());
|
||||||
|
assertEquals("a".repeat(64), captor.getValue().getChecksum());
|
||||||
|
assertEquals(4096L, captor.getValue().getBundleSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindRuntimeArtifact_rejectsInvalidChecksumWithoutWriting() {
|
||||||
|
ProjectVersionBindArtifactReqDTO req = artifactReq("ABC");
|
||||||
|
|
||||||
|
ServiceException error = assertThrows(ServiceException.class,
|
||||||
|
() -> gameVersionService.bindRuntimeArtifact(req));
|
||||||
|
|
||||||
|
assertEquals(PROJECT_VERSION_ARTIFACT_BIND_FAILED.getCode(), error.getCode());
|
||||||
|
verify(gameVersionMapper, never()).updateById(any(GameVersionDO.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindRuntimeArtifact_rejectsMissingVersion() {
|
||||||
|
ProjectVersionBindArtifactReqDTO req = artifactReq("b".repeat(64));
|
||||||
|
when(gameVersionMapper.updateById(any(GameVersionDO.class))).thenReturn(0);
|
||||||
|
|
||||||
|
ServiceException error = assertThrows(ServiceException.class,
|
||||||
|
() -> gameVersionService.bindRuntimeArtifact(req));
|
||||||
|
|
||||||
|
assertEquals(PROJECT_VERSION_ARTIFACT_BIND_FAILED.getCode(), error.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构造 callback 已完成 runtime 落包后的产物绑定入参。 */
|
||||||
|
private static ProjectVersionBindArtifactReqDTO artifactReq(String checksum) {
|
||||||
|
ProjectVersionBindArtifactReqDTO req = new ProjectVersionBindArtifactReqDTO();
|
||||||
|
req.setVersionId(2048L);
|
||||||
|
req.setChecksum(checksum);
|
||||||
|
req.setBundleSize(4096L);
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user