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 jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -19,6 +20,31 @@ public class TenantBalanceWithdrawReqVO {
@DecimalMin(value = "0.01", message = "提现金额必须大于0")
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 = "提现备注")
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 lombok.*;
import java.math.BigDecimal;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
@@ -28,17 +30,17 @@ public class KeyboardTenantWithdrawOrderRespVO {
@ExcelProperty("币种(默认 CNY")
private String currency;
@Schema(description = "提现申请金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("提现申请金额(单位:")
private Long amount;
@Schema(description = "提现申请金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("提现申请金额(单位:")
private BigDecimal amount;
@Schema(description = "手续费金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("手续费金额(单位:")
private Long feeAmount;
@Schema(description = "手续费金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("手续费金额(单位:")
private BigDecimal feeAmount;
@Schema(description = "实际到账金额(单位: = amount - fee_amount", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("实际到账金额(单位: = amount - fee_amount")
private Long actualAmount;
@Schema(description = "实际到账金额(单位: = amount - fee_amount", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("实际到账金额(单位: = amount - fee_amount")
private BigDecimal actualAmount;
@Schema(description = "打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等", requiredMode = Schema.RequiredMode.REQUIRED)
@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 lombok.*;
import java.math.BigDecimal;
import java.util.*;
import jakarta.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat;
@@ -25,17 +27,17 @@ public class KeyboardTenantWithdrawOrderSaveReqVO {
@NotEmpty(message = "币种(默认 CNY不能为空")
private String currency;
@Schema(description = "提现申请金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "提现申请金额(单位:分)不能为空")
private Long amount;
@Schema(description = "提现申请金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "提现申请金额不能为空")
private BigDecimal amount;
@Schema(description = "手续费金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "手续费金额(单位:分)不能为空")
private Long feeAmount;
@Schema(description = "手续费金额(单位:", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "手续费金额不能为空")
private BigDecimal feeAmount;
@Schema(description = "实际到账金额(单位: = amount - fee_amount", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "实际到账金额(单位:分 = amount - fee_amount不能为空")
private Long actualAmount;
@Schema(description = "实际到账金额(单位: = amount - fee_amount", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "实际到账金额不能为空")
private BigDecimal actualAmount;
@Schema(description = "打款渠道BANK/ALIPAY/WECHAT/PAYPAL 等", requiredMode = Schema.RequiredMode.REQUIRED)
@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 lombok.*;
import java.math.BigDecimal;
import java.util.*;
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")
@KeySequence("system_tenant_withdraw_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TenantIgnore
public class KeyboardTenantWithdrawOrderDO extends BaseDO {
public class KeyboardTenantWithdrawOrderDO {
/**
* 主键
@@ -47,17 +48,17 @@ public class KeyboardTenantWithdrawOrderDO extends BaseDO {
*/
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 等
*/
@@ -137,6 +138,7 @@ public class KeyboardTenantWithdrawOrderDO extends BaseDO {
/**
* 乐观锁版本号
*/
@Version
private Integer version;
/**
* 提现申请人ID
@@ -159,5 +161,5 @@ public class KeyboardTenantWithdrawOrderDO extends BaseDO {
*/
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.json.JSONUtil;
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.tenantwithdraworder.KeyboardTenantWithdrawOrderMapper;
import com.yolo.keyboard.framework.common.util.collection.CollectionUtils;
import com.yolo.keyboard.framework.mybatis.core.query.LambdaQueryWrapperX;
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.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import com.yolo.keyboard.controller.admin.tenantbalance.vo.*;
import com.yolo.keyboard.dal.dataobject.tenantbalance.TenantBalanceDO;
@@ -53,6 +56,9 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
@Resource
private ConfigApi configApi;
@Resource
private KeyboardTenantWithdrawOrderMapper tenantWithdrawOrderMapper;
private static final String WITHDRAW_DAYS_CONFIG_KEY = "WITHDRAW-DAYS";
@Override
@@ -231,38 +237,75 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
throw exception(TENANT_BALANCE_NOT_EXISTS);
}
// 3. 校验余额是否充足
// 3. 校验可用余额是否充足(可用余额 = 余额 - 冻结金额)
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);
}
// 4. 扣减余额
// 4. 扣减余额并增加冻结金额
BigDecimal newBalance = balance.getBalance().subtract(withdrawAmount);
BigDecimal newFrozenAmt = frozenAmt.add(withdrawAmount);
balance.setBalance(newBalance);
balance.setFrozenAmt(newFrozenAmt);
int updateCount = tenantBalanceMapper.updateById(balance);
if (updateCount == 0) {
throw exception(TENANT_BALANCE_NOT_EXISTS);
}
// 5. 创建提现交易记录
// 5. 生成提现单号
String withdrawNo = BizNoGenerator.generate("WD");
String bizNo = BizNoGenerator.generate("BIZ");
// 6. 创建冻结交易记录
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
.bizNo(BizNoGenerator.generate("WITHDRAW"))
.points(withdrawAmount.negate()) // 提现为负数
.balance(newBalance)
.bizNo(bizNo)
.points(withdrawAmount.negate()) // 冻结金额(负数表示冻结扣减)
.balance(newBalance) // 扣减后的余额
.tenantId(tenantId)
.type("WITHDRAW")
.description("余额提现")
.type("FREEZE")
.description("提现冻结")
.remark(withdrawReqVO.getRemark())
.operatorId(tenantId)
.build();
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 天开始,连续 days 天可以提现
* 表示每月从第 start 天开始,连续 days 天可以提现(不允许跨月)
*/
private void validateWithdrawDate() {
// 获取提现日期配置
@@ -281,19 +324,11 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
int currentDay = today.getDayOfMonth();
int lastDayOfMonth = today.lengthOfMonth();
// 计算提现日期范围
// 如果 start + days - 1 超过当月最后一天,需要处理跨月的情况
int endDay = startDay + days - 1;
// 计算提现日期范围不允许跨月endDay 最大为当月最后一天)
int endDay = Math.min(startDay + days - 1, lastDayOfMonth);
boolean isInRange;
if (endDay <= lastDayOfMonth) {
// 不跨月:当前日期在 [startDay, endDay] 范围内
isInRange = currentDay >= startDay && currentDay <= endDay;
} else {
// 跨月:当前日期在 [startDay, 月末] 或 [1, endDay - lastDayOfMonth] 范围内
int nextMonthEndDay = endDay - lastDayOfMonth;
isInRange = currentDay >= startDay || currentDay <= nextMonthEndDay;
}
// 判断当前日期是否在 [startDay, endDay] 范围内
boolean isInRange = currentDay >= startDay && currentDay <= endDay;
if (!isInRange) {
throw exception(TENANT_BALANCE_WITHDRAW_NOT_IN_DATE);