@@ -70,7 +70,7 @@ public class ChatCostServiceImpl implements IChatCostService {
BigDecimal numberCost = unitPrice . setScale ( 2 , RoundingMode . HALF_UP ) ;
deductUserBalance ( chatRequest . getUserId ( ) , numberCost . doubleValue ( ) ) ;
log . debug ( " deductToken->按次数扣费,费用: {},模型: {} " , numberCost , modelName ) ;
// 清理可能存在的历史累计token( 模型计费方式可能发生过变更)
ChatUsageToken existingToken = chatTokenService . queryByUserId ( chatRequest . getUserId ( ) , modelName ) ;
if ( existingToken ! = null & & existingToken . getToken ( ) > 0 ) {
@@ -78,14 +78,14 @@ public class ChatCostServiceImpl implements IChatCostService {
chatTokenService . editToken ( existingToken ) ;
log . debug ( " deductToken->按次计费, 清理历史累计token: {} " , existingToken . getToken ( ) ) ;
}
// 记录账单消息
sav eBillingRecord ( chatRequest , tokens , numberCost . doubleValue ( ) , chatModelVo . getModelType ( ) ) ;
// 更新消息的计费信息到备注
updateMes sag eBilling( chatRequest , tokens , numberCost . doubleValue ( ) , chatModelVo . getModelType ( ) ) ;
return ;
}
// 按token计费: 累加并按阈值批量扣费, 保留余数
final int threshold = 100 ;
final int threshold = 1000 ;
// 获得记录的累计token数
// TODO: 这里存在并发竞态条件, 需要在chatTokenService层面添加乐观锁或分布式锁
@@ -105,11 +105,14 @@ public class ChatCostServiceImpl implements IChatCostService {
int remainder = totalTokens - billable ; // 结算后保留的余数
if ( billable > 0 ) {
// 计算批次数: 每1000个Token为一批, 每批扣费单价
int batches = billable / threshold ;
BigDecimal numberCost = unitPrice
. multiply ( BigDecimal . valueOf ( billable ) )
. multiply ( BigDecimal . valueOf ( batches ) )
. setScale ( 2 , RoundingMode . HALF_UP ) ;
log . debug ( " deductToken->按token扣费, 结算token数量: {},单价: {},费用: {} " , billable , unitPrice , numberCost ) ;
log . debug ( " deductToken->按token扣费, 结算token数量: {},批次数: {},单价: {},费用: {} " ,
billable , batches , unitPrice , numberCost ) ;
try {
// 先尝试扣费
deductUserBalance ( chatRequest . getUserId ( ) , numberCost . doubleValue ( ) ) ;
@@ -119,9 +122,9 @@ public class ChatCostServiceImpl implements IChatCostService {
chatToken . setToken ( remainder ) ;
chatTokenService . editToken ( chatToken ) ;
log . debug ( " deductToken->扣费成功,更新余数: {} " , remainder ) ;
// 记录账单消息
sav eBillingRecord ( chatRequest , billable , numberCost . doubleValue ( ) , chatModelVo . getModelType ( ) ) ;
// 更新消息的计费信息到备注
updateMes sag eBilling( chatRequest , billable , numberCost . doubleValue ( ) , chatModelVo . getModelType ( ) ) ;
} catch ( ServiceException e ) {
// 余额不足时, 不更新token累计, 保持原有累计数
log . warn ( " deductToken->余额不足, 本次token累计保持不变: {} " , totalTokens ) ;
@@ -134,11 +137,15 @@ public class ChatCostServiceImpl implements IChatCostService {
chatToken . setUserId ( chatRequest . getUserId ( ) ) ;
chatToken . setToken ( totalTokens ) ;
chatTokenService . editToken ( chatToken ) ;
// 虽未扣费, 但要更新消息的基本信息( 实际token数、计费类型等)
updateMessageWithoutBilling ( chatRequest , tokens , chatModelVo . getModelType ( ) ) ;
}
}
/**
* 保存聊天消息记录(不进行计费)
* 保存成功后将消息ID设置到ChatRequest中, 供后续扣费使用
*/
@Override
public void saveMessage ( ChatRequest chatRequest ) {
@@ -146,45 +153,32 @@ public class ChatCostServiceImpl implements IChatCostService {
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 ( ) ) ;
// 计算并保存本次消息的token数
int tokens = TikTokensUtil . tokens ( chatRequest . getModel ( ) , chatRequest . getPrompt ( ) ) ;
chatMessageBo. setTotalTokens ( tokens ) ;
// 普通消息不涉及扣费, deductCost保持null
chatMessageBo . setDeductCost ( null ) ;
chatMessageBo . setRemark ( " 用户消息 " ) ;
// 设置计费类型(根据模型配置获取计费类型)
try {
ChatModelVo chatModelVo = chatModelService . selectModelByName ( chatRequest . getModel ( ) ) ;
if ( chatModelVo ! = null ) {
chatMessageBo . setBillingType ( chatModelVo . getModelType ( ) ) ;
} else {
chatMessageBo . setBillingType ( null ) ; // 模型不存在时设为null
}
} catch ( Exception e ) {
log . warn ( " saveMessage->获取模型计费类型失败, 设为null, 模型: {} " , chatRequest . getModel ( ) ) ;
chatMessageBo . setBillingType ( null ) ;
}
// // 基础消息信息, 计费相关数据( tokens、费用、计费类型等) 在扣费时统一设置
// chatMessageBo.setTotalTokens(0); // 初始设为0, 扣费时更新
// chatMessageBo.setDeductCost(null) ;
// chatMessageBo.setBillingType(null);
// chatMessageBo.setRemark("用户消息");
try {
chatMessageService . insertByBo ( chatMessageBo ) ;
log . debug ( " saveMessage->成功保存消息, 用户ID: {}, 会话ID: {}, tokens: {} " ,
chatRequest . g etUserId ( ) , chatRequest . getSession Id ( ) , tokens );
// 保存成功后, 将生成的消息ID设置到ChatRequest中
chatRequest . s etMessageId ( chatMessageBo . getId ( ) ) ;
log . debug ( " saveMessage->成功保存消息, 消息ID: {}, 用户ID: {}, 会话ID: {} " ,
chatMessageBo . getId ( ) , chatRequest . getUserId ( ) , chatRequest . getSessionId ( ) ) ;
} catch ( Exception e ) {
log . error ( " saveMessage->保存消息失败 " , e ) ;
throw new ServiceException ( " 保存消息失败 " ) ;
@@ -195,28 +189,29 @@ public class ChatCostServiceImpl implements IChatCostService {
@Override
public void publishBillingEvent ( ChatRequest chatRequest ) {
log . debug ( " publishBillingEvent->发布计费事件, 用户ID: {}, 会话ID: {},模型: {} " ,
log . debug ( " publishBillingEvent->发布计费事件, 用户ID: {}, 会话ID: {},模型: {} " ,
chatRequest . getUserId ( ) , chatRequest . getSessionId ( ) , chatRequest . getModel ( ) ) ;
// 预检查:评估可能的扣费金额,如果余额不足则直接抛异常
try {
preCheckBalance ( chatRequest ) ;
} catch ( ServiceException e ) {
log . warn ( " publishBillingEvent->预检查余额不足, 用户ID: {},模型: {} " ,
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 . getPrompt ( ) ,
chatRequest . getMessageId ( )
) ) ;
log . debug ( " publishBillingEvent->计费事件发布完成 " ) ;
}
/**
* 预检查用户余额是否足够支付可能的费用
*/
@@ -224,34 +219,36 @@ public class ChatCostServiceImpl implements IChatCostService {
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 = 100 ;
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 ( billable ) )
. multiply ( BigDecimal . valueOf ( batches ) )
. setScale ( 2 , RoundingMode . HALF_UP ) ;
checkUserBalanceWithoutDeduct ( chatRequest . getUserId ( ) , numberCost . doubleValue ( ) ) ;
}
}
/**
* 检查用户余额是否足够,但不扣除
*/
@@ -260,69 +257,97 @@ public class ChatCostServiceImpl implements IChatCostService {
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 save BillingRecord ( ChatRequest chatRequest , int billedTokens , double cost , String billingTypeCode ) {
private void updateMessageWithout Billing( ChatRequest chatRequest , int actualTokens , String billingTypeCode ) {
// 检查是否有消息ID可以更新
if ( chatRequest . getMessageId ( ) = = null ) {
log . warn ( " updateMessageWithoutBilling->消息ID为空, 无法更新基本信息 " ) ;
return ;
}
try {
ChatMessageBo billingMessage = new ChatMessageBo ( ) ;
billingMessage . setUserId ( c hatRequest . getUserId ( ) ) ;
billing Message. setSession Id ( chatRequest . getS ession Id ( ) ) ;
billing Message. setRole ( " system " ) ; // 系统账单消息
billing Message. setModelName ( chatRequest . getM odel ( ) ) ;
billing Message. setTotalTokens ( billedTokens ) ;
billingMessage . setDeductCost ( cost ) ;
billingMessage . setRemark ( getBillingTypeName ( billingTypeCode ) ) ;
// 设置计费类型和构建消息内容
setBillingTypeAndContent ( billingMessage , billingTypeCode , billed Tokens, cost ) ;
chatMessageService . insertByBo ( billingMessage ) ;
log . debug ( " saveBillingRecord->保存账单记录成功, 用户ID: {}, 计费类型: {}, 费用: {} " ,
chatRequest . getUserId ( ) , billingTypeCode , cost ) ;
// 创建更新对象,只更新基本信息,不涉及扣费
ChatMessageBo updateMessage = new C hatMessageBo ( ) ;
update Message. setId ( chatRequest . getM essage Id ( ) ) ;
update Message. setTotalTokens ( actualTokens ) ; // 设置实际token数
update Message. setBillingType ( billingTypeC ode ) ; // 设置计费类型
update Message. setRemark ( " 用户消息(累计中,未达扣费阈值) " ) ; // 说明状态
// 更新消息
chatMessageService . updateByBo ( updateMessage ) ;
log . debug ( " updateMessageWithoutBilling->更新消息基本信息成功, 消息ID: {}, 实际tokens: {}, 计费类型: {} " ,
chatRequest . getMessageId ( ) , actual Tokens, billingTypeCode ) ;
} catch ( Exception e ) {
log . error ( " saveBillingRecord->保存账单记录失败 " , e ) ;
// 账单记录 失败不影响主流程,只记录错误日志
log . error ( " updateMessageWithoutBilling->更新消息基本信息失败, 消息ID: {} " , chatRequest . getMessageId ( ) , e ) ;
// 更新 失败不影响主流程,只记录错误日志
}
}
/**
* 设置计费类型和构建消息内容
* 更新消息的计费信息到备注字段
*/
private void setBillingTypeAndContent ( ChatMessageBo billingMessage , String billingTypeCode , int billedTokens , double cost ) {
billingMessage . setBillingType ( billingTypeCode ) ;
// 使用枚举获取计费类型并构建消息内容
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 ) {
String content = switch ( billingType ) {
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 ) ;
} ;
billingMessage . setContent ( content ) ;
} else {
billingMessage . setContent ( String . format ( " 系统计费:处理 %d tokens, 扣费 %.2f 元 " , billedTokens , cost ) ) ;
return String . format ( " 系统计费:处理 %d tokens, 扣费 %.2f 元 " , billedTokens , cost ) ;
}
}
/**
* 获取计费类型名称( 用于remark字段)
*/
private String getBillingTypeName ( String billingTypeCode ) {
BillingType billingType = BillingType . fromCode ( billingTypeCode ) ;
return billingType ! = null ? billingType . getDescription ( ) : " 系统计费 " ;
}
/**
* 从用户余额中扣除费用
@@ -377,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 ) ;
}