diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceWithdrawReqVO.java b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceWithdrawReqVO.java index f195c89..b647dd3 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceWithdrawReqVO.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceWithdrawReqVO.java @@ -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; } diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantwithdraworder/vo/KeyboardTenantWithdrawOrderRespVO.java b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantwithdraworder/vo/KeyboardTenantWithdrawOrderRespVO.java index b21706a..7f65248 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantwithdraworder/vo/KeyboardTenantWithdrawOrderRespVO.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantwithdraworder/vo/KeyboardTenantWithdrawOrderRespVO.java @@ -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 等") diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantwithdraworder/vo/KeyboardTenantWithdrawOrderSaveReqVO.java b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantwithdraworder/vo/KeyboardTenantWithdrawOrderSaveReqVO.java index 1924454..1c81c54 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantwithdraworder/vo/KeyboardTenantWithdrawOrderSaveReqVO.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantwithdraworder/vo/KeyboardTenantWithdrawOrderSaveReqVO.java @@ -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 等不能为空") diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/dal/dataobject/tenantwithdraworder/KeyboardTenantWithdrawOrderDO.java b/keyboard-server/src/main/java/com/yolo/keyboard/dal/dataobject/tenantwithdraworder/KeyboardTenantWithdrawOrderDO.java index f94f749..ebf7cbc 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/dal/dataobject/tenantwithdraworder/KeyboardTenantWithdrawOrderDO.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/dal/dataobject/tenantwithdraworder/KeyboardTenantWithdrawOrderDO.java @@ -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; } \ No newline at end of file diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/service/tenantbalance/TenantBalanceServiceImpl.java b/keyboard-server/src/main/java/com/yolo/keyboard/service/tenantbalance/TenantBalanceServiceImpl.java index f4b3c5d..946f572 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/service/tenantbalance/TenantBalanceServiceImpl.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/service/tenantbalance/TenantBalanceServiceImpl.java @@ -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);