mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-03-13 20:53:42 +08:00
fix(billing): 1. 新增统一计费代理 BillingChatServiceProxy位置:ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/proxy/BillingChatServiceProxy.java 作用:为所有ChatService实现类提供透明的计费代理包装
核心功能:
AI回复前余额预检查,避免无效消耗
自动收集AI回复内容
统一处理AI回复的保存和计费
适配多种AI服务的数据格式
2. 重构工厂类
ChatServiceFactory
改进:自动为所有ChatService包装计费代理
新增方法:getOriginalService() 用于获取未包装的原始服务优势:调用方无需关心计费逻辑,完全透明
3. 增强计费服务 IChatCostService 接口
新增方法:checkBalanceSufficient() - 余额预检查
分离关注点:saveMessage() - 仅保存消息
publishBillingEvent() - 仅发布计费事件
deductToken() - 仅执行计费扣费
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package org.ruoyi.chat.factory;
|
||||
|
||||
import org.ruoyi.chat.service.chat.IChatCostService;
|
||||
import org.ruoyi.chat.service.chat.IChatService;
|
||||
import org.ruoyi.chat.service.chat.proxy.BillingChatServiceProxy;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
@@ -18,13 +20,18 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Component
|
||||
public class ChatServiceFactory implements ApplicationContextAware {
|
||||
private final Map<String, IChatService> chatServiceMap = new ConcurrentHashMap<>();
|
||||
private IChatCostService chatCostService;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
// 获取计费服务
|
||||
this.chatCostService = applicationContext.getBean(IChatCostService.class);
|
||||
|
||||
// 初始化时收集所有IChatService的实现
|
||||
Map<String, IChatService> serviceMap = applicationContext.getBeansOfType(IChatService.class);
|
||||
for (IChatService service : serviceMap.values()) {
|
||||
if (service != null) {
|
||||
if (service != null && !isBillingProxy(service)) {
|
||||
// 只收集非代理的原始服务
|
||||
chatServiceMap.put(service.getCategory(), service);
|
||||
}
|
||||
}
|
||||
@@ -32,12 +39,33 @@ public class ChatServiceFactory implements ApplicationContextAware {
|
||||
|
||||
/**
|
||||
* 根据模型类别获取对应的聊天服务实现
|
||||
* 自动应用计费代理包装
|
||||
*/
|
||||
public IChatService getChatService(String category) {
|
||||
IChatService originalService = chatServiceMap.get(category);
|
||||
if (originalService == null) {
|
||||
throw new IllegalArgumentException("不支持的模型类别: " + category);
|
||||
}
|
||||
|
||||
// 自动包装为计费代理
|
||||
return new BillingChatServiceProxy(originalService, chatCostService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始服务(不包装代理)
|
||||
*/
|
||||
public IChatService getOriginalService(String category) {
|
||||
IChatService service = chatServiceMap.get(category);
|
||||
if (service == null) {
|
||||
throw new IllegalArgumentException("不支持的模型类别: " + category);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为计费代理实例
|
||||
*/
|
||||
private boolean isBillingProxy(IChatService service) {
|
||||
return service instanceof BillingChatServiceProxy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ public class SSEEventSourceListener extends EventSourceListener {
|
||||
emitter.complete();
|
||||
// 清理失败回调(以 emitter 为键)
|
||||
RetryNotifier.clear(emitter);
|
||||
// 🔥 注释:AI回复的保存和计费已由BillingChatServiceProxy统一处理,此处代码已废弃
|
||||
/*
|
||||
// 扣除费用
|
||||
ChatRequest chatRequest = new ChatRequest();
|
||||
// 设置对话角色
|
||||
@@ -97,6 +99,7 @@ public class SSEEventSourceListener extends EventSourceListener {
|
||||
// 先保存助手消息,再发布异步计费事件
|
||||
chatCostService.saveMessage(chatRequest);
|
||||
chatCostService.publishBillingEvent(chatRequest);
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,4 +61,12 @@ public interface IChatCostService {
|
||||
* 获取登录用户id
|
||||
*/
|
||||
Long getUserId();
|
||||
|
||||
/**
|
||||
* 检查用户余额是否足够支付预估费用
|
||||
*
|
||||
* @param chatRequest 对话信息
|
||||
* @return true=余额充足,false=余额不足
|
||||
*/
|
||||
boolean checkBalanceSufficient(ChatRequest chatRequest);
|
||||
}
|
||||
|
||||
@@ -428,4 +428,29 @@ public class ChatCostServiceImpl implements IChatCostService {
|
||||
}
|
||||
return loginUser.getUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户余额是否足够支付预估费用
|
||||
*/
|
||||
@Override
|
||||
public boolean checkBalanceSufficient(ChatRequest chatRequest) {
|
||||
if (chatRequest.getUserId() == null) {
|
||||
log.warn("checkBalanceSufficient->用户ID为空,视为余额不足");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 重用现有的预检查逻辑,但不抛异常,只返回boolean
|
||||
preCheckBalance(chatRequest);
|
||||
return true; // 预检查通过,余额充足
|
||||
} catch (ServiceException e) {
|
||||
log.debug("checkBalanceSufficient->余额不足,用户ID: {}, 模型: {}, 错误: {}",
|
||||
chatRequest.getUserId(), chatRequest.getModel(), e.getMessage());
|
||||
return false; // 预检查失败,余额不足
|
||||
} catch (Exception e) {
|
||||
log.error("checkBalanceSufficient->检查余额时发生异常,用户ID: {}, 模型: {}",
|
||||
chatRequest.getUserId(), chatRequest.getModel(), e);
|
||||
return false; // 异常情况视为余额不足,保守处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,12 +110,12 @@ public class DifyServiceImpl implements IChatService {
|
||||
}
|
||||
|
||||
// 设置对话角色
|
||||
chatRequestResponse.setRole(Message.Role.ASSISTANT.getName());
|
||||
chatRequestResponse.setModel(chatRequest.getModel());
|
||||
chatRequestResponse.setUserId(chatRequest.getUserId());
|
||||
chatRequestResponse.setSessionId(chatRequest.getSessionId());
|
||||
chatRequestResponse.setPrompt(respMessage.toString());
|
||||
chatCostService.deductToken(chatRequestResponse);
|
||||
// chatRequestResponse.setRole(Message.Role.ASSISTANT.getName());
|
||||
// chatRequestResponse.setModel(chatRequest.getModel());
|
||||
// chatRequestResponse.setUserId(chatRequest.getUserId());
|
||||
// chatRequestResponse.setSessionId(chatRequest.getSessionId());
|
||||
// chatRequestResponse.setPrompt(respMessage.toString());
|
||||
// chatCostService.deductToken(chatRequestResponse);
|
||||
RetryNotifier.clear(emitter);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,8 +116,6 @@ public class SseServiceImpl implements ISseService {
|
||||
}
|
||||
|
||||
|
||||
// 先保存消息,再发布异步计费事件
|
||||
chatCostService.saveMessage(chatRequest);
|
||||
|
||||
chatRequest.setUserId(chatCostService.getUserId());
|
||||
if (chatRequest.getSessionId() == null) {
|
||||
@@ -128,11 +126,15 @@ public class SseServiceImpl implements ISseService {
|
||||
chatSessionService.insertByBo(chatSessionBo);
|
||||
chatRequest.setSessionId(chatSessionBo.getId());
|
||||
}
|
||||
|
||||
// 保存用户消息
|
||||
chatCostService.saveMessage(chatRequest);
|
||||
}
|
||||
// 自动选择模型并获取对应的聊天服务
|
||||
IChatService chatService = autoSelectModelAndGetService(chatRequest);
|
||||
chatCostService.publishBillingEvent(chatRequest);
|
||||
// 仅当 autoSelectModel = true 时,才启用重试与降级
|
||||
|
||||
// 用户消息只保存不计费,AI回复由BillingChatServiceProxy自动处理计费
|
||||
// chatCostService.publishBillingEvent(chatRequest); // 用户输入不计费
|
||||
if (Boolean.TRUE.equals(chatRequest.getAutoSelectModel())) {
|
||||
ChatModelVo currentModel = this.chatModelVo;
|
||||
String currentCategory = currentModel.getCategory();
|
||||
|
||||
Reference in New Issue
Block a user