From 41e6b3ccfeaccd53ec098a0d7b6ca600fec1b83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=9D=9E=E5=87=A1?= Date: Tue, 19 May 2026 10:23:43 +0000 Subject: [PATCH] Merge #142 into master from cc_20260515_audio_ge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 音频生成 * cc_20260515_audio_ge: (3 commits squashed) - fix:音频生成 - fix:新增删除音频记录接口 - fix:删除音频记录接口入参修改 Signed-off-by: 王非凡 Merged-by: 正新 CR-link: https://codeup.aliyun.com/692ea314dec569489f6f167c/hangzhou/java/custom_zxjp/change/142 --- .../com/cool/store/enums/ErrorCodeEnum.java | 3 + .../store/dao/AudioGenerateRecordDAO.java | 56 ++++ .../audio/AudioGenerateRecordMapper.java | 20 ++ .../audio/AudioGenerateRecordMapper.xml | 23 ++ .../store/dto/audio/AudioExtraInfoDTO.java | 44 +++ .../dto/audio/AudioGenerateRecordVO.java | 33 +++ .../dto/audio/DeleteAudioRecordReqDTO.java | 16 + .../store/dto/audio/FavoriteVoiceDTO.java | 34 +++ .../store/dto/audio/GenerateAudioReqDTO.java | 26 ++ .../store/dto/audio/OptimizeCopyReqDTO.java | 16 + .../dto/audio/OptimizeCopyRequestDTO.java | 26 ++ .../dto/audio/OptimizeCopyResponseDTO.java | 29 ++ .../store/dto/audio/SpeechRequestDTO.java | 19 ++ .../store/dto/audio/SpeechResponseDTO.java | 29 ++ .../cool/store/dto/audio/VoiceConfigDTO.java | 34 +++ .../com/cool/store/dto/audio/VoiceDTO.java | 31 ++ .../cool/store/dto/audio/VoiceListDTO.java | 31 ++ .../entity/audio/AudioGenerateRecordDO.java | 54 ++++ .../store/service/audio/AudioApiService.java | 276 ++++++++++++++++++ .../audio/AudioGenerateRecordService.java | 46 +++ .../audio/AudioGenerateRecordServiceImpl.java | 138 +++++++++ .../controller/webc/MiniAudioController.java | 90 ++++++ .../main/resources/application-ab.properties | 5 +- .../resources/application-local.properties | 5 +- .../resources/application-online.properties | 5 +- .../resources/application-test.properties | 3 + 26 files changed, 1089 insertions(+), 3 deletions(-) create mode 100644 coolstore-partner-dao/src/main/java/com/cool/store/dao/AudioGenerateRecordDAO.java create mode 100644 coolstore-partner-dao/src/main/java/com/cool/store/mapper/audio/AudioGenerateRecordMapper.java create mode 100644 coolstore-partner-dao/src/main/resources/mapper/audio/AudioGenerateRecordMapper.xml create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/AudioExtraInfoDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/AudioGenerateRecordVO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/DeleteAudioRecordReqDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/FavoriteVoiceDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/GenerateAudioReqDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyReqDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyRequestDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyResponseDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/SpeechRequestDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/SpeechResponseDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceConfigDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceListDTO.java create mode 100644 coolstore-partner-model/src/main/java/com/cool/store/entity/audio/AudioGenerateRecordDO.java create mode 100644 coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioApiService.java create mode 100644 coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioGenerateRecordService.java create mode 100644 coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioGenerateRecordServiceImpl.java create mode 100644 coolstore-partner-web/src/main/java/com/cool/store/controller/webc/MiniAudioController.java diff --git a/coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java b/coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java index 34ab01c82..913d51b57 100644 --- a/coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java +++ b/coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java @@ -502,6 +502,9 @@ public enum ErrorCodeEnum { BONUS_DISTRIBUTE_RATIO_OVER_100(1850003, "分配规则员工比例之和大于100", null), BONUS_PRODUCT_CONFIG_DUPLICATE(1850004, "奖金规则配置存在重复菜品", null), BONUS_RULE_CONFIG_ERROR(1850005, "规则配置异常", null), + + NO_PERMISSION_TO_DELETE_THE_RECORD(1850006, "无权限删除该记录", null), + THE_AUDIO_GENERATION_RECORD_DOES_NOT_EXIST(1850007, "音频生成记录不存在", null), ; diff --git a/coolstore-partner-dao/src/main/java/com/cool/store/dao/AudioGenerateRecordDAO.java b/coolstore-partner-dao/src/main/java/com/cool/store/dao/AudioGenerateRecordDAO.java new file mode 100644 index 000000000..a76556594 --- /dev/null +++ b/coolstore-partner-dao/src/main/java/com/cool/store/dao/AudioGenerateRecordDAO.java @@ -0,0 +1,56 @@ +package com.cool.store.dao; + +import com.cool.store.entity.audio.AudioGenerateRecordDO; +import com.cool.store.mapper.audio.AudioGenerateRecordMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 音频生成记录DAO + */ +@Slf4j +@Repository +public class AudioGenerateRecordDAO { + + @Resource + private AudioGenerateRecordMapper audioGenerateRecordMapper; + + /** + * 插入音频生成记录 + * @param record 音频生成记录 + * @return 影响行数 + */ + public int insert(AudioGenerateRecordDO record) { + return audioGenerateRecordMapper.insertSelective(record); + } + + /** + * 根据创建用户ID查询记录 + * @param createUserId 创建用户ID + * @return 记录列表 + */ + public List queryByCreateUserId(String createUserId) { + return audioGenerateRecordMapper.queryByCreateUserId(createUserId); + } + + /** + * 根据ID查询记录 + * @param id 记录ID + * @return 音频生成记录 + */ + public AudioGenerateRecordDO selectById(Long id) { + return audioGenerateRecordMapper.selectByPrimaryKey(id); + } + + /** + * 根据ID删除记录 + * @param id 记录ID + * @return 影响行数 + */ + public int deleteById(Long id) { + return audioGenerateRecordMapper.deleteByPrimaryKey(id); + } +} \ No newline at end of file diff --git a/coolstore-partner-dao/src/main/java/com/cool/store/mapper/audio/AudioGenerateRecordMapper.java b/coolstore-partner-dao/src/main/java/com/cool/store/mapper/audio/AudioGenerateRecordMapper.java new file mode 100644 index 000000000..3ad0580b6 --- /dev/null +++ b/coolstore-partner-dao/src/main/java/com/cool/store/mapper/audio/AudioGenerateRecordMapper.java @@ -0,0 +1,20 @@ +package com.cool.store.mapper.audio; + +import com.cool.store.entity.audio.AudioGenerateRecordDO; +import org.apache.ibatis.annotations.Param; +import tk.mybatis.mapper.common.Mapper; + +import java.util.List; + +/** + * 音频生成记录 + */ +public interface AudioGenerateRecordMapper extends Mapper { + + /** + * 根据创建用户ID查询记录,按创建时间倒序 + * @param createUserId 创建用户ID + * @return 记录列表 + */ + List queryByCreateUserId(@Param("createUserId") String createUserId); +} \ No newline at end of file diff --git a/coolstore-partner-dao/src/main/resources/mapper/audio/AudioGenerateRecordMapper.xml b/coolstore-partner-dao/src/main/resources/mapper/audio/AudioGenerateRecordMapper.xml new file mode 100644 index 000000000..f363e6348 --- /dev/null +++ b/coolstore-partner-dao/src/main/resources/mapper/audio/AudioGenerateRecordMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/AudioExtraInfoDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/AudioExtraInfoDTO.java new file mode 100644 index 000000000..ec174f26e --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/AudioExtraInfoDTO.java @@ -0,0 +1,44 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +/** + * 音频附加信息DTO + */ +@Data +public class AudioExtraInfoDTO { + /** + * 音频时长(毫秒) + */ + private Long audioLength; + + /** + * 音频采样率 + */ + private Long audioSampleRate; + + /** + * 音频大小 + */ + private Long audioSize; + + /** + * 比特率 + */ + private Long bitrate; + + /** + * 使用字符数 + */ + private Long usageCharacters; + + /** + * 音频格式 + */ + private String audioFormat; + + /** + * 音频声道 + */ + private Integer audioChannel; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/AudioGenerateRecordVO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/AudioGenerateRecordVO.java new file mode 100644 index 000000000..24a075994 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/AudioGenerateRecordVO.java @@ -0,0 +1,33 @@ +package com.cool.store.dto.audio; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 音频生成记录VO + */ +@Data +@ApiModel("音频生成记录") +public class AudioGenerateRecordVO { + + @ApiModelProperty("记录ID") + private Long id; + + @ApiModelProperty("音频URL") + private String url; + + @ApiModelProperty("音色ID") + private String voiceId; + + @ApiModelProperty("音色名称") + private String voiceName; + + @ApiModelProperty("文案内容") + private String content; + + @ApiModelProperty("创建时间") + private Date createTime; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/DeleteAudioRecordReqDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/DeleteAudioRecordReqDTO.java new file mode 100644 index 000000000..0d57d6737 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/DeleteAudioRecordReqDTO.java @@ -0,0 +1,16 @@ +package com.cool.store.dto.audio; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 删除音频生成记录请求DTO + */ +@Data +public class DeleteAudioRecordReqDTO { + @NotNull(message = "记录ID不能为空") + @ApiModelProperty(value = "音频生成记录ID", required = true) + private Long id; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/FavoriteVoiceDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/FavoriteVoiceDTO.java new file mode 100644 index 000000000..7b5819627 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/FavoriteVoiceDTO.java @@ -0,0 +1,34 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +/** + * 音色记忆DTO + */ +@Data +public class FavoriteVoiceDTO { + /** + * 音色ID + */ + private String voiceId; + + /** + * 音色名称 + */ + private String voiceName; + + /** + * 参考文案 + */ + private String text; + + /** + * 模式标记 + */ + private String mode; + + /** + * 模式展示名 + */ + private String modeLabel; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/GenerateAudioReqDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/GenerateAudioReqDTO.java new file mode 100644 index 000000000..7a33c1f62 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/GenerateAudioReqDTO.java @@ -0,0 +1,26 @@ +package com.cool.store.dto.audio; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * 音频生成请求DTO + */ +@Data +public class GenerateAudioReqDTO { + @NotBlank(message = "文案不能为空") + @Size(max = 500, message = "文案长度不能超过500") + @ApiModelProperty(value = "文案", required = true) + private String content; + + @NotBlank(message = "音色不能为空") + @ApiModelProperty(value = "音色ID", required = true) + private String voiceId; + + @NotBlank(message = "音色名称不能为空") + @ApiModelProperty("音色名称") + private String voiceName; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyReqDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyReqDTO.java new file mode 100644 index 000000000..65f19f7ec --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyReqDTO.java @@ -0,0 +1,16 @@ +package com.cool.store.dto.audio; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * AI文案优化请求DTO + */ +@Data +public class OptimizeCopyReqDTO { + @NotBlank(message = "文案不能为空") + @ApiModelProperty(value = "原始文案", required = true) + private String text; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyRequestDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyRequestDTO.java new file mode 100644 index 000000000..5c704851b --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyRequestDTO.java @@ -0,0 +1,26 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +import java.util.List; + +/** + * 文案优化请求DTO + */ +@Data +public class OptimizeCopyRequestDTO { + /** + * 原始文案 + */ + private String text; + + /** + * 当前音色ID + */ + private String voiceId; + + /** + * 音色记忆列表 + */ + private List favorites; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyResponseDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyResponseDTO.java new file mode 100644 index 000000000..95711fd18 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/OptimizeCopyResponseDTO.java @@ -0,0 +1,29 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +/** + * 文案优化响应DTO + */ +@Data +public class OptimizeCopyResponseDTO { + /** + * 优化后的文案 + */ + private String optimizedText; + + /** + * 推荐音色ID + */ + private String recommendedVoiceId; + + /** + * 朗读指令 + */ + private String ttsInstructions; + + /** + * 推荐原因 + */ + private String reason; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/SpeechRequestDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/SpeechRequestDTO.java new file mode 100644 index 000000000..35d143af2 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/SpeechRequestDTO.java @@ -0,0 +1,19 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +/** + * 文本生成语音请求DTO + */ +@Data +public class SpeechRequestDTO { + /** + * 待合成文本 + */ + private String text; + + /** + * 音色配置 + */ + private VoiceConfigDTO voice; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/SpeechResponseDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/SpeechResponseDTO.java new file mode 100644 index 000000000..6c56e7a29 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/SpeechResponseDTO.java @@ -0,0 +1,29 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +/** + * 文本生成语音响应DTO + */ +@Data +public class SpeechResponseDTO { + /** + * 音频Base64 + */ + private String audioBase64; + + /** + * 原始hex音频 + */ + private String audioHex; + + /** + * 请求追踪ID + */ + private String traceId; + + /** + * 音频附加信息 + */ + private AudioExtraInfoDTO extraInfo; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceConfigDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceConfigDTO.java new file mode 100644 index 000000000..cb6acb19e --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceConfigDTO.java @@ -0,0 +1,34 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +/** + * 音色配置DTO + */ +@Data +public class VoiceConfigDTO { + /** + * 音色ID + */ + private String voiceId; + + /** + * 语速 + */ + private Double speed; + + /** + * 音量 + */ + private Double vol; + + /** + * 音高 + */ + private Double pitch; + + /** + * 情绪 + */ + private String emotion; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceDTO.java new file mode 100644 index 000000000..aa9cc3266 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceDTO.java @@ -0,0 +1,31 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +import java.util.List; + +/** + * 音色对象DTO + */ +@Data +public class VoiceDTO { + /** + * 音色ID + */ + private String voiceId; + + /** + * 音色名称 + */ + private String voiceName; + + /** + * 音色描述 + */ + private List description; + + /** + * 创建时间 + */ + private String createdTime; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceListDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceListDTO.java new file mode 100644 index 000000000..215039deb --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/audio/VoiceListDTO.java @@ -0,0 +1,31 @@ +package com.cool.store.dto.audio; + +import lombok.Data; + +import java.util.List; + +/** + * 音色列表响应DTO + */ +@Data +public class VoiceListDTO { + /** + * 系统音色列表 + */ + private List systemVoice; + + /** + * 快速复刻音色列表 + */ + private List voiceCloning; + + /** + * 文生音色列表 + */ + private List voiceGeneration; + + /** + * 自定义音色列表 + */ + private List customVoice; +} \ No newline at end of file diff --git a/coolstore-partner-model/src/main/java/com/cool/store/entity/audio/AudioGenerateRecordDO.java b/coolstore-partner-model/src/main/java/com/cool/store/entity/audio/AudioGenerateRecordDO.java new file mode 100644 index 000000000..3e1421509 --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/entity/audio/AudioGenerateRecordDO.java @@ -0,0 +1,54 @@ +package com.cool.store.entity.audio; + +import lombok.Data; + +import java.util.Date; +import javax.persistence.*; + +/** + * 音频生成记录 + */ +@Table(name = "zxjp_audio_generate_record") +@Data +public class AudioGenerateRecordDO { + /** + * id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 音频路径 + */ + private String url; + + /** + * 音色id + */ + @Column(name = "voice_id") + private String voiceId; + + /** + * 音色名称 + */ + @Column(name = "voice_name") + private String voiceName; + + /** + * 创建时间 + */ + @Column(name = "create_time") + private Date createTime; + + /** + * 创建人id + */ + @Column(name = "create_user_id") + private String createUserId; + + /** + * 文案 + */ + private String content; +} \ No newline at end of file diff --git a/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioApiService.java b/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioApiService.java new file mode 100644 index 000000000..20b1bd64f --- /dev/null +++ b/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioApiService.java @@ -0,0 +1,276 @@ +package com.cool.store.service.audio; + +import com.cool.store.dto.audio.*; +import com.cool.store.enums.ErrorCodeEnum; +import com.cool.store.exception.ServiceException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Map; +import java.util.TreeMap; + +/** + * 音频API调用服务 + */ +@Service +@Slf4j +public class AudioApiService { + + @Value("${audio.api.url}") + private String audioApiUrl; + + @Value("${audio.api.secret}") + private String audioApiSecret; + + @Resource + private OkHttpClient okHttpClient; + + @Resource + private ObjectMapper objectMapper; + + /** + * 查询音色列表 + * @param voiceType 音色类型 + * @return 音色列表 + */ + public VoiceListDTO getVoiceList(String voiceType) { + String path = "/api/audio/voices"; + String query = voiceType != null ? "voiceType=" + voiceType : ""; + return executeGet(path, query, VoiceListDTO.class); + } + + /** + * 文本生成语音 + * @param request 请求参数 + * @return 音频响应 + */ + public SpeechResponseDTO generateSpeech(SpeechRequestDTO request) { + return executePost("/api/audio/speech", request, SpeechResponseDTO.class); + } + + /** + * 智能优化口播文案 + * @param request 请求参数 + * @return 优化后的文案 + */ + public OptimizeCopyResponseDTO optimizeCopy(OptimizeCopyRequestDTO request) { + return executePost("/api/voice-agent/optimize-copy", request, OptimizeCopyResponseDTO.class); + } + + /** + * 发送带签名的GET请求 + */ + private T executeGet(String path, String query, Class responseType) { + try { + String url = audioApiUrl + path + (query.isEmpty() ? "" : "?" + query); + long timestamp = System.currentTimeMillis(); + String signature = generateSignature("GET", path, query, "", timestamp); + + Request request = new Request.Builder() + .url(url) + .get() + .addHeader("X-Signature-Version", "v1") + .addHeader("X-Timestamp", String.valueOf(timestamp)) + .addHeader("X-Signature", signature) + .build(); + + log.info("发送GET请求: {}", url); + Response response = okHttpClient.newCall(request).execute(); + + if (!response.isSuccessful()) { + log.error("GET请求失败: {}", response.code()); + throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, response.code() + " " + response.message()); + } + + String responseBody = response.body().string(); + log.info("收到响应: {}", responseBody); + + return parseResponse(responseBody, responseType); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + log.error("发送GET请求失败: {}", path, e); + throw new RuntimeException("接口调用异常: " + e.getMessage(), e); + } + } + + /** + * 发送带签名的POST请求 + */ + private T executePost(String path, Object body, Class responseType) { + try { + String url = audioApiUrl + path; + String bodyJson = objectMapper.writeValueAsString(body); + long timestamp = System.currentTimeMillis(); + String signature = generateSignature("POST", path, "", bodyJson, timestamp); + + RequestBody requestBody = RequestBody.create( + MediaType.parse("application/json; charset=utf-8"), bodyJson); + + Request httpRequest = new Request.Builder() + .url(url) + .post(requestBody) + .addHeader("Content-Type", "application/json") + .addHeader("X-Signature-Version", "v1") + .addHeader("X-Timestamp", String.valueOf(timestamp)) + .addHeader("X-Signature", signature) + .build(); + + log.info("发送POST请求: {}, 数据: {}", url, bodyJson); + Response response = okHttpClient.newCall(httpRequest).execute(); + + if (!response.isSuccessful()) { + log.error("POST请求失败: {}", response.code()); + throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, response.code() + " " + response.message()); + } + + String responseBody = response.body().string(); + log.info("收到响应长度: {}", responseBody.length()); + + return parseResponse(responseBody, responseType); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + log.error("发送POST请求失败: {}", path, e); + throw new RuntimeException("接口调用异常: " + e.getMessage(), e); + } + } + + /** + * 解析响应 + */ + private T parseResponse(String responseJson, Class responseType) throws IOException { + Map responseMap = objectMapper.readValue(responseJson, + new TypeReference>() {}); + + checkBusinessResponseCode(responseMap); + + Object data = responseMap.get("data"); + if (data != null) { + return objectMapper.convertValue(data, responseType); + } + return objectMapper.convertValue(responseMap, responseType); + } + + /** + * 检查业务响应码 + */ + private void checkBusinessResponseCode(Map responseMap) { + Object codeObj = responseMap.get("code"); + if (codeObj != null) { + int code = 0; + if (codeObj instanceof Number) { + code = ((Number) codeObj).intValue(); + } else { + code = Integer.parseInt(codeObj.toString()); + } + + if (code != 200 && code != 0) { + String msg = (String) responseMap.get("msg"); + throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, + "code: " + code + ", msg: " + msg); + } + } + } + + /** + * 生成HMAC-SHA256签名 + */ + private String generateSignature(String method, String path, String query, String body, long timestamp) { + try { + String bodySha256 = sha256(body); + String canonicalQuery = canonicalizeQuery(query); + + String signingString = method + "\n" + + path + "\n" + + canonicalQuery + "\n" + + bodySha256 + "\n" + + timestamp; + + log.debug("签名原文: {}", signingString); + + return hmacSha256(signingString, audioApiSecret); + } catch (Exception e) { + log.error("生成签名失败", e); + throw new RuntimeException("生成签名失败", e); + } + } + + /** + * 计算SHA256 + */ + private String sha256(String input) { + try { + if (input == null || input.isEmpty()) { + input = ""; + } + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hash); + } catch (Exception e) { + throw new RuntimeException("SHA256计算失败", e); + } + } + + /** + * HMAC-SHA256签名 + */ + private String hmacSha256(String data, String key) { + try { + javax.crypto.spec.SecretKeySpec secretKey = new javax.crypto.spec.SecretKeySpec( + key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256"); + mac.init(secretKey); + byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hash); + } catch (Exception e) { + throw new RuntimeException("HMAC-SHA256签名失败", e); + } + } + + /** + * 字节数组转十六进制字符串 + */ + private String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + /** + * 规范化Query参数 + */ + private String canonicalizeQuery(String query) { + if (query == null || query.isEmpty()) { + return ""; + } + TreeMap params = new TreeMap<>(); + String[] pairs = query.split("&"); + for (String pair : pairs) { + String[] kv = pair.split("=", 2); + if (kv.length == 2) { + params.put(kv[0], kv[1]); + } else if (kv.length == 1) { + params.put(kv[0], ""); + } + } + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : params.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioGenerateRecordService.java b/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioGenerateRecordService.java new file mode 100644 index 000000000..e27c81ec0 --- /dev/null +++ b/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioGenerateRecordService.java @@ -0,0 +1,46 @@ +package com.cool.store.service.audio; + +import com.cool.store.dto.audio.*; +import com.github.pagehelper.PageInfo; + +/** + * 音频生成记录服务接口 + */ +public interface AudioGenerateRecordService { + + /** + * AI文案优化 + * @param text 原始文案 + * @return 优化后的文案 + */ + String optimizeCopy(String text); + + /** + * 查询音色列表 + * @param voiceType 音色类型 + * @return 音色列表 + */ + VoiceListDTO getVoiceList(String voiceType); + + /** + * 根据文案生成音频,上传OSS并入库 + * @param request 音频生成请求DTO + * @return 音频生成记录 + */ + AudioGenerateRecordVO generateAudio(GenerateAudioReqDTO request, String userId); + + /** + * 分页查询当前用户的音频生成记录 + * @param pageNum 页码 + * @param pageSize 每页大小 + * @return 分页结果 + */ + PageInfo queryUserAudioRecords(Integer pageNum, Integer pageSize, String userId); + + /** + * 删除音频生成记录 + * @param id 记录ID + * @param userId 用户ID(用于权限校验) + */ + Boolean deleteAudioRecord(Long id, String userId); +} \ No newline at end of file diff --git a/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioGenerateRecordServiceImpl.java b/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioGenerateRecordServiceImpl.java new file mode 100644 index 000000000..c91dcf0d7 --- /dev/null +++ b/coolstore-partner-service/src/main/java/com/cool/store/service/audio/AudioGenerateRecordServiceImpl.java @@ -0,0 +1,138 @@ +package com.cool.store.service.audio; + +import com.cool.store.dao.AudioGenerateRecordDAO; +import com.cool.store.dto.audio.*; +import com.cool.store.entity.audio.AudioGenerateRecordDO; +import com.cool.store.enums.ErrorCodeEnum; +import com.cool.store.exception.ServiceException; +import com.cool.store.oss.OssClientService; +import com.cool.store.utils.BeanUtil; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.ByteArrayInputStream; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +/** + * 音频生成记录服务实现 + */ +@Service +@Slf4j +public class AudioGenerateRecordServiceImpl implements AudioGenerateRecordService { + + @Resource + private AudioApiService audioApiService; + + @Resource + private OssClientService ossClientService; + + @Resource + private AudioGenerateRecordDAO audioGenerateRecordDAO; + + @Override + public String optimizeCopy(String text) { + OptimizeCopyRequestDTO request = new OptimizeCopyRequestDTO(); + request.setText(text); + + OptimizeCopyResponseDTO response = audioApiService.optimizeCopy(request); + return response.getOptimizedText(); + } + + @Override + public VoiceListDTO getVoiceList(String voiceType) { + return audioApiService.getVoiceList(voiceType); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public AudioGenerateRecordVO generateAudio(GenerateAudioReqDTO request, String userId) { + // 1. 调用音频生成接口 + SpeechRequestDTO speechRequest = new SpeechRequestDTO(); + speechRequest.setText(request.getContent()); + + VoiceConfigDTO voiceConfig = new VoiceConfigDTO(); + voiceConfig.setVoiceId(request.getVoiceId()); + speechRequest.setVoice(voiceConfig); + + SpeechResponseDTO response = audioApiService.generateSpeech(speechRequest); + + // 2. 将Base64音频解码并上传到OSS + String audioUrl = uploadAudioToOss(response.getAudioBase64(), response.getTraceId()); + + // 3. 入库音频生成记录 + AudioGenerateRecordDO record = saveAudioRecord(request.getContent(), request.getVoiceId(), request.getVoiceName(), audioUrl, userId); + + // 4. 转换为VO返回 + return BeanUtil.toBean(record, AudioGenerateRecordVO.class); + } + + /** + * 上传音频到OSS + */ + private String uploadAudioToOss(String audioBase64, String traceId) { + try { + byte[] audioBytes = Base64.getDecoder().decode(audioBase64); + + String fileName = "audio/" + UUID.randomUUID().toString() + ".mp3"; + ByteArrayInputStream inputStream = new ByteArrayInputStream(audioBytes); + + String url = ossClientService.putObject(fileName, inputStream, (long) audioBytes.length, "audio/mpeg"); + log.info("音频上传OSS成功, url: {}", url); + + return url; + } catch (Exception e) { + log.error("音频上传OSS失败", e); + throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, "音频上传OSS失败: " + e.getMessage()); + } + } + + /** + * 保存音频生成记录 + */ + private AudioGenerateRecordDO saveAudioRecord(String content, String voiceId, String voiceName, String audioUrl, String userId) { + AudioGenerateRecordDO record = new AudioGenerateRecordDO(); + record.setContent(content); + record.setVoiceId(voiceId); + record.setVoiceName(voiceName); + record.setUrl(audioUrl); + record.setCreateTime(new Date()); + record.setCreateUserId(userId); + + audioGenerateRecordDAO.insert(record); + log.info("音频生成记录入库成功, id: {}", record.getId()); + return record; + } + + @Override + public PageInfo queryUserAudioRecords(Integer pageNum, Integer pageSize, String userId) { + PageHelper.startPage(pageNum, pageSize); + List records = audioGenerateRecordDAO.queryByCreateUserId(userId); + + return BeanUtil.toPage(new PageInfo<>(records), AudioGenerateRecordVO.class); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteAudioRecord(Long id, String userId) { + // 1. 查询记录是否存在 + AudioGenerateRecordDO record = audioGenerateRecordDAO.selectById(id); + if (record == null) { + throw new ServiceException(ErrorCodeEnum.THE_AUDIO_GENERATION_RECORD_DOES_NOT_EXIST); + } + + // 2. 校验用户权限(只能删除自己创建的记录) + if (!record.getCreateUserId().equals(userId)) { + throw new ServiceException(ErrorCodeEnum.NO_PERMISSION_TO_DELETE_THE_RECORD); + } + + // 3. 删除记录 + return audioGenerateRecordDAO.deleteById(id) > 0; + } +} \ No newline at end of file diff --git a/coolstore-partner-web/src/main/java/com/cool/store/controller/webc/MiniAudioController.java b/coolstore-partner-web/src/main/java/com/cool/store/controller/webc/MiniAudioController.java new file mode 100644 index 000000000..bc8365e4e --- /dev/null +++ b/coolstore-partner-web/src/main/java/com/cool/store/controller/webc/MiniAudioController.java @@ -0,0 +1,90 @@ +package com.cool.store.controller.webc; + +import com.cool.store.common.PageBasicInfo; +import com.cool.store.context.PartnerUserHolder; +import com.cool.store.dto.audio.*; +import com.cool.store.response.ResponseResult; +import com.cool.store.service.audio.AudioGenerateRecordService; +import com.github.pagehelper.PageInfo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * 小程序音频控制器 + */ +@RestController +@RequestMapping("/mini/audio") +@Api(tags = "小程序音频") +@Slf4j +public class MiniAudioController { + + @Resource + private AudioGenerateRecordService audioGenerateRecordService; + + /** + * AI文案优化接口 + * @param request 请求参数 + * @return 优化后的文案 + */ + @PostMapping("/optimizeCopy") + @ApiOperation("AI文案优化") + public ResponseResult optimizeCopy(@Valid @RequestBody OptimizeCopyReqDTO request) { + String optimizedText = audioGenerateRecordService.optimizeCopy(request.getText()); + return ResponseResult.success(optimizedText); + } + + /** + * 音色列表查询接口 + * @param voiceType 音色类型 + * @return 音色列表 + */ + @GetMapping("/voiceList") + @ApiOperation("查询音色列表") + @ApiImplicitParam(name = "voiceType", value = "音色类型,system-系统音色 voice_cloning-快速复刻音色 voice_generation-文生音色 custom-本系统录入音色 all-返回全部分类") + public ResponseResult getVoiceList(@RequestParam(required = false) String voiceType) { + VoiceListDTO voiceList = audioGenerateRecordService.getVoiceList(voiceType); + return ResponseResult.success(voiceList); + } + + /** + * 根据文案生成音频接口 + * @param request 请求参数 + * @return 音频生成记录 + */ + @PostMapping("/generate") + @ApiOperation("生成音频") + public ResponseResult generateAudio(@Valid @RequestBody GenerateAudioReqDTO request) { + AudioGenerateRecordVO record = audioGenerateRecordService.generateAudio(request, PartnerUserHolder.getUser().getPartnerId()); + return ResponseResult.success(record); + } + + /** + * 分页查询当前用户的音频生成记录 + * @param request 请求参数 + * @return 分页结果 + */ + @PostMapping("/recordList") + @ApiOperation("查询音频生成记录") + public ResponseResult> queryAudioRecords(@Valid @RequestBody PageBasicInfo request) { + PageInfo page = audioGenerateRecordService.queryUserAudioRecords( + request.getPageNum(), request.getPageSize(), PartnerUserHolder.getUser().getPartnerId()); + return ResponseResult.success(page); + } + + /** + * 删除音频生成记录 + * @param request 请求参数 + * @return 操作结果 + */ + @PostMapping("/delete") + @ApiOperation("删除音频生成记录") + public ResponseResult deleteAudioRecord(@Valid @RequestBody DeleteAudioRecordReqDTO request) { + return ResponseResult.success(audioGenerateRecordService.deleteAudioRecord(request.getId(), PartnerUserHolder.getUser().getPartnerId())); + } +} \ No newline at end of file diff --git a/coolstore-partner-web/src/main/resources/application-ab.properties b/coolstore-partner-web/src/main/resources/application-ab.properties index 3bc939ec3..eb89dd2ee 100644 --- a/coolstore-partner-web/src/main/resources/application-ab.properties +++ b/coolstore-partner-web/src/main/resources/application-ab.properties @@ -187,4 +187,7 @@ closeup.platform.secret=6ezC98lNx5b1IQt store.open.url=https://zhengxin.zhidiansoft.com:5943 store.open.appId=289704779317445 store.open.appKey=IGSAEQoakR2HEaYx -store.open.secret=aPsA99K1obFeFm3m \ No newline at end of file +store.open.secret=aPsA99K1obFeFm3m + +audio.api.url=https://zx-agent.zhidiansoft.com:6443 +audio.api.secret=12b538d018c396f0a50cb3560d9ba103578f378c7807660b7fc42843382ae03d \ No newline at end of file diff --git a/coolstore-partner-web/src/main/resources/application-local.properties b/coolstore-partner-web/src/main/resources/application-local.properties index 9489f03a0..190dcba34 100644 --- a/coolstore-partner-web/src/main/resources/application-local.properties +++ b/coolstore-partner-web/src/main/resources/application-local.properties @@ -185,4 +185,7 @@ closeup.platform.secret=6ezC98lNx5b1IQt store.open.url=https://zhengxin.zhidiansoft.com:5943 store.open.appId=289704779317445 store.open.appKey=IGSAEQoakR2HEaYx -store.open.secret=aPsA99K1obFeFm3m \ No newline at end of file +store.open.secret=aPsA99K1obFeFm3m + +audio.api.url=https://zx-agent.zhidiansoft.com:6443 +audio.api.secret=12b538d018c396f0a50cb3560d9ba103578f378c7807660b7fc42843382ae03d \ No newline at end of file diff --git a/coolstore-partner-web/src/main/resources/application-online.properties b/coolstore-partner-web/src/main/resources/application-online.properties index 25b7c403b..473641479 100644 --- a/coolstore-partner-web/src/main/resources/application-online.properties +++ b/coolstore-partner-web/src/main/resources/application-online.properties @@ -189,4 +189,7 @@ liePin.secretKey=dns6x4f1p14a36u4t22xvteppmz07ir2 liePin.aesSecretKey=_nkULTpkBHHZeWgQ liePin.baseUrl=https://open-xhopen-qa53.qa.xunhou.cn liePin.tenantId=12833 -liePin.mobile=13345565081 \ No newline at end of file +liePin.mobile=13345565081 + +audio.api.url=https://zx-agent.zhidiansoft.com:6443 +audio.api.secret=12b538d018c396f0a50cb3560d9ba103578f378c7807660b7fc42843382ae03d \ No newline at end of file diff --git a/coolstore-partner-web/src/main/resources/application-test.properties b/coolstore-partner-web/src/main/resources/application-test.properties index f8f9a76fa..9c41d417f 100644 --- a/coolstore-partner-web/src/main/resources/application-test.properties +++ b/coolstore-partner-web/src/main/resources/application-test.properties @@ -188,3 +188,6 @@ store.open.url=https://zhengxin.zhidiansoft.com:5943 store.open.appId=289704779317445 store.open.appKey=IGSAEQoakR2HEaYx store.open.secret=aPsA99K1obFeFm3m + +audio.api.url=https://zx-agent.zhidiansoft.com:6443 +audio.api.secret=12b538d018c396f0a50cb3560d9ba103578f378c7807660b7fc42843382ae03d