diff --git a/src/main/java/com/kexue/skills/common/ResultCode.java b/src/main/java/com/kexue/skills/common/ResultCode.java index f059ae4..4b02aee 100644 --- a/src/main/java/com/kexue/skills/common/ResultCode.java +++ b/src/main/java/com/kexue/skills/common/ResultCode.java @@ -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; diff --git a/src/main/java/com/kexue/skills/common/util/IDUtils.java b/src/main/java/com/kexue/skills/common/util/IDUtils.java index acc478d..6af6d32 100644 --- a/src/main/java/com/kexue/skills/common/util/IDUtils.java +++ b/src/main/java/com/kexue/skills/common/util/IDUtils.java @@ -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()); + } } } diff --git a/src/main/java/com/kexue/skills/controller/AccountFrozenController.java b/src/main/java/com/kexue/skills/controller/AccountFrozenController.java new file mode 100644 index 0000000..943411c --- /dev/null +++ b/src/main/java/com/kexue/skills/controller/AccountFrozenController.java @@ -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 createFrozen(@RequestBody AccountFrozenDto accountFrozenDto) { + AccountFrozen accountFrozen = accountFrozenService.createFrozen(accountFrozenDto); + return new Result().ok().data(accountFrozen); + } + + /** + * 释放冻结单 + * @param accountReleaseDto 冻结单释放DTO + * @return 冻结单信息 + */ + @PostMapping("/release") + @Operation(summary = "释放冻结单", description = "释放账户冻结单") + public Result releaseFrozen(@RequestBody AccountReleaseDto accountReleaseDto) { + AccountFrozen accountFrozen = accountFrozenService.releaseFrozen(accountReleaseDto); + return new Result().ok().data(accountFrozen); + } + +} diff --git a/src/main/java/com/kexue/skills/entity/AccountFrozen.java b/src/main/java/com/kexue/skills/entity/AccountFrozen.java new file mode 100644 index 0000000..26463f7 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/AccountFrozen.java @@ -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; + +} diff --git a/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java b/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java new file mode 100644 index 0000000..fa36224 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java @@ -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; + +} diff --git a/src/main/java/com/kexue/skills/entity/dto/AccountReleaseDto.java b/src/main/java/com/kexue/skills/entity/dto/AccountReleaseDto.java new file mode 100644 index 0000000..d573363 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/dto/AccountReleaseDto.java @@ -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; + +} diff --git a/src/main/java/com/kexue/skills/mapper/AccountFrozenMapper.java b/src/main/java/com/kexue/skills/mapper/AccountFrozenMapper.java new file mode 100644 index 0000000..541e424 --- /dev/null +++ b/src/main/java/com/kexue/skills/mapper/AccountFrozenMapper.java @@ -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 selectByUserIdAndStatus(@Param("userId") Long userId, @Param("status") String status); + + /** + * 查询过期的冻结单 + * @param currentTime 当前时间 + * @return 冻结单列表 + */ + List selectExpiredFrozen(Date currentTime); + +} diff --git a/src/main/java/com/kexue/skills/service/AccountFrozenService.java b/src/main/java/com/kexue/skills/service/AccountFrozenService.java new file mode 100644 index 0000000..8ceb836 --- /dev/null +++ b/src/main/java/com/kexue/skills/service/AccountFrozenService.java @@ -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); + +} diff --git a/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java new file mode 100644 index 0000000..e609454 --- /dev/null +++ b/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java @@ -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); + } + +} diff --git a/src/main/java/com/kexue/skills/task/AccountFrozenTask.java b/src/main/java/com/kexue/skills/task/AccountFrozenTask.java new file mode 100644 index 0000000..c43f692 --- /dev/null +++ b/src/main/java/com/kexue/skills/task/AccountFrozenTask.java @@ -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 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("扫描过期冻结单完成"); + } + +} diff --git a/src/main/resources/mapper/AccountFrozenMapper.xml b/src/main/resources/mapper/AccountFrozenMapper.xml new file mode 100644 index 0000000..93c91c8 --- /dev/null +++ b/src/main/resources/mapper/AccountFrozenMapper.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + 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}) + + + + 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} + + + + + + + + +