diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/ChatServiceFactory.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/ChatServiceFactory.java index f1e88a9a..8fec93e9 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/ChatServiceFactory.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/ChatServiceFactory.java @@ -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 chatServiceMap = new ConcurrentHashMap<>(); + private IChatCostService chatCostService; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + // 获取计费服务 + this.chatCostService = applicationContext.getBean(IChatCostService.class); + // 初始化时收集所有IChatService的实现 Map 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; + } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java index ceeb9e7b..52e94b9e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java @@ -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; } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java index 36104f9f..ccdd0617 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java @@ -61,4 +61,12 @@ public interface IChatCostService { * 获取登录用户id */ Long getUserId(); + + /** + * 检查用户余额是否足够支付预估费用 + * + * @param chatRequest 对话信息 + * @return true=余额充足,false=余额不足 + */ + boolean checkBalanceSufficient(ChatRequest chatRequest); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java index 9c1cf04f..16507b94 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java @@ -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; // 异常情况视为余额不足,保守处理 + } + } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java index 66b32e6f..43608516 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java @@ -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); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index eb9dba8b..0250177a 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -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();