refactor(tenant-withdraw): 金额单位由分改为元并增加冻结逻辑
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 等")
|
||||
|
||||
@@ -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 等不能为空")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user