refactor(account): 重构账户冻结功能实现

- 更新冻结类型枚举说明,统一为token和RMB类型
- 添加日志记录功能并引入常量定义
- 重构创建冻结单方法,增加参数验证和业务流程优化
- 重构释放冻结单方法,完善金额计算和账户更新逻辑
- 修改模型价格查询接口返回类型为列表
- 添加支付订单和系统日志查询条件字段
- 调整应用配置文件环境设置和Redis连接参数
This commit is contained in:
wangzhiwei 2026-04-24 14:28:32 +08:00
parent fc7ba98d6e
commit a608b7a754
12 changed files with 528 additions and 211 deletions

View File

@ -66,11 +66,11 @@ public class ModelPriceController {
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象
* @return 实例对象列表
*/
@Operation(summary = "通过模型名称查询", description = "通过模型名称查询大模型Token价格表")
@GetMapping("/queryByModelName/{modelName}")
public CommonResult<ModelPrice> queryByModelName(@PathVariable("modelName") String modelName) {
public CommonResult<List<ModelPrice>> queryByModelName(@PathVariable("modelName") String modelName) {
return CommonResult.success(this.modelPriceService.queryByModelName(modelName));
}

View File

@ -44,7 +44,7 @@ public class AccountFrozen extends BaseEntity implements Serializable {
@Schema(description ="冻结金额/张数/次数/分钟")
private BigDecimal frozenAmount;
@Schema(description ="冻结类型1余额 2图片张数 3时间 4次数 5积分 99其他")
@Schema(description ="冻结类型1.token 2.RMB(元) 99其他")
private Integer frozenType;
@Schema(description ="最终扣减0=释放")

View File

@ -30,7 +30,7 @@ public class AccountFrozenDto {
@Schema(description ="冻结金额/张数/次数/分钟")
private BigDecimal frozenAmount;
@Schema(description ="冻结类型1余额 2图片张数 3时间 4次数 5积分 99其他")
@Schema(description ="冻结类型1.token 2.RMB(元) 99其他")
private Integer frozenType;
@Schema(description ="预估输入tokens")

View File

@ -2,6 +2,9 @@ package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import lombok.Data;
import org.checkerframework.checker.formatter.qual.Format;
import java.util.Date;
/**
* (PaymentOrder)查询DTO类
@ -32,4 +35,8 @@ public class PaymentOrderDto extends BaseQueryDto {
private Long packageId;
private Date createTimeStart;
private Date createTimeEnd;
}

View File

@ -2,6 +2,7 @@ package com.kexue.skills.entity.dto;
import java.io.Serializable;
import java.util.Date;
import com.kexue.skills.entity.base.BaseQueryDto;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
@ -23,28 +24,24 @@ public class SysLogDto extends BaseQueryDto implements Serializable {
@Schema(description ="主键ID")
private Long logId;
@Schema(description ="用户ID")
private String userId;
@Schema(description ="模块")
private String module;
@Schema(description ="用户名称")
private String userName;
@Schema(description ="描述")
private String description;
@Schema(description ="日志类型")
private String logType;
@Schema(description ="IP地址")
private String ip;
@Schema(description ="日志类容")
private String logContent;
@Schema(description ="状态")
private Integer status;
@Schema(description ="服务端IP")
private String serverIp;
@Schema(description ="开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date startTime;
@Schema(description ="客户端IP")
private String clientIp;
@Schema(description ="yyyyMMddHHmmss")
private String logTime;
@Schema(description ="备注")
private String note;
@Schema(description ="结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date endTime;
}

View File

@ -44,9 +44,9 @@ public interface ModelPriceMapper {
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象
* @return 实例对象列表
*/
ModelPrice queryByModelName(String modelName);
List<ModelPrice> queryByModelName(String modelName);
/**
* 根据模型名称输出模式和token数量查询价格规则

View File

@ -42,9 +42,9 @@ public interface ModelPriceService extends BaseService {
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象
* @return 实例对象列表
*/
ModelPrice queryByModelName(String modelName);
List<ModelPrice> queryByModelName(String modelName);
/**
* 根据模型名称输出模式和token数量查询价格规则

View File

@ -1,6 +1,5 @@
package com.kexue.skills.service.impl;
import com.kexue.skills.common.Assert;
import com.kexue.skills.entity.Account;
import com.kexue.skills.entity.AccountFrozen;
import com.kexue.skills.entity.SysUser;
@ -19,6 +18,8 @@ import com.kexue.skills.mapper.AccountTransactionMapper;
import com.kexue.skills.common.util.IDUtils;
import com.kexue.skills.common.ResultCode;
import com.kexue.skills.config.AccountDeductionProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -38,6 +39,14 @@ import java.util.Objects;
@Transactional(rollbackFor = Exception.class)
public class AccountFrozenServiceImpl implements AccountFrozenService {
private static final Logger LOG = LoggerFactory.getLogger(AccountFrozenServiceImpl.class);
private static final Integer FROZEN_TYPE_TOKEN = 1;
private static final Integer FROZEN_TYPE_RMB = 2;
private static final BigDecimal YUAN_TO_POINTS_COEFFICIENT = BigDecimal.valueOf(100);
@Resource
private AccountFrozenMapper accountFrozenMapper;
@ -58,12 +67,45 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
/**
* 创建冻结单
*
* @param accountFrozenDto 冻结单DTO
* @return 冻结单信息
*/
@Override
public AccountFrozen createFrozen(AccountFrozenDto accountFrozenDto) {
// 1. 验证参数
// 1. 验证创建冻结单参数
validateCreateFrozenParams(accountFrozenDto);
// 2. 通过sessionId获取用户信息
SysUser sysUser = getUserBySessionId(accountFrozenDto.getSessionId());
// 3. 通过用户ID获取账户信息
Account account = getAccountByUserId(sysUser.getUserId());
// 4. 根据冻结类型计算冻结金额
// - 如果是token类型根据预估tokens和模型价格计算
// - 如果是RMB类型将元转换为积分1元=100积分
BigDecimal finalFrozenAmount = calculateFrozenAmount(accountFrozenDto);
// 5. 检查账户余额是否足够冻结
// 计算可用余额总余额 - 已冻结金额确保足够本次冻结
checkBalanceSufficient(account, finalFrozenAmount);
// 6. 更新账户冻结金额
// 将本次冻结金额加到账户的已冻结金额中
updateAccountFrozenAmount(account, finalFrozenAmount);
// 7. 执行创建冻结单
// 构建冻结单对象并保存到数据库
return doCreateFrozen(accountFrozenDto, sysUser.getUserId(), finalFrozenAmount);
}
/**
* 验证创建冻结单参数
*
* @param accountFrozenDto 冻结单DTO
*/
private void validateCreateFrozenParams(AccountFrozenDto accountFrozenDto) {
if (accountFrozenDto == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), ResultCode.PARAMETER_EMPTY.getMessage());
}
@ -73,77 +115,252 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
if (accountFrozenDto.getFrozenType() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "冻结类型不能为空");
}
// 根据冻结类型进行不同的参数校验
if (FROZEN_TYPE_TOKEN.equals(accountFrozenDto.getFrozenType())) {
// token类型需要校验estimatedInputTokensestimatedOutputTokensmodelName
if (accountFrozenDto.getEstimatedInputTokens() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "预估输入tokens不能为空");
}
if (accountFrozenDto.getEstimatedOutputTokens() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "预估输出tokens不能为空");
}
if (accountFrozenDto.getModelName() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "模型名称不能为空");
}
} else if (FROZEN_TYPE_RMB.equals(accountFrozenDto.getFrozenType())) {
// RMB类型需要校验frozenAmount
if (accountFrozenDto.getFrozenAmount() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "冻结金额不能为空");
}
}
// 其他类型的校验可以在此添加
}
// 2. 通过sessionId获取用户ID
SysUser sysUser = sysUserMapper.getBySessionId(accountFrozenDto.getSessionId());
/**
* 通过sessionId获取用户信息
*
* @param sessionId 会话ID
* @return 用户信息
*/
private SysUser getUserBySessionId(String sessionId) {
SysUser sysUser = sysUserMapper.getBySessionId(sessionId);
if (sysUser == null) {
throw new BizException(ResultCode.SESSION_ID_NOT_EXIST.getCode(), ResultCode.SESSION_ID_NOT_EXIST.getMessage());
}
Long userId = sysUser.getUserId();
return sysUser;
}
// 3. 查询用户账户信息
/**
* 通过用户ID获取账户信息
*
* @param userId 用户ID
* @return 账户信息
*/
private Account getAccountByUserId(Long userId) {
Account account = accountMapper.queryByUserId(userId);
if (account == null) {
throw new BizException(ResultCode.ACCOUNT_NOT_EXIST.getCode(), ResultCode.ACCOUNT_NOT_EXIST.getMessage());
}
return account;
}
// 4. 当冻结类型为余额时根据预估tokens计算冻结金额
BigDecimal finalFrozenAmount = accountFrozenDto.getFrozenAmount();
if (accountFrozenDto.getFrozenType() != null && accountFrozenDto.getFrozenType() == 1) {
if (accountFrozenDto.getEstimatedInputTokens() != null &&
accountFrozenDto.getEstimatedOutputTokens() != null &&
accountFrozenDto.getModelName() != null) {
if (accountFrozenDto.getModelName().equals("Qwen 3.5 Plus")) {
accountFrozenDto.setModelName("qwen3.5-plus");
}
// 查询模型价格信息
ModelPrice modelPrice = modelPriceService.queryByModelName(accountFrozenDto.getModelName());
if (modelPrice != null) {
// 计算token费用
long inputFee = accountFrozenDto.getEstimatedInputTokens() / modelPrice.getInputPerCent();
if (accountFrozenDto.getEstimatedInputTokens() % modelPrice.getInputPerCent() > 0) {
inputFee += 1;
}
long outputFee = accountFrozenDto.getEstimatedOutputTokens() / modelPrice.getOutputPerCent();
if (accountFrozenDto.getEstimatedOutputTokens() % modelPrice.getOutputPerCent() > 0) {
outputFee += 1;
}
// 总费用
// 注意因为1分=1积分所以totalFee直接就是积分数量
long totalFee = inputFee + outputFee;
// 转换为积分1分=1积分无需转换
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
// 应用扣费系数
finalFrozenAmount = baseAmount.multiply(accountDeductionProperties.getCoefficient());
}
}
/**
* 根据冻结类型计算冻结金额
*
* @param accountFrozenDto 冻结单DTO
* @return 计算后的冻结金额单位积分
*/
private BigDecimal calculateFrozenAmount(AccountFrozenDto accountFrozenDto) {
BigDecimal frozenAmount = accountFrozenDto.getFrozenAmount();
if (FROZEN_TYPE_TOKEN.equals(accountFrozenDto.getFrozenType())) {
return calculateTokenFrozenAmount(accountFrozenDto);
} else if (FROZEN_TYPE_RMB.equals(accountFrozenDto.getFrozenType())) {
return calculateRmbFrozenAmount(frozenAmount);
}
return frozenAmount;
}
// 5. 检查余额是否足够账户总余额 - 已冻结金额 >= 本次冻结金额
/**
* 计算token类型的冻结金额
* 根据预估输入输出tokens和模型价格计算冻结金额并应用扣费系数
*
* @param accountFrozenDto 冻结单DTO
* @return 冻结金额单位积分
*/
private BigDecimal calculateTokenFrozenAmount(AccountFrozenDto accountFrozenDto) {
if (accountFrozenDto.getEstimatedInputTokens() == null ||
accountFrozenDto.getEstimatedOutputTokens() == null ||
accountFrozenDto.getModelName() == null) {
return accountFrozenDto.getFrozenAmount();
}
String modelName = normalizeModelName(accountFrozenDto.getModelName());
List<ModelPrice> modelPriceList = getModelPriceList(modelName);
if (modelPriceList.isEmpty()) {
return accountFrozenDto.getFrozenAmount();
}
ModelPrice inputModelPrice = findInputModelPrice(modelPriceList, accountFrozenDto.getEstimatedInputTokens());
ModelPrice outputModelPrice = findOutputModelPrice(modelPriceList, accountFrozenDto.getEstimatedOutputTokens());
if (inputModelPrice == null || outputModelPrice == null) {
return accountFrozenDto.getFrozenAmount();
}
long totalFee = calculateTotalTokenFee(accountFrozenDto, inputModelPrice, outputModelPrice);
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
return baseAmount.multiply(accountDeductionProperties.getCoefficient());
}
/**
* 标准化模型名称
* 将前端传入的模型名称转换为数据库中对应的名称
*
* @param modelName 前端传入的模型名称
* @return 标准化后的模型名称
*/
private String normalizeModelName(String modelName) {
if ("Qwen 3.5 Plus".equals(modelName)) {
return "qwen3.5-plus";
}
return modelName;
}
/**
* 获取模型价格列表
*
* @param modelName 模型名称
* @return 模型价格列表
*/
private List<ModelPrice> getModelPriceList(String modelName) {
ModelPriceDto modelPriceDto = new ModelPriceDto();
modelPriceDto.setModelName(modelName);
return modelPriceService.getList(modelPriceDto);
}
/**
* 根据输入tokens查找匹配的输入模型价格
* 使用standard模式过滤价格规则
*
* @param modelPriceList 模型价格列表
* @param estimatedInputTokens 预估输入tokens
* @return 匹配的输入模型价格若无匹配返回null
*/
private ModelPrice findInputModelPrice(List<ModelPrice> modelPriceList, Long estimatedInputTokens) {
return modelPriceList.stream()
.filter(mp -> "standard".equals(mp.getOutputMode()))
.filter(mp -> mp.getMinTokens() < estimatedInputTokens)
.filter(mp -> mp.getMaxTokens() == -1 || mp.getMaxTokens() >= estimatedInputTokens)
.max((mp1, mp2) -> mp1.getMinTokens().compareTo(mp2.getMinTokens()))
.orElse(null);
}
/**
* 根据输出tokens查找匹配的输出模型价格
* 使用thinking模式过滤价格规则
*
* @param modelPriceList 模型价格列表
* @param estimatedOutputTokens 预估输出tokens
* @return 匹配的输出模型价格若无匹配返回null
*/
private ModelPrice findOutputModelPrice(List<ModelPrice> modelPriceList, Long estimatedOutputTokens) {
return modelPriceList.stream()
.filter(mp -> "thinking".equals(mp.getOutputMode()))
.filter(mp -> mp.getMinTokens() < estimatedOutputTokens)
.filter(mp -> mp.getMaxTokens() == -1 || mp.getMaxTokens() >= estimatedOutputTokens)
.max((mp1, mp2) -> mp1.getMinTokens().compareTo(mp2.getMinTokens()))
.orElse(null);
}
/**
* 计算总的token费用
*
* @param accountFrozenDto 冻结单DTO
* @param inputModelPrice 输入模型价格
* @param outputModelPrice 输出模型价格
* @return 总费用单位
*/
private long calculateTotalTokenFee(AccountFrozenDto accountFrozenDto, ModelPrice inputModelPrice, ModelPrice outputModelPrice) {
long inputFee = calculateTokenFee(accountFrozenDto.getEstimatedInputTokens(), inputModelPrice.getInputPerCent());
long outputFee = calculateTokenFee(accountFrozenDto.getEstimatedOutputTokens(), outputModelPrice.getOutputPerCent());
return inputFee + outputFee;
}
/**
* 计算单个token费用
* 采用向上取整的方式计算
*
* @param tokens token数量
* @param perCent 每token对应的分数
* @return 费用单位
*/
private long calculateTokenFee(long tokens, long perCent) {
long fee = tokens / perCent;
if (tokens % perCent > 0) {
fee += 1;
}
return fee;
}
/**
* 计算RMB类型的冻结金额
* 将元转换为积分1元=100积分
*
* @param frozenAmount 冻结金额单位
* @return 冻结金额单位积分
*/
private BigDecimal calculateRmbFrozenAmount(BigDecimal frozenAmount) {
if (frozenAmount == null) {
return BigDecimal.ZERO;
}
return frozenAmount.multiply(YUAN_TO_POINTS_COEFFICIENT).multiply(accountDeductionProperties.getCoefficient());
}
/**
* 检查账户余额是否足够冻结
*
* @param account 账户信息
* @param frozenAmount 本次冻结金额
*/
private void checkBalanceSufficient(Account account, BigDecimal frozenAmount) {
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
BigDecimal frozenAmount = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount();
BigDecimal availableBalance = balance.subtract(frozenAmount);
if (availableBalance.compareTo(finalFrozenAmount) < 0) {
BigDecimal frozen = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount();
BigDecimal availableBalance = balance.subtract(frozen);
LOG.debug("检查余额是否足够 - userId: {}, 总余额: {}, 已冻结: {}, 可用余额: {}, 本次冻结: {}",
account.getUserId(), balance, frozen, availableBalance, frozenAmount);
if (availableBalance.compareTo(frozenAmount) < 0) {
LOG.warn("余额不足 - userId: {}, 可用余额: {}, 本次冻结: {}", account.getUserId(), availableBalance, frozenAmount);
throw new BizException(ResultCode.INSUFFICIENT_BALANCE.getCode(), ResultCode.INSUFFICIENT_BALANCE.getMessage());
}
LOG.debug("余额检查通过 - userId: {}, 可用余额: {}, 本次冻结: {}", account.getUserId(), availableBalance, frozenAmount);
}
// 5. 更新账户冻结金额
account.setFrozenAmount(frozenAmount.add(finalFrozenAmount));
/**
* 更新账户冻结金额
*
* @param account 账户信息
* @param frozenAmount 本次冻结金额
*/
private void updateAccountFrozenAmount(Account account, BigDecimal frozenAmount) {
BigDecimal currentFrozen = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount();
account.setFrozenAmount(currentFrozen.add(frozenAmount));
account.setUpdateTime(new Date());
accountMapper.update(account);
}
// 6. 创建冻结单单位积分
/**
* 执行创建冻结单
*
* @param accountFrozenDto 冻结单DTO
* @param userId 用户ID
* @param finalFrozenAmount 最终冻结金额
* @return 冻结单信息
*/
private AccountFrozen doCreateFrozen(AccountFrozenDto accountFrozenDto, Long userId, BigDecimal finalFrozenAmount) {
AccountFrozen accountFrozen = new AccountFrozen();
accountFrozen.setFrozenId(IDUtils.getSnowflakeIdStr());
accountFrozen.setUserId(userId);
accountFrozen.setSessionId(accountFrozenDto.getSessionId());
accountFrozen.setCallId(accountFrozenDto.getCallId());
accountFrozen.setModelName(accountFrozenDto.getModelName());
if(Objects.nonNull(accountFrozenDto.getQuestion())){
if (Objects.nonNull(accountFrozenDto.getQuestion())) {
accountFrozen.setQuestion(accountFrozenDto.getQuestion());
}
accountFrozen.setFrozenAmount(finalFrozenAmount);
@ -152,173 +369,268 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
accountFrozen.setExpireAt(accountFrozenDto.getExpireAt());
accountFrozen.setCreateTime(new Date());
accountFrozen.setUpdateTime(new Date());
accountFrozenMapper.insert(accountFrozen);
return accountFrozen;
}
/**
* 释放冻结单
*
* @param accountReleaseDto 冻结单释放DTO
* @return 冻结单信息
*/
@Override
public AccountFrozen releaseFrozen(AccountReleaseDto accountReleaseDto) {
// 1. 验证参数
// 1. 验证释放冻结单参数
validateReleaseParams(accountReleaseDto);
// 2. 通过冻结单ID获取冻结单信息
AccountFrozen accountFrozen = getFrozenById(accountReleaseDto.getFrozenId());
// 3. 验证冻结单状态是否为预留状态
// 只有状态为RESERVED的冻结单才能被释放
validateFrozenStatus(accountFrozen);
// 4. 通过用户ID获取账户信息
Account account = getAccountByUserId(accountFrozen.getUserId());
// 5. 根据冻结类型计算最终释放金额
// - 如果是token类型根据实际使用的tokens和模型价格计算
// - 如果是RMB类型将元转换为积分1元=100积分
BigDecimal finalAmount = calculateReleaseAmount(accountReleaseDto, accountFrozen);
// 6. 执行释放时的账户更新
// - 扣减账户的冻结金额
// - 如果有实际扣减金额更新账户余额
doUpdateAccountForRelease(account, accountFrozen, finalAmount);
// 7. 创建交易流水记录
// 当有实际扣减金额时生成交易流水记录
doCreateTransaction(accountFrozen, account, finalAmount);
// 8. 执行冻结单终结
// 更新冻结单状态为已终结并记录最终扣减金额和使用情况
return doFinalizeFrozen(accountFrozen, accountReleaseDto, finalAmount);
}
/**
* 验证释放冻结单参数
*
* @param accountReleaseDto 冻结单释放DTO
*/
private void validateReleaseParams(AccountReleaseDto accountReleaseDto) {
if (accountReleaseDto == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), ResultCode.PARAMETER_EMPTY.getMessage());
}
if (accountReleaseDto.getFrozenId() == null) {
throw new BizException(ResultCode.FROZEN_ID_EMPTY.getCode(), ResultCode.FROZEN_ID_EMPTY.getMessage());
}
// 释放冻结单时的frozenType需要从冻结单中获取所以这里不做类型相关的校验
// 具体的类型相关校验会在calculateReleaseAmount方法中处理
}
// 2. 查询冻结单
AccountFrozen accountFrozen = accountFrozenMapper.selectByPrimaryKey(accountReleaseDto.getFrozenId());
/**
* 通过冻结单ID获取冻结单信息
*
* @param frozenId 冻结单ID
* @return 冻结单信息
*/
private AccountFrozen getFrozenById(String frozenId) {
AccountFrozen accountFrozen = accountFrozenMapper.selectByPrimaryKey(frozenId);
if (accountFrozen == null) {
throw new BizException(ResultCode.FROZEN_NOT_EXIST.getCode(), ResultCode.FROZEN_NOT_EXIST.getMessage());
}
return accountFrozen;
}
// 3. 检查冻结单状态
/**
* 验证冻结单状态是否为预留状态
*
* @param accountFrozen 冻结单信息
*/
private void validateFrozenStatus(AccountFrozen accountFrozen) {
if (!"RESERVED".equals(accountFrozen.getStatus())) {
throw new BizException(ResultCode.FROZEN_STATUS_ERROR.getCode(), ResultCode.FROZEN_STATUS_ERROR.getMessage());
}
}
// 4. 查询用户账户信息
Account account = accountMapper.queryByUserId(accountFrozen.getUserId());
if (account == null) {
throw new BizException(ResultCode.ACCOUNT_NOT_EXIST.getCode(), ResultCode.ACCOUNT_NOT_EXIST.getMessage());
/**
* 根据冻结类型计算最终释放金额
*
* @param accountReleaseDto 冻结单释放DTO
* @param accountFrozen 冻结单信息
* @return 最终释放金额单位积分
*/
private BigDecimal calculateReleaseAmount(AccountReleaseDto accountReleaseDto, AccountFrozen accountFrozen) {
BigDecimal finalAmount = accountReleaseDto.getFinalAmount() != null ?
accountReleaseDto.getFinalAmount() : BigDecimal.ZERO;
if (FROZEN_TYPE_TOKEN.equals(accountFrozen.getFrozenType())) {
return calculateTokenReleaseAmount(accountReleaseDto, accountFrozen, finalAmount);
} else if (FROZEN_TYPE_RMB.equals(accountFrozen.getFrozenType())) {
return calculateRmbFrozenAmount(finalAmount);
}
return finalAmount;
}
// 5. 计算最终扣减金额
BigDecimal finalAmount = accountReleaseDto.getFinalAmount() != null ? accountReleaseDto.getFinalAmount() : BigDecimal.ZERO;
// 6. 当冻结类型为余额时考虑token消费逻辑
if (accountFrozen.getFrozenType() != null && accountFrozen.getFrozenType() == 1) {
// 如果没有提供最终扣减金额但提供了tokens使用情况则根据tokens计算费用
if (finalAmount.compareTo(BigDecimal.ZERO) == 0 &&
accountReleaseDto.getUsageInputTokens() != null &&
accountReleaseDto.getUsageOutputTokens() != null &&
accountFrozen.getModelName() != null) {
// 查询模型价格信息一次性查询所有价格规则减少数据库IO
ModelPriceDto modelPriceDto = new ModelPriceDto();
modelPriceDto.setModelName(accountFrozen.getModelName());
List<ModelPrice> modelPriceList = modelPriceService.getList(modelPriceDto);
if (!modelPriceList.isEmpty()) {
// 过滤输入token的价格规则使用standard模式
String inputOutputMode = "standard";
ModelPrice inputModelPrice = modelPriceList.stream()
.filter(mp -> inputOutputMode.equals(mp.getOutputMode()))
.filter(mp -> mp.getMinTokens() < accountReleaseDto.getUsageInputTokens())
.filter(mp -> mp.getMaxTokens() == -1 || mp.getMaxTokens() >= accountReleaseDto.getUsageInputTokens())
.max((mp1, mp2) -> mp1.getMinTokens().compareTo(mp2.getMinTokens()))
.orElse(null);
// 过滤输出token的价格规则使用thinking模式
String outputOutputMode = "thinking";
ModelPrice outputModelPrice = modelPriceList.stream()
.filter(mp -> outputOutputMode.equals(mp.getOutputMode()))
.filter(mp -> mp.getMinTokens() < accountReleaseDto.getUsageOutputTokens())
.filter(mp -> mp.getMaxTokens() == -1 || mp.getMaxTokens() >= accountReleaseDto.getUsageOutputTokens())
.max((mp1, mp2) -> mp1.getMinTokens().compareTo(mp2.getMinTokens()))
.orElse(null);
if (inputModelPrice != null && outputModelPrice != null) {
// 计算token费用
long inputFee = accountReleaseDto.getUsageInputTokens() / inputModelPrice.getInputPerCent();
if (accountReleaseDto.getUsageInputTokens() % inputModelPrice.getInputPerCent() > 0) {
inputFee += 1;
}
long outputFee = accountReleaseDto.getUsageOutputTokens() / outputModelPrice.getOutputPerCent();
if (accountReleaseDto.getUsageOutputTokens() % outputModelPrice.getOutputPerCent() > 0) {
outputFee += 1;
}
// 总费用
// 注意因为1分=1积分所以totalFee直接就是积分数量
long totalFee = inputFee + outputFee;
// 转换为积分1分=1积分无需转换
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
// 应用扣费系数
finalAmount = baseAmount.multiply(accountDeductionProperties.getCoefficient());
}
}
}
/**
* 计算token类型的释放金额
* 根据实际使用的tokens和模型价格计算释放金额并应用扣费系数
*
* @param accountReleaseDto 冻结单释放DTO
* @param accountFrozen 冻结单信息
* @param finalAmount 当前计算的最终金额
* @return 计算后的释放金额单位积分
*/
private BigDecimal calculateTokenReleaseAmount(AccountReleaseDto accountReleaseDto, AccountFrozen accountFrozen,
BigDecimal finalAmount) {
if (finalAmount.compareTo(BigDecimal.ZERO) != 0 ||
accountReleaseDto.getUsageInputTokens() == null ||
accountReleaseDto.getUsageOutputTokens() == null ||
accountFrozen.getModelName() == null) {
return finalAmount;
}
List<ModelPrice> modelPriceList = getModelPriceList(accountFrozen.getModelName());
if (modelPriceList.isEmpty()) {
return finalAmount;
}
ModelPrice inputModelPrice = findInputModelPrice(modelPriceList, accountReleaseDto.getUsageInputTokens());
ModelPrice outputModelPrice = findOutputModelPrice(modelPriceList, accountReleaseDto.getUsageOutputTokens());
if (inputModelPrice == null || outputModelPrice == null) {
return finalAmount;
}
long totalFee = calculateReleaseTokenFee(accountReleaseDto, inputModelPrice, outputModelPrice);
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
return baseAmount.multiply(accountDeductionProperties.getCoefficient());
}
// 7. 更新账户余额和冻结金额单位积分
/**
* 计算释放时的token总费用
*
* @param accountReleaseDto 冻结单释放DTO
* @param inputModelPrice 输入模型价格
* @param outputModelPrice 输出模型价格
* @return 总费用单位
*/
private long calculateReleaseTokenFee(AccountReleaseDto accountReleaseDto,
ModelPrice inputModelPrice, ModelPrice outputModelPrice) {
long inputFee = calculateTokenFee(accountReleaseDto.getUsageInputTokens(), inputModelPrice.getInputPerCent());
long outputFee = calculateTokenFee(accountReleaseDto.getUsageOutputTokens(), outputModelPrice.getOutputPerCent());
return inputFee + outputFee;
}
/**
* 执行释放时的账户更新
* 包括扣减冻结金额和实际余额
*
* @param account 账户信息
* @param accountFrozen 冻结单信息
* @param finalAmount 最终扣减金额
*/
private void doUpdateAccountForRelease(Account account, AccountFrozen accountFrozen, BigDecimal finalAmount) {
BigDecimal frozenAmount = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount();
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
BigDecimal beforeBalance = balance;
// 释放冻结金额
account.setFrozenAmount(frozenAmount.subtract(accountFrozen.getFrozenAmount()));
// 如果需要扣减余额
if (finalAmount.compareTo(BigDecimal.ZERO) > 0) {
// 检查实际扣减是否大于预扣减
if (finalAmount.compareTo(accountFrozen.getFrozenAmount()) > 0) {
// 实际扣减大于预扣减根据实际扣减进行扣除
// 如果余额不够实际扣减将balance设置为0
if (balance.compareTo(finalAmount) < 0) {
account.setBalance(BigDecimal.ZERO);
} else {
account.setBalance(balance.subtract(finalAmount));
}
} else {
// 实际扣减小于等于预扣减正常扣减
if (balance.compareTo(finalAmount) < 0) {
account.setBalance(BigDecimal.ZERO);
} else {
account.setBalance(balance.subtract(finalAmount));
}
}
balance = calculateNewBalance(balance, finalAmount, accountFrozen.getFrozenAmount());
account.setBalance(balance);
}
account.setUpdateTime(new Date());
accountMapper.update(account);
}
// 8. 生成流水记录单位积分
if (finalAmount.compareTo(BigDecimal.ZERO) > 0) {
AccountTransaction transaction = new AccountTransaction();
transaction.setUserId(accountFrozen.getUserId());
transaction.setUserName(account.getUserName());
transaction.setTransactionType(3); // 购买内容
transaction.setAmount(finalAmount);
transaction.setBeforeBalance(beforeBalance);
transaction.setAfterBalance(account.getBalance());
transaction.setStatus(1); // 成功
transaction.setTransactionNo(IDUtils.getSnowflakeIdStr());
transaction.setPayType(3); // 余额支付
transaction.setBusinessType("frozen_release"); // 冻结单释放
transaction.setRemark("冻结单释放扣减: " + accountFrozen.getFrozenId());
transaction.setIsExpense(1); // 支出
transaction.setQuestion(accountFrozen.getQuestion());
if(Objects.nonNull(accountFrozen.getQuestion())){
transaction.setQuestion(accountFrozen.getQuestion());
}
if(Objects.nonNull(accountFrozen.getCallId())){
transaction.setCallId(accountFrozen.getCallId()); // 设置调用ID
}
// 如果是token消费记录token信息
if (accountReleaseDto.getUsageInputTokens() != null && accountReleaseDto.getUsageOutputTokens() != null) {
transaction.setInputToken(accountReleaseDto.getUsageInputTokens().intValue());
transaction.setOutputToken(accountReleaseDto.getUsageOutputTokens().intValue());
if (accountReleaseDto.getUsageTotalTokens() != null) {
transaction.setTotalTokens(accountReleaseDto.getUsageTotalTokens().intValue());
}
transaction.setModelName(accountFrozen.getModelName());
}
transaction.setCreateTime(new Date());
transaction.setUpdateTime(new Date());
accountTransactionMapper.insert(transaction);
/**
* 计算释放后的新余额
* 比较实际扣减金额和预扣减金额取较大值进行扣减
*
* @param balance 当前余额
* @param finalAmount 实际扣减金额
* @param frozenAmount 预扣减金额
* @return 新的余额
*/
private BigDecimal calculateNewBalance(BigDecimal balance, BigDecimal finalAmount, BigDecimal frozenAmount) {
BigDecimal amountToDeduct = finalAmount.compareTo(frozenAmount) > 0 ? finalAmount : frozenAmount;
if (balance.compareTo(amountToDeduct) < 0) {
return BigDecimal.ZERO;
}
return balance.subtract(amountToDeduct);
}
// 9. 更新冻结单状态单位积分
/**
* 创建交易流水记录
*
* @param accountFrozen 冻结单信息
* @param account 账户信息
* @param finalAmount 最终扣减金额
*/
private void doCreateTransaction(AccountFrozen accountFrozen, Account account, BigDecimal finalAmount) {
if (finalAmount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
BigDecimal beforeBalance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
AccountTransaction transaction = buildTransaction(accountFrozen, account, finalAmount, beforeBalance);
accountTransactionMapper.insert(transaction);
}
/**
* 构建交易流水对象
*
* @param accountFrozen 冻结单信息
* @param account 账户信息
* @param finalAmount 最终扣减金额
* @param beforeBalance 扣减前余额
* @return 交易流水对象
*/
private AccountTransaction buildTransaction(AccountFrozen accountFrozen, Account account,
BigDecimal finalAmount, BigDecimal beforeBalance) {
AccountTransaction transaction = new AccountTransaction();
transaction.setUserId(accountFrozen.getUserId());
transaction.setUserName(account.getUserName());
transaction.setTransactionType(3);
transaction.setAmount(finalAmount);
transaction.setBeforeBalance(beforeBalance);
transaction.setAfterBalance(account.getBalance());
transaction.setStatus(1);
transaction.setTransactionNo(IDUtils.getSnowflakeIdStr());
transaction.setPayType(3);
transaction.setBusinessType("frozen_release");
transaction.setRemark("冻结单释放扣减: " + accountFrozen.getFrozenId());
transaction.setIsExpense(1);
if (Objects.nonNull(accountFrozen.getQuestion())) {
transaction.setQuestion(accountFrozen.getQuestion());
}
if (Objects.nonNull(accountFrozen.getCallId())) {
transaction.setCallId(accountFrozen.getCallId());
}
if (accountFrozen.getFrozenType().equals(FROZEN_TYPE_TOKEN) &&
accountFrozen.getModelName() != null) {
if (accountFrozen.getUsageInputTokens() != null) {
transaction.setInputToken(accountFrozen.getUsageInputTokens().intValue());
}
if (accountFrozen.getUsageOutputTokens() != null) {
transaction.setOutputToken(accountFrozen.getUsageOutputTokens().intValue());
}
if (accountFrozen.getUsageTotalTokens() != null) {
transaction.setTotalTokens(accountFrozen.getUsageTotalTokens().intValue());
}
transaction.setModelName(accountFrozen.getModelName());
}
transaction.setCreateTime(new Date());
transaction.setUpdateTime(new Date());
return transaction;
}
/**
* 执行冻结单终结
* 更新冻结单状态为已终结
*
* @param accountFrozen 冻结单信息
* @param accountReleaseDto 冻结单释放DTO
* @param finalAmount 最终扣减金额
* @return 冻结单信息
*/
private AccountFrozen doFinalizeFrozen(AccountFrozen accountFrozen, AccountReleaseDto accountReleaseDto,
BigDecimal finalAmount) {
accountFrozen.setFinalAmount(finalAmount);
accountFrozen.setUsageInputTokens(accountReleaseDto.getUsageInputTokens());
accountFrozen.setUsageOutputTokens(accountReleaseDto.getUsageOutputTokens());
@ -326,13 +638,13 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
accountFrozen.setFinalizeReason(accountReleaseDto.getFinalizeReason());
accountFrozen.setStatus("FINALIZED");
accountFrozen.setUpdateTime(new Date());
accountFrozenMapper.updateByPrimaryKey(accountFrozen);
return accountFrozen;
}
/**
* 根据ID查询冻结单
*
* @param frozenId 冻结单ID
* @return 冻结单信息
*/
@ -343,6 +655,7 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
/**
* 根据会话ID查询冻结单
*
* @param sessionId 会话ID
* @return 冻结单信息
*/
@ -351,4 +664,4 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
return accountFrozenMapper.selectBySessionId(sessionId);
}
}
}

View File

@ -63,10 +63,10 @@ public class ModelPriceServiceImpl implements ModelPriceService {
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象
* @return 实例对象列表
*/
@Override
public ModelPrice queryByModelName(String modelName) {
public List<ModelPrice> queryByModelName(String modelName) {
return this.modelPriceMapper.queryByModelName(modelName);
}

View File

@ -2,9 +2,9 @@
common:
# Redis配置
redis:
# host: 127.0.0.1
# port: 6379
host: 43.248.97.19
port: 16379
host: 127.0.0.1
port: 6379
# host: 43.248.97.19
# port: 16379
password: 654321
database: 1

View File

@ -61,7 +61,7 @@ sa-token:
# 验证码配置
captcha:
# 是否启用验证码验证
enabled: false
enabled: true
# 验证码有效期(秒)
expire-time: 300
# 验证码长度

View File

@ -1,6 +1,6 @@
spring:
profiles:
active: prod
active: dev
application:
name: agentSkills
version: 1.0.0