feat(chat): 新增AI陪聊角色voiceId字段及使用逻辑
- AiCompanionVO、KeyboardAiCompanion实体新增voiceId字段与Swagger注解 - ChatServiceImpl同步/异步消息接口优先使用角色配置voiceId进行TTS - 补充角色状态校验,下线角色禁止对话
This commit is contained in:
@@ -66,7 +66,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/user/verifyMailCode",
|
"/user/verifyMailCode",
|
||||||
"/character/listWithNotLogin",
|
"/character/listWithNotLogin",
|
||||||
"/character/listByTagWithNotLogin",
|
"/character/listByTagWithNotLogin",
|
||||||
"/ai-companion/report"
|
"/ai-companion/report",
|
||||||
|
"/apple/notification"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -146,4 +146,8 @@ public class KeyboardAiCompanion {
|
|||||||
@TableField(value = "prologue_audio")
|
@TableField(value = "prologue_audio")
|
||||||
@Schema(description="开场白音频")
|
@Schema(description="开场白音频")
|
||||||
private String prologueAudio;
|
private String prologueAudio;
|
||||||
|
|
||||||
|
@TableField(value = "voice_id")
|
||||||
|
@Schema(description="角色音频Id")
|
||||||
|
private String voiceId;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.yolo.keyborad.model.vo;
|
package com.yolo.keyborad.model.vo;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -66,4 +67,9 @@ public class AiCompanionVO {
|
|||||||
|
|
||||||
@Schema(description = "创建时间")
|
@Schema(description = "创建时间")
|
||||||
private Date createdAt;
|
private Date createdAt;
|
||||||
|
|
||||||
|
@TableField(value = "voice_id")
|
||||||
|
@Schema(description="角色音频Id")
|
||||||
|
private String voiceId;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,6 @@ public class KeyboardProductItemRespVO {
|
|||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Schema(description = "级别")
|
@Schema(description = "级别")
|
||||||
private Integer level;
|
private Integer level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
|||||||
// 检查VIP是否已过期
|
// 检查VIP是否已过期
|
||||||
if (user.getVipExpiry() != null && user.getVipExpiry().toInstant().isBefore(Instant.now())) {
|
if (user.getVipExpiry() != null && user.getVipExpiry().toInstant().isBefore(Instant.now())) {
|
||||||
user.setIsVip(false);
|
user.setIsVip(false);
|
||||||
|
user.setVipLevel(null);
|
||||||
userService.updateById(user);
|
userService.updateById(user);
|
||||||
log.info("User VIP expired: userId={}", userId);
|
log.info("User VIP expired: userId={}", userId);
|
||||||
}
|
}
|
||||||
@@ -464,6 +465,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
|||||||
} else {
|
} else {
|
||||||
// 倒扣后已过期,取消VIP
|
// 倒扣后已过期,取消VIP
|
||||||
user.setIsVip(false);
|
user.setIsVip(false);
|
||||||
|
user.setVipLevel(null);
|
||||||
user.setVipExpiry(Date.from(newExpiry)); // 保留倒扣后的时间,而不是设为null
|
user.setVipExpiry(Date.from(newExpiry)); // 保留倒扣后的时间,而不是设为null
|
||||||
log.info("Subscription refunded, VIP expired after deduction: userId={}, newExpiry={}",
|
log.info("Subscription refunded, VIP expired after deduction: userId={}, newExpiry={}",
|
||||||
userId, newExpiry);
|
userId, newExpiry);
|
||||||
@@ -607,6 +609,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
|||||||
|
|
||||||
// 4. 更新用户VIP状态
|
// 4. 更新用户VIP状态
|
||||||
user.setIsVip(true);
|
user.setIsVip(true);
|
||||||
|
user.setVipLevel(product.getLevel());
|
||||||
user.setVipExpiry(Date.from(newExpiry));
|
user.setVipExpiry(Date.from(newExpiry));
|
||||||
|
|
||||||
// 5. 保存用户信息到数据库
|
// 5. 保存用户信息到数据库
|
||||||
@@ -764,6 +767,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
|||||||
|
|
||||||
// 6. 更新用户VIP状态
|
// 6. 更新用户VIP状态
|
||||||
user.setIsVip(true); // 设置为VIP用户
|
user.setIsVip(true); // 设置为VIP用户
|
||||||
|
user.setVipLevel(product.getLevel());
|
||||||
user.setVipExpiry(Date.from(newExpiry)); // 更新VIP过期时间
|
user.setVipExpiry(Date.from(newExpiry)); // 更新VIP过期时间
|
||||||
|
|
||||||
// 7. 保存用户信息到数据库
|
// 7. 保存用户信息到数据库
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.yolo.keyborad.config.NacosAppConfigCenter;
|
|||||||
import com.yolo.keyborad.exception.BusinessException;
|
import com.yolo.keyborad.exception.BusinessException;
|
||||||
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||||
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardAiChatMessage;
|
import com.yolo.keyborad.model.entity.KeyboardAiChatMessage;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||||
@@ -406,8 +407,16 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// 获取AI人设的系统提示词
|
// 获取AI角色信息(用于系统提示词 + voiceId)
|
||||||
String systemPrompt = aiCompanionService.getSystemPromptById(companionId);
|
KeyboardAiCompanion companion = aiCompanionService.getById(companionId);
|
||||||
|
if (companion == null) {
|
||||||
|
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "AI陪聊角色不存在");
|
||||||
|
}
|
||||||
|
if (companion.getStatus() == null || companion.getStatus() != 1) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "AI陪聊角色已下线");
|
||||||
|
}
|
||||||
|
String systemPrompt = companion.getSystemPrompt();
|
||||||
|
String voiceId = companion.getVoiceId();
|
||||||
|
|
||||||
// 获取最近20条聊天记录作为上下文
|
// 获取最近20条聊天记录作为上下文
|
||||||
List<KeyboardAiChatMessage> historyMessages = aiChatMessageService.getRecentMessages(
|
List<KeyboardAiChatMessage> historyMessages = aiChatMessageService.getRecentMessages(
|
||||||
@@ -444,8 +453,8 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
// 初始化音频任务状态为 processing
|
// 初始化音频任务状态为 processing
|
||||||
setAudioTaskStatus(audioId, AudioTaskVO.STATUS_PROCESSING, null, null);
|
setAudioTaskStatus(audioId, AudioTaskVO.STATUS_PROCESSING, null, null);
|
||||||
|
|
||||||
// 异步执行 TTS + R2 上传
|
// 异步执行 TTS + R2 上传(优先使用角色配置的 voiceId)
|
||||||
CompletableFuture.runAsync(() -> processAudioAsync(audioId, response, userId));
|
CompletableFuture.runAsync(() -> processAudioAsync(audioId, response, userId, voiceId));
|
||||||
|
|
||||||
return ChatMessageVO.builder()
|
return ChatMessageVO.builder()
|
||||||
.aiResponse(response)
|
.aiResponse(response)
|
||||||
@@ -515,14 +524,14 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
/**
|
/**
|
||||||
* 异步处理音频:TTS 转换 + 上传 R2
|
* 异步处理音频:TTS 转换 + 上传 R2
|
||||||
*/
|
*/
|
||||||
private void processAudioAsync(String audioId, String text, String userId) {
|
private void processAudioAsync(String audioId, String text, String userId, String voiceId) {
|
||||||
try {
|
try {
|
||||||
log.info("开始异步音频处理, audioId: {}", audioId);
|
log.info("开始异步音频处理, audioId: {}", audioId);
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// 1. TTS 转换
|
// 1. TTS 转换
|
||||||
long ttsStart = System.currentTimeMillis();
|
long ttsStart = System.currentTimeMillis();
|
||||||
TextToSpeechVO ttsResult = elevenLabsService.textToSpeechWithTimestamps(text);
|
TextToSpeechVO ttsResult = elevenLabsService.textToSpeechWithTimestamps(text, voiceId);
|
||||||
long ttsDuration = System.currentTimeMillis() - ttsStart;
|
long ttsDuration = System.currentTimeMillis() - ttsStart;
|
||||||
log.info("TTS 完成, audioId: {}, 耗时: {}ms", audioId, ttsDuration);
|
log.info("TTS 完成, audioId: {}, 耗时: {}ms", audioId, ttsDuration);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user