From e01bb0bb5f3fef4c8e88cbf706e94fbaa6ffdb5b Mon Sep 17 00:00:00 2001 From: shenaowei <450702724@qq.com> Date: Tue, 24 Feb 2026 01:11:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web-gold/src/components/VoiceSelector.vue | 27 ++-- .../tik/tikhup/service/TikHupServiceImpl.java | 152 ++++++++++-------- 2 files changed, 100 insertions(+), 79 deletions(-) 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, "文案改写失败!"); } }