This commit is contained in:
2026-02-24 00:38:23 +08:00
parent f63e6b92bd
commit d69a99882f
12 changed files with 133 additions and 77 deletions

View File

@@ -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());

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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 = "随便")

View File

@@ -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);
/**
* 预检积分(余额不足抛异常)

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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 合成(支持前端选择供应商,不传则使用默认)