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:
Administrator
2025-09-04 15:37:52 +08:00
parent 1e4af3d01b
commit c7554d7e35
6 changed files with 77 additions and 11 deletions

View File

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

View File

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

View File

@@ -61,4 +61,12 @@ public interface IChatCostService {
* 获取登录用户id
*/
Long getUserId();
/**
* 检查用户余额是否足够支付预估费用
*
* @param chatRequest 对话信息
* @return true=余额充足false=余额不足
*/
boolean checkBalanceSufficient(ChatRequest chatRequest);
}

View File

@@ -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; // 异常情况视为余额不足,保守处理
}
}
}

View File

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

View File

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