From a608b7a754c626fa80b10356e667f2026496ee4a Mon Sep 17 00:00:00 2001 From: wangzhiwei Date: Fri, 24 Apr 2026 14:28:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor(account):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E5=86=BB=E7=BB=93=E5=8A=9F=E8=83=BD=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新冻结类型枚举说明,统一为token和RMB类型 - 添加日志记录功能并引入常量定义 - 重构创建冻结单方法,增加参数验证和业务流程优化 - 重构释放冻结单方法,完善金额计算和账户更新逻辑 - 修改模型价格查询接口返回类型为列表 - 添加支付订单和系统日志查询条件字段 - 调整应用配置文件环境设置和Redis连接参数 --- .../controller/ModelPriceController.java | 4 +- .../kexue/skills/entity/AccountFrozen.java | 2 +- .../skills/entity/dto/AccountFrozenDto.java | 2 +- .../skills/entity/dto/PaymentOrderDto.java | 7 + .../kexue/skills/entity/dto/SysLogDto.java | 33 +- .../kexue/skills/mapper/ModelPriceMapper.java | 4 +- .../skills/service/ModelPriceService.java | 4 +- .../impl/AccountFrozenServiceImpl.java | 667 +++++++++++++----- .../service/impl/ModelPriceServiceImpl.java | 4 +- src/main/resources/application-common.yml | 8 +- src/main/resources/application-dev.yml | 2 +- src/main/resources/application.yml | 2 +- 12 files changed, 528 insertions(+), 211 deletions(-) diff --git a/src/main/java/com/kexue/skills/controller/ModelPriceController.java b/src/main/java/com/kexue/skills/controller/ModelPriceController.java index 4440aca..1694081 100644 --- a/src/main/java/com/kexue/skills/controller/ModelPriceController.java +++ b/src/main/java/com/kexue/skills/controller/ModelPriceController.java @@ -66,11 +66,11 @@ public class ModelPriceController { * 通过模型名称查询数据 * * @param modelName 模型名称 - * @return 实例对象 + * @return 实例对象列表 */ @Operation(summary = "通过模型名称查询", description = "通过模型名称查询大模型Token价格表") @GetMapping("/queryByModelName/{modelName}") - public CommonResult queryByModelName(@PathVariable("modelName") String modelName) { + public CommonResult> queryByModelName(@PathVariable("modelName") String modelName) { return CommonResult.success(this.modelPriceService.queryByModelName(modelName)); } diff --git a/src/main/java/com/kexue/skills/entity/AccountFrozen.java b/src/main/java/com/kexue/skills/entity/AccountFrozen.java index cdf807c..bfac8b7 100644 --- a/src/main/java/com/kexue/skills/entity/AccountFrozen.java +++ b/src/main/java/com/kexue/skills/entity/AccountFrozen.java @@ -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=释放") diff --git a/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java b/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java index 4f34071..51d6acd 100644 --- a/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java +++ b/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java @@ -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") diff --git a/src/main/java/com/kexue/skills/entity/dto/PaymentOrderDto.java b/src/main/java/com/kexue/skills/entity/dto/PaymentOrderDto.java index 5b5af4f..dcf74e1 100644 --- a/src/main/java/com/kexue/skills/entity/dto/PaymentOrderDto.java +++ b/src/main/java/com/kexue/skills/entity/dto/PaymentOrderDto.java @@ -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; + } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/dto/SysLogDto.java b/src/main/java/com/kexue/skills/entity/dto/SysLogDto.java index 1608f00..9925a03 100644 --- a/src/main/java/com/kexue/skills/entity/dto/SysLogDto.java +++ b/src/main/java/com/kexue/skills/entity/dto/SysLogDto.java @@ -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; } diff --git a/src/main/java/com/kexue/skills/mapper/ModelPriceMapper.java b/src/main/java/com/kexue/skills/mapper/ModelPriceMapper.java index f80baea..438d2ce 100644 --- a/src/main/java/com/kexue/skills/mapper/ModelPriceMapper.java +++ b/src/main/java/com/kexue/skills/mapper/ModelPriceMapper.java @@ -44,9 +44,9 @@ public interface ModelPriceMapper { * 通过模型名称查询数据 * * @param modelName 模型名称 - * @return 实例对象 + * @return 实例对象列表 */ - ModelPrice queryByModelName(String modelName); + List queryByModelName(String modelName); /** * 根据模型名称、输出模式和token数量查询价格规则 diff --git a/src/main/java/com/kexue/skills/service/ModelPriceService.java b/src/main/java/com/kexue/skills/service/ModelPriceService.java index ffa26e4..4da6e6c 100644 --- a/src/main/java/com/kexue/skills/service/ModelPriceService.java +++ b/src/main/java/com/kexue/skills/service/ModelPriceService.java @@ -42,9 +42,9 @@ public interface ModelPriceService extends BaseService { * 通过模型名称查询数据 * * @param modelName 模型名称 - * @return 实例对象 + * @return 实例对象列表 */ - ModelPrice queryByModelName(String modelName); + List queryByModelName(String modelName); /** * 根据模型名称、输出模式和token数量查询价格规则 diff --git a/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java index 146382f..61f06ca 100644 --- a/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java @@ -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类型需要校验:estimatedInputTokens、estimatedOutputTokens、modelName + 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 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 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 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 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 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 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); } -} +} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/impl/ModelPriceServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/ModelPriceServiceImpl.java index 7e70ef9..a21597d 100644 --- a/src/main/java/com/kexue/skills/service/impl/ModelPriceServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/ModelPriceServiceImpl.java @@ -63,10 +63,10 @@ public class ModelPriceServiceImpl implements ModelPriceService { * 通过模型名称查询数据 * * @param modelName 模型名称 - * @return 实例对象 + * @return 实例对象列表 */ @Override - public ModelPrice queryByModelName(String modelName) { + public List queryByModelName(String modelName) { return this.modelPriceMapper.queryByModelName(modelName); } diff --git a/src/main/resources/application-common.yml b/src/main/resources/application-common.yml index 40c38ee..bd923b8 100644 --- a/src/main/resources/application-common.yml +++ b/src/main/resources/application-common.yml @@ -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 diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 2c69fb1..621a5ef 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -61,7 +61,7 @@ sa-token: # 验证码配置 captcha: # 是否启用验证码验证 - enabled: false + enabled: true # 验证码有效期(秒) expire-time: 300 # 验证码长度 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e2ad121..021f8c8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: profiles: - active: prod + active: dev application: name: agentSkills version: 1.0.0