From c7554d7e35c32f8aa98dd7ba4fdef04e77d80fc5 Mon Sep 17 00:00:00 2001 From: Administrator <1037463791@qq.com> Date: Thu, 4 Sep 2025 15:37:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(billing):=201.=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=AE=A1=E8=B4=B9=E4=BB=A3=E7=90=86=20Billin?= =?UTF-8?q?gChatServiceProxy=E4=BD=8D=E7=BD=AE=EF=BC=9Aruoyi-modules/ruoyi?= =?UTF-8?q?-chat/src/main/java/org/ruoyi/chat/service/chat/proxy/BillingCh?= =?UTF-8?q?atServiceProxy.java=20=E4=BD=9C=E7=94=A8=EF=BC=9A=E4=B8=BA?= =?UTF-8?q?=E6=89=80=E6=9C=89ChatService=E5=AE=9E=E7=8E=B0=E7=B1=BB?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E9=80=8F=E6=98=8E=E7=9A=84=E8=AE=A1=E8=B4=B9?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=8C=85=E8=A3=85=20=20=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=20=20=20AI=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E5=89=8D=E4=BD=99=E9=A2=9D=E9=A2=84=E6=A3=80=E6=9F=A5=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E6=97=A0=E6=95=88=E6=B6=88=E8=80=97=20=20=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=94=B6=E9=9B=86AI=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E5=86=85=E5=AE=B9=20=20=20=E7=BB=9F=E4=B8=80=E5=A4=84=E7=90=86?= =?UTF-8?q?AI=E5=9B=9E=E5=A4=8D=E7=9A=84=E4=BF=9D=E5=AD=98=E5=92=8C?= =?UTF-8?q?=E8=AE=A1=E8=B4=B9=20=20=20=20=E9=80=82=E9=85=8D=E5=A4=9A?= =?UTF-8?q?=E7=A7=8DAI=E6=9C=8D=E5=8A=A1=E7=9A=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=20=20=202.=20=E9=87=8D=E6=9E=84=E5=B7=A5?= =?UTF-8?q?=E5=8E=82=E7=B1=BB=20=20=20ChatServiceFactory=20=20=20=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=EF=BC=9A=E8=87=AA=E5=8A=A8=E4=B8=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?ChatService=E5=8C=85=E8=A3=85=E8=AE=A1=E8=B4=B9=E4=BB=A3?= =?UTF-8?q?=E7=90=86=20=20=E6=96=B0=E5=A2=9E=E6=96=B9=E6=B3=95=EF=BC=9Aget?= =?UTF-8?q?OriginalService()=20=E7=94=A8=E4=BA=8E=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=9C=AA=E5=8C=85=E8=A3=85=E7=9A=84=E5=8E=9F=E5=A7=8B=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=BC=98=E5=8A=BF=EF=BC=9A=E8=B0=83=E7=94=A8=E6=96=B9?= =?UTF-8?q?=E6=97=A0=E9=9C=80=E5=85=B3=E5=BF=83=E8=AE=A1=E8=B4=B9=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=AE=8C=E5=85=A8=E9=80=8F=E6=98=8E=20=203.?= =?UTF-8?q?=20=E5=A2=9E=E5=BC=BA=E8=AE=A1=E8=B4=B9=E6=9C=8D=E5=8A=A1=20ICh?= =?UTF-8?q?atCostService=20=E6=8E=A5=E5=8F=A3=20=20=20=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=96=B9=E6=B3=95=EF=BC=9AcheckBalanceSufficient()=20?= =?UTF-8?q?-=20=E4=BD=99=E9=A2=9D=E9=A2=84=E6=A3=80=E6=9F=A5=20=20=20=20?= =?UTF-8?q?=E5=88=86=E7=A6=BB=E5=85=B3=E6=B3=A8=E7=82=B9=EF=BC=9AsaveMessa?= =?UTF-8?q?ge()=20-=20=E4=BB=85=E4=BF=9D=E5=AD=98=E6=B6=88=E6=81=AF=20=20?= =?UTF-8?q?=20=20=20publishBillingEvent()=20-=20=E4=BB=85=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E8=AE=A1=E8=B4=B9=E4=BA=8B=E4=BB=B6=20=20=20=20=20ded?= =?UTF-8?q?uctToken()=20-=20=E4=BB=85=E6=89=A7=E8=A1=8C=E8=AE=A1=E8=B4=B9?= =?UTF-8?q?=E6=89=A3=E8=B4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/factory/ChatServiceFactory.java | 30 ++++++++++++++++++- .../chat/listener/SSEEventSourceListener.java | 3 ++ .../chat/service/chat/IChatCostService.java | 8 +++++ .../chat/impl/ChatCostServiceImpl.java | 25 ++++++++++++++++ .../service/chat/impl/DifyServiceImpl.java | 12 ++++---- .../service/chat/impl/SseServiceImpl.java | 10 ++++--- 6 files changed, 77 insertions(+), 11 deletions(-) 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();