feat(account): 添加账户冻结单功能和雪花算法ID生成器

- 在IDUtils中集成雪花算法ID生成器,支持分布式ID生成
- 添加AccountFrozen实体类,定义冻结单数据结构和字段
- 创建AccountFrozenController提供冻结单创建和释放接口
- 实现AccountFrozenService和AccountFrozenServiceImpl业务逻辑
- 添加AccountFrozenDto和AccountReleaseDto数据传输对象
- 创建AccountFrozenMapper和对应XML映射文件进行数据库操作
- 实现冻结单创建、释放、查询等完整业务流程
- 添加账户冻结单相关异常返回码定义
- 实现定时任务自动清理过期冻结单功能
- 支持基于tokens使用量的费用计算和扣减逻辑
This commit is contained in:
wangzhiwei 2026-04-11 15:11:53 +08:00
parent 8a80b31c2f
commit 51fce1ece6
11 changed files with 838 additions and 2 deletions

View File

@ -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;

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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("扫描过期冻结单完成");
}
}

View File

@ -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 &lt; #{currentTime}
</select>
</mapper>