修复
This commit is contained in:
@@ -58,7 +58,7 @@ public class DifyServiceImpl implements DifyService {
|
||||
: AiModelTypeEnum.DIFY_WRITING_PRO;
|
||||
AiModelConfigDO config = pointsService.getConfig(
|
||||
AiPlatformEnum.DIFY.getPlatform(),
|
||||
modelTypeEnum.getModelType());
|
||||
modelTypeEnum.getModelCode());
|
||||
|
||||
// 3. 预检积分
|
||||
pointsService.checkPoints(userId, config.getConsumePoints());
|
||||
|
||||
@@ -17,27 +17,27 @@ import java.util.Arrays;
|
||||
public enum AiModelTypeEnum implements ArrayValuable<String> {
|
||||
|
||||
// ========== Dify 写作模型 ==========
|
||||
DIFY_WRITING_PRO("writing_pro", "Pro深度版", AiPlatformEnum.DIFY),
|
||||
DIFY_WRITING_STANDARD("writing_standard", "标准版", AiPlatformEnum.DIFY),
|
||||
DIFY_WRITING_PRO("writing_pro", "Pro深度版", AiPlatformEnum.DIFY, "text"),
|
||||
DIFY_WRITING_STANDARD("writing_standard", "标准版", AiPlatformEnum.DIFY, "text"),
|
||||
|
||||
// ========== 数字人模型 ==========
|
||||
DIGITAL_HUMAN_LATENTSYNC("latentsync", "LatentSync", AiPlatformEnum.DIGITAL_HUMAN),
|
||||
DIGITAL_HUMAN_KLING("kling", "可灵", AiPlatformEnum.DIGITAL_HUMAN),
|
||||
DIGITAL_HUMAN_LATENTSYNC("latentsync", "LatentSync", AiPlatformEnum.DIGITAL_HUMAN, "video"),
|
||||
DIGITAL_HUMAN_KLING("kling", "可灵", AiPlatformEnum.DIGITAL_HUMAN, "video"),
|
||||
|
||||
// ========== TikHub 爬虫 ==========
|
||||
TIKHUB_CRAWLER("crawler", "爬虫", AiPlatformEnum.TIKHUB),
|
||||
TIKHUB_CRAWLER("crawler", "爬虫", AiPlatformEnum.TIKHUB, "third"),
|
||||
|
||||
// ========== 阿里云语音服务 ==========
|
||||
ALICLOUD_VOICE_TO_TEXT("voice_to_text", "语音转文字", AiPlatformEnum.ALICLOUD),
|
||||
ALICLOUD_VOICE_TO_TEXT("voice_to_text", "语音转文字", AiPlatformEnum.ALICLOUD, "audio"),
|
||||
|
||||
// ========== SiliconFlow 语音服务 ==========
|
||||
SILICONFLOW_INDEXTTS("indextts", "IndexTTS", AiPlatformEnum.SILICONFLOW),
|
||||
SILICONFLOW_INDEXTTS("indextts", "IndexTTS", AiPlatformEnum.SILICONFLOW, "audio"),
|
||||
;
|
||||
|
||||
/**
|
||||
* 模型类型标识
|
||||
* 模型标识(业务类型)
|
||||
*/
|
||||
private final String modelType;
|
||||
private final String modelCode;
|
||||
/**
|
||||
* 模型类型名称
|
||||
*/
|
||||
@@ -46,8 +46,12 @@ public enum AiModelTypeEnum implements ArrayValuable<String> {
|
||||
* 所属平台
|
||||
*/
|
||||
private final AiPlatformEnum platform;
|
||||
/**
|
||||
* 模型类型(媒体分类)
|
||||
*/
|
||||
private final String modelType;
|
||||
|
||||
public static final String[] ARRAYS = Arrays.stream(values()).map(AiModelTypeEnum::getModelType).toArray(String[]::new);
|
||||
public static final String[] ARRAYS = Arrays.stream(values()).map(AiModelTypeEnum::getModelCode).toArray(String[]::new);
|
||||
|
||||
@Override
|
||||
public String[] array() {
|
||||
@@ -55,11 +59,11 @@ public enum AiModelTypeEnum implements ArrayValuable<String> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型类型标识获取枚举
|
||||
* 根据模型标识获取枚举
|
||||
*/
|
||||
public static AiModelTypeEnum valueOfModelType(String modelType) {
|
||||
public static AiModelTypeEnum valueOfModelCode(String modelCode) {
|
||||
return Arrays.stream(values())
|
||||
.filter(e -> e.getModelType().equals(modelType))
|
||||
.filter(e -> e.getModelCode().equals(modelCode))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@ public interface AiModelConfigMapper extends BaseMapperX<AiModelConfigDO> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据平台和模型类型查询配置
|
||||
* 根据平台和模型标识查询配置
|
||||
*/
|
||||
default AiModelConfigDO selectByPlatformAndModelType(String platform, String modelType) {
|
||||
default AiModelConfigDO selectByPlatformAndModelCode(String platform, String modelCode) {
|
||||
return selectOne(new LambdaQueryWrapperX<AiModelConfigDO>()
|
||||
.eq(AiModelConfigDO::getPlatform, platform)
|
||||
.eq(AiModelConfigDO::getModelType, modelType)
|
||||
.eq(AiModelConfigDO::getModelCode, modelCode)
|
||||
.eq(AiModelConfigDO::getStatus, 1));
|
||||
}
|
||||
|
||||
|
||||
@@ -55,16 +55,13 @@ public class AiModelConfigSaveReqVO {
|
||||
@Schema(description = "最大文本数量")
|
||||
private Integer maxTextLength;
|
||||
|
||||
@Schema(description = "图片最大像素", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "图片最大像素不能为空")
|
||||
@Schema(description = "图片最大像素")
|
||||
private String maxImageSize;
|
||||
|
||||
@Schema(description = "视频最大时长(秒)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "视频最大时长(秒)不能为空")
|
||||
@Schema(description = "视频最大时长(秒)")
|
||||
private Integer maxVideoDuration;
|
||||
|
||||
@Schema(description = "视频最大质量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "视频最大质量不能为空")
|
||||
@Schema(description = "视频最大质量")
|
||||
private String maxVideoQuality;
|
||||
|
||||
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便")
|
||||
|
||||
@@ -13,10 +13,10 @@ public interface PointsService {
|
||||
* 获取积分配置
|
||||
*
|
||||
* @param platform 平台标识(dify/tikhub/voice/digital_human)
|
||||
* @param modelType 模型类型
|
||||
* @param modelCode 模型标识
|
||||
* @return 积分配置
|
||||
*/
|
||||
AiModelConfigDO getConfig(String platform, String modelType);
|
||||
AiModelConfigDO getConfig(String platform, String modelCode);
|
||||
|
||||
/**
|
||||
* 预检积分(余额不足抛异常)
|
||||
|
||||
@@ -44,8 +44,8 @@ public class PointsServiceImpl implements PointsService {
|
||||
private PointRecordMapper pointRecordMapper;
|
||||
|
||||
@Override
|
||||
public AiModelConfigDO getConfig(String platform, String modelType) {
|
||||
AiModelConfigDO config = aiModelConfigMapper.selectByPlatformAndModelType(platform, modelType);
|
||||
public AiModelConfigDO getConfig(String platform, String modelCode) {
|
||||
AiModelConfigDO config = aiModelConfigMapper.selectByPlatformAndModelCode(platform, modelCode);
|
||||
if (config == null) {
|
||||
throw exception(POINTS_CONFIG_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,13 @@ import io.reactivex.Flowable;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -43,7 +48,11 @@ public class TikHupServiceImpl implements TikHupService {
|
||||
|
||||
// 平台和模型标识
|
||||
private static final String PLATFORM_TIKHUB = "tikhub";
|
||||
private static final String MODEL_TYPE_CRAWLER = "crawler";
|
||||
private static final String MODEL_CODE_CRAWLER = "crawler";
|
||||
|
||||
// 缓存配置
|
||||
private static final String CACHE_KEY_PREFIX = "tikhub:cache:";
|
||||
private static final long CACHE_EXPIRE_HOURS = 24;
|
||||
|
||||
// API 配置(建议后续迁移到配置文件)
|
||||
private static final String ALIYUN_API_URL = "https://dashscope.aliyuncs.com/api/v1";
|
||||
@@ -63,6 +72,7 @@ public class TikHupServiceImpl implements TikHupService {
|
||||
private final TikTokenMapper tikTokenMapper;
|
||||
private final TikPromptMapper tikPromptMapper;
|
||||
private final PointsService pointsService;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Override
|
||||
public Object postTikHup(String type, String methodType, String urlParams, String paramType) {
|
||||
@@ -81,25 +91,39 @@ public class TikHupServiceImpl implements TikHupService {
|
||||
String userId = loginUserId != null ? loginUserId.toString() : "1";
|
||||
AiModelConfigDO config;
|
||||
try {
|
||||
config = pointsService.getConfig(PLATFORM_TIKHUB, MODEL_TYPE_CRAWLER);
|
||||
config = pointsService.getConfig(PLATFORM_TIKHUB, MODEL_CODE_CRAWLER);
|
||||
pointsService.checkPoints(userId, config.getConsumePoints());
|
||||
} catch (Exception e) {
|
||||
log.error("[postTikHup] 积分预检失败: {}", e.getMessage());
|
||||
return CommonResult.error(400, e.getMessage());
|
||||
}
|
||||
|
||||
// 3. 从 muye_ai_model_config 获取 apiKey
|
||||
// 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);
|
||||
try {
|
||||
return JSON.parseObject(cachedResult);
|
||||
} catch (Exception e) {
|
||||
return cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 从 muye_ai_model_config 获取 apiKey
|
||||
String authorization = config.getApiKey();
|
||||
if (StringUtils.isBlank(authorization)) {
|
||||
log.error("postTikHup: TikHub 配置的 apiKey 为空");
|
||||
return CommonResult.error(500, "接口配置错误:apiKey 为空");
|
||||
}
|
||||
|
||||
// 4. 获取接口 URL
|
||||
// 5. 获取接口 URL(从 tik_token 表)
|
||||
TikTokenVO tikTokenVO = tikTokenMapper.getInterfaceUrl(type);
|
||||
if (tikTokenVO == null) {
|
||||
log.error("postTikHup: 未找到接口类型 {} 的 URL 配置", type);
|
||||
return CommonResult.error(404, "未找到接口类型 " + type + " 的 URL 配置");
|
||||
return CommonResult.error(404, "未找到接口类型 " + type + " URL 配置");
|
||||
}
|
||||
String url = tikTokenVO.getPlatformUrl();
|
||||
if (StringUtils.isBlank(url)) {
|
||||
@@ -107,7 +131,7 @@ public class TikHupServiceImpl implements TikHupService {
|
||||
return CommonResult.error(500, "接口配置错误:URL 为空");
|
||||
}
|
||||
|
||||
// 5. 统一转换为小写进行比较
|
||||
// 6. 统一转换为小写进行比较
|
||||
String methodTypeLower = methodType.toLowerCase();
|
||||
String paramTypeLower = paramType != null ? paramType.toLowerCase() : "";
|
||||
|
||||
@@ -115,8 +139,8 @@ public class TikHupServiceImpl implements TikHupService {
|
||||
Unirest.setTimeouts(0, 0);
|
||||
HttpResponse<String> response = executeRequest(url, urlParams, authorization, methodTypeLower, paramTypeLower);
|
||||
|
||||
// 6. 处理响应
|
||||
return handleResponse(response, url, userId, config.getConsumePoints(), type);
|
||||
// 7. 处理响应并缓存
|
||||
return handleResponse(response, url, userId, config.getConsumePoints(), type, cacheKey);
|
||||
} catch (Exception e) {
|
||||
log.error("postTikHup: 接口调用异常, URL: {}, 错误信息: {}", url, e.getMessage(), e);
|
||||
return CommonResult.error(500, "接口调用异常: " + e.getMessage());
|
||||
@@ -154,7 +178,7 @@ public class TikHupServiceImpl implements TikHupService {
|
||||
}
|
||||
|
||||
private Object handleResponse(HttpResponse<String> response, String url, String userId,
|
||||
Integer consumePoints, String type) {
|
||||
Integer consumePoints, String type, String cacheKey) {
|
||||
int statusCode = response.getStatus();
|
||||
String responseBody = response.getBody();
|
||||
|
||||
@@ -172,6 +196,10 @@ public class TikHupServiceImpl implements TikHupService {
|
||||
// 扣减积分
|
||||
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);
|
||||
@@ -189,6 +217,34 @@ public class TikHupServiceImpl implements TikHupService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建缓存 Key
|
||||
* 格式: tikhub:cache:{type}:{methodType}:{paramType}:{paramsHash}
|
||||
*/
|
||||
private String buildCacheKey(String type, String methodType, String urlParams, String paramType) {
|
||||
String rawKey = type + ":" + methodType + ":" + (paramType != null ? paramType : "") + ":" + (urlParams != null ? urlParams : "");
|
||||
String hash = md5(rawKey);
|
||||
return CACHE_KEY_PREFIX + type + ":" + hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 哈希
|
||||
*/
|
||||
private String md5(String input) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
// 降级为直接使用原始字符串的 hashCode
|
||||
return String.valueOf(input.hashCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object videoToCharacters(String fileLink) {
|
||||
log.info("[videoToCharacters] 开始识别,文件链接: {}", fileLink);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package cn.iocoder.yudao.module.tik.tikhup.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@TableName("tik_token")
|
||||
@Data
|
||||
public class TikTokenVO {
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class TikTokenVO extends TenantBaseDO {
|
||||
|
||||
@TableField(value = "ID")
|
||||
private Long id;
|
||||
|
||||
@TableField(value = "platform_url")
|
||||
private String platformUrl;
|
||||
|
||||
@@ -84,8 +84,8 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
||||
|
||||
/** 积分平台和类型常量 */
|
||||
private static final String PLATFORM_DIGITAL_HUMAN = "digital_human";
|
||||
private static final String MODEL_TYPE_LATENTSYNC = "latentsync";
|
||||
private static final String MODEL_TYPE_KLING = "kling";
|
||||
private static final String MODEL_CODE_LATENTSYNC = "latentsync";
|
||||
private static final String MODEL_CODE_KLING = "kling";
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -97,8 +97,8 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
||||
|
||||
// 2. 积分预检和预扣
|
||||
String aiProvider = StrUtil.blankToDefault(reqVO.getAiProvider(), "302ai");
|
||||
String modelType = "kling".equalsIgnoreCase(aiProvider) ? MODEL_TYPE_KLING : MODEL_TYPE_LATENTSYNC;
|
||||
AiModelConfigDO config = pointsService.getConfig(PLATFORM_DIGITAL_HUMAN, modelType);
|
||||
String modelCode = "kling".equalsIgnoreCase(aiProvider) ? MODEL_CODE_KLING : MODEL_CODE_LATENTSYNC;
|
||||
AiModelConfigDO config = pointsService.getConfig(PLATFORM_DIGITAL_HUMAN, modelCode);
|
||||
pointsService.checkPoints(userId.toString(), config.getConsumePoints());
|
||||
Long pendingRecordId = pointsService.createPendingDeduct(
|
||||
userId.toString(),
|
||||
|
||||
@@ -112,8 +112,8 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
|
||||
|
||||
/** 积分平台和类型常量 */
|
||||
private static final String PLATFORM_VOICE = "voice";
|
||||
private static final String MODEL_TYPE_TTS = "tts";
|
||||
private static final String MODEL_TYPE_CLONE = "clone";
|
||||
private static final String MODEL_CODE_TTS = "tts";
|
||||
private static final String MODEL_CODE_CLONE = "clone";
|
||||
|
||||
@Resource
|
||||
private PointsService pointsService;
|
||||
@@ -162,7 +162,7 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
|
||||
if (StrUtil.isNotBlank(createReqVO.getText())) {
|
||||
try {
|
||||
// 4.1 获取积分配置并预检
|
||||
AiModelConfigDO config = pointsService.getConfig(PLATFORM_VOICE, MODEL_TYPE_CLONE);
|
||||
AiModelConfigDO config = pointsService.getConfig(PLATFORM_VOICE, MODEL_CODE_CLONE);
|
||||
pointsService.checkPoints(userId.toString(), config.getConsumePoints());
|
||||
|
||||
log.info("[createVoice][开始语音复刻,配音编号({}),文件ID({}),供应商({})]",
|
||||
@@ -472,7 +472,7 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
|
||||
}
|
||||
|
||||
// 获取积分配置并预检(缓存未命中,需要实际调用 TTS)
|
||||
AiModelConfigDO ttsConfig = pointsService.getConfig(PLATFORM_VOICE, MODEL_TYPE_TTS);
|
||||
AiModelConfigDO ttsConfig = pointsService.getConfig(PLATFORM_VOICE, MODEL_CODE_TTS);
|
||||
pointsService.checkPoints(userId.toString(), ttsConfig.getConsumePoints());
|
||||
|
||||
// 使用 Provider 接口进行 TTS 合成(支持前端选择供应商,不传则使用默认)
|
||||
|
||||
Reference in New Issue
Block a user