feat(account): 添加账户冻结单功能和雪花算法ID生成器
- 在IDUtils中集成雪花算法ID生成器,支持分布式ID生成 - 添加AccountFrozen实体类,定义冻结单数据结构和字段 - 创建AccountFrozenController提供冻结单创建和释放接口 - 实现AccountFrozenService和AccountFrozenServiceImpl业务逻辑 - 添加AccountFrozenDto和AccountReleaseDto数据传输对象 - 创建AccountFrozenMapper和对应XML映射文件进行数据库操作 - 实现冻结单创建、释放、查询等完整业务流程 - 添加账户冻结单相关异常返回码定义 - 实现定时任务自动清理过期冻结单功能 - 支持基于tokens使用量的费用计算和扣减逻辑
This commit is contained in:
parent
8a80b31c2f
commit
51fce1ece6
|
|
@ -92,7 +92,19 @@ public enum ResultCode implements IErrorCode {
|
|||
/**
|
||||
* 统一异常返回码
|
||||
* */
|
||||
EXCEPTION_HANDLER(-2500,"服务异常,请联系管理员");
|
||||
EXCEPTION_HANDLER(-2500,"服务异常,请联系管理员"),
|
||||
|
||||
/**
|
||||
* 账户冻结单相关错误
|
||||
* */
|
||||
PARAMETER_EMPTY(-1100, "参数不能为空"),
|
||||
FROZEN_ID_EMPTY(-1101, "冻结单ID不能为空"),
|
||||
FROZEN_NOT_EXIST(-1102, "冻结单不存在"),
|
||||
FROZEN_STATUS_ERROR(-1103, "冻结单状态不正确,无法释放"),
|
||||
SESSION_ID_NOT_EXIST(-1104, "会话ID不存在"),
|
||||
ACCOUNT_NOT_EXIST(-1105, "用户账户不存在"),
|
||||
INSUFFICIENT_BALANCE(-1106, "账户余额不足");
|
||||
|
||||
|
||||
private final long code;
|
||||
private final String message;
|
||||
|
|
|
|||
|
|
@ -2,22 +2,119 @@ package com.kexue.skills.common.util;
|
|||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @description: ID管理类
|
||||
**/
|
||||
@Component
|
||||
public class IDUtils {
|
||||
public static final Logger logger = LoggerFactory.getLogger(IDUtils.class);
|
||||
|
||||
@Value("${snowflake.workid:1}")
|
||||
private long workId;
|
||||
|
||||
private static SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
public static String getUUID(){
|
||||
return UUID.randomUUID().toString().replace("-","");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
snowflakeIdGenerator = new SnowflakeIdGenerator(workId);
|
||||
logger.info("Snowflake ID generator initialized with workId: {}", workId);
|
||||
}
|
||||
|
||||
public static long getSnowflakeId() {
|
||||
if (snowflakeIdGenerator == null) {
|
||||
// 如果未初始化,使用默认workId
|
||||
snowflakeIdGenerator = new SnowflakeIdGenerator(1L);
|
||||
}
|
||||
return snowflakeIdGenerator.nextId();
|
||||
}
|
||||
|
||||
public static String getSnowflakeIdStr() {
|
||||
if (snowflakeIdGenerator == null) {
|
||||
// 如果未初始化,使用默认workId
|
||||
snowflakeIdGenerator = new SnowflakeIdGenerator(1L);
|
||||
}
|
||||
return String.valueOf(snowflakeIdGenerator.nextId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 雪花算法实现
|
||||
*/
|
||||
static class SnowflakeIdGenerator {
|
||||
// 起始时间戳 (2020-01-01 00:00:00)
|
||||
private static final long START_TIMESTAMP = 1577808000000L;
|
||||
// 机器ID位数
|
||||
private static final long MACHINE_ID_BITS = 10L;
|
||||
// 序列号位数
|
||||
private static final long SEQUENCE_BITS = 12L;
|
||||
// 机器ID最大值
|
||||
private static final long MAX_MACHINE_ID = (1L << MACHINE_ID_BITS) - 1;
|
||||
// 序列号最大值
|
||||
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
|
||||
// 机器ID左移位数
|
||||
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
|
||||
// 时间戳左移位数
|
||||
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
|
||||
|
||||
private long machineId;
|
||||
private long sequence = 0L;
|
||||
private long lastTimestamp = -1L;
|
||||
|
||||
public SnowflakeIdGenerator(long machineId) {
|
||||
if (machineId > MAX_MACHINE_ID || machineId < 0) {
|
||||
throw new IllegalArgumentException("Machine ID must be between 0 and " + MAX_MACHINE_ID);
|
||||
}
|
||||
this.machineId = machineId;
|
||||
}
|
||||
|
||||
public synchronized long nextId() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
if (timestamp < lastTimestamp) {
|
||||
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
|
||||
}
|
||||
|
||||
if (timestamp == lastTimestamp) {
|
||||
sequence = (sequence + 1) & MAX_SEQUENCE;
|
||||
if (sequence == 0) {
|
||||
timestamp = tilNextMillis(lastTimestamp);
|
||||
}
|
||||
} else {
|
||||
sequence = 0L;
|
||||
}
|
||||
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT) |
|
||||
(machineId << MACHINE_ID_SHIFT) |
|
||||
sequence;
|
||||
}
|
||||
|
||||
private long tilNextMillis(long lastTimestamp) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(getUUID());
|
||||
logger.debug("test");
|
||||
|
||||
// 测试雪花算法
|
||||
for (int i = 0; i < 10; i++) {
|
||||
System.out.println("Snowflake ID: " + getSnowflakeId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.kexue.skills.controller;
|
||||
|
||||
import com.kexue.skills.entity.AccountFrozen;
|
||||
import com.kexue.skills.entity.dto.AccountFrozenDto;
|
||||
import com.kexue.skills.entity.dto.AccountReleaseDto;
|
||||
import com.kexue.skills.service.AccountFrozenService;
|
||||
import com.kexue.skills.common.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 账户冻结单控制器
|
||||
*
|
||||
* @author 系统生成
|
||||
* @since 2026-04-11
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/accountFrozen")
|
||||
@Tag(name = "账户冻结单", description = "账户冻结单管理接口")
|
||||
public class AccountFrozenController {
|
||||
|
||||
@Resource
|
||||
private AccountFrozenService accountFrozenService;
|
||||
|
||||
/**
|
||||
* 创建冻结单
|
||||
* @param accountFrozenDto 冻结单DTO
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
@PostMapping("/frozen")
|
||||
@Operation(summary = "创建冻结单", description = "创建账户冻结单")
|
||||
public Result<AccountFrozen> createFrozen(@RequestBody AccountFrozenDto accountFrozenDto) {
|
||||
AccountFrozen accountFrozen = accountFrozenService.createFrozen(accountFrozenDto);
|
||||
return new Result<AccountFrozen>().ok().data(accountFrozen);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放冻结单
|
||||
* @param accountReleaseDto 冻结单释放DTO
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
@PostMapping("/release")
|
||||
@Operation(summary = "释放冻结单", description = "释放账户冻结单")
|
||||
public Result<AccountFrozen> releaseFrozen(@RequestBody AccountReleaseDto accountReleaseDto) {
|
||||
AccountFrozen accountFrozen = accountFrozenService.releaseFrozen(accountReleaseDto);
|
||||
return new Result<AccountFrozen>().ok().data(accountFrozen);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.kexue.skills.entity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kexue.skills.entity.base.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* (AccountFrozen)实体类
|
||||
* 账户冻结单
|
||||
*
|
||||
* @author 系统生成
|
||||
* @since 2026-04-11
|
||||
*/
|
||||
@Data
|
||||
public class AccountFrozen extends BaseEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description ="冻结单ID,字符型")
|
||||
private String frozenId;
|
||||
|
||||
@Schema(description ="流水ID")
|
||||
private String accountTransactionId;
|
||||
|
||||
@Schema(description ="用户ID,关联冻结单")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description ="调用ID,关联冻结单")
|
||||
private String callId;
|
||||
|
||||
@Schema(description ="会话ID")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description ="模型名称")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description ="冻结金额/张数/次数/分钟")
|
||||
private BigDecimal frozenAmount;
|
||||
|
||||
@Schema(description ="冻结类型:1余额 2图片张数 3时间 4次数 5积分 99其他")
|
||||
private Integer frozenType;
|
||||
|
||||
@Schema(description ="最终扣减,0=释放")
|
||||
private BigDecimal finalAmount;
|
||||
|
||||
@Schema(description ="输入tokens")
|
||||
private Long usageInputTokens;
|
||||
|
||||
@Schema(description ="输出tokens")
|
||||
private Long usageOutputTokens;
|
||||
|
||||
@Schema(description ="总tokens")
|
||||
private Long usageTotalTokens;
|
||||
|
||||
@Schema(description ="终结原因:success/cancel/timeout/error/system_recovery")
|
||||
private String finalizeReason;
|
||||
|
||||
@Schema(description ="状态:RESERVED 已预留,FINALIZED 已终结")
|
||||
private String status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@Schema(description ="过期时间")
|
||||
private Date expireAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@Schema(description ="创建时间")
|
||||
private Date createTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@Schema(description ="更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.kexue.skills.entity.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 账户冻结单DTO
|
||||
*
|
||||
* @author 系统生成
|
||||
* @since 2026-04-11
|
||||
*/
|
||||
@Data
|
||||
public class AccountFrozenDto {
|
||||
|
||||
@Schema(description ="会话ID")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description ="调用ID,关联冻结单")
|
||||
private String callId;
|
||||
|
||||
@Schema(description ="模型名称")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description ="冻结金额/张数/次数/分钟")
|
||||
private BigDecimal frozenAmount;
|
||||
|
||||
@Schema(description ="冻结类型:1余额 2图片张数 3时间 4次数 5积分 99其他")
|
||||
private Integer frozenType;
|
||||
|
||||
@Schema(description ="过期时间")
|
||||
private Date expireAt;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.kexue.skills.entity.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 账户冻结单释放DTO
|
||||
*
|
||||
* @author 系统生成
|
||||
* @since 2026-04-11
|
||||
*/
|
||||
@Data
|
||||
public class AccountReleaseDto {
|
||||
|
||||
@Schema(description ="冻结单ID,字符型")
|
||||
private String frozenId;
|
||||
|
||||
@Schema(description ="最终扣减,0=释放")
|
||||
private BigDecimal finalAmount;
|
||||
|
||||
@Schema(description ="输入tokens")
|
||||
private Long usageInputTokens;
|
||||
|
||||
@Schema(description ="输出tokens")
|
||||
private Long usageOutputTokens;
|
||||
|
||||
@Schema(description ="总tokens")
|
||||
private Long usageTotalTokens;
|
||||
|
||||
@Schema(description ="终结原因:success/cancel/timeout/error/system_recovery")
|
||||
private String finalizeReason;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.kexue.skills.mapper;
|
||||
|
||||
import com.kexue.skills.entity.AccountFrozen;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 账户冻结单Mapper
|
||||
*
|
||||
* @author 系统生成
|
||||
* @since 2026-04-11
|
||||
*/
|
||||
@Mapper
|
||||
public interface AccountFrozenMapper {
|
||||
|
||||
/**
|
||||
* 根据ID查询冻结单
|
||||
* @param frozenId 冻结单ID
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
AccountFrozen selectByPrimaryKey(String frozenId);
|
||||
|
||||
/**
|
||||
* 插入冻结单
|
||||
* @param accountFrozen 冻结单信息
|
||||
* @return 影响行数
|
||||
*/
|
||||
int insert(AccountFrozen accountFrozen);
|
||||
|
||||
/**
|
||||
* 更新冻结单
|
||||
* @param accountFrozen 冻结单信息
|
||||
* @return 影响行数
|
||||
*/
|
||||
int updateByPrimaryKey(AccountFrozen accountFrozen);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询冻结单
|
||||
* @param sessionId 会话ID
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
AccountFrozen selectBySessionId(String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和状态查询冻结单
|
||||
* @param userId 用户ID
|
||||
* @param status 状态
|
||||
* @return 冻结单列表
|
||||
*/
|
||||
List<AccountFrozen> selectByUserIdAndStatus(@Param("userId") Long userId, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 查询过期的冻结单
|
||||
* @param currentTime 当前时间
|
||||
* @return 冻结单列表
|
||||
*/
|
||||
List<AccountFrozen> selectExpiredFrozen(Date currentTime);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.kexue.skills.service;
|
||||
|
||||
import com.kexue.skills.entity.AccountFrozen;
|
||||
import com.kexue.skills.entity.dto.AccountFrozenDto;
|
||||
import com.kexue.skills.entity.dto.AccountReleaseDto;
|
||||
|
||||
/**
|
||||
* 账户冻结单服务
|
||||
*
|
||||
* @author 系统生成
|
||||
* @since 2026-04-11
|
||||
*/
|
||||
public interface AccountFrozenService {
|
||||
|
||||
/**
|
||||
* 创建冻结单
|
||||
* @param accountFrozenDto 冻结单DTO
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
AccountFrozen createFrozen(AccountFrozenDto accountFrozenDto);
|
||||
|
||||
/**
|
||||
* 释放冻结单
|
||||
* @param accountReleaseDto 冻结单释放DTO
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
AccountFrozen releaseFrozen(AccountReleaseDto accountReleaseDto);
|
||||
|
||||
/**
|
||||
* 根据ID查询冻结单
|
||||
* @param frozenId 冻结单ID
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
AccountFrozen getByFrozenId(String frozenId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询冻结单
|
||||
* @param sessionId 会话ID
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
AccountFrozen getBySessionId(String sessionId);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
package com.kexue.skills.service.impl;
|
||||
|
||||
import com.kexue.skills.entity.Account;
|
||||
import com.kexue.skills.entity.AccountFrozen;
|
||||
import com.kexue.skills.entity.SysUser;
|
||||
import com.kexue.skills.entity.dto.AccountFrozenDto;
|
||||
import com.kexue.skills.entity.dto.AccountReleaseDto;
|
||||
import com.kexue.skills.exception.BizException;
|
||||
import com.kexue.skills.mapper.AccountFrozenMapper;
|
||||
import com.kexue.skills.mapper.AccountMapper;
|
||||
import com.kexue.skills.mapper.SysUserMapper;
|
||||
import com.kexue.skills.service.AccountFrozenService;
|
||||
import com.kexue.skills.service.ModelPriceService;
|
||||
import com.kexue.skills.entity.ModelPrice;
|
||||
import com.kexue.skills.entity.AccountTransaction;
|
||||
import com.kexue.skills.mapper.AccountTransactionMapper;
|
||||
import com.kexue.skills.common.util.IDUtils;
|
||||
import com.kexue.skills.common.ResultCode;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 账户冻结单服务实现
|
||||
*
|
||||
* @author 系统生成
|
||||
* @since 2026-04-11
|
||||
*/
|
||||
@Service("accountFrozenService")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public class AccountFrozenServiceImpl implements AccountFrozenService {
|
||||
|
||||
@Resource
|
||||
private AccountFrozenMapper accountFrozenMapper;
|
||||
|
||||
@Resource
|
||||
private AccountMapper accountMapper;
|
||||
|
||||
@Resource
|
||||
private SysUserMapper sysUserMapper;
|
||||
|
||||
@Resource
|
||||
private ModelPriceService modelPriceService;
|
||||
|
||||
@Resource
|
||||
private AccountTransactionMapper accountTransactionMapper;
|
||||
|
||||
/**
|
||||
* 创建冻结单
|
||||
* @param accountFrozenDto 冻结单DTO
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
@Override
|
||||
public AccountFrozen createFrozen(AccountFrozenDto accountFrozenDto) {
|
||||
// 1. 验证参数
|
||||
if (accountFrozenDto == null) {
|
||||
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), ResultCode.PARAMETER_EMPTY.getMessage());
|
||||
}
|
||||
if (accountFrozenDto.getSessionId() == null) {
|
||||
throw new BizException(ResultCode.SESSION_ID_NOT_EXIST.getCode(), ResultCode.SESSION_ID_NOT_EXIST.getMessage());
|
||||
}
|
||||
if (accountFrozenDto.getFrozenAmount() == null || accountFrozenDto.getFrozenAmount().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "冻结金额必须大于0");
|
||||
}
|
||||
if (accountFrozenDto.getFrozenType() == null) {
|
||||
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "冻结类型不能为空");
|
||||
}
|
||||
|
||||
// 2. 通过sessionId获取用户ID
|
||||
SysUser sysUser = sysUserMapper.getBySessionId(accountFrozenDto.getSessionId());
|
||||
if (sysUser == null) {
|
||||
throw new BizException(ResultCode.SESSION_ID_NOT_EXIST.getCode(), ResultCode.SESSION_ID_NOT_EXIST.getMessage());
|
||||
}
|
||||
Long userId = sysUser.getUserId();
|
||||
|
||||
// 3. 查询用户账户信息
|
||||
Account account = accountMapper.queryByUserId(userId);
|
||||
if (account == null) {
|
||||
throw new BizException(ResultCode.ACCOUNT_NOT_EXIST.getCode(), ResultCode.ACCOUNT_NOT_EXIST.getMessage());
|
||||
}
|
||||
|
||||
// 4. 检查余额是否足够(账户总余额 - 已冻结金额 >= 本次冻结金额)
|
||||
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(accountFrozenDto.getFrozenAmount()) < 0) {
|
||||
throw new BizException(ResultCode.INSUFFICIENT_BALANCE.getCode(), ResultCode.INSUFFICIENT_BALANCE.getMessage());
|
||||
}
|
||||
|
||||
// 5. 更新账户冻结金额
|
||||
account.setFrozenAmount(frozenAmount.add(accountFrozenDto.getFrozenAmount()));
|
||||
account.setUpdateTime(new Date());
|
||||
accountMapper.update(account);
|
||||
|
||||
// 6. 创建冻结单
|
||||
AccountFrozen accountFrozen = new AccountFrozen();
|
||||
accountFrozen.setFrozenId(IDUtils.getSnowflakeIdStr());
|
||||
accountFrozen.setUserId(userId);
|
||||
accountFrozen.setSessionId(accountFrozenDto.getSessionId());
|
||||
accountFrozen.setCallId(accountFrozenDto.getCallId());
|
||||
accountFrozen.setModelName(accountFrozenDto.getModelName());
|
||||
accountFrozen.setFrozenAmount(accountFrozenDto.getFrozenAmount());
|
||||
accountFrozen.setFrozenType(accountFrozenDto.getFrozenType());
|
||||
accountFrozen.setStatus("RESERVED");
|
||||
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. 验证参数
|
||||
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());
|
||||
}
|
||||
|
||||
// 2. 查询冻结单
|
||||
AccountFrozen accountFrozen = accountFrozenMapper.selectByPrimaryKey(accountReleaseDto.getFrozenId());
|
||||
if (accountFrozen == null) {
|
||||
throw new BizException(ResultCode.FROZEN_NOT_EXIST.getCode(), ResultCode.FROZEN_NOT_EXIST.getMessage());
|
||||
}
|
||||
|
||||
// 3. 检查冻结单状态
|
||||
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());
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
// 查询模型价格信息
|
||||
ModelPrice modelPrice = modelPriceService.queryByModelName(accountFrozen.getModelName());
|
||||
if (modelPrice != null) {
|
||||
// 计算token费用
|
||||
long inputFee = accountReleaseDto.getUsageInputTokens() / modelPrice.getInputPerCent();
|
||||
if (accountReleaseDto.getUsageInputTokens() % modelPrice.getInputPerCent() > 0) {
|
||||
inputFee += 1;
|
||||
}
|
||||
|
||||
long outputFee = accountReleaseDto.getUsageOutputTokens() / modelPrice.getOutputPerCent();
|
||||
if (accountReleaseDto.getUsageOutputTokens() % modelPrice.getOutputPerCent() > 0) {
|
||||
outputFee += 1;
|
||||
}
|
||||
|
||||
// 总费用(分)
|
||||
long totalFee = inputFee + outputFee;
|
||||
// 转换为元
|
||||
finalAmount = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 更新账户余额和冻结金额
|
||||
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 (balance.compareTo(finalAmount) < 0) {
|
||||
throw new BizException(ResultCode.INSUFFICIENT_BALANCE.getCode(), ResultCode.INSUFFICIENT_BALANCE.getMessage());
|
||||
}
|
||||
account.setBalance(balance.subtract(finalAmount));
|
||||
}
|
||||
|
||||
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); // 支出
|
||||
|
||||
// 如果是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);
|
||||
}
|
||||
|
||||
// 9. 更新冻结单状态
|
||||
accountFrozen.setFinalAmount(finalAmount);
|
||||
accountFrozen.setUsageInputTokens(accountReleaseDto.getUsageInputTokens());
|
||||
accountFrozen.setUsageOutputTokens(accountReleaseDto.getUsageOutputTokens());
|
||||
accountFrozen.setUsageTotalTokens(accountReleaseDto.getUsageTotalTokens());
|
||||
accountFrozen.setFinalizeReason(accountReleaseDto.getFinalizeReason());
|
||||
accountFrozen.setStatus("FINALIZED");
|
||||
accountFrozen.setUpdateTime(new Date());
|
||||
|
||||
accountFrozenMapper.updateByPrimaryKey(accountFrozen);
|
||||
return accountFrozen;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询冻结单
|
||||
* @param frozenId 冻结单ID
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
@Override
|
||||
public AccountFrozen getByFrozenId(String frozenId) {
|
||||
return accountFrozenMapper.selectByPrimaryKey(frozenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID查询冻结单
|
||||
* @param sessionId 会话ID
|
||||
* @return 冻结单信息
|
||||
*/
|
||||
@Override
|
||||
public AccountFrozen getBySessionId(String sessionId) {
|
||||
return accountFrozenMapper.selectBySessionId(sessionId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.kexue.skills.task;
|
||||
|
||||
import com.kexue.skills.entity.AccountFrozen;
|
||||
import com.kexue.skills.mapper.AccountFrozenMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 账户冻结单定时任务
|
||||
* 扫描过期的冻结单并更新状态
|
||||
*
|
||||
* @author 系统生成
|
||||
* @since 2026-04-11
|
||||
*/
|
||||
@Component
|
||||
public class AccountFrozenTask {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AccountFrozenTask.class);
|
||||
|
||||
@Resource
|
||||
private AccountFrozenMapper accountFrozenMapper;
|
||||
|
||||
/**
|
||||
* 定时扫描过期的冻结单
|
||||
* 每隔30分钟执行一次
|
||||
*/
|
||||
@Scheduled(cron = "0 0/30 * * * ?")
|
||||
public void scanExpiredFrozen() {
|
||||
logger.info("开始扫描过期的冻结单");
|
||||
|
||||
try {
|
||||
// 查询所有状态为RESERVED且过期的冻结单
|
||||
List<AccountFrozen> expiredFrozenList = accountFrozenMapper.selectExpiredFrozen(new Date());
|
||||
|
||||
if (expiredFrozenList != null && !expiredFrozenList.isEmpty()) {
|
||||
logger.info("发现 {} 个过期的冻结单", expiredFrozenList.size());
|
||||
|
||||
for (AccountFrozen frozen : expiredFrozenList) {
|
||||
// 更新状态为FINALIZED
|
||||
frozen.setStatus("FINALIZED");
|
||||
frozen.setFinalizeReason("timeout");
|
||||
frozen.setUpdateTime(new Date());
|
||||
|
||||
// 执行更新
|
||||
accountFrozenMapper.updateByPrimaryKey(frozen);
|
||||
logger.info("已处理过期冻结单: {}", frozen.getFrozenId());
|
||||
}
|
||||
} else {
|
||||
logger.info("未发现过期的冻结单");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("扫描过期冻结单时发生错误", e);
|
||||
}
|
||||
|
||||
logger.info("扫描过期冻结单完成");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.kexue.skills.mapper.AccountFrozenMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.kexue.skills.entity.AccountFrozen">
|
||||
<id column="frozen_id" property="frozenId" />
|
||||
<result column="account_transaction_id" property="accountTransactionId" />
|
||||
<result column="user_id" property="userId" />
|
||||
<result column="call_id" property="callId" />
|
||||
<result column="session_id" property="sessionId" />
|
||||
<result column="model_name" property="modelName" />
|
||||
<result column="frozen_amount" property="frozenAmount" />
|
||||
<result column="frozen_type" property="frozenType" />
|
||||
<result column="final_amount" property="finalAmount" />
|
||||
<result column="usage_input_tokens" property="usageInputTokens" />
|
||||
<result column="usage_output_tokens" property="usageOutputTokens" />
|
||||
<result column="usage_total_tokens" property="usageTotalTokens" />
|
||||
<result column="finalize_reason" property="finalizeReason" />
|
||||
<result column="status" property="status" />
|
||||
<result column="expire_at" property="expireAt" />
|
||||
<result column="create_time" property="createTime" />
|
||||
<result column="update_time" property="updateTime" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
frozen_id, account_transaction_id, user_id, call_id, session_id, model_name, frozen_amount,
|
||||
frozen_type, final_amount, usage_input_tokens, usage_output_tokens, usage_total_tokens,
|
||||
finalize_reason, status, expire_at, create_time, update_time
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.String">
|
||||
select
|
||||
<include refid="Base_Column_List" />
|
||||
from account_frozen
|
||||
where frozen_id = #{frozenId}
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="com.kexue.skills.entity.AccountFrozen">
|
||||
insert into account_frozen
|
||||
(frozen_id, account_transaction_id, user_id, call_id, session_id, model_name, frozen_amount,
|
||||
frozen_type, final_amount, usage_input_tokens, usage_output_tokens, usage_total_tokens,
|
||||
finalize_reason, status, expire_at, create_time, update_time)
|
||||
values
|
||||
(#{frozenId}, #{accountTransactionId}, #{userId}, #{callId}, #{sessionId}, #{modelName}, #{frozenAmount},
|
||||
#{frozenType}, #{finalAmount}, #{usageInputTokens}, #{usageOutputTokens}, #{usageTotalTokens},
|
||||
#{finalizeReason}, #{status}, #{expireAt}, #{createTime}, #{updateTime})
|
||||
</insert>
|
||||
|
||||
<update id="updateByPrimaryKey" parameterType="com.kexue.skills.entity.AccountFrozen">
|
||||
update account_frozen
|
||||
set
|
||||
account_transaction_id = #{accountTransactionId},
|
||||
user_id = #{userId},
|
||||
call_id = #{callId},
|
||||
session_id = #{sessionId},
|
||||
model_name = #{modelName},
|
||||
frozen_amount = #{frozenAmount},
|
||||
frozen_type = #{frozenType},
|
||||
final_amount = #{finalAmount},
|
||||
usage_input_tokens = #{usageInputTokens},
|
||||
usage_output_tokens = #{usageOutputTokens},
|
||||
usage_total_tokens = #{usageTotalTokens},
|
||||
finalize_reason = #{finalizeReason},
|
||||
status = #{status},
|
||||
expire_at = #{expireAt},
|
||||
update_time = #{updateTime}
|
||||
where frozen_id = #{frozenId}
|
||||
</update>
|
||||
|
||||
<select id="selectBySessionId" resultMap="BaseResultMap" parameterType="java.lang.String">
|
||||
select
|
||||
<include refid="Base_Column_List" />
|
||||
from account_frozen
|
||||
where session_id = #{sessionId}
|
||||
</select>
|
||||
|
||||
<select id="selectByUserIdAndStatus" resultMap="BaseResultMap">
|
||||
select
|
||||
<include refid="Base_Column_List" />
|
||||
from account_frozen
|
||||
where user_id = #{userId} and status = #{status}
|
||||
</select>
|
||||
|
||||
<select id="selectExpiredFrozen" resultMap="BaseResultMap" parameterType="java.util.Date">
|
||||
select
|
||||
<include refid="Base_Column_List" />
|
||||
from account_frozen
|
||||
where status = 'RESERVED' and expire_at is not null and expire_at < #{currentTime}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Loading…
Reference in New Issue