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 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 等")
|
||||||
|
|||||||
@@ -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 等不能为空")
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user