From 76dedb657a8192f8e9477c5075aa51c1e637f86b Mon Sep 17 00:00:00 2001 From: spllzh <28668817@qq.com> Date: Mon, 6 Oct 2025 23:16:59 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E9=9F=B3=E9=A2=91?= =?UTF-8?q?=E5=90=88=E6=88=90,=20=E5=A4=B4=E8=A1=94=E7=9B=91=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/rj/common/PasswordUtil.java | 2 + .../java/com/rj/common/ServiceManager.java | 2 + src/main/java/com/rj/config/AliyunConfig.java | 2 + .../rj/controller/FaceDetectController.java | 19 +- .../VideoSynthesisFromController.java | 180 ++++++++++++ .../com/rj/controller/sys/MenuController.java | 2 + .../com/rj/controller/sys/RoleController.java | 2 + .../rj/controller/sys/UserRoleController.java | 2 + .../com/rj/dto/DifyWorkflowResponseDto.java | 2 + .../com/rj/dto/VideoSynthesisRequestDto.java | 62 ++++ .../java/com/rj/entity/VideoSynthesisLog.java | 189 +++++++++++++ .../mapper/CustomerProfileAnalysisMapper.java | 2 + .../rj/mapper/VideoSynthesisLogMapper.java | 15 + .../scheduler/AudioStatisticsScheduler.java | 2 + .../com/rj/service/IFaceDetectLogService.java | 6 +- .../rj/service/IVideoSynthesisService.java | 45 +++ .../biz/ICustomerProfileAnalysisService.java | 2 + .../CustomerProfileAnalysisServiceImpl.java | 2 + .../impl/FaceDetectLogServiceImpl.java | 14 +- .../impl/VideoSynthesisServiceImpl.java | 151 ++++++++++ .../java/com/rj/service/sys/IMenuService.java | 2 + .../com/rj/service/sys/IUserRoleService.java | 2 + .../rj/service/sys/impl/MenuServiceImpl.java | 2 + .../rj/service/sys/impl/RoleServiceImpl.java | 2 + .../service/sys/impl/UserRoleServiceImpl.java | 2 + src/main/resources/application-audio.yml | 2 + .../AudioManagementStatisticsMapper.xml | 2 + src/main/resources/static/tts-demo.html | 2 + .../sql/add_audio_management_sales_phone.sql | 2 + .../sql/add_audio_name_to_tts_request_log.sql | 11 + src/main/sql/video_synthesis_log.sql | 38 +++ .../FaceDetectAvatarUploadTest.java | 265 ++++++++++++++++++ .../MinIOControllerTempUrlTest.java | 241 ++++++++++++++++ .../VideoSynthesisControllerTest.java | 213 ++++++++++++++ .../rj/service/TtsRequestLogShortUrlTest.java | 141 ++++++++++ .../TtsRequestLogWithAudioNameTest.java | 114 ++++++++ 36 files changed, 1732 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/rj/controller/VideoSynthesisFromController.java create mode 100644 src/main/java/com/rj/dto/VideoSynthesisRequestDto.java create mode 100644 src/main/java/com/rj/entity/VideoSynthesisLog.java create mode 100644 src/main/java/com/rj/mapper/VideoSynthesisLogMapper.java create mode 100644 src/main/java/com/rj/service/IVideoSynthesisService.java create mode 100644 src/main/java/com/rj/service/impl/VideoSynthesisServiceImpl.java create mode 100644 src/main/sql/add_audio_name_to_tts_request_log.sql create mode 100644 src/main/sql/video_synthesis_log.sql create mode 100644 src/test/java/com/rj/controller/FaceDetectAvatarUploadTest.java create mode 100644 src/test/java/com/rj/controller/MinIOControllerTempUrlTest.java create mode 100644 src/test/java/com/rj/controller/VideoSynthesisControllerTest.java create mode 100644 src/test/java/com/rj/service/TtsRequestLogShortUrlTest.java create mode 100644 src/test/java/com/rj/service/TtsRequestLogWithAudioNameTest.java diff --git a/src/main/java/com/rj/common/PasswordUtil.java b/src/main/java/com/rj/common/PasswordUtil.java index 485d55d..28e5b14 100644 --- a/src/main/java/com/rj/common/PasswordUtil.java +++ b/src/main/java/com/rj/common/PasswordUtil.java @@ -134,6 +134,8 @@ public class PasswordUtil { + + diff --git a/src/main/java/com/rj/common/ServiceManager.java b/src/main/java/com/rj/common/ServiceManager.java index fba9829..692e8d7 100644 --- a/src/main/java/com/rj/common/ServiceManager.java +++ b/src/main/java/com/rj/common/ServiceManager.java @@ -118,6 +118,8 @@ public class ServiceManager { + + diff --git a/src/main/java/com/rj/config/AliyunConfig.java b/src/main/java/com/rj/config/AliyunConfig.java index e48ddeb..5795b11 100644 --- a/src/main/java/com/rj/config/AliyunConfig.java +++ b/src/main/java/com/rj/config/AliyunConfig.java @@ -96,6 +96,8 @@ public class AliyunConfig { + + diff --git a/src/main/java/com/rj/controller/FaceDetectController.java b/src/main/java/com/rj/controller/FaceDetectController.java index 63eefd4..e94a1db 100644 --- a/src/main/java/com/rj/controller/FaceDetectController.java +++ b/src/main/java/com/rj/controller/FaceDetectController.java @@ -106,7 +106,7 @@ public class FaceDetectController { try { log.info("开始处理头衔上传,文件名: {}, 大小: {} bytes, 用户ID: {}", file.getOriginalFilename(), file.getSize(), userId); - expiresInSeconds = 30*24*3600; // 默认30天 + expiresInSeconds = 7*24*3600; // 默认7天 // 1. 验证文件 if (file.isEmpty()) { result.put("success", false); @@ -167,12 +167,7 @@ public class FaceDetectController { result.put("avatarName", avatarName); // 人脸检测结果 - result.put("faceDetection", Map.of( - "success", detectLog.getSuccess(), - "faceCount", detectLog.getFaceCount(), - "processingTimeMs", detectLog.getProcessingTimeMs(), - "requestId", detectLog.getRequestId() - )); + result.put("faceDetection", detectLog.toString()); if (!detectLog.getSuccess()) { result.put("faceDetectionError", detectLog.getErrorMessage()); @@ -225,12 +220,18 @@ public class FaceDetectController { @Parameter(description = "开始时间") @RequestParam(required = false) String startTime, @Parameter(description = "结束时间") - @RequestParam(required = false) String endTime) { + @RequestParam(required = false) String endTime, + @Parameter(description = "所有人姓名") + @RequestParam(required = false) String ownerName, + @Parameter(description = "所有人电话") + @RequestParam(required = false) String ownerPhone, + @Parameter(description = "头像名字") + @RequestParam(required = false) String avatarName) { Map result = new HashMap<>(); try { com.baomidou.mybatisplus.extension.plugins.pagination.Page page = - faceDetectLogService.getPageList(current, size, success, startTime, endTime); + faceDetectLogService.getPageList(current, size, success, startTime, endTime, ownerName, ownerPhone, avatarName); result.put("success", true); result.put("message", "查询成功"); diff --git a/src/main/java/com/rj/controller/VideoSynthesisFromController.java b/src/main/java/com/rj/controller/VideoSynthesisFromController.java new file mode 100644 index 0000000..7f901b4 --- /dev/null +++ b/src/main/java/com/rj/controller/VideoSynthesisFromController.java @@ -0,0 +1,180 @@ +package com.rj.controller; + +import com.rj.dto.VideoSynthesisRequestDto; +import com.rj.entity.VideoSynthesisLog; +import com.rj.service.IVideoSynthesisService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.HashMap; +import java.util.Map; + +/** + * 视频合成控制器 + * + * @author rj + * @date 2025-01-02 + */ +@Slf4j +@RestController +@RequestMapping("/api/video-synthesis") +@Tag(name = "视频合成,根据头像和音频生成视频", description = "视频合成相关接口,根据头像和音频生成视频") +public class VideoSynthesisFromController { + + @Autowired + private IVideoSynthesisService videoSynthesisService; + + /** + * 视频合成接口 + */ + @PostMapping("/synthesize") + @Operation(summary = "视频合成", description = "根据图片和音频生成视频") + public ResponseEntity> synthesizeVideo(@Valid @RequestBody VideoSynthesisRequestDto requestDto) { + + Map result = new HashMap<>(); + + try { + log.info("开始视频合成,图片URL: {}, 音频URL: {}", requestDto.getImageUrl(), requestDto.getAudioUrl()); + + // 执行视频合成 + VideoSynthesisLog synthesisLog = videoSynthesisService.synthesizeVideoAndSave(requestDto); + + // 构建响应结果 + result.put("success", synthesisLog.getSuccess()); + result.put("message", synthesisLog.getSuccess() ? "视频合成成功" : "视频合成失败"); + result.put("requestId", synthesisLog.getRequestId()); + result.put("taskId", synthesisLog.getTaskId()); + result.put("taskStatus", synthesisLog.getTaskStatus()); + result.put("videoUrl", synthesisLog.getVideoUrl()); + result.put("videoDuration", synthesisLog.getVideoDuration()); + result.put("videoRatio", synthesisLog.getVideoRatio()); + result.put("processingTimeMs", synthesisLog.getProcessingTimeMs()); + result.put("imageId", synthesisLog.getImageId()); + result.put("audioId", synthesisLog.getAudioId()); + result.put("ownerName", synthesisLog.getOwnerName()); + result.put("ownerPhone", synthesisLog.getOwnerPhone()); + result.put("modelProvider", synthesisLog.getModelProvider()); + + if (!synthesisLog.getSuccess()) { + result.put("errorMessage", synthesisLog.getErrorMessage()); + return ResponseEntity.badRequest().body(result); + } + + return ResponseEntity.ok(result); + + } catch (Exception e) { + log.error("视频合成失败: {}", e.getMessage(), e); + result.put("success", false); + result.put("message", "视频合成失败: " + e.getMessage()); + return ResponseEntity.internalServerError().body(result); + } + } + + /** + * 根据请求ID查询视频合成结果 + */ + @GetMapping("/result/{requestId}") + @Operation(summary = "查询合成结果", description = "根据请求ID查询视频合成结果") + public ResponseEntity> getSynthesisResult( + @Parameter(description = "请求ID", required = true) + @PathVariable String requestId) { + + Map result = new HashMap<>(); + + try { + VideoSynthesisLog synthesisLog = videoSynthesisService.getVideoSynthesisLogByRequestId(requestId); + + if (synthesisLog == null) { + result.put("success", false); + result.put("message", "未找到对应的视频合成记录"); + return ResponseEntity.notFound().build(); + } + + result.put("success", true); + result.put("message", "查询成功"); + result.put("requestId", synthesisLog.getRequestId()); + result.put("taskId", synthesisLog.getTaskId()); + result.put("taskStatus", synthesisLog.getTaskStatus()); + result.put("videoUrl", synthesisLog.getVideoUrl()); + result.put("videoDuration", synthesisLog.getVideoDuration()); + result.put("videoRatio", synthesisLog.getVideoRatio()); + result.put("processingTimeMs", synthesisLog.getProcessingTimeMs()); + result.put("requestTime", synthesisLog.getRequestTime()); + result.put("responseTime", synthesisLog.getResponseTime()); + result.put("imageId", synthesisLog.getImageId()); + result.put("audioId", synthesisLog.getAudioId()); + result.put("ownerName", synthesisLog.getOwnerName()); + result.put("ownerPhone", synthesisLog.getOwnerPhone()); + result.put("modelProvider", synthesisLog.getModelProvider()); + + if (!synthesisLog.getSuccess()) { + result.put("errorMessage", synthesisLog.getErrorMessage()); + } + + return ResponseEntity.ok(result); + + } catch (Exception e) { + log.error("查询视频合成结果失败: {}", e.getMessage(), e); + result.put("success", false); + result.put("message", "查询失败: " + e.getMessage()); + return ResponseEntity.internalServerError().body(result); + } + } + + /** + * 根据任务ID查询视频合成结果 + */ + @GetMapping("/result/task/{taskId}") + @Operation(summary = "根据任务ID查询结果", description = "根据任务ID查询视频合成结果") + public ResponseEntity> getSynthesisResultByTaskId( + @Parameter(description = "任务ID", required = true) + @PathVariable String taskId) { + + Map result = new HashMap<>(); + + try { + VideoSynthesisLog synthesisLog = videoSynthesisService.getVideoSynthesisLogByTaskId(taskId); + + if (synthesisLog == null) { + result.put("success", false); + result.put("message", "未找到对应的视频合成记录"); + return ResponseEntity.notFound().build(); + } + + result.put("success", true); + result.put("message", "查询成功"); + result.put("requestId", synthesisLog.getRequestId()); + result.put("taskId", synthesisLog.getTaskId()); + result.put("taskStatus", synthesisLog.getTaskStatus()); + result.put("videoUrl", synthesisLog.getVideoUrl()); + result.put("videoDuration", synthesisLog.getVideoDuration()); + result.put("videoRatio", synthesisLog.getVideoRatio()); + result.put("processingTimeMs", synthesisLog.getProcessingTimeMs()); + result.put("requestTime", synthesisLog.getRequestTime()); + result.put("responseTime", synthesisLog.getResponseTime()); + result.put("imageId", synthesisLog.getImageId()); + result.put("audioId", synthesisLog.getAudioId()); + result.put("ownerName", synthesisLog.getOwnerName()); + result.put("ownerPhone", synthesisLog.getOwnerPhone()); + result.put("modelProvider", synthesisLog.getModelProvider()); + + if (!synthesisLog.getSuccess()) { + result.put("errorMessage", synthesisLog.getErrorMessage()); + } + + return ResponseEntity.ok(result); + + } catch (Exception e) { + log.error("根据任务ID查询视频合成结果失败: {}", e.getMessage(), e); + result.put("success", false); + result.put("message", "查询失败: " + e.getMessage()); + return ResponseEntity.internalServerError().body(result); + } + } +} diff --git a/src/main/java/com/rj/controller/sys/MenuController.java b/src/main/java/com/rj/controller/sys/MenuController.java index 7b89c4b..e849200 100644 --- a/src/main/java/com/rj/controller/sys/MenuController.java +++ b/src/main/java/com/rj/controller/sys/MenuController.java @@ -356,6 +356,8 @@ public class MenuController { + + diff --git a/src/main/java/com/rj/controller/sys/RoleController.java b/src/main/java/com/rj/controller/sys/RoleController.java index d0913b3..a1ea756 100644 --- a/src/main/java/com/rj/controller/sys/RoleController.java +++ b/src/main/java/com/rj/controller/sys/RoleController.java @@ -326,6 +326,8 @@ public class RoleController { + + diff --git a/src/main/java/com/rj/controller/sys/UserRoleController.java b/src/main/java/com/rj/controller/sys/UserRoleController.java index 32e92f4..30ff08c 100644 --- a/src/main/java/com/rj/controller/sys/UserRoleController.java +++ b/src/main/java/com/rj/controller/sys/UserRoleController.java @@ -332,6 +332,8 @@ public class UserRoleController { + + diff --git a/src/main/java/com/rj/dto/DifyWorkflowResponseDto.java b/src/main/java/com/rj/dto/DifyWorkflowResponseDto.java index b8f9517..c4040d5 100644 --- a/src/main/java/com/rj/dto/DifyWorkflowResponseDto.java +++ b/src/main/java/com/rj/dto/DifyWorkflowResponseDto.java @@ -99,6 +99,8 @@ public class DifyWorkflowResponseDto { + + diff --git a/src/main/java/com/rj/dto/VideoSynthesisRequestDto.java b/src/main/java/com/rj/dto/VideoSynthesisRequestDto.java new file mode 100644 index 0000000..5e66dad --- /dev/null +++ b/src/main/java/com/rj/dto/VideoSynthesisRequestDto.java @@ -0,0 +1,62 @@ +package com.rj.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import java.math.BigDecimal; + +/** + * 视频合成请求DTO + * + * @author rj + * @date 2025-01-02 + */ +@Data +@Schema(description = "视频合成请求参数") +public class VideoSynthesisRequestDto { + + @NotBlank(message = "图片URL不能为空") + @Schema(description = "图片URL", required = true, example = "https://example.com/image.jpg") + private String imageUrl; + + @NotBlank(message = "音频URL不能为空") + @Schema(description = "音频URL", required = true, example = "https://example.com/audio.wav") + private String audioUrl; + + @Schema(description = "使用的模型", example = "liveportrait") + private String model = "liveportrait"; + + @Schema(description = "图片ID", example = "img-12345") + private String imageId; + + @Schema(description = "音频ID", example = "audio-67890") + private String audioId; + + @Schema(description = "归属人姓名", example = "张三") + private String ownerName; + + @Schema(description = "归属人电话", example = "13800138000") + private String ownerPhone; + + @Schema(description = "模型供应商", example = "dashscope") + private String modelProvider; + + @Schema(description = "模板ID", example = "normal") + private String templateId = "normal"; + + @Schema(description = "眼部动作频率", example = "0.5") + private BigDecimal eyeMoveFreq = new BigDecimal("0.5"); + + @Schema(description = "视频帧率", example = "30") + private Integer videoFps = 30; + + @Schema(description = "嘴部动作强度", example = "1.0") + private BigDecimal mouthMoveStrength = new BigDecimal("1.0"); + + @Schema(description = "是否粘贴背景", example = "true") + private Boolean pasteBack = true; + + @Schema(description = "头部动作强度", example = "0.7") + private BigDecimal headMoveStrength = new BigDecimal("0.7"); +} diff --git a/src/main/java/com/rj/entity/VideoSynthesisLog.java b/src/main/java/com/rj/entity/VideoSynthesisLog.java new file mode 100644 index 0000000..aa413e4 --- /dev/null +++ b/src/main/java/com/rj/entity/VideoSynthesisLog.java @@ -0,0 +1,189 @@ +package com.rj.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 视频合成日志实体类 + * + * @author rj + * @date 2025-01-02 + */ +@Data +@TableName("video_synthesis_log") +public class VideoSynthesisLog { + + /** + * 主键ID(UUID) + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 请求ID(唯一) + */ + @TableField("request_id") + private String requestId; + + /** + * 任务ID + */ + @TableField("task_id") + private String taskId; + + /** + * 使用的模型 + */ + @TableField("model") + private String model; + + /** + * 图片URL + */ + @TableField("image_url") + private String imageUrl; + + /** + * 音频URL + */ + @TableField("audio_url") + private String audioUrl; + + /** + * 音频ID + */ + @TableField("audio_id") + private String audioId; + + /** + * 图片ID + */ + @TableField("image_id") + private String imageId; + + /** + * 归属人姓名 + */ + @TableField("owner_name") + private String ownerName; + + /** + * 归属人电话 + */ + @TableField("owner_phone") + private String ownerPhone; + + /** + * 模型供应商 + */ + @TableField("model_provider") + private String modelProvider; + + /** + * 模板ID + */ + @TableField("template_id") + private String templateId; + + /** + * 眼部动作频率 + */ + @TableField("eye_move_freq") + private BigDecimal eyeMoveFreq; + + /** + * 视频帧率 + */ + @TableField("video_fps") + private Integer videoFps; + + /** + * 嘴部动作强度 + */ + @TableField("mouth_move_strength") + private BigDecimal mouthMoveStrength; + + /** + * 是否粘贴背景 + */ + @TableField("paste_back") + private Boolean pasteBack; + + /** + * 头部动作强度 + */ + @TableField("head_move_strength") + private BigDecimal headMoveStrength; + + /** + * 请求时间 + */ + @TableField("request_time") + private LocalDateTime requestTime; + + /** + * 响应时间 + */ + @TableField("response_time") + private LocalDateTime responseTime; + + /** + * 任务状态 + */ + @TableField("task_status") + private String taskStatus; + + /** + * 生成的视频URL + */ + @TableField("video_url") + private String videoUrl; + + /** + * 视频时长(秒) + */ + @TableField("video_duration") + private BigDecimal videoDuration; + + /** + * 视频比例 + */ + @TableField("video_ratio") + private String videoRatio; + + /** + * 是否成功 + */ + @TableField("success") + private Boolean success; + + /** + * 错误信息 + */ + @TableField("error_message") + private String errorMessage; + + /** + * 处理时间(毫秒) + */ + @TableField("processing_time_ms") + private Long processingTimeMs; + + /** + * 创建时间 + */ + @TableField("created_at") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @TableField("updated_at") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/rj/mapper/CustomerProfileAnalysisMapper.java b/src/main/java/com/rj/mapper/CustomerProfileAnalysisMapper.java index 75d5ab7..a5c8a3c 100644 --- a/src/main/java/com/rj/mapper/CustomerProfileAnalysisMapper.java +++ b/src/main/java/com/rj/mapper/CustomerProfileAnalysisMapper.java @@ -42,6 +42,8 @@ public interface CustomerProfileAnalysisMapper extends BaseMapper { +} diff --git a/src/main/java/com/rj/scheduler/AudioStatisticsScheduler.java b/src/main/java/com/rj/scheduler/AudioStatisticsScheduler.java index 712edd2..36cba04 100644 --- a/src/main/java/com/rj/scheduler/AudioStatisticsScheduler.java +++ b/src/main/java/com/rj/scheduler/AudioStatisticsScheduler.java @@ -102,6 +102,8 @@ public class AudioStatisticsScheduler { + + diff --git a/src/main/java/com/rj/service/IFaceDetectLogService.java b/src/main/java/com/rj/service/IFaceDetectLogService.java index 0fb5eee..aa2a994 100644 --- a/src/main/java/com/rj/service/IFaceDetectLogService.java +++ b/src/main/java/com/rj/service/IFaceDetectLogService.java @@ -40,10 +40,14 @@ public interface IFaceDetectLogService extends IService { * @param success 成功状态筛选 * @param startTime 开始时间 * @param endTime 结束时间 + * @param ownerName 所有人姓名 + * @param ownerPhone 所有人电话 + * @param avatarName 头像名字 * @return 分页结果 */ com.baomidou.mybatisplus.extension.plugins.pagination.Page getPageList( - Integer current, Integer size, Boolean success, String startTime, String endTime); + Integer current, Integer size, Boolean success, String startTime, String endTime, + String ownerName, String ownerPhone, String avatarName); /** * 获取统计信息 diff --git a/src/main/java/com/rj/service/IVideoSynthesisService.java b/src/main/java/com/rj/service/IVideoSynthesisService.java new file mode 100644 index 0000000..f32f8e2 --- /dev/null +++ b/src/main/java/com/rj/service/IVideoSynthesisService.java @@ -0,0 +1,45 @@ +package com.rj.service; + +import com.rj.dto.VideoSynthesisRequestDto; +import com.rj.entity.VideoSynthesisLog; + +/** + * 视频合成服务接口 + * + * @author rj + * @date 2025-01-02 + */ +public interface IVideoSynthesisService { + + /** + * 执行视频合成并保存日志 + * + * @param request 视频合成请求 + * @return 视频合成日志 + */ + VideoSynthesisLog synthesizeVideoAndSave(VideoSynthesisRequestDto request); + + /** + * 保存视频合成日志 + * + * @param log 视频合成日志 + * @return 是否保存成功 + */ + boolean saveVideoSynthesisLog(VideoSynthesisLog log); + + /** + * 根据请求ID查询视频合成日志 + * + * @param requestId 请求ID + * @return 视频合成日志 + */ + VideoSynthesisLog getVideoSynthesisLogByRequestId(String requestId); + + /** + * 根据任务ID查询视频合成日志 + * + * @param taskId 任务ID + * @return 视频合成日志 + */ + VideoSynthesisLog getVideoSynthesisLogByTaskId(String taskId); +} diff --git a/src/main/java/com/rj/service/biz/ICustomerProfileAnalysisService.java b/src/main/java/com/rj/service/biz/ICustomerProfileAnalysisService.java index 7807991..ecc3e26 100644 --- a/src/main/java/com/rj/service/biz/ICustomerProfileAnalysisService.java +++ b/src/main/java/com/rj/service/biz/ICustomerProfileAnalysisService.java @@ -63,6 +63,8 @@ public interface ICustomerProfileAnalysisService extends IService getPageList( - Integer current, Integer size, Boolean success, String startTime, String endTime) { + Integer current, Integer size, Boolean success, String startTime, String endTime, + String ownerName, String ownerPhone, String avatarName) { com.baomidou.mybatisplus.extension.plugins.pagination.Page page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(current, size); @@ -279,6 +280,15 @@ public class FaceDetectLogServiceImpl extends ServiceImpl entity = new HttpEntity<>(request, headers); + + // 发送请求 + ResponseEntity response = restTemplate.postForEntity(API_URL, entity, String.class); + + LocalDateTime endTime = LocalDateTime.now(); + synthesisLog.setResponseTime(endTime); + synthesisLog.setProcessingTimeMs(java.time.Duration.between(startTime, endTime).toMillis()); + + // 解析响应 + if (response.getStatusCode() == HttpStatus.OK) { + // 直接解析JSON响应 + String responseBody = response.getBody(); + log.info("API响应: {}", responseBody); + + // 简单解析关键字段 + if (responseBody != null && responseBody.contains("task_id")) { + // 这里可以添加更复杂的JSON解析逻辑 + // 暂时设置基本状态 + synthesisLog.setTaskStatus("PENDING"); + synthesisLog.setSuccess(true); + } else { + synthesisLog.setErrorMessage("API响应格式异常"); + synthesisLog.setSuccess(false); + } + + log.info("视频合成请求成功,任务ID: {}, 状态: {}", + synthesisLog.getTaskId(), synthesisLog.getTaskStatus()); + } else { + synthesisLog.setErrorMessage("HTTP请求失败,状态码: " + response.getStatusCode()); + log.error("视频合成请求失败,状态码: {}, 响应: {}", response.getStatusCode(), response.getBody()); + } + + } catch (Exception e) { + LocalDateTime endTime = LocalDateTime.now(); + synthesisLog.setResponseTime(endTime); + synthesisLog.setProcessingTimeMs(java.time.Duration.between(startTime, endTime).toMillis()); + synthesisLog.setErrorMessage("视频合成异常: " + e.getMessage()); + log.error("视频合成失败: {}", e.getMessage(), e); + } + + // 保存日志 + saveVideoSynthesisLog(synthesisLog); + return synthesisLog; + } + + @Override + public boolean saveVideoSynthesisLog(VideoSynthesisLog synthesisLog) { + try { + synthesisLog.setCreatedAt(LocalDateTime.now()); + synthesisLog.setUpdatedAt(LocalDateTime.now()); + + int result = videoSynthesisLogMapper.insert(synthesisLog); + log.info("视频合成日志保存{}: 请求ID={}, 任务ID={}, 成功={}", + result > 0 ? "成功" : "失败", synthesisLog.getRequestId(), synthesisLog.getTaskId(), synthesisLog.getSuccess()); + return result > 0; + } catch (Exception e) { + log.error("保存视频合成日志失败: {}", e.getMessage(), e); + return false; + } + } + + @Override + public VideoSynthesisLog getVideoSynthesisLogByRequestId(String requestId) { + try { + return videoSynthesisLogMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper() + .eq("request_id", requestId) + ); + } catch (Exception e) { + log.error("根据请求ID查询视频合成日志失败: {}", e.getMessage(), e); + return null; + } + } + + @Override + public VideoSynthesisLog getVideoSynthesisLogByTaskId(String taskId) { + try { + return videoSynthesisLogMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper() + .eq("task_id", taskId) + ); + } catch (Exception e) { + log.error("根据任务ID查询视频合成日志失败: {}", e.getMessage(), e); + return null; + } + } +} diff --git a/src/main/java/com/rj/service/sys/IMenuService.java b/src/main/java/com/rj/service/sys/IMenuService.java index bec5eb4..58c3f2d 100644 --- a/src/main/java/com/rj/service/sys/IMenuService.java +++ b/src/main/java/com/rj/service/sys/IMenuService.java @@ -65,6 +65,8 @@ public interface IMenuService extends IService { + + diff --git a/src/main/java/com/rj/service/sys/IUserRoleService.java b/src/main/java/com/rj/service/sys/IUserRoleService.java index ef1b603..13a8161 100644 --- a/src/main/java/com/rj/service/sys/IUserRoleService.java +++ b/src/main/java/com/rj/service/sys/IUserRoleService.java @@ -65,6 +65,8 @@ public interface IUserRoleService extends IService { + + diff --git a/src/main/java/com/rj/service/sys/impl/MenuServiceImpl.java b/src/main/java/com/rj/service/sys/impl/MenuServiceImpl.java index dcb66c3..67ac65c 100644 --- a/src/main/java/com/rj/service/sys/impl/MenuServiceImpl.java +++ b/src/main/java/com/rj/service/sys/impl/MenuServiceImpl.java @@ -69,6 +69,8 @@ public class MenuServiceImpl extends ServiceImpl implements IM + + diff --git a/src/main/java/com/rj/service/sys/impl/RoleServiceImpl.java b/src/main/java/com/rj/service/sys/impl/RoleServiceImpl.java index 6fe7ff5..f03e754 100644 --- a/src/main/java/com/rj/service/sys/impl/RoleServiceImpl.java +++ b/src/main/java/com/rj/service/sys/impl/RoleServiceImpl.java @@ -69,6 +69,8 @@ public class RoleServiceImpl extends ServiceImpl implements IR + + diff --git a/src/main/java/com/rj/service/sys/impl/UserRoleServiceImpl.java b/src/main/java/com/rj/service/sys/impl/UserRoleServiceImpl.java index 68a018d..cabb77b 100644 --- a/src/main/java/com/rj/service/sys/impl/UserRoleServiceImpl.java +++ b/src/main/java/com/rj/service/sys/impl/UserRoleServiceImpl.java @@ -69,6 +69,8 @@ public class UserRoleServiceImpl extends ServiceImpl i + + diff --git a/src/main/resources/application-audio.yml b/src/main/resources/application-audio.yml index 2498919..42c475c 100644 --- a/src/main/resources/application-audio.yml +++ b/src/main/resources/application-audio.yml @@ -79,6 +79,8 @@ spring: + + diff --git a/src/main/resources/mapper/AudioManagementStatisticsMapper.xml b/src/main/resources/mapper/AudioManagementStatisticsMapper.xml index bff2f4f..48636f1 100644 --- a/src/main/resources/mapper/AudioManagementStatisticsMapper.xml +++ b/src/main/resources/mapper/AudioManagementStatisticsMapper.xml @@ -55,6 +55,8 @@ + + diff --git a/src/main/resources/static/tts-demo.html b/src/main/resources/static/tts-demo.html index e3aada0..f447a92 100644 --- a/src/main/resources/static/tts-demo.html +++ b/src/main/resources/static/tts-demo.html @@ -258,3 +258,5 @@ + + diff --git a/src/main/sql/add_audio_management_sales_phone.sql b/src/main/sql/add_audio_management_sales_phone.sql index c41e112..369d06a 100644 --- a/src/main/sql/add_audio_management_sales_phone.sql +++ b/src/main/sql/add_audio_management_sales_phone.sql @@ -18,3 +18,5 @@ AFTER `sales_name`; + + diff --git a/src/main/sql/add_audio_name_to_tts_request_log.sql b/src/main/sql/add_audio_name_to_tts_request_log.sql new file mode 100644 index 0000000..fcc237c --- /dev/null +++ b/src/main/sql/add_audio_name_to_tts_request_log.sql @@ -0,0 +1,11 @@ +-- 为 tts_request_log 表添加音频名称字段 +ALTER TABLE `tts_request_log` +ADD COLUMN `audio_name` varchar(255) DEFAULT NULL COMMENT '音频文件名' AFTER `minio_url`; + +-- 添加索引以提高查询性能 +ALTER TABLE `tts_request_log` +ADD INDEX `idx_audio_name` (`audio_name`); + + + + diff --git a/src/main/sql/video_synthesis_log.sql b/src/main/sql/video_synthesis_log.sql new file mode 100644 index 0000000..33188c0 --- /dev/null +++ b/src/main/sql/video_synthesis_log.sql @@ -0,0 +1,38 @@ +-- 视频合成日志表 +CREATE TABLE IF NOT EXISTS `video_synthesis_log` ( + `id` varchar(36) NOT NULL COMMENT '主键ID(UUID)', + `request_id` varchar(64) NOT NULL COMMENT '请求ID(唯一)', + `task_id` varchar(64) DEFAULT NULL COMMENT '任务ID', + `model` varchar(100) NOT NULL COMMENT '使用的模型', + `image_url` varchar(500) NOT NULL COMMENT '图片URL', + `audio_url` varchar(500) NOT NULL COMMENT '音频URL', + `audio_id` varchar(64) DEFAULT NULL COMMENT '音频ID', + `image_id` varchar(64) DEFAULT NULL COMMENT '图片ID', + `owner_name` varchar(100) DEFAULT NULL COMMENT '归属人姓名', + `owner_phone` varchar(20) DEFAULT NULL COMMENT '归属人电话', + `model_provider` varchar(50) DEFAULT NULL COMMENT '模型供应商', + `template_id` varchar(50) DEFAULT 'normal' COMMENT '模板ID', + `eye_move_freq` decimal(3,2) DEFAULT 0.5 COMMENT '眼部动作频率', + `video_fps` int DEFAULT 30 COMMENT '视频帧率', + `mouth_move_strength` decimal(3,2) DEFAULT 1.0 COMMENT '嘴部动作强度', + `paste_back` tinyint(1) DEFAULT 1 COMMENT '是否粘贴背景', + `head_move_strength` decimal(3,2) DEFAULT 0.7 COMMENT '头部动作强度', + `request_time` datetime NOT NULL COMMENT '请求时间', + `response_time` datetime DEFAULT NULL COMMENT '响应时间', + `task_status` varchar(20) DEFAULT NULL COMMENT '任务状态', + `video_url` varchar(500) DEFAULT NULL COMMENT '生成的视频URL', + `video_duration` decimal(10,2) DEFAULT NULL COMMENT '视频时长(秒)', + `video_ratio` varchar(20) DEFAULT NULL COMMENT '视频比例', + `success` tinyint(1) DEFAULT 0 COMMENT '是否成功', + `error_message` text COMMENT '错误信息', + `processing_time_ms` bigint DEFAULT NULL COMMENT '处理时间(毫秒)', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_request_id` (`request_id`), + KEY `idx_task_id` (`task_id`), + KEY `idx_image_id` (`image_id`), + KEY `idx_owner_phone` (`owner_phone`), + KEY `idx_created_at` (`created_at`), + KEY `idx_success` (`success`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='视频合成日志表'; diff --git a/src/test/java/com/rj/controller/FaceDetectAvatarUploadTest.java b/src/test/java/com/rj/controller/FaceDetectAvatarUploadTest.java new file mode 100644 index 0000000..2a523d7 --- /dev/null +++ b/src/test/java/com/rj/controller/FaceDetectAvatarUploadTest.java @@ -0,0 +1,265 @@ +package com.rj.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * 头衔上传接口测试 + * 测试 FaceDetectController 中的头衔上传功能 + * + * @author rj + * @date 2025-01-02 + */ +@SpringBootTest +@AutoConfigureWebMvc +@TestPropertySource(properties = { + "minio.endpoint=http://101.35.52.237:19005", + "minio.access-key=minioadmin", + "minio.secret-key=minioadmin", + "minio.bucket-name=car", + "dashscope.api.key=${DASHSCOPE_API_KEY:test-key}" +}) +@Slf4j +public class FaceDetectAvatarUploadTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + private MockMvc mockMvc; + + @org.junit.jupiter.api.BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + } + + /** + * 测试头衔上传接口 - 正常情况 + */ + @Test + void testAvatarUpload_Success() throws Exception { + log.info("=== 测试头衔上传接口 - 正常情况 ==="); + + // 1. 创建测试图片文件 + String testContent = "这是一个测试图片文件内容"; + MockMultipartFile testFile = new MockMultipartFile( + "file", + "test-avatar.jpg", + "image/jpeg", + testContent.getBytes(StandardCharsets.UTF_8) + ); + + // 2. 调用头衔上传接口 + MvcResult result = mockMvc.perform(multipart("/api/face-detect/avatar/upload") + .file(testFile) + .param("userId", "test-user-123") + .param("expiresInSeconds", "3600")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("头衔上传响应: {}", responseContent); + + // 3. 验证响应内容 + assertNotNull(responseContent); + assertTrue(responseContent.contains("\"success\":true")); + assertTrue(responseContent.contains("\"originalFileName\":\"test-avatar.jpg\"")); + assertTrue(responseContent.contains("\"fileUrl\"")); + assertTrue(responseContent.contains("\"shortUrl\"")); + assertTrue(responseContent.contains("\"faceDetection\"")); + assertTrue(responseContent.contains("\"userId\":\"test-user-123\"")); + } + + /** + * 测试头衔上传接口 - 空文件 + */ + @Test + void testAvatarUpload_EmptyFile() throws Exception { + log.info("=== 测试头衔上传接口 - 空文件 ==="); + + MockMultipartFile emptyFile = new MockMultipartFile( + "file", + "empty.jpg", + "image/jpeg", + new byte[0] + ); + + MvcResult result = mockMvc.perform(multipart("/api/face-detect/avatar/upload") + .file(emptyFile)) + .andExpect(status().isBadRequest()) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("空文件上传响应: {}", responseContent); + + assertTrue(responseContent.contains("\"success\":false")); + assertTrue(responseContent.contains("文件不能为空")); + } + + /** + * 测试头衔上传接口 - 非图片文件 + */ + @Test + void testAvatarUpload_NonImageFile() throws Exception { + log.info("=== 测试头衔上传接口 - 非图片文件 ==="); + + MockMultipartFile nonImageFile = new MockMultipartFile( + "file", + "test.txt", + "text/plain", + "这是一个文本文件".getBytes(StandardCharsets.UTF_8) + ); + + MvcResult result = mockMvc.perform(multipart("/api/face-detect/avatar/upload") + .file(nonImageFile)) + .andExpect(status().isBadRequest()) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("非图片文件上传响应: {}", responseContent); + + assertTrue(responseContent.contains("\"success\":false")); + assertTrue(responseContent.contains("只支持图片文件格式")); + } + + /** + * 测试头衔上传接口 - 大文件 + */ + @Test + void testAvatarUpload_LargeFile() throws Exception { + log.info("=== 测试头衔上传接口 - 大文件 ==="); + + // 创建一个51MB的文件(超过50MB限制) + byte[] largeContent = new byte[51 * 1024 * 1024]; + MockMultipartFile largeFile = new MockMultipartFile( + "file", + "large-image.jpg", + "image/jpeg", + largeContent + ); + + MvcResult result = mockMvc.perform(multipart("/api/face-detect/avatar/upload") + .file(largeFile)) + .andExpect(status().isBadRequest()) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("大文件上传响应: {}", responseContent); + + assertTrue(responseContent.contains("\"success\":false")); + assertTrue(responseContent.contains("文件大小不能超过50MB")); + } + + /** + * 测试头衔上传接口 - 不同过期时间 + */ + @Test + void testAvatarUpload_DifferentExpires() throws Exception { + log.info("=== 测试头衔上传接口 - 不同过期时间 ==="); + + String testContent = "测试图片内容"; + MockMultipartFile testFile = new MockMultipartFile( + "file", + "test-expires.jpg", + "image/jpeg", + testContent.getBytes(StandardCharsets.UTF_8) + ); + + // 测试5分钟过期时间 + MvcResult result = mockMvc.perform(multipart("/api/face-detect/avatar/upload") + .file(testFile) + .param("expiresInSeconds", "300")) + .andExpect(status().isOk()) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("5分钟过期时间响应: {}", responseContent); + + assertTrue(responseContent.contains("\"success\":true")); + assertTrue(responseContent.contains("\"shortUrlExpiresInSeconds\":300")); + } + + /** + * 测试头衔上传接口 - 带用户ID + */ + @Test + void testAvatarUpload_WithUserId() throws Exception { + log.info("=== 测试头衔上传接口 - 带用户ID ==="); + + String testContent = "测试图片内容"; + MockMultipartFile testFile = new MockMultipartFile( + "file", + "test-user.jpg", + "image/jpeg", + testContent.getBytes(StandardCharsets.UTF_8) + ); + + MvcResult result = mockMvc.perform(multipart("/api/face-detect/avatar/upload") + .file(testFile) + .param("userId", "user-12345") + .param("expiresInSeconds", "7200")) + .andExpect(status().isOk()) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("带用户ID上传响应: {}", responseContent); + + assertTrue(responseContent.contains("\"success\":true")); + assertTrue(responseContent.contains("\"userId\":\"user-12345\"")); + assertTrue(responseContent.contains("\"shortUrlExpiresInSeconds\":7200")); + } + + /** + * 测试头衔上传接口 - 验证返回的数据结构 + */ + @Test + void testAvatarUpload_ResponseStructure() throws Exception { + log.info("=== 测试头衔上传接口 - 验证返回的数据结构 ==="); + + String testContent = "测试图片内容"; + MockMultipartFile testFile = new MockMultipartFile( + "file", + "structure-test.jpg", + "image/jpeg", + testContent.getBytes(StandardCharsets.UTF_8) + ); + + MvcResult result = mockMvc.perform(multipart("/api/face-detect/avatar/upload") + .file(testFile) + .param("userId", "structure-test-user")) + .andExpect(status().isOk()) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("数据结构测试响应: {}", responseContent); + + // 验证必要的字段存在 + assertTrue(responseContent.contains("\"success\"")); + assertTrue(responseContent.contains("\"message\"")); + assertTrue(responseContent.contains("\"originalFileName\"")); + assertTrue(responseContent.contains("\"fileSize\"")); + assertTrue(responseContent.contains("\"fileUrl\"")); + assertTrue(responseContent.contains("\"shortUrl\"")); + assertTrue(responseContent.contains("\"shortUrlExpiresAt\"")); + assertTrue(responseContent.contains("\"shortUrlExpiresInSeconds\"")); + assertTrue(responseContent.contains("\"userId\"")); + assertTrue(responseContent.contains("\"faceDetection\"")); + } +} diff --git a/src/test/java/com/rj/controller/MinIOControllerTempUrlTest.java b/src/test/java/com/rj/controller/MinIOControllerTempUrlTest.java new file mode 100644 index 0000000..9154b54 --- /dev/null +++ b/src/test/java/com/rj/controller/MinIOControllerTempUrlTest.java @@ -0,0 +1,241 @@ +package com.rj.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rj.service.MinIOService; +import com.rj.utils.MinIOUrlGenerator; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * MinIO 临时URL接口测试 + * 测试 generateTempUrlWithExpires 接口的功能 + * + * @author rj + * @date 2025-01-02 + */ +@SpringBootTest +@AutoConfigureWebMvc +@TestPropertySource(properties = { + "minio.endpoint=http://101.35.52.237:19005", + "minio.access-key=minioadmin", + "minio.secret-key=minioadmin", + "minio.bucket-name=car" +}) +@Slf4j +public class MinIOControllerTempUrlTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private MinIOService minioService; + + @Autowired + private MinIOUrlGenerator urlGenerator; + + private MockMvc mockMvc; + private ObjectMapper objectMapper = new ObjectMapper(); + + @org.junit.jupiter.api.BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + } + + /** + * 测试生成临时URL接口 - 正常情况 + */ + @Test + void testGenerateTempUrlWithExpires_Success() throws Exception { + // 1. 先上传一个测试文件 + + MinIOUrlGenerator.UrlInfo urlInfo = urlGenerator.generateTempUrl("微信图片_20251005120205_56_49.jpg"); + + log.info("临时url: "+urlInfo.toString()); + + } + + /** + * 测试生成临时URL接口 - 文件不存在 + */ + @Test + void testGenerateTempUrlWithExpires_FileNotFound() throws Exception { + String nonExistentFile = "non-existent-file-" + System.currentTimeMillis() + ".txt"; + int expiresInSeconds = 300; + + MvcResult result = mockMvc.perform(get("/minio/temp-url/{fileName}/{expiresInSeconds}", + nonExistentFile, expiresInSeconds)) + .andExpect(status().isOk()) // 接口返回200,但success为false + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + System.out.println("文件不存在时的响应: " + responseContent); + + // 验证响应内容 + assertNotNull(responseContent); + assertTrue(responseContent.contains("\"success\":false")); + } + + /** + * 测试生成临时URL接口 - 过期时间超出范围 + */ + @Test + void testGenerateTempUrlWithExpires_InvalidExpires() throws Exception { + // 先上传一个测试文件 + String testFileName = "test-invalid-expires-" + System.currentTimeMillis() + ".txt"; + String testContent = "测试文件"; + + MockMultipartFile testFile = new MockMultipartFile( + "file", + testFileName, + "text/plain", + testContent.getBytes(StandardCharsets.UTF_8) + ); + + // 上传文件 + mockMvc.perform(multipart("/minio/upload") + .file(testFile)) + .andExpect(status().isOk()); + + // 测试过期时间过短(小于60秒) + MvcResult result1 = mockMvc.perform(get("/minio/temp-url/{fileName}/{expiresInSeconds}", + testFileName, 30)) // 30秒,小于最小值60秒 + .andExpect(status().isBadRequest()) + .andReturn(); + + System.out.println("过期时间过短的响应: " + result1.getResponse().getContentAsString()); + + // 测试过期时间过长(超过7天) + MvcResult result2 = mockMvc.perform(get("/minio/temp-url/{fileName}/{expiresInSeconds}", + testFileName, 604801)) // 超过7天 + .andExpect(status().isBadRequest()) + .andReturn(); + + System.out.println("过期时间过长的响应: " + result2.getResponse().getContentAsString()); + + // 清理测试文件 + minioService.deleteFile(testFileName); + } + + /** + * 测试边界值 - 最小和最大过期时间 + */ + @Test + void testGenerateTempUrlWithExpires_BoundaryValues() throws Exception { + // 先上传一个测试文件 + String testFileName = "test-boundary-" + System.currentTimeMillis() + ".txt"; + String testContent = "边界值测试文件"; + + MockMultipartFile testFile = new MockMultipartFile( + "file", + testFileName, + "text/plain", + testContent.getBytes(StandardCharsets.UTF_8) + ); + + // 上传文件 + mockMvc.perform(multipart("/minio/upload") + .file(testFile)) + .andExpect(status().isOk()); + + // 测试最小过期时间(60秒) + MvcResult result1 = mockMvc.perform(get("/minio/temp-url/{fileName}/{expiresInSeconds}", + testFileName, 60)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andReturn(); + + System.out.println("最小过期时间测试: " + result1.getResponse().getContentAsString()); + + // 测试最大过期时间(7天) + MvcResult result2 = mockMvc.perform(get("/minio/temp-url/{fileName}/{expiresInSeconds}", + testFileName, 604800)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andReturn(); + + System.out.println("最大过期时间测试: " + result2.getResponse().getContentAsString()); + + // 清理测试文件 + minioService.deleteFile(testFileName); + } + + /** + * 从响应中提取临时URL + */ + private String extractTempUrlFromResponse(String responseContent) { + try { + // 简单的JSON解析来提取tempUrl + int startIndex = responseContent.indexOf("\"tempUrl\":\""); + if (startIndex != -1) { + startIndex += 11; // 跳过 "tempUrl":" + int endIndex = responseContent.indexOf("\"", startIndex); + if (endIndex != -1) { + return responseContent.substring(startIndex, endIndex); + } + } + } catch (Exception e) { + System.err.println("提取临时URL失败: " + e.getMessage()); + } + return null; + } + + /** + * 测试临时URL的可访问性 + */ + private void testTempUrlAccessibility(String tempUrl) { + try { + System.out.println("测试临时URL可访问性: " + tempUrl); + + URL url = new URL(tempUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(10000); // 10秒超时 + connection.setReadTimeout(10000); + + int responseCode = connection.getResponseCode(); + System.out.println("临时URL响应码: " + responseCode); + + if (responseCode == 200) { + System.out.println("✅ 临时URL可以正常访问"); + + // 读取响应内容验证 + InputStream inputStream = connection.getInputStream(); + byte[] buffer = new byte[1024]; + int bytesRead = inputStream.read(buffer); + if (bytesRead > 0) { + String content = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8); + System.out.println("文件内容预览: " + content.substring(0, Math.min(100, content.length()))); + } + inputStream.close(); + } else { + System.out.println("❌ 临时URL无法访问,响应码: " + responseCode); + } + + connection.disconnect(); + + } catch (Exception e) { + System.err.println("测试临时URL访问失败: " + e.getMessage()); + } + } +} diff --git a/src/test/java/com/rj/controller/VideoSynthesisControllerTest.java b/src/test/java/com/rj/controller/VideoSynthesisControllerTest.java new file mode 100644 index 0000000..746e291 --- /dev/null +++ b/src/test/java/com/rj/controller/VideoSynthesisControllerTest.java @@ -0,0 +1,213 @@ +package com.rj.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rj.dto.VideoSynthesisRequestDto; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * 视频合成接口测试 + * + * @author rj + * @date 2025-01-02 + */ +@SpringBootTest +@AutoConfigureWebMvc +@TestPropertySource(properties = { + "dashscope.api.key=${DASHSCOPE_API_KEY:test-key}" +}) +@Slf4j +public class VideoSynthesisControllerTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private ObjectMapper objectMapper; + + private MockMvc mockMvc; + + @org.junit.jupiter.api.BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + } + + /** + * 测试视频合成接口 - 正常情况 + */ + @Test + void testSynthesizeVideo_Success() throws Exception { + log.info("=== 测试视频合成接口 - 正常情况 ==="); + + // 构建请求参数 + VideoSynthesisRequestDto requestDto = new VideoSynthesisRequestDto(); + requestDto.setImageUrl("https://example.com/test-image.jpg"); + requestDto.setAudioUrl("https://example.com/test-audio.wav"); + requestDto.setImageId("img-12345"); + requestDto.setAudioId("audio-67890"); + requestDto.setOwnerName("张三"); + requestDto.setOwnerPhone("13800138000"); + requestDto.setModelProvider("dashscope"); + requestDto.setTemplateId("normal"); + requestDto.setEyeMoveFreq(java.math.BigDecimal.valueOf(0.5)); + requestDto.setVideoFps(30); + requestDto.setMouthMoveStrength(java.math.BigDecimal.valueOf(1.0)); + requestDto.setPasteBack(true); + requestDto.setHeadMoveStrength(java.math.BigDecimal.valueOf(0.7)); + + MvcResult result = mockMvc.perform(post("/api/video-synthesis/synthesize") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("视频合成响应: {}", responseContent); + + // 验证响应内容 + assertNotNull(responseContent); + assertTrue(responseContent.contains("\"requestId\"")); + assertTrue(responseContent.contains("\"taskId\"")); + assertTrue(responseContent.contains("\"taskStatus\"")); + } + + /** + * 测试视频合成接口 - 缺少必需参数 + */ + @Test + void testSynthesizeVideo_MissingRequiredParams() throws Exception { + log.info("=== 测试视频合成接口 - 缺少必需参数 ==="); + + // 测试缺少图片URL + VideoSynthesisRequestDto requestDto1 = new VideoSynthesisRequestDto(); + requestDto1.setAudioUrl("https://example.com/test-audio.wav"); + + MvcResult result1 = mockMvc.perform(post("/api/video-synthesis/synthesize") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto1))) + .andExpect(status().isBadRequest()) + .andReturn(); + + log.info("缺少图片URL响应: {}", result1.getResponse().getContentAsString()); + + // 测试缺少音频URL + VideoSynthesisRequestDto requestDto2 = new VideoSynthesisRequestDto(); + requestDto2.setImageUrl("https://example.com/test-image.jpg"); + + MvcResult result2 = mockMvc.perform(post("/api/video-synthesis/synthesize") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto2))) + .andExpect(status().isBadRequest()) + .andReturn(); + + log.info("缺少音频URL响应: {}", result2.getResponse().getContentAsString()); + } + + /** + * 测试查询合成结果接口 + */ + @Test + void testGetSynthesisResult() throws Exception { + log.info("=== 测试查询合成结果接口 ==="); + + String testRequestId = "test-request-id-12345"; + + MvcResult result = mockMvc.perform(get("/api/video-synthesis/result/{requestId}", testRequestId)) + .andExpect(status().isNotFound()) // 由于测试数据不存在,应该返回404 + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("查询合成结果响应: {}", responseContent); + } + + /** + * 测试根据任务ID查询结果接口 + */ + @Test + void testGetSynthesisResultByTaskId() throws Exception { + log.info("=== 测试根据任务ID查询结果接口 ==="); + + String testTaskId = "test-task-id-12345"; + + MvcResult result = mockMvc.perform(get("/api/video-synthesis/result/task/{taskId}", testTaskId)) + .andExpect(status().isNotFound()) // 由于测试数据不存在,应该返回404 + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("根据任务ID查询结果响应: {}", responseContent); + } + + /** + * 测试视频合成接口 - 使用默认参数 + */ + @Test + void testSynthesizeVideo_WithDefaultParams() throws Exception { + log.info("=== 测试视频合成接口 - 使用默认参数 ==="); + + // 构建请求参数(只设置必需参数,其他使用默认值) + VideoSynthesisRequestDto requestDto = new VideoSynthesisRequestDto(); + requestDto.setImageUrl("https://example.com/test-image.jpg"); + requestDto.setAudioUrl("https://example.com/test-audio.wav"); + + MvcResult result = mockMvc.perform(post("/api/video-synthesis/synthesize") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("使用默认参数响应: {}", responseContent); + + // 验证响应内容 + assertNotNull(responseContent); + assertTrue(responseContent.contains("\"requestId\"")); + } + + /** + * 测试视频合成接口 - 自定义参数 + */ + @Test + void testSynthesizeVideo_WithCustomParams() throws Exception { + log.info("=== 测试视频合成接口 - 自定义参数 ==="); + + // 构建自定义参数 + VideoSynthesisRequestDto requestDto = new VideoSynthesisRequestDto(); + requestDto.setImageUrl("https://example.com/custom-image.jpg"); + requestDto.setAudioUrl("https://example.com/custom-audio.wav"); + requestDto.setTemplateId("custom"); + requestDto.setEyeMoveFreq(java.math.BigDecimal.valueOf(0.8)); + requestDto.setVideoFps(25); + requestDto.setMouthMoveStrength(java.math.BigDecimal.valueOf(1.2)); + requestDto.setPasteBack(false); + requestDto.setHeadMoveStrength(java.math.BigDecimal.valueOf(0.9)); + + MvcResult result = mockMvc.perform(post("/api/video-synthesis/synthesize") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + log.info("自定义参数响应: {}", responseContent); + + // 验证响应内容 + assertNotNull(responseContent); + assertTrue(responseContent.contains("\"requestId\"")); + } +} diff --git a/src/test/java/com/rj/service/TtsRequestLogShortUrlTest.java b/src/test/java/com/rj/service/TtsRequestLogShortUrlTest.java new file mode 100644 index 0000000..356d21f --- /dev/null +++ b/src/test/java/com/rj/service/TtsRequestLogShortUrlTest.java @@ -0,0 +1,141 @@ +package com.rj.service; + +import com.rj.entity.TtsRequestLog; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; + +/** + * TTS请求日志短链接设置测试 + * + * @author rj + * @date 2025-01-02 + */ +@Slf4j +@SpringBootTest +public class TtsRequestLogShortUrlTest { + + @Autowired + private ITtsRequestLogService ttsRequestLogService; + + /** + * 测试根据音频名称设置短链接(默认15分钟) + */ + @Test + public void testSetShortUrlByAudioNameDefault() { + try { + String audioName = "test_audio_20250102_150000_12345.mp3"; + + log.info("开始测试根据音频名称设置短链接(默认15分钟)..."); + log.info("音频名称: {}", audioName); + + // 先创建一个测试日志记录 + TtsRequestLog requestLog = createTestRequestLog(audioName); + boolean saveResult = ttsRequestLogService.saveTtsRequestLog(requestLog); + + if (saveResult) { + log.info("测试日志记录保存成功,ID: {}", requestLog.getId()); + + // 测试设置短链接 + boolean result = ttsRequestLogService.setShortUrlByAudioName(audioName); + + if (result) { + log.info("短链接设置成功!"); + + // 查询验证 + TtsRequestLog updatedLog = ttsRequestLogService.getTtsRequestLogById(requestLog.getId()); + if (updatedLog != null) { + log.info("验证结果:"); + log.info(" 音频名称: {}", updatedLog.getAudioName()); + log.info(" 短链接: {}", updatedLog.getShortUrl()); + log.info(" 过期时间: {}", updatedLog.getShortUrlExpireTime()); + } + } else { + log.error("短链接设置失败"); + } + } else { + log.error("测试日志记录保存失败"); + } + + } catch (Exception e) { + log.error("测试过程中发生异常: {}", e.getMessage(), e); + } + } + + + + /** + * 测试不存在的音频名称 + */ + @Test + public void testSetShortUrlByNonExistentAudioName() { + try { + String audioName = "non_existent_audio.mp3"; + + log.info("开始测试不存在的音频名称..."); + log.info("音频名称: {}", audioName); + + // 测试设置短链接 + boolean result = ttsRequestLogService.setShortUrlByAudioName(audioName); + + if (result) { + log.error("意外成功:不存在的音频名称不应该设置成功"); + } else { + log.info("测试通过:不存在的音频名称正确返回失败"); + } + + } catch (Exception e) { + log.error("测试过程中发生异常: {}", e.getMessage(), e); + } + } + + /** + * 测试空音频名称 + */ + @Test + public void testSetShortUrlByEmptyAudioName() { + try { + String audioName = ""; + + log.info("开始测试空音频名称..."); + log.info("音频名称: '{}'", audioName); + + // 测试设置短链接 + boolean result = ttsRequestLogService.setShortUrlByAudioName(audioName); + + if (result) { + log.error("意外成功:空音频名称不应该设置成功"); + } else { + log.info("测试通过:空音频名称正确返回失败"); + } + + } catch (Exception e) { + log.error("测试过程中发生异常: {}", e.getMessage(), e); + } + } + + /** + * 创建测试用的TTS请求日志 + */ + private TtsRequestLog createTestRequestLog(String audioName) { + TtsRequestLog requestLog = new TtsRequestLog(); + requestLog.setRequestTime(LocalDateTime.now()); + requestLog.setModel("fnlp/MOSS-TTSD-v0.5"); + requestLog.setInputText("测试音频文件"); + requestLog.setInputLength(6); + requestLog.setVoice("fnlp/MOSS-TTSD-v0.5:claire"); + requestLog.setStatus("SUCCESS"); + requestLog.setAudioName(audioName); + requestLog.setMinioUrl("http://localhost:9000/audio/" + audioName); + requestLog.setCreatorName("测试用户"); + requestLog.setCreatorPhone("13800138000"); + requestLog.setCreateTime(LocalDateTime.now()); + requestLog.setUpdateTime(LocalDateTime.now()); + return requestLog; + } +} + + diff --git a/src/test/java/com/rj/service/TtsRequestLogWithAudioNameTest.java b/src/test/java/com/rj/service/TtsRequestLogWithAudioNameTest.java new file mode 100644 index 0000000..9aa6296 --- /dev/null +++ b/src/test/java/com/rj/service/TtsRequestLogWithAudioNameTest.java @@ -0,0 +1,114 @@ +package com.rj.service; + +import com.rj.dto.SiliconFlowTtsRequest; +import com.rj.dto.SiliconFlowTtsResponse; +import com.rj.entity.TtsRequestLog; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; + +/** + * TTS请求日志音频名称字段测试 + * + * @author rj + * @date 2025-01-02 + */ +@Slf4j +@SpringBootTest +public class TtsRequestLogWithAudioNameTest { + + @Autowired + private ITtsRequestLogService ttsRequestLogService; + + @Autowired + private SiliconFlowTtsService siliconFlowTtsService; + + /** + * 测试TTS请求并验证音频名称字段 + */ + @Test + public void testTtsRequestWithAudioName() { + try { + // 创建TTS请求 + SiliconFlowTtsRequest request = new SiliconFlowTtsRequest(); + request.setInput("这是一个测试音频文件,用于验证音频名称字段功能。"); + request.setModel("fnlp/MOSS-TTSD-v0.5"); + request.setVoice("fnlp/MOSS-TTSD-v0.5:claire"); + request.setCreatorName("测试用户"); + request.setCreatorPhone("13800138000"); + + log.info("开始TTS请求测试..."); + log.info("请求参数: 模型={}, 语音={}, 输入文本={}", + request.getModel(), request.getVoice(), request.getInput()); + + // 执行TTS请求 + SiliconFlowTtsResponse response = siliconFlowTtsService.textToSpeech(request); + + if (response.isSuccess()) { + log.info("TTS请求成功!"); + log.info("响应信息: 格式={}, 采样率={}, 时长={}秒", + response.getFormat(), response.getSampleRate(), response.getDuration()); + log.info("MinIO URL: {}", response.getMinioUrl()); + log.info("临时URL: {}", response.getShortUrl()); + + // 验证音频名称字段是否正确保存 + // 注意:这里需要根据实际的日志ID来查询,实际使用时可能需要调整 + log.info("请检查数据库中的 tts_request_log 表,确认 audio_name 字段是否正确保存"); + + } else { + log.error("TTS请求失败: {}", response.getMessage()); + } + + } catch (Exception e) { + log.error("测试过程中发生异常: {}", e.getMessage(), e); + } + } + + /** + * 测试手动创建TTS请求日志并设置音频名称 + */ + @Test + public void testManualTtsRequestLogWithAudioName() { + try { + // 创建测试日志 + TtsRequestLog requestLog = new TtsRequestLog(); + requestLog.setRequestTime(LocalDateTime.now()); + requestLog.setModel("fnlp/MOSS-TTSD-v0.5"); + requestLog.setInputText("测试音频文件"); + requestLog.setInputLength(6); + requestLog.setVoice("fnlp/MOSS-TTSD-v0.5:claire"); + requestLog.setStatus("SUCCESS"); + requestLog.setAudioName("test_audio_20250102_120000_12345.mp3"); + requestLog.setMinioUrl("http://localhost:9000/audio/test_audio_20250102_120000_12345.mp3"); + requestLog.setCreatorName("测试用户"); + requestLog.setCreatorPhone("13800138000"); + requestLog.setCreateTime(LocalDateTime.now()); + requestLog.setUpdateTime(LocalDateTime.now()); + + // 保存到数据库 + boolean result = ttsRequestLogService.saveTtsRequestLog(requestLog); + + if (result) { + log.info("TTS请求日志保存成功,ID: {}", requestLog.getId()); + log.info("音频名称: {}", requestLog.getAudioName()); + + // 查询验证 + TtsRequestLog savedLog = ttsRequestLogService.getTtsRequestLogById(requestLog.getId()); + if (savedLog != null) { + log.info("查询验证成功,音频名称: {}", savedLog.getAudioName()); + } else { + log.error("查询验证失败,未找到保存的日志"); + } + + } else { + log.error("TTS请求日志保存失败"); + } + + } catch (Exception e) { + log.error("手动测试过程中发生异常: {}", e.getMessage(), e); + } + } +}