新增数字人版本管理

This commit is contained in:
super
2026-06-14 19:00:55 +08:00
parent ac19637ad6
commit f44110fae5
15 changed files with 971 additions and 32 deletions

View File

@@ -0,0 +1,91 @@
package com.nanri.aiimage.modules.digitalhuman.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.digitalhuman.model.dto.UploadVersionRequest;
import com.nanri.aiimage.modules.digitalhuman.model.vo.DigitalHumanVersionVo;
import com.nanri.aiimage.modules.digitalhuman.service.DigitalHumanVersionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/digital-human/versions")
@Tag(name = "数字人版本管理", description = "数字人程序版本管理接口,包括上传、查询、发布、删除等功能")
public class DigitalHumanVersionController {
private final DigitalHumanVersionService versionService;
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "上传新版本", description = "上传数字人程序新版本到 OSS并记录版本信息")
public ApiResponse<DigitalHumanVersionVo> uploadVersion(@Valid @ModelAttribute UploadVersionRequest request) {
DigitalHumanVersionVo vo = versionService.uploadVersion(
request.getVersion(),
request.getFile(),
request.getChangelog(),
request.getMinClientVersion(),
request.getCreatedBy()
);
return ApiResponse.success(vo);
}
@GetMapping
@Operation(summary = "查询版本列表", description = "查询所有数字人版本,按创建时间倒序")
public ApiResponse<List<DigitalHumanVersionVo>> listVersions() {
return ApiResponse.success(versionService.listVersions());
}
@GetMapping("/latest")
@Operation(summary = "获取最新版本", description = "获取当前标记为最新且已发布的版本")
public ApiResponse<DigitalHumanVersionVo> getLatestVersion() {
return ApiResponse.success(versionService.getLatestVersion());
}
@GetMapping("/{version}")
@Operation(summary = "获取版本详情", description = "根据版本号获取版本详细信息")
public ApiResponse<DigitalHumanVersionVo> getVersionByVersion(
@Parameter(description = "版本号", example = "1.0.0") @PathVariable String version) {
return ApiResponse.success(versionService.getVersionByVersion(version));
}
@PostMapping("/{version}/release")
@Operation(summary = "发布版本", description = "将草稿状态的版本发布,状态变更为 RELEASED")
public ApiResponse<DigitalHumanVersionVo> releaseVersion(
@Parameter(description = "版本号", example = "1.0.0") @PathVariable String version) {
return ApiResponse.success(versionService.releaseVersion(version));
}
@PostMapping("/{version}/set-latest")
@Operation(summary = "设为最新版本", description = "将指定已发布版本设置为最新版本,清除其他版本的最新标记")
public ApiResponse<DigitalHumanVersionVo> setLatest(
@Parameter(description = "版本号", example = "1.0.0") @PathVariable String version) {
return ApiResponse.success(versionService.setLatest(version));
}
@DeleteMapping("/{version}")
@Operation(summary = "删除版本", description = "删除指定版本,同时删除 OSS 上的文件。最新版本不允许删除")
public ApiResponse<Void> deleteVersion(
@Parameter(description = "版本号", example = "1.0.0") @PathVariable String version) {
versionService.deleteVersion(version);
return ApiResponse.success(null);
}
@GetMapping("/{version}/download-url")
@Operation(summary = "获取下载链接", description = "获取指定版本的 OSS 下载链接")
public ApiResponse<String> getDownloadUrl(
@Parameter(description = "版本号", example = "1.0.0") @PathVariable String version) {
return ApiResponse.success(versionService.getDownloadUrl(version));
}
}

View File

@@ -0,0 +1,9 @@
package com.nanri.aiimage.modules.digitalhuman.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanri.aiimage.modules.digitalhuman.model.entity.DigitalHumanVersionEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DigitalHumanVersionMapper extends BaseMapper<DigitalHumanVersionEntity> {
}

View File

@@ -0,0 +1,29 @@
package com.nanri.aiimage.modules.digitalhuman.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
@Schema(description = "上传数字人版本请求")
public class UploadVersionRequest {
@NotBlank(message = "版本号不能为空")
@Schema(description = "版本号", example = "1.0.0", required = true)
private String version;
@NotNull(message = "文件不能为空")
@Schema(description = "数字人程序压缩包", required = true)
private MultipartFile file;
@Schema(description = "更新日志")
private String changelog;
@Schema(description = "最低客户端版本要求", example = "1.0.0")
private String minClientVersion;
@Schema(description = "创建人")
private String createdBy;
}

View File

@@ -0,0 +1,27 @@
package com.nanri.aiimage.modules.digitalhuman.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("biz_digital_human_version")
public class DigitalHumanVersionEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String version;
private String ossObjectKey;
private Long fileSize;
private String md5;
private String changelog;
private String minClientVersion;
private Boolean isLatest;
private String status;
private String createdBy;
private LocalDateTime createdAt;
private LocalDateTime releasedAt;
}

View File

@@ -0,0 +1,47 @@
package com.nanri.aiimage.modules.digitalhuman.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "数字人版本信息")
public class DigitalHumanVersionVo {
@Schema(description = "版本ID")
private Long id;
@Schema(description = "版本号", example = "1.0.0")
private String version;
@Schema(description = "文件大小(字节)")
private Long fileSize;
@Schema(description = "文件MD5")
private String md5;
@Schema(description = "更新日志")
private String changelog;
@Schema(description = "最低客户端版本要求", example = "1.0.0")
private String minClientVersion;
@Schema(description = "是否最新版本")
private Boolean isLatest;
@Schema(description = "状态DRAFT/RELEASED")
private String status;
@Schema(description = "下载链接")
private String downloadUrl;
@Schema(description = "创建人")
private String createdBy;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "发布时间")
private LocalDateTime releasedAt;
}

View File

@@ -0,0 +1,238 @@
package com.nanri.aiimage.modules.digitalhuman.service;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.OssProperties;
import com.nanri.aiimage.modules.digitalhuman.mapper.DigitalHumanVersionMapper;
import com.nanri.aiimage.modules.digitalhuman.model.entity.DigitalHumanVersionEntity;
import com.nanri.aiimage.modules.digitalhuman.model.vo.DigitalHumanVersionVo;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class DigitalHumanVersionService {
private static final String STATUS_DRAFT = "DRAFT";
private static final String STATUS_RELEASED = "RELEASED";
private static final String OSS_PATH_PREFIX = "digital-human/versions/";
private final DigitalHumanVersionMapper versionMapper;
private final OssStorageService ossStorageService;
private final OssProperties ossProperties;
@Transactional
public DigitalHumanVersionVo uploadVersion(String version, MultipartFile file, String changelog,
String minClientVersion, String createdBy) {
// 检查版本号是否已存在
DigitalHumanVersionEntity existing = versionMapper.selectOne(new LambdaQueryWrapper<DigitalHumanVersionEntity>()
.eq(DigitalHumanVersionEntity::getVersion, version));
if (existing != null) {
throw new BusinessException("版本号已存在:" + version);
}
// 保存临时文件
File tempFile = null;
try {
tempFile = Files.createTempFile("digital-human-", ".zip").toFile();
file.transferTo(tempFile);
// 计算 MD5
String md5 = calculateMd5(tempFile);
// 构建 OSS 路径
String ossObjectKey = OSS_PATH_PREFIX + "v" + version + "/ShuFuDigitalHuman.zip";
// 上传到 OSS
OSS ossClient = buildOssClient();
try {
ossClient.putObject(ossProperties.getBucket(), ossObjectKey, tempFile);
} finally {
ossClient.shutdown();
}
// 保存数据库记录
DigitalHumanVersionEntity entity = new DigitalHumanVersionEntity();
entity.setVersion(version);
entity.setOssObjectKey(ossObjectKey);
entity.setFileSize(tempFile.length());
entity.setMd5(md5);
entity.setChangelog(changelog);
entity.setMinClientVersion(minClientVersion);
entity.setIsLatest(false);
entity.setStatus(STATUS_DRAFT);
entity.setCreatedBy(createdBy);
entity.setCreatedAt(LocalDateTime.now());
versionMapper.insert(entity);
return toVo(entity);
} catch (IOException e) {
log.error("上传数字人版本失败", e);
throw new BusinessException("文件处理失败:" + e.getMessage());
} finally {
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
}
}
}
public List<DigitalHumanVersionVo> listVersions() {
List<DigitalHumanVersionEntity> entities = versionMapper.selectList(
new LambdaQueryWrapper<DigitalHumanVersionEntity>()
.orderByDesc(DigitalHumanVersionEntity::getCreatedAt)
);
return entities.stream().map(this::toVo).collect(Collectors.toList());
}
public DigitalHumanVersionVo getLatestVersion() {
DigitalHumanVersionEntity entity = versionMapper.selectOne(new LambdaQueryWrapper<DigitalHumanVersionEntity>()
.eq(DigitalHumanVersionEntity::getIsLatest, true)
.eq(DigitalHumanVersionEntity::getStatus, STATUS_RELEASED)
.last("limit 1"));
if (entity == null) {
throw new BusinessException("暂无已发布的版本");
}
return toVo(entity);
}
public DigitalHumanVersionVo getVersionByVersion(String version) {
DigitalHumanVersionEntity entity = versionMapper.selectOne(new LambdaQueryWrapper<DigitalHumanVersionEntity>()
.eq(DigitalHumanVersionEntity::getVersion, version));
if (entity == null) {
throw new BusinessException("版本不存在:" + version);
}
return toVo(entity);
}
@Transactional
public DigitalHumanVersionVo releaseVersion(String version) {
DigitalHumanVersionEntity entity = versionMapper.selectOne(new LambdaQueryWrapper<DigitalHumanVersionEntity>()
.eq(DigitalHumanVersionEntity::getVersion, version));
if (entity == null) {
throw new BusinessException("版本不存在:" + version);
}
if (STATUS_RELEASED.equals(entity.getStatus())) {
throw new BusinessException("版本已发布");
}
entity.setStatus(STATUS_RELEASED);
entity.setReleasedAt(LocalDateTime.now());
versionMapper.updateById(entity);
return toVo(entity);
}
@Transactional
public DigitalHumanVersionVo setLatest(String version) {
DigitalHumanVersionEntity entity = versionMapper.selectOne(new LambdaQueryWrapper<DigitalHumanVersionEntity>()
.eq(DigitalHumanVersionEntity::getVersion, version));
if (entity == null) {
throw new BusinessException("版本不存在:" + version);
}
if (!STATUS_RELEASED.equals(entity.getStatus())) {
throw new BusinessException("只有已发布的版本才能设为最新");
}
// 清除其他版本的 is_latest 标记
versionMapper.update(null, new LambdaUpdateWrapper<DigitalHumanVersionEntity>()
.set(DigitalHumanVersionEntity::getIsLatest, false)
.eq(DigitalHumanVersionEntity::getIsLatest, true));
// 设置当前版本为最新
entity.setIsLatest(true);
versionMapper.updateById(entity);
return toVo(entity);
}
@Transactional
public void deleteVersion(String version) {
DigitalHumanVersionEntity entity = versionMapper.selectOne(new LambdaQueryWrapper<DigitalHumanVersionEntity>()
.eq(DigitalHumanVersionEntity::getVersion, version));
if (entity == null) {
throw new BusinessException("版本不存在:" + version);
}
if (Boolean.TRUE.equals(entity.getIsLatest())) {
throw new BusinessException("最新版本不能删除");
}
// 删除 OSS 文件
try {
ossStorageService.deleteObject(entity.getOssObjectKey());
} catch (Exception e) {
log.warn("删除 OSS 文件失败:{}", entity.getOssObjectKey(), e);
}
// 删除数据库记录
versionMapper.deleteById(entity.getId());
}
public String getDownloadUrl(String version) {
DigitalHumanVersionEntity entity = versionMapper.selectOne(new LambdaQueryWrapper<DigitalHumanVersionEntity>()
.eq(DigitalHumanVersionEntity::getVersion, version));
if (entity == null) {
throw new BusinessException("版本不存在:" + version);
}
return ossStorageService.generateDownloadUrl(entity.getOssObjectKey());
}
private DigitalHumanVersionVo toVo(DigitalHumanVersionEntity entity) {
DigitalHumanVersionVo vo = new DigitalHumanVersionVo();
vo.setId(entity.getId());
vo.setVersion(entity.getVersion());
vo.setFileSize(entity.getFileSize());
vo.setMd5(entity.getMd5());
vo.setChangelog(entity.getChangelog());
vo.setMinClientVersion(entity.getMinClientVersion());
vo.setIsLatest(Boolean.TRUE.equals(entity.getIsLatest()));
vo.setStatus(entity.getStatus());
vo.setDownloadUrl(ossStorageService.generateDownloadUrl(entity.getOssObjectKey()));
vo.setCreatedBy(entity.getCreatedBy());
vo.setCreatedAt(entity.getCreatedAt());
vo.setReleasedAt(entity.getReleasedAt());
return vo;
}
private String calculateMd5(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
md.update(buffer, 0, bytesRead);
}
byte[] digest = md.digest();
BigInteger bigInt = new BigInteger(1, digest);
return bigInt.toString(16).toLowerCase();
} catch (Exception e) {
throw new BusinessException("计算 MD5 失败:" + e.getMessage());
}
}
private OSS buildOssClient() {
return new OSSClientBuilder().build(
"https://" + ossProperties.getEndpoint(),
ossProperties.getAccessKeyId(),
ossProperties.getAccessKeySecret()
);
}
}

View File

@@ -6,8 +6,8 @@ spring:
name: aiimage-backend
servlet:
multipart:
max-file-size: 200MB
max-request-size: 500MB
max-file-size: 2GB
max-request-size: 2GB
jackson:
time-zone: Asia/Shanghai
datasource:

View File

@@ -0,0 +1,17 @@
CREATE TABLE `biz_digital_human_version` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`version` VARCHAR(32) NOT NULL COMMENT '版本号',
`oss_object_key` VARCHAR(512) NOT NULL COMMENT 'OSS 路径',
`file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',
`md5` VARCHAR(64) NOT NULL COMMENT '文件MD5',
`changelog` TEXT COMMENT '更新日志',
`min_client_version` VARCHAR(32) COMMENT '最低客户端版本要求',
`is_latest` TINYINT(1) DEFAULT 0 COMMENT '是否最新版本',
`status` VARCHAR(32) DEFAULT 'DRAFT' COMMENT '状态DRAFT/RELEASED',
`created_by` VARCHAR(128) COMMENT '创建人',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`released_at` DATETIME COMMENT '发布时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_version` (`version`),
KEY `idx_status_is_latest` (`status`, `is_latest`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字人版本管理表';

View File

@@ -0,0 +1,47 @@
-- 插入现有的数字人版本记录OSS 上已存在的版本)
INSERT INTO `biz_digital_human_version` (
`version`,
`oss_object_key`,
`file_size`,
`md5`,
`changelog`,
`min_client_version`,
`is_latest`,
`status`,
`created_by`,
`created_at`,
`released_at`
) VALUES (
'1.0.0',
'ShuFuDigitalHuman.zip',
0,
'unknown',
'初始版本(已存在于 OSS',
NULL,
1,
'RELEASED',
'system',
NOW(),
NOW()
);
-- 插入菜单权限配置(数字人版本管理)
-- 注意:需要根据实际的 biz_permission_menu 表结构调整
-- 如果表名或字段不同,请手动执行对应的 INSERT 语句
INSERT INTO `biz_permission_menu` (
`name`,
`column_key`,
`route_path`,
`menu_type`,
`sort_order`,
`created_at`
) VALUES (
'数字人版本管理',
'digital_human_version',
'version',
'admin',
100,
NOW()
) ON DUPLICATE KEY UPDATE
`name` = '数字人版本管理',
`route_path` = 'version';