This commit is contained in:
2026-02-24 01:11:04 +08:00
parent d69a99882f
commit e01bb0bb5f
2 changed files with 100 additions and 79 deletions

View File

@@ -30,11 +30,11 @@
</div>
<!-- APlayer 播放器容器 -->
<div v-if="audioUrl" ref="playerContainer" class="aplayer-container"></div>
<div v-show="audioUrl" ref="playerContainer" class="aplayer-container"></div>
<!-- 下载按钮 -->
<a-button
v-if="audioUrl"
v-show="audioUrl"
type="link"
size="small"
@click="downloadAudio"
@@ -47,8 +47,8 @@
<script setup>
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({})

View File

@@ -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<String> 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<String> 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<String> 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<String> 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为nullfileLink: {}", fileLink);
log.error("{} 提交识别请求失败taskId为nullfileLink: {}", 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<String> 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, "文案改写失败!");
}
}