refactor(tenant-withdraw): 金额单位由分改为元并增加冻结逻辑

This commit is contained in:
2025-12-26 20:46:48 +08:00
parent 0c5a038595
commit cfa2c89600
5 changed files with 116 additions and 49 deletions

View File

@@ -2,6 +2,7 @@ package com.yolo.keyboard.controller.admin.tenantbalance.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
@@ -19,6 +20,31 @@ public class TenantBalanceWithdrawReqVO {
@DecimalMin(value = "0.01", message = "提现金额必须大于0") @DecimalMin(value = "0.01", message = "提现金额必须大于0")
private BigDecimal amount; private BigDecimal amount;
@Schema(description = "打款渠道BANK/ALIPAY/WECHAT", requiredMode = Schema.RequiredMode.REQUIRED, example = "BANK")
@NotEmpty(message = "打款渠道不能为空")
private String payChannel;
@Schema(description = "收款方类型PERSON个人/COMPANY企业", requiredMode = Schema.RequiredMode.REQUIRED, example = "PERSON")
@NotEmpty(message = "收款方类型不能为空")
private String payeeType;
@Schema(description = "收款人姓名或公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
@NotEmpty(message = "收款人姓名不能为空")
private String payeeName;
@Schema(description = "收款账号(银行卡/支付宝等)", requiredMode = Schema.RequiredMode.REQUIRED, example = "6222021234567890123")
@NotEmpty(message = "收款账号不能为空")
private String payeeAccount;
@Schema(description = "收款银行名称(银行渠道必填)", example = "中国工商银行")
private String payeeBankName;
@Schema(description = "银行编码", example = "ICBC")
private String payeeBankCode;
@Schema(description = "银行支行名称", example = "北京分行朝阳支行")
private String payeeBankBranch;
@Schema(description = "备注", example = "提现备注") @Schema(description = "备注", example = "提现备注")
private String remark; private String remark;
} }

View File

@@ -2,6 +2,8 @@ package com.yolo.keyboard.controller.admin.tenantwithdraworder.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*; import lombok.*;
import java.math.BigDecimal;
import java.util.*; import java.util.*;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -28,17 +30,17 @@ public class KeyboardTenantWithdrawOrderRespVO {
@ExcelProperty("币种(默认 CNY") @ExcelProperty("币种(默认 CNY")
private String currency; private String currency;
@Schema(description = "提现申请金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "提现申请金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("提现申请金额(单位:") @ExcelProperty("提现申请金额(单位:")
private Long amount; private BigDecimal amount;
@Schema(description = "手续费金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "手续费金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("手续费金额(单位:") @ExcelProperty("手续费金额(单位:")
private Long feeAmount; private BigDecimal feeAmount;
@Schema(description = "实际到账金额(单位: = amount - fee_amount", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "实际到账金额(单位: = amount - fee_amount", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("实际到账金额(单位: = amount - fee_amount") @ExcelProperty("实际到账金额(单位: = amount - fee_amount")
private Long actualAmount; private BigDecimal actualAmount;
@Schema(description = "打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等") @ExcelProperty("打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等")

View File

@@ -2,6 +2,8 @@ package com.yolo.keyboard.controller.admin.tenantwithdraworder.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*; import lombok.*;
import java.math.BigDecimal;
import java.util.*; import java.util.*;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
@@ -25,17 +27,17 @@ public class KeyboardTenantWithdrawOrderSaveReqVO {
@NotEmpty(message = "币种(默认 CNY不能为空") @NotEmpty(message = "币种(默认 CNY不能为空")
private String currency; private String currency;
@Schema(description = "提现申请金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "提现申请金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "提现申请金额(单位:分)不能为空") @NotNull(message = "提现申请金额不能为空")
private Long amount; private BigDecimal amount;
@Schema(description = "手续费金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "手续费金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "手续费金额(单位:分)不能为空") @NotNull(message = "手续费金额不能为空")
private Long feeAmount; private BigDecimal feeAmount;
@Schema(description = "实际到账金额(单位: = amount - fee_amount", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "实际到账金额(单位: = amount - fee_amount", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "实际到账金额(单位:分 = amount - fee_amount不能为空") @NotNull(message = "实际到账金额不能为空")
private Long actualAmount; private BigDecimal actualAmount;
@Schema(description = "打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等不能为空") @NotEmpty(message = "打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等不能为空")

View File

@@ -2,6 +2,8 @@ package com.yolo.keyboard.dal.dataobject.tenantwithdraworder;
import com.yolo.keyboard.framework.tenant.core.aop.TenantIgnore; import com.yolo.keyboard.framework.tenant.core.aop.TenantIgnore;
import lombok.*; import lombok.*;
import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -21,13 +23,12 @@ import com.yolo.keyboard.framework.mybatis.core.dataobject.BaseDO;
@TableName("system_tenant_withdraw_order") @TableName("system_tenant_withdraw_order")
@KeySequence("system_tenant_withdraw_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @KeySequence("system_tenant_withdraw_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data @Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true) @ToString(callSuper = true)
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@TenantIgnore @TenantIgnore
public class KeyboardTenantWithdrawOrderDO extends BaseDO { public class KeyboardTenantWithdrawOrderDO {
/** /**
* 主键 * 主键
@@ -47,17 +48,17 @@ public class KeyboardTenantWithdrawOrderDO extends BaseDO {
*/ */
private String currency; private String currency;
/** /**
* 提现申请金额(单位: * 提现申请金额(单位:
*/ */
private Long amount; private BigDecimal amount;
/** /**
* 手续费金额(单位: * 手续费金额(单位:
*/ */
private Long feeAmount; private BigDecimal feeAmount;
/** /**
* 实际到账金额(单位: = amount - fee_amount * 实际到账金额(单位: = amount - fee_amount
*/ */
private Long actualAmount; private BigDecimal actualAmount;
/** /**
* 打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等 * 打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等
*/ */
@@ -137,6 +138,7 @@ public class KeyboardTenantWithdrawOrderDO extends BaseDO {
/** /**
* 乐观锁版本号 * 乐观锁版本号
*/ */
@Version
private Integer version; private Integer version;
/** /**
* 提现申请人ID * 提现申请人ID
@@ -159,5 +161,5 @@ public class KeyboardTenantWithdrawOrderDO extends BaseDO {
*/ */
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
private Long tenantId;
} }

View File

@@ -3,7 +3,9 @@ package com.yolo.keyboard.service.tenantbalance;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.yolo.keyboard.dal.dataobject.tenantbalancetransaction.TenantBalanceTransactionDO; import com.yolo.keyboard.dal.dataobject.tenantbalancetransaction.TenantBalanceTransactionDO;
import com.yolo.keyboard.dal.dataobject.tenantwithdraworder.KeyboardTenantWithdrawOrderDO;
import com.yolo.keyboard.dal.mysql.tenantbalancetransaction.TenantBalanceTransactionMapper; import com.yolo.keyboard.dal.mysql.tenantbalancetransaction.TenantBalanceTransactionMapper;
import com.yolo.keyboard.dal.mysql.tenantwithdraworder.KeyboardTenantWithdrawOrderMapper;
import com.yolo.keyboard.framework.common.util.collection.CollectionUtils; import com.yolo.keyboard.framework.common.util.collection.CollectionUtils;
import com.yolo.keyboard.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yolo.keyboard.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.yolo.keyboard.framework.tenant.core.context.TenantContextHolder; import com.yolo.keyboard.framework.tenant.core.context.TenantContextHolder;
@@ -16,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import com.yolo.keyboard.controller.admin.tenantbalance.vo.*; import com.yolo.keyboard.controller.admin.tenantbalance.vo.*;
import com.yolo.keyboard.dal.dataobject.tenantbalance.TenantBalanceDO; import com.yolo.keyboard.dal.dataobject.tenantbalance.TenantBalanceDO;
@@ -53,6 +56,9 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
@Resource @Resource
private ConfigApi configApi; private ConfigApi configApi;
@Resource
private KeyboardTenantWithdrawOrderMapper tenantWithdrawOrderMapper;
private static final String WITHDRAW_DAYS_CONFIG_KEY = "WITHDRAW-DAYS"; private static final String WITHDRAW_DAYS_CONFIG_KEY = "WITHDRAW-DAYS";
@Override @Override
@@ -231,38 +237,75 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
throw exception(TENANT_BALANCE_NOT_EXISTS); throw exception(TENANT_BALANCE_NOT_EXISTS);
} }
// 3. 校验余额是否充足 // 3. 校验可用余额是否充足(可用余额 = 余额 - 冻结金额)
BigDecimal withdrawAmount = withdrawReqVO.getAmount(); BigDecimal withdrawAmount = withdrawReqVO.getAmount();
if (balance.getBalance().compareTo(withdrawAmount) < 0) { BigDecimal frozenAmt = balance.getFrozenAmt() != null ? balance.getFrozenAmt() : BigDecimal.ZERO;
BigDecimal availableBalance = balance.getBalance().subtract(frozenAmt);
if (availableBalance.compareTo(withdrawAmount) < 0) {
throw exception(TENANT_BALANCE_WITHDRAW_INSUFFICIENT); throw exception(TENANT_BALANCE_WITHDRAW_INSUFFICIENT);
} }
// 4. 扣减余额 // 4. 扣减余额并增加冻结金额
BigDecimal newBalance = balance.getBalance().subtract(withdrawAmount); BigDecimal newBalance = balance.getBalance().subtract(withdrawAmount);
BigDecimal newFrozenAmt = frozenAmt.add(withdrawAmount);
balance.setBalance(newBalance); balance.setBalance(newBalance);
balance.setFrozenAmt(newFrozenAmt);
int updateCount = tenantBalanceMapper.updateById(balance); int updateCount = tenantBalanceMapper.updateById(balance);
if (updateCount == 0) { if (updateCount == 0) {
throw exception(TENANT_BALANCE_NOT_EXISTS); throw exception(TENANT_BALANCE_NOT_EXISTS);
} }
// 5. 创建提现交易记录 // 5. 生成提现单号
String withdrawNo = BizNoGenerator.generate("WD");
String bizNo = BizNoGenerator.generate("BIZ");
// 6. 创建冻结交易记录
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder() TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
.bizNo(BizNoGenerator.generate("WITHDRAW")) .bizNo(bizNo)
.points(withdrawAmount.negate()) // 提现为负数 .points(withdrawAmount.negate()) // 冻结金额(负数表示冻结扣减)
.balance(newBalance) .balance(newBalance) // 扣减后的余额
.tenantId(tenantId) .tenantId(tenantId)
.type("WITHDRAW") .type("FREEZE")
.description("余额提现") .description("提现冻结")
.remark(withdrawReqVO.getRemark()) .remark(withdrawReqVO.getRemark())
.operatorId(tenantId) .operatorId(tenantId)
.build(); .build();
tenantBalanceTransactionMapper.insert(transaction); tenantBalanceTransactionMapper.insert(transaction);
// 7. 创建提现申请订单
LocalDateTime now = LocalDateTime.now();
KeyboardTenantWithdrawOrderDO withdrawOrder = KeyboardTenantWithdrawOrderDO.builder()
.withdrawNo(withdrawNo)
.bizNo(bizNo)
.tenantId(tenantId)
.currency("CNY")
.amount(withdrawAmount) // 直接存储 BigDecimal
.feeAmount(BigDecimal.ZERO) // 手续费暂时为0
.actualAmount(withdrawAmount) // 实际到账金额
// 收款信息
.payChannel(withdrawReqVO.getPayChannel())
.payeeType(withdrawReqVO.getPayeeType())
.payeeName(withdrawReqVO.getPayeeName())
.payeeAccount(withdrawReqVO.getPayeeAccount())
.payeeBankName(withdrawReqVO.getPayeeBankName())
.payeeBankCode(withdrawReqVO.getPayeeBankCode())
.payeeBankBranch(withdrawReqVO.getPayeeBankBranch())
// 状态
.status("APPLIED") // 申请中
.auditStatus("PENDING") // 待审核
.applyTime(now)
.balanceTxnId(transaction.getId()) // 关联冻结交易记录
.creatorId(tenantId)
.createdAt(now)
.updatedAt(now)
.build();
tenantWithdrawOrderMapper.insert(withdrawOrder);
} }
/** /**
* 校验是否在提现日期范围内 * 校验是否在提现日期范围内
* 配置格式:{"start": 25, "days": 6} * 配置格式:{"start": 25, "days": 6}
* 表示每月从第 start 天开始,连续 days 天可以提现 * 表示每月从第 start 天开始,连续 days 天可以提现(不允许跨月)
*/ */
private void validateWithdrawDate() { private void validateWithdrawDate() {
// 获取提现日期配置 // 获取提现日期配置
@@ -281,19 +324,11 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
int currentDay = today.getDayOfMonth(); int currentDay = today.getDayOfMonth();
int lastDayOfMonth = today.lengthOfMonth(); int lastDayOfMonth = today.lengthOfMonth();
// 计算提现日期范围 // 计算提现日期范围不允许跨月endDay 最大为当月最后一天)
// 如果 start + days - 1 超过当月最后一天,需要处理跨月的情况 int endDay = Math.min(startDay + days - 1, lastDayOfMonth);
int endDay = startDay + days - 1;
boolean isInRange; // 判断当前日期是否在 [startDay, endDay] 范围内
if (endDay <= lastDayOfMonth) { boolean isInRange = currentDay >= startDay && currentDay <= endDay;
// 不跨月:当前日期在 [startDay, endDay] 范围内
isInRange = currentDay >= startDay && currentDay <= endDay;
} else {
// 跨月:当前日期在 [startDay, 月末] 或 [1, endDay - lastDayOfMonth] 范围内
int nextMonthEndDay = endDay - lastDayOfMonth;
isInRange = currentDay >= startDay || currentDay <= nextMonthEndDay;
}
if (!isInRange) { if (!isInRange) {
throw exception(TENANT_BALANCE_WITHDRAW_NOT_IN_DATE); throw exception(TENANT_BALANCE_WITHDRAW_NOT_IN_DATE);