Merge #142 into master from cc_20260515_audio_ge

音频生成

* cc_20260515_audio_ge: (3 commits squashed)

  - fix:音频生成

  - fix:新增删除音频记录接口

  - fix:删除音频记录接口入参修改

Signed-off-by: 王非凡 <accounts_67eba0c5fee9c49c80c8e2b4@mail.teambition.com>
Merged-by: 正新 <accounts_6964c7bcd2a2c377c5bbd01b@mail.teambition.com>

CR-link: https://codeup.aliyun.com/692ea314dec569489f6f167c/hangzhou/java/custom_zxjp/change/142
This commit is contained in:
王非凡
2026-05-19 10:23:43 +00:00
committed by 正新
parent e6bcfac086
commit 41e6b3ccfe
26 changed files with 1089 additions and 3 deletions

View File

@@ -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> T executeGet(String path, String query, Class<T> 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> T executePost(String path, Object body, Class<T> 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> T parseResponse(String responseJson, Class<T> responseType) throws IOException {
Map<String, Object> responseMap = objectMapper.readValue(responseJson,
new TypeReference<Map<String, Object>>() {});
checkBusinessResponseCode(responseMap);
Object data = responseMap.get("data");
if (data != null) {
return objectMapper.convertValue(data, responseType);
}
return objectMapper.convertValue(responseMap, responseType);
}
/**
* 检查业务响应码
*/
private void checkBusinessResponseCode(Map<String, Object> 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<String, String> 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<String, String> entry : params.entrySet()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
return sb.toString();
}
}

View File

@@ -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<AudioGenerateRecordVO> queryUserAudioRecords(Integer pageNum, Integer pageSize, String userId);
/**
* 删除音频生成记录
* @param id 记录ID
* @param userId 用户ID用于权限校验
*/
Boolean deleteAudioRecord(Long id, String userId);
}

View File

@@ -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<AudioGenerateRecordVO> queryUserAudioRecords(Integer pageNum, Integer pageSize, String userId) {
PageHelper.startPage(pageNum, pageSize);
List<AudioGenerateRecordDO> 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;
}
}