diff --git a/frontend/app/web-gold/src/components/VoiceSelector.vue b/frontend/app/web-gold/src/components/VoiceSelector.vue
index 0fe5ce25be..4cd9f64ee9 100644
--- a/frontend/app/web-gold/src/components/VoiceSelector.vue
+++ b/frontend/app/web-gold/src/components/VoiceSelector.vue
@@ -30,11 +30,11 @@
-
+
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
-import { useRouter } from 'vue-router'
import { Empty } from 'ant-design-vue'
+import { SoundOutlined } from '@ant-design/icons-vue'
import { useVoiceCopyStore } from '@/stores/voiceCopy'
import { useTTS, TTS_PROVIDERS } from '@/composables/useTTS'
import APlayer from 'aplayer'
@@ -64,13 +64,8 @@ const props = defineProps({
}
})
-const router = useRouter()
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
-const goToVoiceManage = () => {
- router.push('/digital-human/voice-copy')
-}
-
const voiceStore = useVoiceCopyStore()
const emit = defineEmits(['select'])
@@ -205,14 +200,12 @@ const initPlayer = (url) => {
audioUrl.value = url
nextTick(() => {
- if (!playerContainer.value) return
-
player = new APlayer({
container: playerContainer.value,
autoplay: true,
theme: '#3b82f6',
- preload: 'auto',
volume: 0.7,
+ loop: 'none',
audio: [{
name: currentVoiceName.value || '语音合成',
artist: '合成',
@@ -221,7 +214,9 @@ const initPlayer = (url) => {
}]
})
- // 播放结束后保留播放器,让用户可以再次播放
+ player.on('ended', () => {
+ player.seek(0)
+ })
player.on('error', (e) => {
console.error('APlayer 播放错误:', e)
@@ -235,9 +230,10 @@ const initPlayer = (url) => {
const downloadAudio = () => {
if (!audioUrl.value) return
+ const filename = `${currentVoiceName.value || '语音合成'}.mp3`
const link = document.createElement('a')
link.href = audioUrl.value
- link.download = `${currentVoiceName.value || '语音合成'}.mp3`
+ link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
@@ -255,10 +251,11 @@ const destroyPlayer = () => {
}
player = null
}
- if (audioUrl.value) {
+ // 只对 blob URL 调用 revokeObjectURL
+ if (audioUrl.value && audioUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(audioUrl.value)
- audioUrl.value = ''
}
+ audioUrl.value = ''
}
defineExpose({})
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java
index 1d075268a6..6eafbca82c 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java
@@ -29,16 +29,15 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
+import reactor.core.scheduler.Schedulers;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
-import java.util.concurrent.TimeUnit;
-import reactor.core.scheduler.Schedulers;
-
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Slf4j
@@ -50,6 +49,12 @@ public class TikHupServiceImpl implements TikHupService {
private static final String PLATFORM_TIKHUB = "tikhub";
private static final String MODEL_CODE_CRAWLER = "crawler";
+ // 日志前缀
+ private static final String LOG_PREFIX_TIKHUP = "[postTikHup]";
+ private static final String LOG_PREFIX_VIDEO = "[videoToCharacters]";
+ private static final String LOG_PREFIX_VIDEO2 = "[videoToCharacters2]";
+ private static final String LOG_PREFIX_DEEPSEEK = "[deepseekAnalysis]";
+
// 缓存配置
private static final String CACHE_KEY_PREFIX = "tikhub:cache:";
private static final long CACHE_EXPIRE_HOURS = 24;
@@ -78,11 +83,11 @@ public class TikHupServiceImpl implements TikHupService {
public Object postTikHup(String type, String methodType, String urlParams, String paramType) {
// 1. 参数校验
if (StringUtils.isBlank(type)) {
- log.error("postTikHup: type 参数为空");
+ log.error("{} type 参数为空", LOG_PREFIX_TIKHUP);
return CommonResult.error(400, "接口类型不能为空");
}
if (StringUtils.isBlank(methodType)) {
- log.error("postTikHup: methodType 参数为空");
+ log.error("{} methodType 参数为空", LOG_PREFIX_TIKHUP);
return CommonResult.error(400, "请求方法类型不能为空");
}
@@ -94,55 +99,62 @@ public class TikHupServiceImpl implements TikHupService {
config = pointsService.getConfig(PLATFORM_TIKHUB, MODEL_CODE_CRAWLER);
pointsService.checkPoints(userId, config.getConsumePoints());
} catch (Exception e) {
- log.error("[postTikHup] 积分预检失败: {}", e.getMessage());
+ log.error("{} 积分预检失败: {}", LOG_PREFIX_TIKHUP, e.getMessage());
return CommonResult.error(400, e.getMessage());
}
- // 3. 尝试从缓存获取结果(缓存命中也要扣积分)
+ // 3. 尝试从缓存获取结果
String cacheKey = buildCacheKey(type, methodType, urlParams, paramType);
- String cachedResult = stringRedisTemplate.opsForValue().get(cacheKey);
- if (StringUtils.isNotBlank(cachedResult)) {
- log.info("[postTikHup] 命中缓存, cacheKey: {}", cacheKey);
- // 缓存命中也扣积分
- deductPointsSafely(userId, config.getConsumePoints(), type);
+ String cachedUrl = stringRedisTemplate.opsForValue().get(cacheKey);
+ if (StringUtils.isNotBlank(cachedUrl)) {
+ log.info("{} 命中缓存 URL, cacheKey: {}, cacheUrl: {}", LOG_PREFIX_TIKHUP, cacheKey, cachedUrl);
try {
- return JSON.parseObject(cachedResult);
+ Unirest.setTimeouts(0, 0);
+ HttpResponse cachedResponse = Unirest.get(cachedUrl)
+ .header("Authorization", "Bearer " + config.getApiKey())
+ .asString();
+
+ if (cachedResponse.getStatus() == 200 && StringUtils.isNotBlank(cachedResponse.getBody())) {
+ deductPointsSafely(userId, config.getConsumePoints(), type);
+ return JSON.parseObject(cachedResponse.getBody()).get("data");
+ }
+ log.warn("{} 缓存 URL 请求失败, 状态码: {}, 清除缓存", LOG_PREFIX_TIKHUP, cachedResponse.getStatus());
+ stringRedisTemplate.delete(cacheKey);
} catch (Exception e) {
- return cachedResult;
+ log.warn("{} 缓存 URL 请求异常: {}, 清除缓存", LOG_PREFIX_TIKHUP, e.getMessage());
+ stringRedisTemplate.delete(cacheKey);
}
}
- // 4. 从 muye_ai_model_config 获取 apiKey
+ // 4. 获取 apiKey
String authorization = config.getApiKey();
if (StringUtils.isBlank(authorization)) {
- log.error("postTikHup: TikHub 配置的 apiKey 为空");
+ log.error("{} TikHub 配置的 apiKey 为空", LOG_PREFIX_TIKHUP);
return CommonResult.error(500, "接口配置错误:apiKey 为空");
}
- // 5. 获取接口 URL(从 tik_token 表)
+ // 5. 获取接口 URL
TikTokenVO tikTokenVO = tikTokenMapper.getInterfaceUrl(type);
if (tikTokenVO == null) {
- log.error("postTikHup: 未找到接口类型 {} 的 URL 配置", type);
+ log.error("{} 未找到接口类型 {} 的 URL 配置", LOG_PREFIX_TIKHUP, type);
return CommonResult.error(404, "未找到接口类型 " + type + " URL 配置");
}
String url = tikTokenVO.getPlatformUrl();
if (StringUtils.isBlank(url)) {
- log.error("postTikHup: 接口类型 {} 的 URL 为空", type);
+ log.error("{} 接口类型 {} 的 URL 为空", LOG_PREFIX_TIKHUP, type);
return CommonResult.error(500, "接口配置错误:URL 为空");
}
- // 6. 统一转换为小写进行比较
+ // 6. 执行请求
String methodTypeLower = methodType.toLowerCase();
String paramTypeLower = paramType != null ? paramType.toLowerCase() : "";
try {
Unirest.setTimeouts(0, 0);
HttpResponse response = executeRequest(url, urlParams, authorization, methodTypeLower, paramTypeLower);
-
- // 7. 处理响应并缓存
return handleResponse(response, url, userId, config.getConsumePoints(), type, cacheKey);
} catch (Exception e) {
- log.error("postTikHup: 接口调用异常, URL: {}, 错误信息: {}", url, e.getMessage(), e);
+ log.error("{} 接口调用异常, URL: {}, 错误信息: {}", LOG_PREFIX_TIKHUP, url, e.getMessage(), e);
return CommonResult.error(500, "接口调用异常: " + e.getMessage());
}
}
@@ -150,24 +162,24 @@ public class TikHupServiceImpl implements TikHupService {
private HttpResponse executeRequest(String url, String urlParams, String authorization,
String methodTypeLower, String paramTypeLower) throws Exception {
if ("post".equals(methodTypeLower) && "json".equals(paramTypeLower)) {
- log.debug("postTikHup: POST JSON 请求, URL: {}, Body: {}", url, urlParams);
+ log.debug("{} POST JSON 请求, URL: {}, Body: {}", LOG_PREFIX_TIKHUP, url, urlParams);
return Unirest.post(url)
.header("Authorization", "Bearer " + authorization)
.header("Content-Type", "application/json")
.body(urlParams)
.asString();
- } else if ("post".equals(methodTypeLower)) {
- log.debug("postTikHup: POST 表单请求, URL: {}?{}", url, urlParams);
+ }
+ if ("post".equals(methodTypeLower)) {
+ log.debug("{} POST 表单请求, URL: {}?{}", LOG_PREFIX_TIKHUP, url, urlParams);
return Unirest.post(url + "?" + urlParams)
.header("Authorization", "Bearer " + authorization)
.asString();
- } else {
- String finalUrl = buildUrlWithParams(url, urlParams);
- log.info("postTikHup: GET 请求, 最终URL: {}", finalUrl);
- return Unirest.get(finalUrl)
- .header("Authorization", "Bearer " + authorization)
- .asString();
}
+ String finalUrl = buildUrlWithParams(url, urlParams);
+ log.info("{} GET 请求, 最终URL: {}", LOG_PREFIX_TIKHUP, finalUrl);
+ return Unirest.get(finalUrl)
+ .header("Authorization", "Bearer " + authorization)
+ .asString();
}
private String buildUrlWithParams(String url, String urlParams) {
@@ -179,41 +191,53 @@ public class TikHupServiceImpl implements TikHupService {
private Object handleResponse(HttpResponse response, String url, String userId,
Integer consumePoints, String type, String cacheKey) {
- int statusCode = response.getStatus();
+ int httpStatus = response.getStatus();
String responseBody = response.getBody();
- if (statusCode != 200) {
- log.error("postTikHup: 接口调用失败, URL: {}, 状态码: {}, 响应: {}", url, statusCode, responseBody);
- return CommonResult.error(statusCode,
- "接口调用失败: " + (StringUtils.isNotBlank(responseBody) ? responseBody : "HTTP " + statusCode));
+ if (httpStatus != 200) {
+ log.error("{} HTTP 请求失败, URL: {}, 状态码: {}, 响应: {}", LOG_PREFIX_TIKHUP, url, httpStatus, responseBody);
+ return CommonResult.error(httpStatus,
+ "HTTP 请求失败: " + (StringUtils.isNotBlank(responseBody) ? responseBody : "HTTP " + httpStatus));
}
if (StringUtils.isBlank(responseBody)) {
- log.warn("postTikHup: 接口返回空响应, URL: {}", url);
+ log.warn("{} 接口返回空响应, URL: {}", LOG_PREFIX_TIKHUP, url);
return CommonResult.error(500, "接口返回空响应");
}
- // 扣减积分
+ com.alibaba.fastjson.JSONObject respJson;
+ try {
+ respJson = JSON.parseObject(responseBody);
+ } catch (Exception e) {
+ log.error("{} 响应体 JSON 解析失败, URL: {}, 响应: {}", LOG_PREFIX_TIKHUP, url, responseBody);
+ return CommonResult.error(500, "响应体解析失败");
+ }
+
+ Integer bizCode = respJson.getInteger("code");
+ String bizMsg = respJson.getString("message");
+ if (bizCode == null || bizCode != 200) {
+ log.error("{} 业务调用失败, URL: {}, code: {}, message: {}", LOG_PREFIX_TIKHUP, url, bizCode, bizMsg);
+ return CommonResult.error(bizCode != null ? bizCode : 500,
+ "业务调用失败: " + (StringUtils.isNotBlank(bizMsg) ? bizMsg : "code=" + bizCode));
+ }
+
deductPointsSafely(userId, consumePoints, type);
- // 缓存成功响应(24小时)
- stringRedisTemplate.opsForValue().set(cacheKey, responseBody, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
- log.info("[postTikHup] 结果已缓存, cacheKey: {}, 有效期: {}小时", cacheKey, CACHE_EXPIRE_HOURS);
-
- // 尝试解析 JSON,失败则返回原始字符串
- try {
- return JSON.parseObject(responseBody);
- } catch (Exception e) {
- return responseBody;
+ String cacheUrl = respJson.getString("cache_url");
+ if (StringUtils.isNotBlank(cacheUrl)) {
+ stringRedisTemplate.opsForValue().set(cacheKey, cacheUrl, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+ log.info("{} 缓存 URL 已保存, cacheKey: {}, cacheUrl: {}", LOG_PREFIX_TIKHUP, cacheKey, cacheUrl);
}
+
+ return respJson;
}
private void deductPointsSafely(String userId, Integer consumePoints, String type) {
try {
pointsService.deductPoints(userId, consumePoints, "tikhub_fetch", type);
- log.info("[postTikHup] 用户 {} 扣减 {} 积分", userId, consumePoints);
+ log.info("{} 用户 {} 扣减 {} 积分", LOG_PREFIX_TIKHUP, userId, consumePoints);
} catch (Exception e) {
- log.error("[postTikHup] 积分扣减失败: {}", e.getMessage());
+ log.error("{} 积分扣减失败: {}", LOG_PREFIX_TIKHUP, e.getMessage());
}
}
@@ -247,31 +271,31 @@ public class TikHupServiceImpl implements TikHupService {
@Override
public Object videoToCharacters(String fileLink) {
- log.info("[videoToCharacters] 开始识别,文件链接: {}", fileLink);
+ log.info("{} 开始识别,文件链接: {}", LOG_PREFIX_VIDEO, fileLink);
TikFileTransCharacters transCharacters = new TikFileTransCharacters(accessKeyId, accessKeySecret);
String taskId = transCharacters.submitFileTransRequest(appKey, fileLink);
if (taskId == null) {
- log.error("[videoToCharacters] 提交识别请求失败,taskId为null,fileLink: {}", fileLink);
+ log.error("{} 提交识别请求失败,taskId为null,fileLink: {}", LOG_PREFIX_VIDEO, fileLink);
return CommonResult.error(500, "录音文件识别请求失败!");
}
- log.info("[videoToCharacters] 提交识别请求成功,taskId: {}", taskId);
+ log.info("{} 提交识别请求成功,taskId: {}", LOG_PREFIX_VIDEO, taskId);
String transResult = transCharacters.getFileTransResult(taskId);
if (transResult == null) {
- log.error("[videoToCharacters] 识别结果查询失败,taskId: {}", taskId);
+ log.error("{} 识别结果查询失败,taskId: {}", LOG_PREFIX_VIDEO, taskId);
return CommonResult.error(501, "录音文件识别请求失败!");
}
- log.info("[videoToCharacters] 识别成功,taskId: {},结果长度: {}", taskId, transResult.length());
+ log.info("{} 识别成功,taskId: {},结果长度: {}", LOG_PREFIX_VIDEO, taskId, transResult.length());
return CommonResult.success(transResult);
}
@Override
public Object videoToCharacters2(List fileLinkList) {
- log.info("[videoToCharacters2] 开始识别,文件数量: {},文件URL: {}",
- fileLinkList != null ? fileLinkList.size() : 0, fileLinkList);
+ log.info("{} 开始识别,文件数量: {},文件URL: {}",
+ LOG_PREFIX_VIDEO2, fileLinkList != null ? fileLinkList.size() : 0, fileLinkList);
TranscriptionParam param = TranscriptionParam.builder()
.apiKey(ALIYUN_API_KEY)
@@ -283,16 +307,16 @@ public class TikHupServiceImpl implements TikHupService {
try {
Transcription transcription = new Transcription();
TranscriptionResult result = transcription.asyncCall(param);
- log.info("[videoToCharacters2] 提交转写请求成功,TaskId: {}", result.getTaskId());
+ log.info("{} 提交转写请求成功,TaskId: {}", LOG_PREFIX_VIDEO2, result.getTaskId());
result = transcription.wait(TranscriptionQueryParam.FromTranscriptionParam(param, result.getTaskId()));
String outputJson = new GsonBuilder().setPrettyPrinting().create().toJson(result.getOutput());
- log.info("[videoToCharacters2] 识别成功,TaskId: {},结果长度: {}",
- result.getTaskId(), outputJson != null ? outputJson.length() : 0);
+ log.info("{} 识别成功,TaskId: {},结果长度: {}",
+ LOG_PREFIX_VIDEO2, result.getTaskId(), outputJson != null ? outputJson.length() : 0);
return CommonResult.success(outputJson);
} catch (Exception e) {
- log.error("[videoToCharacters2] 识别失败,文件URL: {},异常: {}", fileLinkList, e.getMessage(), e);
+ log.error("{} 识别失败,文件URL: {},异常: {}", LOG_PREFIX_VIDEO2, fileLinkList, e.getMessage(), e);
return CommonResult.error(500, "录音文件识别请求失败!");
}
}
@@ -301,7 +325,7 @@ public class TikHupServiceImpl implements TikHupService {
public Object deepseekAnalysis(String promptType, String content) {
TikPromptVO tikPromptVO = tikPromptMapper.getTikPromptVO(promptType);
if (tikPromptVO == null) {
- log.error("[deepseekAnalysis] 未找到提示词类型: {}", promptType);
+ log.error("{} 未找到提示词类型: {}", LOG_PREFIX_DEEPSEEK, promptType);
return CommonResult.error(404, "未找到提示词类型");
}
@@ -317,10 +341,10 @@ public class TikHupServiceImpl implements TikHupService {
if (response.getStatus() == 200) {
return response.getBody();
}
- log.error("[deepseekAnalysis] 调用失败,状态码: {}", response.getStatus());
+ log.error("{} 调用失败,状态码: {}", LOG_PREFIX_DEEPSEEK, response.getStatus());
return CommonResult.error(500, "文案改写失败!");
} catch (Exception e) {
- log.error("[deepseekAnalysis] 异常: {}", e.getMessage(), e);
+ log.error("{} 异常: {}", LOG_PREFIX_DEEPSEEK, e.getMessage(), e);
return CommonResult.error(500, "文案改写失败!");
}
}