From f44110fae5b36ae7173f37d679ae8d5d9a54dbcb Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Sun, 14 Jun 2026 19:00:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=95=B0=E5=AD=97=E4=BA=BA?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DigitalHumanVersionController.java | 91 +++++++ .../mapper/DigitalHumanVersionMapper.java | 9 + .../model/dto/UploadVersionRequest.java | 29 +++ .../entity/DigitalHumanVersionEntity.java | 27 ++ .../model/vo/DigitalHumanVersionVo.java | 47 ++++ .../service/DigitalHumanVersionService.java | 238 ++++++++++++++++++ .../src/main/resources/application.yml | 4 +- .../db/V59__digital_human_version.sql | 17 ++ .../insert_digital_human_initial_version.sql | 47 ++++ backend/app.py | 2 + backend/blueprints/admin_api.py | 128 ++++++++++ backend/config.py | 4 +- backend/static/admin.js | 162 ++++++++++-- backend/web_source/admin.html | 41 ++- frontend-vue/src/shared/api/digital-human.ts | 157 ++++++++++++ 15 files changed, 971 insertions(+), 32 deletions(-) create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/controller/DigitalHumanVersionController.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/mapper/DigitalHumanVersionMapper.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/dto/UploadVersionRequest.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/entity/DigitalHumanVersionEntity.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/vo/DigitalHumanVersionVo.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/service/DigitalHumanVersionService.java create mode 100644 backend-java/src/main/resources/db/V59__digital_human_version.sql create mode 100644 backend-java/src/main/resources/db/insert_digital_human_initial_version.sql create mode 100644 frontend-vue/src/shared/api/digital-human.ts diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/controller/DigitalHumanVersionController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/controller/DigitalHumanVersionController.java new file mode 100644 index 0000000..98030f0 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/controller/DigitalHumanVersionController.java @@ -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 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> listVersions() { + return ApiResponse.success(versionService.listVersions()); + } + + @GetMapping("/latest") + @Operation(summary = "获取最新版本", description = "获取当前标记为最新且已发布的版本") + public ApiResponse getLatestVersion() { + return ApiResponse.success(versionService.getLatestVersion()); + } + + @GetMapping("/{version}") + @Operation(summary = "获取版本详情", description = "根据版本号获取版本详细信息") + public ApiResponse 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 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 setLatest( + @Parameter(description = "版本号", example = "1.0.0") @PathVariable String version) { + return ApiResponse.success(versionService.setLatest(version)); + } + + @DeleteMapping("/{version}") + @Operation(summary = "删除版本", description = "删除指定版本,同时删除 OSS 上的文件。最新版本不允许删除") + public ApiResponse 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 getDownloadUrl( + @Parameter(description = "版本号", example = "1.0.0") @PathVariable String version) { + return ApiResponse.success(versionService.getDownloadUrl(version)); + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/mapper/DigitalHumanVersionMapper.java b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/mapper/DigitalHumanVersionMapper.java new file mode 100644 index 0000000..53d0069 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/mapper/DigitalHumanVersionMapper.java @@ -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 { +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/dto/UploadVersionRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/dto/UploadVersionRequest.java new file mode 100644 index 0000000..4008dac --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/dto/UploadVersionRequest.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/entity/DigitalHumanVersionEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/entity/DigitalHumanVersionEntity.java new file mode 100644 index 0000000..ba03aea --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/entity/DigitalHumanVersionEntity.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/vo/DigitalHumanVersionVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/vo/DigitalHumanVersionVo.java new file mode 100644 index 0000000..f42d635 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/model/vo/DigitalHumanVersionVo.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/service/DigitalHumanVersionService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/service/DigitalHumanVersionService.java new file mode 100644 index 0000000..4167aeb --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/digitalhuman/service/DigitalHumanVersionService.java @@ -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() + .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 listVersions() { + List entities = versionMapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DigitalHumanVersionEntity::getCreatedAt) + ); + return entities.stream().map(this::toVo).collect(Collectors.toList()); + } + + public DigitalHumanVersionVo getLatestVersion() { + DigitalHumanVersionEntity entity = versionMapper.selectOne(new LambdaQueryWrapper() + .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() + .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() + .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() + .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() + .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() + .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() + .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() + ); + } +} diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index df99564..133ba93 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -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: diff --git a/backend-java/src/main/resources/db/V59__digital_human_version.sql b/backend-java/src/main/resources/db/V59__digital_human_version.sql new file mode 100644 index 0000000..2885c20 --- /dev/null +++ b/backend-java/src/main/resources/db/V59__digital_human_version.sql @@ -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='数字人版本管理表'; diff --git a/backend-java/src/main/resources/db/insert_digital_human_initial_version.sql b/backend-java/src/main/resources/db/insert_digital_human_initial_version.sql new file mode 100644 index 0000000..50fff15 --- /dev/null +++ b/backend-java/src/main/resources/db/insert_digital_human_initial_version.sql @@ -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'; diff --git a/backend/app.py b/backend/app.py index 201ace9..a90a9f5 100644 --- a/backend/app.py +++ b/backend/app.py @@ -21,6 +21,8 @@ app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR) CORS(app) app.secret_key = os.environ.get('SECRET_KEY', secrets.token_hex(32)) app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) +# 文件上传大小限制:2GB(数字人 ZIP 包等大文件) +app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024 * 1024 # 注册蓝图 app.register_blueprint(auth) diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index 5d0a380..af1c25c 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -1430,6 +1430,134 @@ def list_versions(): return jsonify({'success': False, 'error': str(e)}) +# ========== 数字人版本管理(代理到 Java 后端)========== + +@admin_api.route('/digital-human-versions') +@admin_required +def list_digital_human_versions(): + """代理:查询数字人版本列表""" + _, _, denied = _ensure_admin_menu_access('version') + if denied: + return denied + page = request.args.get('page', '1') + page_size = request.args.get('pageSize', '20') + status = request.args.get('status', '') + params = {'page': page, 'pageSize': page_size} + if status: + params['status'] = status + result, error_response, status_code = _proxy_backend_java( + 'GET', + '/api/digital-human/versions', + params=params, + timeout=30 + ) + if error_response: + return error_response, status_code + return jsonify(result) + + +@admin_api.route('/digital-human-versions/latest') +@admin_required +def get_latest_digital_human_version(): + """代理:获取最新数字人版本""" + result, error_response, status_code = _proxy_backend_java( + 'GET', + '/api/digital-human/versions/latest', + timeout=10 + ) + if error_response: + return error_response, status_code + return jsonify(result) + + +@admin_api.route('/digital-human-versions/upload', methods=['POST']) +@admin_required +def upload_digital_human_version(): + """代理:上传数字人版本""" + _, _, denied = _ensure_admin_menu_access('version') + if denied: + return denied + + version = request.form.get('version', '').strip() + changelog = request.form.get('changelog', '').strip() + min_client_version = request.form.get('minClientVersion', '').strip() + file_storage = request.files.get('file') + + if not version: + return jsonify({'success': False, 'message': '请填写版本号'}), 400 + if not file_storage: + return jsonify({'success': False, 'message': '请选择文件'}), 400 + + files = {'file': (file_storage.filename, file_storage.stream, file_storage.content_type)} + data = {'version': version} + if changelog: + data['changelog'] = changelog + if min_client_version: + data['minClientVersion'] = min_client_version + + result, error_response, status_code = _proxy_backend_java( + 'POST', + '/api/digital-human/versions/upload', + files=files, + data=data, + timeout=300 + ) + if error_response: + return error_response, status_code + return jsonify(result) + + +@admin_api.route('/digital-human-versions//release', methods=['POST']) +@admin_required +def release_digital_human_version(version): + """代理:发布数字人版本""" + _, _, denied = _ensure_admin_menu_access('version') + if denied: + return denied + result, error_response, status_code = _proxy_backend_java( + 'POST', + f'/api/digital-human/versions/{version}/release', + timeout=10 + ) + if error_response: + return error_response, status_code + return jsonify(result) + + +@admin_api.route('/digital-human-versions//set-latest', methods=['POST']) +@admin_required +def set_latest_digital_human_version(version): + """代理:设为最新数字人版本""" + _, _, denied = _ensure_admin_menu_access('version') + if denied: + return denied + result, error_response, status_code = _proxy_backend_java( + 'POST', + f'/api/digital-human/versions/{version}/set-latest', + timeout=10 + ) + if error_response: + return error_response, status_code + return jsonify(result) + + +@admin_api.route('/digital-human-versions/', methods=['DELETE']) +@admin_required +def delete_digital_human_version(version): + """代理:删除数字人版本""" + _, _, denied = _ensure_admin_menu_access('version') + if denied: + return denied + result, error_response, status_code = _proxy_backend_java( + 'DELETE', + f'/api/digital-human/versions/{version}', + timeout=30 + ) + if error_response: + return error_response, status_code + return jsonify(result) + + @admin_api.route('/version', methods=['POST']) @admin_required def upload_version(): diff --git a/backend/config.py b/backend/config.py index 8e1beb2..32e80cc 100644 --- a/backend/config.py +++ b/backend/config.py @@ -59,8 +59,8 @@ file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/" backend_java_base_url, backend_java_base_url_source = _get_env( "java_api_base", "BACKEND_JAVA_BASE_URL", - default="http://api.aishufu.top:18080/", - # default="http://127.0.0.1:18080/", + # default="http://api.aishufu.top:18080/", + default="http://127.0.0.1:18080/", ) backend_java_base_url = backend_java_base_url.rstrip("/") os.environ["OSS_ACCESS_KEY_ID"] = accessKeyId diff --git a/backend/static/admin.js b/backend/static/admin.js index 048d3bd..0e85bf2 100644 --- a/backend/static/admin.js +++ b/backend/static/admin.js @@ -3519,35 +3519,98 @@ }); }; - function loadVersions() { - fetch('/api/admin/versions') + function loadVersions(page) { + page = page || 1; + fetch('/api/admin/digital-human-versions?page=' + page + '&pageSize=20') .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('versionListBody'); if (!res.success) { - tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + tbody.innerHTML = '加载失败: ' + (res.message || res.error || '') + ''; return; } - var items = res.items || []; + // 兼容两种返回格式:直接数组 或 分页对象 + var data = res.data; + var items = []; + var pages = 1; + var current = 1; + if (Array.isArray(data)) { + items = data; + } else if (data && Array.isArray(data.records)) { + items = data.records; + pages = data.pages || 1; + current = data.current || 1; + } else if (data && Array.isArray(data.items)) { + items = data.items; + } if (items.length === 0) { - tbody.innerHTML = '暂无版本记录'; + tbody.innerHTML = '暂无版本记录'; } else { tbody.innerHTML = items.map(function (v) { - var url = (v.file_url || '').replace(/"/g, '"'); - return '' + (v.version || '') + '' + url + '' + (v.created_at || '') + '下载'; + var statusText = v.status === 'DRAFT' ? '草稿' : (v.status === 'RELEASED' ? '已发布' : '已废弃'); + var statusColor = v.status === 'DRAFT' ? '#999' : (v.status === 'RELEASED' ? '#52c41a' : '#ff4d4f'); + var isLatestBadge = v.isLatest ? '' : ''; + var fileSize = v.fileSize ? (v.fileSize / 1024 / 1024).toFixed(2) + ' MB' : '-'; + var md5Short = (v.md5 || 'unknown').substring(0, 12) + '...'; + var changelog = (v.changelog || '-').substring(0, 50); + if ((v.changelog || '').length > 50) changelog += '...'; + var releasedAt = v.releasedAt || '-'; + var downloadUrl = v.downloadUrl || ''; + + var actions = ''; + if (v.status === 'DRAFT') { + actions += ' '; + } + if (v.status === 'RELEASED' && !v.isLatest) { + actions += ' '; + } + if (downloadUrl) { + actions += '下载 '; + } + if (!v.isLatest) { + actions += ''; + } + + return '' + + '' + (v.version || '') + '' + + '' + statusText + '' + + '' + isLatestBadge + '' + + '' + fileSize + '' + + '' + md5Short + '' + + '' + changelog + '' + + '' + releasedAt + '' + + '' + actions + '' + + ''; }).join(''); } + + // 分页 + var pagination = document.getElementById('versionPagination'); + if (pages > 1) { + var html = ''; + for (var i = 1; i <= pages; i++) { + var active = i === current ? ' active' : ''; + html += '' + i + ''; + } + pagination.innerHTML = html; + } else { + pagination.innerHTML = ''; + } }) - .catch(function () { - document.getElementById('versionListBody').innerHTML = '请求失败'; + .catch(function (err) { + document.getElementById('versionListBody').innerHTML = '请求失败: ' + (err.message || '') + ''; }); } + document.getElementById('btnUploadVersion').onclick = function () { var version = (document.getElementById('versionNumber').value || '').trim(); + var minClientVersion = (document.getElementById('versionMinClientVersion').value || '').trim(); + var changelog = (document.getElementById('versionChangelog').value || '').trim(); var fileInput = document.getElementById('versionZip'); var msgEl = document.getElementById('msgVersion'); msgEl.textContent = ''; msgEl.className = 'msg'; + if (!version) { msgEl.textContent = '请填写版本号'; msgEl.classList.add('err'); @@ -3564,34 +3627,99 @@ msgEl.classList.add('err'); return; } + var formData = new FormData(); - formData.append('version', version); formData.append('file', file); - msgEl.textContent = '上传中...'; + formData.append('version', version); + if (minClientVersion) formData.append('minClientVersion', minClientVersion); + if (changelog) formData.append('changelog', changelog); + + msgEl.textContent = '上传中,请稍候...'; msgEl.classList.remove('err', 'ok'); - fetch('/api/admin/version', { + + fetch('/api/admin/digital-human-versions/upload', { method: 'POST', body: formData }) .then(function (r) { return r.json(); }) .then(function (res) { - if (res.success) { - msgEl.textContent = '发布成功。版本:' + res.version + ',链接:' + (res.file_url || ''); + if (res.success && res.data) { + msgEl.textContent = '上传成功!版本:' + res.data.version + '(状态:草稿,请在列表中点击"发布"按钮)'; msgEl.classList.add('ok'); document.getElementById('versionNumber').value = ''; + document.getElementById('versionMinClientVersion').value = ''; + document.getElementById('versionChangelog').value = ''; fileInput.value = ''; loadVersions(); } else { - msgEl.textContent = res.error || '上传失败'; + msgEl.textContent = res.message || res.error || '上传失败'; msgEl.classList.add('err'); } }) - .catch(function () { - msgEl.textContent = '请求失败'; + .catch(function (err) { + msgEl.textContent = '请求失败: ' + (err.message || ''); msgEl.classList.add('err'); }); }; + window.releaseVersion = function(version) { + if (!confirm('确认发布版本 ' + version + ' 吗?')) return; + fetch('/api/admin/digital-human-versions/' + version + '/release', { + method: 'POST' + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + alert('发布成功!'); + loadVersions(); + } else { + alert('发布失败:' + (res.message || res.error || '')); + } + }) + .catch(function (err) { + alert('请求失败:' + (err.message || '')); + }); + }; + + window.setLatestVersion = function(version) { + if (!confirm('确认将版本 ' + version + ' 设为最新吗?客户端将自动检测并更新到此版本。')) return; + fetch('/api/admin/digital-human-versions/' + version + '/set-latest', { + method: 'POST' + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + alert('设置成功!版本 ' + version + ' 已标记为最新。'); + loadVersions(); + } else { + alert('设置失败:' + (res.message || res.error || '')); + } + }) + .catch(function (err) { + alert('请求失败:' + (err.message || '')); + }); + }; + + window.deleteVersion = function(version) { + if (!confirm('确认删除版本 ' + version + ' 吗?此操作将删除 OSS 文件,不可恢复!')) return; + fetch('/api/admin/digital-human-versions/' + version, { + method: 'DELETE' + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + alert('删除成功!'); + loadVersions(); + } else { + alert('删除失败:' + (res.message || res.error || '')); + } + }) + .catch(function (err) { + alert('请求失败:' + (err.message || '')); + }); + }; + + // ========== 栏目权限配置 ========== function loadColumns() { fetch('/api/admin/columns') diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index 2a32366..c7ca3df 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -916,19 +916,33 @@ - +
-

发布新版本

-
- - +

上传新版本

+

+ 上传数字人程序 ZIP 包,系统会自动计算 MD5 并存储到 OSS。上传后状态为草稿,需要手动发布。 +

+
+
+ + +
+
+ + +
- + + +
+
+ +

仅支持 .zip 格式,文件名:ShuFuDigitalHuman.zip

- +

@@ -936,14 +950,19 @@ - - - - + + + + + + + +
版本号下载链接发布时间操作版本号状态最新文件大小MD5更新日志发布时间操作
+
diff --git a/frontend-vue/src/shared/api/digital-human.ts b/frontend-vue/src/shared/api/digital-human.ts new file mode 100644 index 0000000..d8a13f1 --- /dev/null +++ b/frontend-vue/src/shared/api/digital-human.ts @@ -0,0 +1,157 @@ +import { get, post, del, type JavaApiResponse, unwrapJavaResponse } from '@/shared/api/http' + +const JAVA_API_PREFIX = '/newApi/api/digital-human/versions' + +/** + * 数字人版本信息 + */ +export interface DigitalHumanVersion { + id: number + version: string + ossObjectKey: string + fileSize: number + md5: string + changelog: string | null + minClientVersion: string | null + isLatest: boolean + status: 'DRAFT' | 'RELEASED' | 'DEPRECATED' + createdBy: string | null + createdAt: string + releasedAt: string | null + downloadUrl?: string +} + +/** + * 下载链接信息 + */ +export interface DownloadUrlVo { + version: string + downloadUrl: string + expiresIn: number +} + +/** + * 分页查询参数 + */ +export interface VersionListParams { + page?: number + pageSize?: number + status?: 'DRAFT' | 'RELEASED' | 'DEPRECATED' +} + +/** + * 分页结果 + */ +export interface PageResult { + records: T[] + total: number + size: number + current: number + pages: number +} + +/** + * 上传新版本 + */ +export function uploadDigitalHumanVersion( + file: File, + version: string, + changelog?: string, + minClientVersion?: string, +) { + const formData = new FormData() + formData.append('file', file) + formData.append('version', version) + if (changelog) formData.append('changelog', changelog) + if (minClientVersion) formData.append('minClientVersion', minClientVersion) + + return unwrapJavaResponse( + post>( + `${JAVA_API_PREFIX}/upload`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ), + ) +} + +/** + * 查询版本列表 + */ +export function getDigitalHumanVersions(params?: VersionListParams) { + return unwrapJavaResponse( + get>>( + JAVA_API_PREFIX, + { params }, + ), + ) +} + +/** + * 获取最新版本 + */ +export function getLatestDigitalHumanVersion() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/latest`, + ), + ) +} + +/** + * 获取指定版本详情 + */ +export function getDigitalHumanVersion(version: string) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/${version}`, + ), + ) +} + +/** + * 发布版本 + */ +export function releaseDigitalHumanVersion(version: string) { + return unwrapJavaResponse( + post>( + `${JAVA_API_PREFIX}/${version}/release`, + ), + ) +} + +/** + * 设为最新版本 + */ +export function setLatestDigitalHumanVersion(version: string) { + return unwrapJavaResponse( + post>( + `${JAVA_API_PREFIX}/${version}/set-latest`, + ), + ) +} + +/** + * 删除版本 + */ +export function deleteDigitalHumanVersion(version: string) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/${version}`, + ), + ) +} + +/** + * 获取下载链接 + */ +export function getDigitalHumanVersionDownloadUrl(version: string) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/${version}/download-url`, + ), + ) +}