feat(tenant-balance): 新增租户余额提现功能

新增 /withdraw 接口,支持租户在限定日期内发起余额提现;增加冻结金额字段;补充提现相关错误码与日期校验逻辑。
This commit is contained in:
2025-12-26 19:23:40 +08:00
parent 419d9a930c
commit d517682c62
9 changed files with 148 additions and 4 deletions

View File

@@ -126,6 +126,14 @@ public class TenantBalanceController {
return success(BeanUtils.toBean(tenantBalancePage, TenantBalanceRespVO.class));
}
@PostMapping("/withdraw")
@Operation(summary = "租户提现")
@PreAuthorize("@ss.hasPermission('system:tenant-balance:withdraw')")
public CommonResult<Boolean> withdraw(@Valid @RequestBody TenantBalanceWithdrawReqVO withdrawReqVO) {
tenantBalanceService.withdraw(withdrawReqVO);
return success(true);
}
// ==================== 子表(租户积分记录) ====================

View File

@@ -1,6 +1,8 @@
package com.yolo.keyboard.controller.admin.tenantbalance.vo;
import lombok.*;
import java.math.BigDecimal;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import com.yolo.keyboard.framework.common.pojo.PageParam;
@@ -23,5 +25,6 @@ public class TenantBalancePageReqVO extends PageParam {
private LocalDateTime updatedAt;
@Schema(description = "冻结金额")
private BigDecimal frozenAmt;
}

View File

@@ -2,6 +2,8 @@ package com.yolo.keyboard.controller.admin.tenantbalance.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,5 +30,6 @@ public class TenantBalanceRespVO {
@ExcelProperty("更新时间")
private LocalDateTime updatedAt;
@Schema(description = "冻结金额")
private BigDecimal frozenAmt;
}

View File

@@ -2,6 +2,8 @@ package com.yolo.keyboard.controller.admin.tenantbalance.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;
@@ -26,5 +28,7 @@ public class TenantBalanceSaveReqVO {
@NotNull(message = "更新时间不能为空")
private LocalDateTime updatedAt;
@Schema(description = "冻结金额")
private BigDecimal frozenAmt;
}

View File

@@ -0,0 +1,24 @@
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.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* 管理后台 - 租户余额提现 Request VO
*/
@Schema(description = "管理后台 - 租户余额提现 Request VO")
@Data
public class TenantBalanceWithdrawReqVO {
@Schema(description = "提现金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000")
@NotNull(message = "提现金额不能为空")
@DecimalMin(value = "0.01", message = "提现金额必须大于0")
private BigDecimal amount;
@Schema(description = "备注", example = "提现备注")
private String remark;
}

View File

@@ -44,5 +44,9 @@ public class TenantBalanceDO extends BaseDO {
*/
private LocalDateTime updatedAt;
/**
* 冻结金额
*/
private BigDecimal frozenAmt;
}

View File

@@ -129,5 +129,11 @@ public interface TenantBalanceService {
*/
TenantBalanceTransactionDO getTenantBalanceTransaction(Long id);
/**
* 租户提现
*
* @param withdrawReqVO 提现信息
*/
void withdraw(@Valid TenantBalanceWithdrawReqVO withdrawReqVO);
}

View File

@@ -1,11 +1,13 @@
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.mysql.tenantbalancetransaction.TenantBalanceTransactionMapper;
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;
import com.yolo.keyboard.module.infra.api.config.ConfigApi;
import com.yolo.keyboard.utils.BizNoGenerator;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
@@ -13,6 +15,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import com.yolo.keyboard.controller.admin.tenantbalance.vo.*;
import com.yolo.keyboard.dal.dataobject.tenantbalance.TenantBalanceDO;
@@ -27,8 +30,7 @@ import com.yolo.keyboard.module.system.dal.mysql.tenant.TenantMapper;
import static com.yolo.keyboard.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.yolo.keyboard.framework.common.util.collection.CollectionUtils.convertList;
import static com.yolo.keyboard.framework.common.util.collection.CollectionUtils.diffList;
import static com.yolo.keyboard.module.infra.enums.ErrorCodeConstants.TENANT_BALANCE_NOT_EXISTS;
import static com.yolo.keyboard.module.infra.enums.ErrorCodeConstants.TENANT_BALANCE_TRANSACTION_NOT_EXISTS;
import static com.yolo.keyboard.module.infra.enums.ErrorCodeConstants.*;
/**
* 租户余额 Service 实现类
@@ -48,6 +50,11 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
@Resource
private TenantMapper tenantMapper;
@Resource
private ConfigApi configApi;
private static final String WITHDRAW_DAYS_CONFIG_KEY = "WITHDRAW-DAYS";
@Override
public Long createTenantBalance(TenantBalanceSaveReqVO createReqVO) {
// 插入
@@ -211,5 +218,87 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
tenantBalanceTransactionMapper.deleteByTenantIds(tenantIds);
}
@Override
@Transactional
public void withdraw(TenantBalanceWithdrawReqVO withdrawReqVO) {
// 1. 校验是否在提现日期范围内
validateWithdrawDate();
// 2. 获取当前租户的余额记录
Long tenantId = TenantContextHolder.getRequiredTenantId();
TenantBalanceDO balance = tenantBalanceMapper.selectById(tenantId);
if (balance == null) {
throw exception(TENANT_BALANCE_NOT_EXISTS);
}
// 3. 校验余额是否充足
BigDecimal withdrawAmount = withdrawReqVO.getAmount();
if (balance.getBalance().compareTo(withdrawAmount) < 0) {
throw exception(TENANT_BALANCE_WITHDRAW_INSUFFICIENT);
}
// 4. 扣减余额
BigDecimal newBalance = balance.getBalance().subtract(withdrawAmount);
balance.setBalance(newBalance);
int updateCount = tenantBalanceMapper.updateById(balance);
if (updateCount == 0) {
throw exception(TENANT_BALANCE_NOT_EXISTS);
}
// 5. 创建提现交易记录
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
.bizNo(BizNoGenerator.generate("WITHDRAW"))
.points(withdrawAmount.negate()) // 提现为负数
.balance(newBalance)
.tenantId(tenantId)
.type("WITHDRAW")
.description("余额提现")
.remark(withdrawReqVO.getRemark())
.operatorId(tenantId)
.build();
tenantBalanceTransactionMapper.insert(transaction);
}
/**
* 校验是否在提现日期范围内
* 配置格式:{"start": 25, "days": 6}
* 表示每月从第 start 天开始,连续 days 天可以提现
*/
private void validateWithdrawDate() {
// 获取提现日期配置
String configValue = configApi.getConfigValueByKey(WITHDRAW_DAYS_CONFIG_KEY);
if (configValue == null || configValue.isEmpty()) {
throw exception(TENANT_BALANCE_WITHDRAW_CONFIG_NOT_EXISTS);
}
// 解析配置
cn.hutool.json.JSONObject config = JSONUtil.parseObj(configValue);
int startDay = config.getInt("start");
int days = config.getInt("days");
// 获取当前日期
LocalDate today = LocalDate.now();
int currentDay = today.getDayOfMonth();
int lastDayOfMonth = today.lengthOfMonth();
// 计算提现日期范围
// 如果 start + days - 1 超过当月最后一天,需要处理跨月的情况
int endDay = startDay + days - 1;
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;
}
if (!isInRange) {
throw exception(TENANT_BALANCE_WITHDRAW_NOT_IN_DATE);
}
}
}

View File

@@ -87,6 +87,9 @@ public interface ErrorCodeConstants {
ErrorCode USER_THEMES_NOT_EXISTS = new ErrorCode(1_001_202_013, "用户主题不存在");
ErrorCode TENANT_BALANCE_NOT_EXISTS = new ErrorCode(1_001_202_014, "租户余额不存在");
ErrorCode TENANT_BALANCE_TRANSACTION_NOT_EXISTS = new ErrorCode(1_001_202_015, "租户积分记录不存在");
ErrorCode TENANT_BALANCE_WITHDRAW_NOT_IN_DATE = new ErrorCode(1_001_202_016, "当前不在提现日期范围内");
ErrorCode TENANT_BALANCE_WITHDRAW_INSUFFICIENT = new ErrorCode(1_001_202_017, "余额不足,无法提现");
ErrorCode TENANT_BALANCE_WITHDRAW_CONFIG_NOT_EXISTS = new ErrorCode(1_001_202_018, "提现配置不存在");
}