43 Commits

Author SHA1 Message Date
evo
13da60e151 Merge pull request #189 from MuSan-Li/feature_20250904_fix_sql
feat: 添加session表会话ID
2025-09-04 17:02:06 +08:00
l90215
1f0c0ba0a9 feat: 添加session表会话ID 2025-09-04 17:01:13 +08:00
ageerle
ef3541fe77 Merge pull request #188 from xiaonieli7/feature_20250813_fix_codeOptimization
补充统一计费代理类BillingChatServiceProxy
2025-09-04 16:44:52 +08:00
Administrator
2b5fd810a4 fix(billing): 统一计费代理类BillingChatServiceProxy 2025-09-04 16:41:14 +08:00
Administrator
4a8d21a742 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() - 仅执行计费扣费
2025-09-04 16:35:55 +08:00
ageerle
c62530176f Merge pull request #187 from xiaonieli7/feature_20250813_fix_codeOptimization
修改了计费逻辑
2025-09-04 15:43:48 +08:00
Administrator
c7554d7e35 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() - 仅执行计费扣费
2025-09-04 15:37:52 +08:00
Administrator
1e4af3d01b fix(billing): 修复Token计费逻辑和消息更新机制
* 修复Token计费算法:按批次计费而非Token数量计费
* 添加ChatRequest.messageId字段支持消息关联更新
* 优化消息保存流程:分离基础信息保存和计费信息更新
* 修复预检查逻辑:统一预检查和实际扣费计算方式
* 调整Token阈值:100 → 1000,减少扣费频次
* 完善事件传递:ChatMessageCreatedEvent增加messageId

Fixes: 余额预检查误判、消息计费信息缺失、Token计费不准确
2025-08-29 15:19:37 +08:00
Administrator
1e3b49c9b8 用户发送消息 → 预检查余额 → 保存用户消息 → 发布计费事件 → 异步扣费 → 保存账单记录
添加了billingType计费类型字段消息保存的时候写入进去
2025-08-27 16:48:48 +08:00
Administrator
9f7f00e50c 用户发送消息 → 预检查余额 → 保存用户消息 → 发布计费事件 → 异步扣费 → 保存账单记录
添加了billingType计费类型字段消息保存的时候写入进去
2025-08-27 15:30:59 +08:00
Administrator
1c721981db Merge remote-tracking branch 'origin/main'
# Conflicts:
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java
#	ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java
2025-08-27 10:49:37 +08:00
AmAzing129
6e6ba84fd2 docs: add contributors bubble 2025-08-27 10:47:34 +08:00
likunlong
ef69778bb7 feat: 下掉模型能力逻辑代码; 2025-08-27 10:47:34 +08:00
l90215
7a374d877b feat: 调整知识库问答接入提示词模板 2025-08-27 10:47:34 +08:00
likunlong
43426054ec feat: 兼容不选自动模型时的原先逻辑;封装通用方法,简化创建有监控的SSE,简化流式错误输出并通知重试; 2025-08-27 10:47:31 +08:00
likunlong
ccdbb20935 feat: 不选择模型自动选择时走原始默认逻辑; 2025-08-27 10:47:29 +08:00
likunlong
4b37cfe97d feat: 失败回调器中使用emitter对象的唯一hash作为key,不再使用session,不与业务进行绑定,同时也保证跨线程调用的正确性; 2025-08-27 10:47:26 +08:00
likunlong
c43d4784de feat: 处理在非Web线程中获取Request中token失败的问题; 2025-08-27 10:47:25 +08:00
likunlong
359cee28d5 feat: 修改目前实现类使用统一重试降级逻辑; 2025-08-27 10:47:22 +08:00
likunlong
aa11c1f233 feat: 问答时添加统一重试和降级逻辑; 2025-08-27 10:47:20 +08:00
likunlong
a0d029c142 feat: 自动设置请求参数中的模型名称; 2025-08-27 10:47:20 +08:00
likunlong
6ce52befe2 feat: 根据是否有附件和是否自动,自动选择模型并且获取服务; 2025-08-27 10:47:19 +08:00
likunlong
330bdc3761 feat: 数据库chat_model添加优先级字段; 2025-08-27 10:47:19 +08:00
likunlong
4f7ad59e46 feat: 添加自动获取高优先级模型和服务的逻辑; 2025-08-27 10:47:19 +08:00
l90215
b696fde881 feat: 合并代码 删除不需要的文件 2025-08-27 10:47:19 +08:00
fy53888
00f9a1a55b 修改字典下拉带查找功能 2025-08-27 10:47:19 +08:00
fy53888
a1c7b86e72 备分一下2 2025-08-27 10:47:19 +08:00
fy53888
62676a54fb 备分一下2 2025-08-27 10:47:19 +08:00
fy53888
e51425a951 备分一下 2025-08-27 10:47:17 +08:00
fy53888
268be2d9ec 更新后端生成類型 Integer出錯的問 2025-08-27 10:47:09 +08:00
fy53888
f448a18e44 更新后端生成類型 Integer出錯的問題 2025-08-27 10:47:07 +08:00
lixiang
22e59fe5a1 向量库sql查询去除匹配分值字段 2025-08-27 10:47:01 +08:00
violateer
0780e3b8c9 fix: 修改krole_group_ids字段名 2025-08-27 10:47:01 +08:00
violateer
0cdba56a07 feature: 添加开启知识库角色,用户可见个人知识库及角色分配知识库 2025-08-27 10:47:01 +08:00
l90215
ebc13c06af feat: 更新只是库角色默认不开启 2025-08-27 10:47:00 +08:00
l90215
416f011c73 feat: fix代码生成类型问题 2025-08-27 10:47:00 +08:00
likunlong
bfeb389171 feat: 获取模型接口支持返回模型能力;模型表增加模型能力字段; 2025-08-27 10:47:00 +08:00
violateer
caf7f14781 feature: 新增生成前端文件模板接口 2025-08-27 10:46:51 +08:00
l90215
a6eb98daab feat: fix代码生成类型问题 2025-08-27 10:46:37 +08:00
l90215
4834b615a6 feat: fix代码生成类型问题 2025-08-27 10:46:37 +08:00
l90215
42aabeed96 feat: 添加空格 2025-08-27 10:46:37 +08:00
Administrator
affdc5e3a6 问题概述
1.保存消息和计费逻辑存在耦合
2.修改计费逻辑:
按次计费被阈值限制:旧逻辑把 TIMES 分支放在 totalTokens ≥ 100 的大分支里,导致没到100 token时不扣费,违背“每次调用就扣费”的语义。
token累计不当:TIMES 分支只扣费不处理累计,同时在 totalTokens < 100 时不会进入任何TIMES逻辑,累计会无意义增长。
粒度不稳定:TOKEN 计费一旦达阈值就把 total 全扣完并清零,不利于对账与用户体验。
打印方式:使用 System.out.println,不利于生产追踪。

改动要点
1.新增独立方法
saveMessage(ChatRequest): 只落库。
publishBillingEvent(ChatRequest): 只发布异步计费事件。
保留组合方法 saveMessageAndPublishEvent(ChatRequest) 以便需要一行调用时使用。
调用处已改为“先保存,再发布事件”
SseServiceImpl: 先 saveMessage,再 publishBillingEvent。
SSEEventSourceListener: 同上。
DifyServiceImpl: 同上。

2.计费模式分流:
TIMES:每次调用直接扣费,不累计。
TOKEN:按阈值(100)批量扣费,保留余数,账单颗粒稳定。
保留余数:total = prev + delta;billable = floor(total/threshold)threshold;remainder = total % threshold。
日志替换:统一使用 log.debug。
结构更清晰、可维护。
所有金额计算统一用 BigDecimal,保留两位小数,RoundingMode.HALF_UP
按次计费:每次直接扣费(BigDecimal),边界转 Double
按 token 计费:按阈值批量结算,保留余数;费用=单价(BigDecimal)×可结算token数
1. 消息分类存储
用户消息:role="user", deductCost=null, totalTokens=本次token数, remark="用户消息"
系统账单:role="system", deductCost=实际扣费, totalTokens=计费token数, remark="TIMES_BILLING/TOKEN_BILLING"
2. 数据流程
用户发送消息 → 预检查余额 → 保存用户消息 → 发布计费事件 → 异步扣费 → 保存账单记录
2025-08-14 14:00:48 +08:00
Administrator
5a2e08f87d 问题概述
1.保存消息和计费逻辑存在耦合
2.修改计费逻辑:
按次计费被阈值限制:旧逻辑把 TIMES 分支放在 totalTokens ≥ 100 的大分支里,导致没到100 token时不扣费,违背“每次调用就扣费”的语义。
token累计不当:TIMES 分支只扣费不处理累计,同时在 totalTokens < 100 时不会进入任何TIMES逻辑,累计会无意义增长。
粒度不稳定:TOKEN 计费一旦达阈值就把 total 全扣完并清零,不利于对账与用户体验。
打印方式:使用 System.out.println,不利于生产追踪。
3.建议数据库不要存扣除金额和累计消耗token,消息表里不需要存“累计到目前为止多少”,否则每条消息都变成快照,既冗余又易不一致

改动要点
1.新增独立方法
saveMessage(ChatRequest): 只落库。
publishBillingEvent(ChatRequest): 只发布异步计费事件。
保留组合方法 saveMessageAndPublishEvent(ChatRequest) 以便需要一行调用时使用。
调用处已改为“先保存,再发布事件”
SseServiceImpl: 先 saveMessage,再 publishBillingEvent。
SSEEventSourceListener: 同上。
DifyServiceImpl: 同上。

2.计费模式分流:
TIMES:每次调用直接扣费,不累计。
TOKEN:按阈值(100)批量扣费,保留余数,账单颗粒稳定。
保留余数:total = prev + delta;billable = floor(total/threshold)threshold;remainder = total % threshold。
日志替换:统一使用 log.debug。
结构更清晰、可维护。
所有金额计算统一用 BigDecimal,保留两位小数,RoundingMode.HALF_UP
按次计费:每次直接扣费(BigDecimal),边界转 Double
按 token 计费:按阈值批量结算,保留余数;费用=单价(BigDecimal)×可结算token数
2025-08-08 13:39:37 +08:00
16 changed files with 804 additions and 87 deletions

View File

@@ -162,7 +162,7 @@ tenant:
- sys_user_role
knowledge-role:
enable: true
enable: false
# MyBatisPlus配置
# https://baomidou.com/config/

View File

@@ -82,4 +82,9 @@ public class ChatRequest {
*/
private String token;
/**
* 消息ID保存消息成功后设置用于后续扣费更新
*/
private Long messageId;
}

View File

@@ -69,5 +69,10 @@ public class ChatMessage extends BaseEntity {
*/
private String remark;
/**
* 计费类型1-token计费2-次数计费null-普通消息)
*/
private String billingType;
}

View File

@@ -75,5 +75,10 @@ public class ChatMessageBo extends BaseEntity {
@NotBlank(message = "备注不能为空", groups = { AddGroup.class, EditGroup.class })
private String remark;
/**
* 计费类型1-token计费2-次数计费null-普通消息)
*/
private String billingType;
}

View File

@@ -4,6 +4,8 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.ruoyi.common.excel.annotation.ExcelDictFormat;
import org.ruoyi.common.excel.convert.ExcelDictConvert;
import org.ruoyi.domain.ChatMessage;
import java.io.Serial;
@@ -73,6 +75,13 @@ public class ChatMessageVo implements Serializable {
@ExcelProperty(value = "模型名称")
private String modelName;
/**
* 计费类型1-token计费2-次数计费)
*/
@ExcelProperty(value = "计费类型", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_model_billing")
private String billingType;
/**
* 备注
*/
@@ -87,4 +96,5 @@ public class ChatMessageVo implements Serializable {
private Date createTime;
}

View File

@@ -0,0 +1,34 @@
package org.ruoyi.chat.event;
import org.springframework.context.ApplicationEvent;
/**
* 聊天消息创建事件(用于异步计费/累计等)
*/
public class ChatMessageCreatedEvent extends ApplicationEvent {
private final Long userId;
private final Long sessionId;
private final String modelName;
private final String role;
private final String content;
private final Long messageId;
public ChatMessageCreatedEvent(Long userId, Long sessionId, String modelName, String role, String content, Long messageId) {
super(userId);
this.userId = userId;
this.sessionId = sessionId;
this.modelName = modelName;
this.role = role;
this.content = content;
this.messageId = messageId;
}
public Long getUserId() { return userId; }
public Long getSessionId() { return sessionId; }
public String getModelName() { return modelName; }
public String getRole() { return role; }
public String getContent() { return content; }
public Long getMessageId() { return messageId; }
}

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

@@ -0,0 +1,49 @@
package org.ruoyi.chat.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.event.ChatMessageCreatedEvent;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.common.chat.request.ChatRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Slf4j
@Component
@RequiredArgsConstructor
public class BillingEventListener {
private final IChatCostService chatCostService;
@Async
@EventListener
public void onChatMessageCreated(ChatMessageCreatedEvent event) {
log.debug("BillingEventListener->接收到计费事件用户ID: {}会话ID: {},模型: {}",
event.getUserId(), event.getSessionId(), event.getModelName());
try {
ChatRequest chatRequest = new ChatRequest();
chatRequest.setUserId(event.getUserId());
chatRequest.setSessionId(event.getSessionId());
chatRequest.setModel(event.getModelName());
chatRequest.setRole(event.getRole());
chatRequest.setPrompt(event.getContent());
chatRequest.setMessageId(event.getMessageId()); // 设置消息ID
// 异步执行计费累计与扣费
log.debug("BillingEventListener->开始执行计费逻辑");
chatCostService.deductToken(chatRequest);
log.debug("BillingEventListener->计费逻辑执行完成");
} catch (Exception ex) {
// 由于已有预检查,这里的异常主要是系统异常(数据库连接等)
// 记录错误但不中断异步线程
log.error("BillingEventListener->异步计费异常用户ID: {},模型: {},错误: {}",
event.getUserId(), event.getModelName(), ex.getMessage(), ex);
// TODO: 可以考虑加入重试机制或者错误通知机制
// 例如:发送到死信队列,或者通知运维人员
}
}
}

View File

@@ -84,6 +84,8 @@ public class SSEEventSourceListener extends EventSourceListener {
emitter.complete();
// 清理失败回调(以 emitter 为键)
RetryNotifier.clear(emitter);
// 🔥 注释AI回复的保存和计费已由BillingChatServiceProxy统一处理此处代码已废弃
/*
// 扣除费用
ChatRequest chatRequest = new ChatRequest();
// 设置对话角色
@@ -94,7 +96,10 @@ public class SSEEventSourceListener extends EventSourceListener {
chatRequest.setPrompt(stringBuffer.toString());
// 记录会话token
BaseContext.setCurrentToken(token);
chatCostService.deductToken(chatRequest);
// 先保存助手消息,再发布异步计费事件
chatCostService.saveMessage(chatRequest);
chatCostService.publishBillingEvent(chatRequest);
*/
return;
}

View File

@@ -19,6 +19,22 @@ public interface IChatCostService {
void deductToken(ChatRequest chatRequest);
/**
* 保存聊天消息记录(不进行计费)
*
* @param chatRequest 对话信息
*/
void saveMessage(ChatRequest chatRequest);
/**
* 仅发布异步计费事件(不做入库)
*
* @param chatRequest 对话信息
*/
void publishBillingEvent(ChatRequest chatRequest);
/**
* 直接扣除用户的余额
*
@@ -45,4 +61,12 @@ public interface IChatCostService {
* 获取登录用户id
*/
Long getUserId();
/**
* 检查用户余额是否足够支付预估费用
*
* @param chatRequest 对话信息
* @return true=余额充足false=余额不足
*/
boolean checkBalanceSufficient(ChatRequest chatRequest);
}

View File

@@ -3,7 +3,10 @@ package org.ruoyi.chat.service.chat.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.ruoyi.chat.enums.BillingType;
import org.ruoyi.chat.event.ChatMessageCreatedEvent;
import org.ruoyi.chat.enums.UserGradeType;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.common.chat.request.ChatRequest;
@@ -20,6 +23,7 @@ import org.ruoyi.service.IChatModelService;
import org.ruoyi.service.IChatTokenService;
import org.ruoyi.system.domain.SysUser;
import org.ruoyi.system.mapper.SysUserMapper;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@@ -42,108 +46,309 @@ public class ChatCostServiceImpl implements IChatCostService {
private final IChatModelService chatModelService;
private final ApplicationEventPublisher eventPublisher;
/**
* 扣除用户余额
* 扣除用户余额(仅计费与累计,不保存消息)
*/
@Override
public void deductToken(ChatRequest chatRequest) {
if(chatRequest.getUserId()==null || chatRequest.getSessionId()==null){
if (chatRequest.getUserId() == null) {
log.warn("deductToken->用户ID为空跳过计费");
return;
}
int tokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt());
System.out.println("deductToken->本次提交token数 : "+tokens);
log.debug("deductToken->本次提交token数: {}", tokens);
String modelName = chatRequest.getModel();
ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName);
BigDecimal unitPrice = BigDecimal.valueOf(chatModelVo.getModelPrice());
ChatMessageBo chatMessageBo = new ChatMessageBo();
// 按次计费每次调用都直接扣费不累计token
if (BillingType.TIMES.getCode().equals(chatModelVo.getModelType())) {
BigDecimal numberCost = unitPrice.setScale(2, RoundingMode.HALF_UP);
deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue());
log.debug("deductToken->按次数扣费,费用: {},模型: {}", numberCost, modelName);
// 设置用户id
chatMessageBo.setUserId(chatRequest.getUserId());
// 设置会话id
chatMessageBo.setSessionId(chatRequest.getSessionId());
// 清理可能存在的历史累计token模型计费方式可能发生过变更
ChatUsageToken existingToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName);
if (existingToken != null && existingToken.getToken() > 0) {
existingToken.setToken(0);
chatTokenService.editToken(existingToken);
log.debug("deductToken->按次计费清理历史累计token: {}", existingToken.getToken());
}
// 设置对话角色
chatMessageBo.setRole(chatRequest.getRole());
// 更新消息的计费信息到备注
updateMessageBilling(chatRequest, tokens, numberCost.doubleValue(), chatModelVo.getModelType());
return;
}
// 设置对话内容
chatMessageBo.setContent(chatRequest.getPrompt());
// 设置模型名字
chatMessageBo.setModelName(chatRequest.getModel());
// 按token计费累加并按阈值批量扣费保留余数
final int threshold = 1000;
// 获得记录的累计token数
ChatUsageToken chatToken = chatTokenService.queryByUserId(chatMessageBo.getUserId(), modelName);
// TODO: 这里存在并发竞态条件需要在chatTokenService层面添加乐观锁或分布式锁
ChatUsageToken chatToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName);
if (chatToken == null) {
chatToken = new ChatUsageToken();
chatToken.setToken(0);
chatToken.setModelName(modelName);
chatToken.setUserId(chatRequest.getUserId());
}
// 计算总token数
int totalTokens = chatToken.getToken() + tokens;
int previousUnpaid = chatToken.getToken();
int totalTokens = previousUnpaid + tokens;
log.debug("deductToken->未付费token数: {},本次累计后总数: {}", previousUnpaid, totalTokens);
//当前未付费token
int token = chatToken.getToken();
int billable = (totalTokens / threshold) * threshold; // 可计费整批token
int remainder = totalTokens - billable; // 结算后保留的余数
System.out.println("deductToken->未付费的token数 : "+token);
System.out.println("deductToken->本次提交+未付费token数 : "+totalTokens);
if (billable > 0) {
// 计算批次数每1000个Token为一批每批扣费单价
int batches = billable / threshold;
BigDecimal numberCost = unitPrice
.multiply(BigDecimal.valueOf(batches))
.setScale(2, RoundingMode.HALF_UP);
log.debug("deductToken->按token扣费结算token数量: {},批次数: {},单价: {},费用: {}",
billable, batches, unitPrice, numberCost);
//扣费核心逻辑总token大于100就要对未结清的token进行扣费
if (totalTokens >= 100) {// 如果总token数大于等于100,进行费用扣除
ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName);
double cost = chatModelVo.getModelPrice();
if (BillingType.TIMES.getCode().equals(chatModelVo.getModelType())) {
// 按次数扣费
deductUserBalance(chatMessageBo.getUserId(), cost);
chatMessageBo.setDeductCost(cost);
}else {
// 按token扣费
Double numberCost = totalTokens * cost;
System.out.println("deductToken->按token扣费 计算token数量: "+totalTokens);
System.out.println("deductToken->按token扣费 每token的价格: "+cost);
deductUserBalance(chatMessageBo.getUserId(), numberCost);
chatMessageBo.setDeductCost(numberCost);
// 保存剩余tokens
try {
// 先尝试扣费
deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue());
// 扣费成功后,保存余数
chatToken.setModelName(modelName);
chatToken.setUserId(chatMessageBo.getUserId());
chatToken.setToken(0);//因为判断大于100token直接全部计算扣除了所以这里直接=0就可以了
chatToken.setUserId(chatRequest.getUserId());
chatToken.setToken(remainder);
chatTokenService.editToken(chatToken);
log.debug("deductToken->扣费成功,更新余数: {}", remainder);
// 更新消息的计费信息到备注
updateMessageBilling(chatRequest, billable, numberCost.doubleValue(), chatModelVo.getModelType());
} catch (ServiceException e) {
// 余额不足时不更新token累计保持原有累计数
log.warn("deductToken->余额不足本次token累计保持不变: {}", totalTokens);
throw e; // 重新抛出异常
}
} else {
//不满100Token,不需要进行扣费啊啊啊
//deductUserBalance(chatMessageBo.getUserId(), 0.0);
chatMessageBo.setDeductCost(0d);
chatMessageBo.setRemark("不满100Token,计入下一次!");
System.out.println("deductToken->不满100Token,计入下一次!");
// 未达阈值累积token
log.debug("deductToken->未达到计费阈值({}),累积到下次", threshold);
chatToken.setModelName(modelName);
chatToken.setUserId(chatRequest.getUserId());
chatToken.setToken(totalTokens);
chatToken.setModelName(chatMessageBo.getModelName());
chatToken.setUserId(chatMessageBo.getUserId());
chatTokenService.editToken(chatToken);
// 虽未扣费但要更新消息的基本信息实际token数、计费类型等
updateMessageWithoutBilling(chatRequest, tokens, chatModelVo.getModelType());
}
}
/**
* 保存聊天消息记录(不进行计费)
* 保存成功后将消息ID设置到ChatRequest中供后续扣费使用
*/
@Override
public void saveMessage(ChatRequest chatRequest) {
if (chatRequest.getUserId() == null || chatRequest.getSessionId() == null) {
log.warn("saveMessage->用户ID或会话ID为空跳过保存消息");
return;
}
// 验证消息内容
if (chatRequest.getPrompt() == null || chatRequest.getPrompt().trim().isEmpty()) {
log.warn("saveMessage->消息内容为空,跳过保存");
return;
}
ChatMessageBo chatMessageBo = new ChatMessageBo();
chatMessageBo.setUserId(chatRequest.getUserId());
chatMessageBo.setSessionId(chatRequest.getSessionId());
chatMessageBo.setRole(chatRequest.getRole());
chatMessageBo.setContent(chatRequest.getPrompt().trim());
chatMessageBo.setModelName(chatRequest.getModel());
// // 基础消息信息计费相关数据tokens、费用、计费类型等在扣费时统一设置
// chatMessageBo.setTotalTokens(0); // 初始设为0扣费时更新
// chatMessageBo.setDeductCost(null);
// chatMessageBo.setBillingType(null);
// chatMessageBo.setRemark("用户消息");
// 保存消息记录
chatMessageService.insertByBo(chatMessageBo);
System.out.println("deductToken->chatMessageService.insertByBo(: "+chatMessageBo);
System.out.println("----------------------------------------");
try {
chatMessageService.insertByBo(chatMessageBo);
// 保存成功后将生成的消息ID设置到ChatRequest中
chatRequest.setMessageId(chatMessageBo.getId());
log.debug("saveMessage->成功保存消息消息ID: {}, 用户ID: {}, 会话ID: {}",
chatMessageBo.getId(), chatRequest.getUserId(), chatRequest.getSessionId());
} catch (Exception e) {
log.error("saveMessage->保存消息失败", e);
throw new ServiceException("保存消息失败");
}
}
@Override
public void publishBillingEvent(ChatRequest chatRequest) {
log.debug("publishBillingEvent->发布计费事件用户ID: {}会话ID: {},模型: {}",
chatRequest.getUserId(), chatRequest.getSessionId(), chatRequest.getModel());
// 预检查:评估可能的扣费金额,如果余额不足则直接抛异常
try {
preCheckBalance(chatRequest);
} catch (ServiceException e) {
log.warn("publishBillingEvent->预检查余额不足用户ID: {},模型: {}",
chatRequest.getUserId(), chatRequest.getModel());
throw e; // 直接抛出,阻止消息保存和对话继续
}
eventPublisher.publishEvent(new ChatMessageCreatedEvent(
chatRequest.getUserId(),
chatRequest.getSessionId(),
chatRequest.getModel(),
chatRequest.getRole(),
chatRequest.getPrompt(),
chatRequest.getMessageId()
));
log.debug("publishBillingEvent->计费事件发布完成");
}
/**
* 预检查用户余额是否足够支付可能的费用
*/
private void preCheckBalance(ChatRequest chatRequest) {
if (chatRequest.getUserId() == null) {
return;
}
int tokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt());
String modelName = chatRequest.getModel();
ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName);
BigDecimal unitPrice = BigDecimal.valueOf(chatModelVo.getModelPrice());
// 按次计费:直接检查单次费用
if (BillingType.TIMES.getCode().equals(chatModelVo.getModelType())) {
BigDecimal numberCost = unitPrice.setScale(2, RoundingMode.HALF_UP);
checkUserBalanceWithoutDeduct(chatRequest.getUserId(), numberCost.doubleValue());
return;
}
// 按token计费检查累计后可能的费用
final int threshold = 1000;
ChatUsageToken chatToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName);
int previousUnpaid = (chatToken == null) ? 0 : chatToken.getToken();
int totalTokens = previousUnpaid + tokens;
int billable = (totalTokens / threshold) * threshold;
if (billable > 0) {
// 计算批次数每1000个Token为一批每批扣费单价
int batches = billable / threshold;
BigDecimal numberCost = unitPrice
.multiply(BigDecimal.valueOf(batches))
.setScale(2, RoundingMode.HALF_UP);
checkUserBalanceWithoutDeduct(chatRequest.getUserId(), numberCost.doubleValue());
}
}
/**
* 检查用户余额是否足够,但不扣除
*/
private void checkUserBalanceWithoutDeduct(Long userId, Double numberCost) {
SysUser sysUser = sysUserMapper.selectById(userId);
if (sysUser == null) {
throw new ServiceException("用户不存在");
}
BigDecimal userBalance = BigDecimal.valueOf(sysUser.getUserBalance() == null ? 0D : sysUser.getUserBalance())
.setScale(2, RoundingMode.HALF_UP);
BigDecimal cost = BigDecimal.valueOf(numberCost == null ? 0D : numberCost)
.setScale(2, RoundingMode.HALF_UP);
if (userBalance.compareTo(cost) < 0 || userBalance.compareTo(BigDecimal.ZERO) == 0) {
throw new ServiceException("余额不足, 请充值。当前余额: " + userBalance + ",需要: " + cost);
}
}
/**
* 更新消息的基本信息(不涉及扣费)
*/
private void updateMessageWithoutBilling(ChatRequest chatRequest, int actualTokens, String billingTypeCode) {
// 检查是否有消息ID可以更新
if (chatRequest.getMessageId() == null) {
log.warn("updateMessageWithoutBilling->消息ID为空无法更新基本信息");
return;
}
try {
// 创建更新对象,只更新基本信息,不涉及扣费
ChatMessageBo updateMessage = new ChatMessageBo();
updateMessage.setId(chatRequest.getMessageId());
updateMessage.setTotalTokens(actualTokens); // 设置实际token数
updateMessage.setBillingType(billingTypeCode); // 设置计费类型
updateMessage.setRemark("用户消息(累计中,未达扣费阈值)"); // 说明状态
// 更新消息
chatMessageService.updateByBo(updateMessage);
log.debug("updateMessageWithoutBilling->更新消息基本信息成功消息ID: {}, 实际tokens: {}, 计费类型: {}",
chatRequest.getMessageId(), actualTokens, billingTypeCode);
} catch (Exception e) {
log.error("updateMessageWithoutBilling->更新消息基本信息失败消息ID: {}", chatRequest.getMessageId(), e);
// 更新失败不影响主流程,只记录错误日志
}
}
/**
* 更新消息的计费信息到备注字段
*/
private void updateMessageBilling(ChatRequest chatRequest, int billedTokens, double cost, String billingTypeCode) {
// 检查是否有消息ID可以更新
if (chatRequest.getMessageId() == null) {
log.warn("updateMessageBilling->消息ID为空无法更新计费信息");
return;
}
try {
// 计算本次消息的实际token数
int actualTokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt());
// 构建计费信息
String billingInfo = buildBillingInfo(billingTypeCode, billedTokens, cost);
// 创建更新对象
ChatMessageBo updateMessage = new ChatMessageBo();
updateMessage.setId(chatRequest.getMessageId());
updateMessage.setTotalTokens(actualTokens); // 设置实际token数
updateMessage.setDeductCost(cost);
updateMessage.setRemark(billingInfo);
updateMessage.setBillingType(billingTypeCode);
// 更新消息
chatMessageService.updateByBo(updateMessage);
log.debug("updateMessageBilling->更新消息计费信息成功消息ID: {}, 实际tokens: {}, 计费tokens: {}, 费用: {}",
chatRequest.getMessageId(), actualTokens, billedTokens, cost);
} catch (Exception e) {
log.error("updateMessageBilling->更新消息计费信息失败消息ID: {}", chatRequest.getMessageId(), e);
// 更新失败不影响主流程,只记录错误日志
}
}
/**
* 构建计费信息字符串
*/
private String buildBillingInfo(String billingTypeCode, int billedTokens, double cost) {
// 使用枚举获取计费类型并构建计费信息
BillingType billingType = BillingType.fromCode(billingTypeCode);
if (billingType != null) {
return switch (billingType) {
case TIMES -> String.format("%s消耗 %d tokens扣费 %.2f 元", billingType.getDescription(), billedTokens, cost);
case TOKEN -> String.format("%s结算 %d tokens扣费 %.2f 元", billingType.getDescription(), billedTokens, cost);
};
} else {
return String.format("系统计费:处理 %d tokens扣费 %.2f 元", billedTokens, cost);
}
}
/**
* 从用户余额中扣除费用
*
@@ -158,22 +363,26 @@ public class ChatCostServiceImpl implements IChatCostService {
return;
}
Double userBalance = sysUser.getUserBalance();
BigDecimal userBalance = BigDecimal.valueOf(sysUser.getUserBalance() == null ? 0D : sysUser.getUserBalance())
.setScale(2, RoundingMode.HALF_UP);
BigDecimal cost = BigDecimal.valueOf(numberCost == null ? 0D : numberCost)
.setScale(2, RoundingMode.HALF_UP);
log.debug("deductUserBalance->准备扣除: {},当前余额: {}", cost, userBalance);
System.out.println("deductUserBalance->准备扣除numberCost: "+numberCost);
System.out.println("deductUserBalance->剩余金额userBalance: "+userBalance);
if (userBalance < numberCost || userBalance == 0) {
if (userBalance.compareTo(cost) < 0 || userBalance.compareTo(BigDecimal.ZERO) == 0) {
throw new ServiceException("余额不足, 请充值");
}
BigDecimal newBalance = userBalance.subtract(cost);
if (newBalance.compareTo(BigDecimal.ZERO) < 0) {
newBalance = BigDecimal.ZERO;
}
newBalance = newBalance.setScale(2, RoundingMode.HALF_UP);
sysUserMapper.update(null,
new LambdaUpdateWrapper<SysUser>()
.set(SysUser::getUserBalance, Math.max(userBalance - numberCost, 0))
.set(SysUser::getUserBalance, newBalance.doubleValue())
.eq(SysUser::getUserId, userId));
}
@@ -193,6 +402,7 @@ public class ChatCostServiceImpl implements IChatCostService {
chatMessageBo.setContent(prompt);
chatMessageBo.setDeductCost(cost);
chatMessageBo.setTotalTokens(0);
chatMessageBo.setRemark(String.format("任务计费:%s扣费 %.2f 元", type, cost));
chatMessageService.insertByBo(chatMessageBo);
}
@@ -218,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

@@ -91,6 +91,8 @@ public class DifyServiceImpl implements IChatService {
public void onMessageEnd(MessageEndEvent event) {
emitter.complete();
log.info("消息结束完整消息ID: {}", event.getMessageId());
// 扣除费用
ChatRequest chatRequestResponse = new ChatRequest();
// 更新conversationId
if (StrUtil.isBlank(sessionInfo.getConversationId())) {
String conversationId = event.getConversationId();
@@ -104,16 +106,16 @@ public class DifyServiceImpl implements IChatService {
chatSessionBo.setSessionContent(sessionInfo.getSessionContent());
chatSessionBo.setRemark(sessionInfo.getRemark());
chatSessionService.updateByBo(chatSessionBo);
chatRequestResponse.setMessageId(chatSessionBo.getId());
}
// 扣除费用
ChatRequest chatRequestResponse = new ChatRequest();
// 设置对话角色
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,7 @@ public class SseServiceImpl implements ISseService {
}
// 保存消息记录 并扣除费用
chatCostService.deductToken(chatRequest);
chatRequest.setUserId(chatCostService.getUserId());
if (chatRequest.getSessionId() == null) {
ChatSessionBo chatSessionBo = new ChatSessionBo();
@@ -127,11 +126,15 @@ public class SseServiceImpl implements ISseService {
chatSessionService.insertByBo(chatSessionBo);
chatRequest.setSessionId(chatSessionBo.getId());
}
// 保存用户消息
chatCostService.saveMessage(chatRequest);
}
// 自动选择模型并获取对应的聊天服务
IChatService chatService = autoSelectModelAndGetService(chatRequest);
// 仅当 autoSelectModel = true 时,才启用重试与降级
// 用户消息只保存不计费AI回复由BillingChatServiceProxy自动处理计费
// chatCostService.publishBillingEvent(chatRequest); // 用户输入不计费
if (Boolean.TRUE.equals(chatRequest.getAutoSelectModel())) {
ChatModelVo currentModel = this.chatModelVo;
String currentCategory = currentModel.getCategory();

View File

@@ -0,0 +1,305 @@
package org.ruoyi.chat.service.chat.proxy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.common.chat.utils.TikTokensUtil;
import org.ruoyi.common.core.service.BaseContext;
import org.ruoyi.domain.bo.ChatMessageBo;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
* 统一计费代理类
* 自动处理所有ChatService的AI回复保存和计费逻辑
*
*/
@Slf4j
@RequiredArgsConstructor
public class BillingChatServiceProxy implements IChatService {
private final IChatService delegate;
private final IChatCostService chatCostService;
@Override
public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) {
// 🔥 在AI回复开始前检查余额是否充足
if (!chatCostService.checkBalanceSufficient(chatRequest)) {
String errorMsg = "余额不足无法使用AI服务请充值后再试";
log.warn("余额不足阻止AI回复用户ID: {}, 模型: {}",
chatRequest.getUserId(), chatRequest.getModel());
throw new RuntimeException(errorMsg);
}
log.debug("余额检查通过开始AI回复用户ID: {}, 模型: {}",
chatRequest.getUserId(), chatRequest.getModel());
// 创建增强的SseEmitter自动收集AI回复
BillingSseEmitter billingEmitter = new BillingSseEmitter(emitter, chatRequest, chatCostService);
try {
// 调用实际的聊天服务
return delegate.chat(chatRequest, billingEmitter);
} catch (Exception e) {
log.error("聊天服务执行失败", e);
throw e;
}
}
@Override
public String getCategory() {
return delegate.getCategory();
}
/**
* 增强的SseEmitter自动处理AI回复的保存和计费
*/
private static class BillingSseEmitter extends SseEmitter {
private final SseEmitter delegate;
private final ChatRequest chatRequest;
private final IChatCostService chatCostService;
private final StringBuilder aiResponseBuilder = new StringBuilder();
private final AtomicBoolean completed = new AtomicBoolean(false);
public BillingSseEmitter(SseEmitter delegate, ChatRequest chatRequest, IChatCostService chatCostService) {
super(delegate.getTimeout());
this.delegate = delegate;
this.chatRequest = chatRequest;
this.chatCostService = chatCostService;
}
@Override
public void send(Object object) throws IOException {
// 先发送给前端
delegate.send(object);
// 提取AI回复内容并累积
String content = extractContentFromSseData(object);
if (content != null && !content.trim().isEmpty()) {
aiResponseBuilder.append(content);
log.debug("收集AI回复片段: {}", content);
}
}
@Override
public void complete() {
if (completed.compareAndSet(false, true)) {
try {
// AI回复完成保存消息和计费
saveAiResponseAndBilling();
delegate.complete();
log.debug("AI回复完成已保存并计费");
} catch (Exception e) {
log.error("保存AI回复和计费失败", e);
delegate.completeWithError(e);
}
}
}
@Override
public void completeWithError(Throwable ex) {
if (completed.compareAndSet(false, true)) {
log.warn("AI回复出错跳过计费", ex);
delegate.completeWithError(ex);
}
}
/**
* 保存AI回复并进行计费
*/
private void saveAiResponseAndBilling() {
String aiResponse = aiResponseBuilder.toString().trim();
if (aiResponse.isEmpty()) {
log.warn("AI回复内容为空跳过保存和计费");
return;
}
try {
// 创建AI回复的ChatRequest
ChatRequest aiChatRequest = new ChatRequest();
aiChatRequest.setUserId(chatRequest.getUserId());
aiChatRequest.setSessionId(chatRequest.getSessionId());
aiChatRequest.setRole(Message.Role.ASSISTANT.getName());
aiChatRequest.setModel(chatRequest.getModel());
aiChatRequest.setPrompt(aiResponse);
// 设置会话token供异步线程使用
if (chatRequest.getToken() != null) {
BaseContext.setCurrentToken(chatRequest.getToken());
}
// 保存AI回复消息
chatCostService.saveMessage(aiChatRequest);
// 发布计费事件
chatCostService.publishBillingEvent(aiChatRequest);
log.debug("AI回复保存和计费完成用户ID: {}, 会话ID: {}, 回复长度: {}",
chatRequest.getUserId(), chatRequest.getSessionId(), aiResponse.length());
} catch (Exception e) {
log.error("保存AI回复和计费失败用户ID: {}, 会话ID: {}",
chatRequest.getUserId(), chatRequest.getSessionId(), e);
// 不抛出异常,避免影响用户体验
}
}
/**
* 从SSE数据中提取AI回复内容
* 适配不同AI服务的数据格式
*/
private String extractContentFromSseData(Object sseData) {
if (sseData == null) {
return null;
}
String dataStr = sseData.toString();
// 过滤明显的控制信号
if (isControlSignal(dataStr)) {
return null;
}
// 策略1: 直接字符串内容DeepSeek等简单格式
String directContent = extractDirectContent(dataStr);
if (directContent != null) {
return directContent;
}
// 策略2: 解析JSON格式OpenAI兼容格式
String jsonContent = extractJsonContent(dataStr);
if (jsonContent != null) {
return jsonContent;
}
// 策略3: SSE事件格式解析
String sseContent = extractSseEventContent(dataStr);
if (sseContent != null) {
return sseContent;
}
// 策略4: 兜底策略 - 如果是纯文本且不是控制信号,直接返回
if (isPureTextContent(dataStr)) {
return dataStr;
}
log.debug("无法解析的SSE数据格式: {}", dataStr);
return null;
}
/**
* 判断是否为控制信号
*/
private boolean isControlSignal(String data) {
if (data == null || data.trim().isEmpty()) {
return true;
}
String trimmed = data.trim();
return "[DONE]".equals(trimmed)
|| "null".equals(trimmed)
|| trimmed.startsWith("event:")
|| trimmed.startsWith("id:")
|| trimmed.startsWith("retry:");
}
/**
* 提取直接文本内容
*/
private String extractDirectContent(String data) {
// 如果是纯文本且长度合理,直接返回
if (data.length() > 0 && data.length() < 1000 && !data.contains("{") && !data.contains("[")) {
return data;
}
return null;
}
/**
* 提取JSON格式内容
*/
private String extractJsonContent(String data) {
try {
// 简化的JSON解析
if (data.contains("\"content\":")) {
return parseContentFromJson(data);
}
} catch (Exception e) {
log.debug("JSON解析失败: {}", e.getMessage());
}
return null;
}
/**
* 提取SSE事件格式内容
*/
private String extractSseEventContent(String data) {
if (data.startsWith("data:")) {
String jsonPart = data.substring(5).trim();
return extractJsonContent(jsonPart);
}
return null;
}
/**
* 判断是否为纯文本内容
*/
private boolean isPureTextContent(String data) {
return data != null
&& !data.trim().isEmpty()
&& !data.contains("{")
&& !data.contains("[")
&& !data.contains("data:")
&& data.length() < 500; // 合理的文本长度
}
/**
* 从事件字符串中解析内容
*/
private String parseContentFromEventString(String eventString) {
// 简单的字符串解析逻辑,可以根据实际格式优化
if (eventString.contains("data:")) {
int dataIndex = eventString.indexOf("data:");
String dataContent = eventString.substring(dataIndex + 5).trim();
return parseContentFromJson(dataContent);
}
return null;
}
/**
* 从JSON字符串中解析内容
*/
private String parseContentFromJson(String jsonStr) {
// 简化的JSON解析实际项目中建议使用Jackson
if (jsonStr.contains("\"content\":\"")) {
int startIndex = jsonStr.indexOf("\"content\":\"") + 11;
int endIndex = jsonStr.indexOf("\"", startIndex);
if (endIndex > startIndex) {
return jsonStr.substring(startIndex, endIndex);
}
}
return null;
}
// 委托其他方法到原始emitter
@Override
public void onCompletion(Runnable callback) {
delegate.onCompletion(callback);
}
@Override
public void onError(Consumer<Throwable> callback) {
delegate.onError(callback);
}
@Override
public void onTimeout(Runnable callback) {
delegate.onTimeout(callback);
}
}
}

View File

@@ -2629,3 +2629,6 @@ CREATE TABLE `knowledge_role_relation` (
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '知识库角色与知识库关联表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
-- 增加会话ID
alter table chat_session add conversation_id varchar(32) null comment '会话ID';

View File

@@ -0,0 +1,4 @@
-- 为 chat_message 表添加 billing_type 字段
ALTER TABLE chat_message
ADD COLUMN billing_type char NULL COMMENT '计费类型1-token计费2-次数计费null-普通消息)';