feat: 功能

This commit is contained in:
2026-03-05 23:21:00 +08:00
parent 9b132082d2
commit dff90abbb4
11 changed files with 87 additions and 104 deletions

View File

@@ -113,11 +113,21 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
task.setPendingRecordId(pendingRecordId); // 保存预扣记录ID
taskMapper.insert(task);
// 4. ✅ 立即处理预生成音频(保存为临时文件,供后续步骤使用
// 4. ✅ 立即处理预生成音频(优先使用 URL降级使用 base64
Long taskId = task.getId();
if (reqVO.getPreGeneratedAudio() != null && StrUtil.isNotBlank(reqVO.getPreGeneratedAudio().getAudioBase64())) {
// 优先使用前端传递的 audioUrl性能优化避免 base64 编解码)
if (StrUtil.isNotBlank(reqVO.getAudioUrl())) {
TikDigitalHumanTaskDO updateObj = new TikDigitalHumanTaskDO();
updateObj.setId(taskId);
updateObj.setAudioUrl(reqVO.getAudioUrl());
taskMapper.updateById(updateObj);
log.info("[createTask][任务({})使用前端传递的音频URL][audioUrl={}]", taskId, reqVO.getAudioUrl());
}
// 降级:处理 preGeneratedAudio.base64兼容旧版本
else if (reqVO.getPreGeneratedAudio() != null && StrUtil.isNotBlank(reqVO.getPreGeneratedAudio().getAudioBase64())) {
try {
log.info("[createTask][任务({})正在保存预生成音频...]", taskId);
log.info("[createTask][任务({})正在保存预生成音频(base64)...]", taskId);
String audioUrl = saveTempAudioFile(reqVO.getPreGeneratedAudio().getAudioBase64(),
reqVO.getPreGeneratedAudio().getFormat());
// 更新任务记录保存音频URL

View File

@@ -234,10 +234,10 @@ public class LatentsyncPollingService {
TenantUtils.executeIgnore(() -> taskMapper.updateById(updateObj));
// 3. 确认预扣(任务成功,实际扣费)
// 3. 确认预扣(任务成功,实际扣费)- 使用 TenantUtils.executeIgnore 避免 MyBatis Plus 租户过滤
if (task != null && task.getPendingRecordId() != null) {
try {
pointsService.confirmPendingDeduct(task.getPendingRecordId());
TenantUtils.executeIgnore(() -> pointsService.confirmPendingDeduct(task.getPendingRecordId()));
log.info("[completeTask][任务({})成功确认扣费预扣记录ID({})]", taskId, task.getPendingRecordId());
} catch (Exception e) {
log.error("[completeTask][确认扣费失败taskId={}recordId={}]", taskId, task.getPendingRecordId(), e);
@@ -276,10 +276,10 @@ public class LatentsyncPollingService {
TenantUtils.executeIgnore(() -> taskMapper.updateById(updateObj));
// 3. 取消预扣(任务失败,不扣费)
// 3. 取消预扣(任务失败,不扣费)- 使用 TenantUtils.executeIgnore 避免 MyBatis Plus 租户过滤
if (task != null && task.getPendingRecordId() != null) {
try {
pointsService.cancelPendingDeduct(task.getPendingRecordId());
TenantUtils.executeIgnore(() -> pointsService.cancelPendingDeduct(task.getPendingRecordId()));
log.info("[markTaskFailed][任务({})失败取消预扣预扣记录ID({})]", taskId, task.getPendingRecordId());
} catch (Exception e) {
log.error("[markTaskFailed][取消预扣失败taskId={}recordId={}]", taskId, task.getPendingRecordId(), e);
@@ -308,10 +308,10 @@ public class LatentsyncPollingService {
if ("SUCCESS".equals(status)) {
updateObj.setFinishTime(LocalDateTime.now());
// 确认预扣(任务成功)
// 确认预扣(任务成功)- 使用 TenantUtils.executeIgnore 避免 MyBatis Plus 租户过滤
if (task != null && task.getPendingRecordId() != null) {
try {
pointsService.confirmPendingDeduct(task.getPendingRecordId());
TenantUtils.executeIgnore(() -> pointsService.confirmPendingDeduct(task.getPendingRecordId()));
log.info("[updateTaskStatus][任务({})成功,确认扣费]", taskId);
} catch (Exception e) {
log.error("[updateTaskStatus][确认扣费失败taskId={}]", taskId, e);
@@ -322,10 +322,10 @@ public class LatentsyncPollingService {
} else if ("FAILED".equals(status)) {
updateObj.setErrorMessage(errorMessage);
updateObj.setFinishTime(LocalDateTime.now());
// 取消预扣(任务失败)
// 取消预扣(任务失败)- 使用 TenantUtils.executeIgnore 避免 MyBatis Plus 租户过滤
if (task != null && task.getPendingRecordId() != null) {
try {
pointsService.cancelPendingDeduct(task.getPendingRecordId());
TenantUtils.executeIgnore(() -> pointsService.cancelPendingDeduct(task.getPendingRecordId()));
log.info("[updateTaskStatus][任务({})失败,取消预扣]", taskId);
} catch (Exception e) {
log.error("[updateTaskStatus][取消预扣失败taskId={}]", taskId, e);

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.tik.voice.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
@@ -445,21 +446,21 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
log.error("[synthesizeVoice][积分扣减失败: {}]", e.getMessage());
}
// 不暴露OSS链接直接返回Base64编码的音频数据
String audioBase64 = Base64.getEncoder().encodeToString(ttsResult.getAudio());
// 保存到 OSS 临时目录,返回预签名 URL性能优化避免 base64 编码
String audioUrl = saveToTempOss(ttsResult.getAudio(), format);
log.info("[synthesizeVoice][合成成功,配音编号({})voiceId({})format({})audioSize={}]",
voiceConfigId, finalVoiceId, format, ttsResult.getAudio().length);
AppTikVoiceTtsRespVO respVO = new AppTikVoiceTtsRespVO();
respVO.setFileId(null); // 不返回fileId避免暴露
respVO.setAudioBase64(audioBase64); // 返回Base64数据,前端可直接播放
respVO.setAudioUrl(audioUrl); // 返回预签名 URL,前端可直接播放
respVO.setFormat(format);
respVO.setSampleRate(ttsResult.getSampleRate());
respVO.setRequestId(""); // 不返回Request ID避免暴露技术细节
respVO.setVoiceId(finalVoiceId);
saveSynthCache(cacheKey, new SynthCacheEntry(
Base64.getEncoder().encodeToString(ttsResult.getAudio()),
audioUrl, // 缓存 URL 而不是 base64
format,
ttsResult.getSampleRate(),
ttsResult.getRequestId(),
@@ -631,6 +632,24 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
}
}
/**
* 保存音频到临时 OSS 目录(预签名 URL1小时过期
*
* @param audioBytes 音频字节数组
* @param format 音频格式(如 mp3
* @return 预签名 URL
*/
private String saveToTempOss(byte[] audioBytes, String format) {
String fileName = "temp/tts/" + IdUtil.fastSimpleUUID() + "." + format;
String mimeType = "audio/" + format;
// 上传到 OSS返回文件路径
String filePath = fileApi.createFile(audioBytes, fileName, "temp/tts", mimeType);
// 返回预签名 URL1小时过期
return fileApi.presignGetUrl(filePath, 3600);
}
private String buildCacheKey(String prefix,
String voiceId,
String fileUrl,
@@ -714,13 +733,13 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
}
private AppTikVoiceTtsRespVO buildSynthResponseFromCache(AppTikVoiceTtsReqVO reqVO, SynthCacheEntry cache) {
// 直接使用缓存的Base64数据不上传OSS
// 使用缓存的 URL性能优化避免重复上传
String format = defaultFormat(cache.getFormat(), reqVO.getAudioFormat());
String voiceId = StrUtil.blankToDefault(reqVO.getVoiceId(), cache.getVoiceId());
AppTikVoiceTtsRespVO respVO = new AppTikVoiceTtsRespVO();
respVO.setFileId(null); // 不返回fileId避免暴露
respVO.setAudioBase64(cache.getAudioBase64()); // 返回Base64数据
respVO.setAudioUrl(cache.getAudioUrl()); // 返回预签名 URL
respVO.setFormat(format);
respVO.setSampleRate(cache.getSampleRate());
respVO.setRequestId(""); // 不返回Request ID避免暴露技术细节
@@ -753,7 +772,7 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
private static class SynthCacheEntry {
private String audioBase64;
private String audioUrl; // 改为存储 URL
private String format;
private Integer sampleRate;
private String requestId;

View File

@@ -95,10 +95,14 @@ public class AppTikDigitalHumanCreateReqVO {
@JsonProperty("pre_generated_audio")
private PreGeneratedAudioVO preGeneratedAudio;
@Schema(description = "预生成音频URL与 preGeneratedAudio 二选一,优先使用)", example = "https://oss.example.com/temp/tts/xxx.mp3")
@JsonProperty("audio_url")
private String audioUrl;
@Data
@Schema(description = "预生成音频信息")
public static class PreGeneratedAudioVO {
@Schema(description = "音频Base64数据", example = "data:audio/mp3;base64,...")
@Schema(description = "音频Base64数据(降级方案)", example = "data:audio/mp3;base64,...")
private String audioBase64;
@Schema(description = "音频格式", example = "mp3")

View File

@@ -10,11 +10,10 @@ public class AppTikVoiceTtsRespVO {
@Schema(description = "用户文件编号", example = "1024")
private Long fileId;
@Schema(description = "音频Base64数据可直接播放,使用 data:audio/...;base64,... 格式")
@Schema(description = "音频Base64数据降级方案,优先使用 audioUrl")
private String audioBase64;
@Schema(description = "音频播放地址(预签名 URL已废弃,不推荐使用")
@Deprecated
@Schema(description = "音频播放地址(预签名 URL1小时过期")
private String audioUrl;
@Schema(description = "音频格式", example = "mp3")