Compare commits

..

14 Commits

Author SHA1 Message Date
a99f05d029 fix(tenant): 限制一级代理下级沿用分成比例 2026-01-06 21:20:08 +08:00
bd315fcbd9 refactor(balance): 补充冻结金额和可提现余额字段 2026-01-06 20:50:16 +08:00
716cde6ea0 fix(tenantwithdraworder): 修复提现失败时冻结金额未退还问题
新增 handleWithdrawRefund 逻辑,当状态为 REJECTED/CANCELED/FAILED 时自动返还冻结金额至可提现余额并生成对应流水。
2026-01-06 20:07:30 +08:00
04755188d6 fix(service): 提现成功时同步生成余额流水记录 2026-01-06 19:48:06 +08:00
b12f232f56 fix(service): 提现成功时同步扣减租户冻结金额 2026-01-06 19:40:04 +08:00
d75a3e0212 fix(tenantbalance): 修正提现校验逻辑为可提现金额 2026-01-06 19:27:47 +08:00
299bc5e28b fix(service): 修复系统管理员只能查看1级代理提现申请的逻辑 2026-01-06 19:21:10 +08:00
d7ed10f45d fix(tenant-withdraw): 增加下级租户提现订单数据权限过滤 2026-01-06 19:14:15 +08:00
ca6e3d20f6 refactor(tenant-withdraw): 优化分页查询返回VO并填充租户名称 2026-01-06 14:20:41 +08:00
6fad3b45fe refactor(tenant-commission): 移除我的分页接口并统一按租户权限过滤 2025-12-31 10:33:37 +08:00
a8da54c130 fix(tenant): 限制一级代理只能查看直属下级租户 2025-12-30 21:54:07 +08:00
d105bd4fa6 feat(tenant): 新增获取当前登录租户信息接口 2025-12-30 21:44:54 +08:00
be54601cdd feat(tenant-commission): 新增当前租户分成记录分页接口
在 KeyboardTenantCommissionController 增加 /my-page 端点,仅返回当前登录租户的分成记录;
Mapper 新增 selectPageByTenantId 方法,Service 层实现 getMyTenantCommissionPage,通过 TenantContextHolder 获取当前租户 ID 进行过滤。
2025-12-30 20:58:50 +08:00
2207add193 feat(commission): 支持二级代理分成计算与余额精度改为BigDecimal
- 新增Mapper方法:按内购记录ID+租户ID查重,避免重复分成
- 将租户余额字段从Integer升级为BigDecimal,保证金额精度
- 重构定时任务:拆分一级/二级代理逻辑,按返点比例分别创建分成记录
- 提取createCommissionRecord方法,复用余额更新与交易流水创建逻辑
2025-12-30 19:29:49 +08:00
25 changed files with 592 additions and 115 deletions

View File

@@ -141,9 +141,10 @@ public class TenantBalanceController {
@Operation(summary = "获得租户积分记录分页")
@Parameter(name = "tenantId", description = "租户 Id")
@PreAuthorize("@ss.hasPermission('keyboard:tenant-balance:query')")
public CommonResult<PageResult<TenantBalanceTransactionDO>> getTenantBalanceTransactionPage(PageParam pageReqVO,
public CommonResult<PageResult<TenantBalanceTransactionRespVO>> getTenantBalanceTransactionPage(PageParam pageReqVO,
@RequestParam("tenantId") Long tenantId) {
return success(tenantBalanceService.getTenantBalanceTransactionPage(pageReqVO, tenantId));
PageResult<TenantBalanceTransactionDO> pageResult = tenantBalanceService.getTenantBalanceTransactionPage(pageReqVO, tenantId);
return success(BeanUtils.toBean(pageResult, TenantBalanceTransactionRespVO.class));
}
@PostMapping("/tenant-balance-transaction/create")
@@ -183,7 +184,8 @@ public class TenantBalanceController {
@Operation(summary = "获得租户积分记录")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('keyboard:tenant-balance:query')")
public CommonResult<TenantBalanceTransactionDO> getTenantBalanceTransaction(@RequestParam("id") Long id) {
return success(tenantBalanceService.getTenantBalanceTransaction(id));
public CommonResult<TenantBalanceTransactionRespVO> getTenantBalanceTransaction(@RequestParam("id") Long id) {
TenantBalanceTransactionDO transaction = tenantBalanceService.getTenantBalanceTransaction(id);
return success(BeanUtils.toBean(transaction, TenantBalanceTransactionRespVO.class));
}
}

View File

@@ -16,7 +16,7 @@ import static com.yolo.keyboard.framework.common.util.date.DateUtils.FORMAT_YEAR
public class TenantBalancePageReqVO extends PageParam {
@Schema(description = "当前积分余额")
private Integer balance;
private BigDecimal balance;
@Schema(description = "乐观锁版本号")
private Integer version;

View File

@@ -20,7 +20,7 @@ public class TenantBalanceRespVO {
@Schema(description = "当前积分余额", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("当前积分余额")
private Integer balance;
private BigDecimal balance;
@Schema(description = "乐观锁版本号", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("乐观锁版本号")

View File

@@ -18,7 +18,7 @@ public class TenantBalanceSaveReqVO {
@Schema(description = "当前积分余额", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "当前积分余额不能为空")
private Integer balance;
private BigDecimal balance;
@Schema(description = "乐观锁版本号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "乐观锁版本号不能为空")

View File

@@ -0,0 +1,52 @@
package com.yolo.keyboard.controller.admin.tenantbalance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 租户积分记录 Response VO")
@Data
public class TenantBalanceTransactionRespVO {
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "本次变动点数,正加负减", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
private BigDecimal points;
@Schema(description = "变动后余额快照", example = "1000.00")
private BigDecimal balance;
@Schema(description = "变动后冻结金额快照", example = "200.00")
private BigDecimal frozenAmt;
@Schema(description = "变动后可提现余额快照", example = "800.00")
private BigDecimal withdrawableBalance;
@Schema(description = "变动类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "RECHARGE")
private String type;
@Schema(description = "变动描述", example = "余额充值")
private String description;
@Schema(description = "订单Id/业务单号", example = "ORD123456")
private String orderId;
@Schema(description = "业务流水号", example = "BIZ123456")
private String bizNo;
@Schema(description = "操作人Id", example = "1")
private Long operatorId;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "备注", example = "管理员充值")
private String remark;
@Schema(description = "租户Id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long tenantId;
}

View File

@@ -38,10 +38,12 @@ public class KeyboardTenantCommissionController {
@Resource
private KeyboardTenantCommissionService tenantCommissionService;
@PostMapping("/create")
@Operation(summary = "创建租户内购分成记录")
@PreAuthorize("@ss.hasPermission('keyboard:tenant-commission:create')")
public CommonResult<Long> createTenantCommission(@Valid @RequestBody KeyboardTenantCommissionSaveReqVO createReqVO) {
return success(tenantCommissionService.createTenantCommission(createReqVO));
}

View File

@@ -14,6 +14,9 @@ import static com.yolo.keyboard.framework.common.util.date.DateUtils.FORMAT_YEAR
@Data
public class KeyboardTenantCommissionPageReqVO extends PageParam {
@Schema(description = "租户ID", example = "1", hidden = true)
private Long tenantId;
@Schema(description = "内购记录ID", example = "20900")
private Integer purchaseRecordId;

View File

@@ -84,8 +84,8 @@ public class KeyboardTenantWithdrawOrderController {
@Operation(summary = "获得租户提现订单表(申请-审核-打款-完成/失败)分页")
@PreAuthorize("@ss.hasPermission('keyboard:tenant-withdraw-order:query')")
public CommonResult<PageResult<KeyboardTenantWithdrawOrderRespVO>> getTenantWithdrawOrderPage(@Valid KeyboardTenantWithdrawOrderPageReqVO pageReqVO) {
PageResult<KeyboardTenantWithdrawOrderDO> pageResult = tenantWithdrawOrderService.getTenantWithdrawOrderPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, KeyboardTenantWithdrawOrderRespVO.class));
PageResult<KeyboardTenantWithdrawOrderRespVO> pageResult = tenantWithdrawOrderService.getTenantWithdrawOrderPage(pageReqVO);
return success(pageResult);
}
@GetMapping("/export-excel")
@@ -95,10 +95,9 @@ public class KeyboardTenantWithdrawOrderController {
public void exportTenantWithdrawOrderExcel(@Valid KeyboardTenantWithdrawOrderPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<KeyboardTenantWithdrawOrderDO> list = tenantWithdrawOrderService.getTenantWithdrawOrderPage(pageReqVO).getList();
List<KeyboardTenantWithdrawOrderRespVO> list = tenantWithdrawOrderService.getTenantWithdrawOrderPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "租户提现订单表(申请-审核-打款-完成/失败).xls", "数据", KeyboardTenantWithdrawOrderRespVO.class,
BeanUtils.toBean(list, KeyboardTenantWithdrawOrderRespVO.class));
ExcelUtils.write(response, "租户提现订单表(申请-审核-打款-完成/失败).xls", "数据", KeyboardTenantWithdrawOrderRespVO.class, list);
}
}

View File

@@ -111,4 +111,7 @@ public class KeyboardTenantWithdrawOrderPageReqVO extends PageParam {
@Schema(description = "更新时间")
private LocalDateTime updatedAt;
@Schema(description = "租户ID列表用于下级租户过滤", hidden = true)
private List<Long> tenantIds;
}

View File

@@ -142,4 +142,12 @@ public class KeyboardTenantWithdrawOrderRespVO {
@ExcelProperty("更新时间")
private LocalDateTime updatedAt;
@Schema(description = "租户编号", example = "1")
@ExcelProperty("租户编号")
private Long tenantId;
@Schema(description = "租户名称", example = "芋道源码")
@ExcelProperty("租户名称")
private String tenantName;
}

View File

@@ -67,4 +67,8 @@ public class TenantBalanceTransactionDO {
private String remark;
private Long tenantId;
private BigDecimal frozenAmt;
private BigDecimal withdrawableBalance;
}

View File

@@ -29,8 +29,19 @@ public interface KeyboardTenantCommissionMapper extends BaseMapperX<KeyboardTena
return selectOne(KeyboardTenantCommissionDO::getPurchaseRecordId, purchaseRecordId);
}
/**
* 根据内购记录ID和租户ID查询分成记录
* 用于检查某个租户是否已经对某条内购记录计算过分成
*/
default KeyboardTenantCommissionDO selectByPurchaseRecordIdAndTenantId(Integer purchaseRecordId, Long tenantId) {
return selectOne(new LambdaQueryWrapperX<KeyboardTenantCommissionDO>()
.eq(KeyboardTenantCommissionDO::getPurchaseRecordId, purchaseRecordId)
.eq(KeyboardTenantCommissionDO::getTenantId, tenantId));
}
default PageResult<KeyboardTenantCommissionDO> selectPage(KeyboardTenantCommissionPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<KeyboardTenantCommissionDO>()
.eqIfPresent(KeyboardTenantCommissionDO::getTenantId, reqVO.getTenantId())
.eqIfPresent(KeyboardTenantCommissionDO::getPurchaseRecordId, reqVO.getPurchaseRecordId())
.eqIfPresent(KeyboardTenantCommissionDO::getTransactionId, reqVO.getTransactionId())
.eqIfPresent(KeyboardTenantCommissionDO::getInviteeUserId, reqVO.getInviteeUserId())
@@ -47,5 +58,4 @@ public interface KeyboardTenantCommissionMapper extends BaseMapperX<KeyboardTena
.eqIfPresent(KeyboardTenantCommissionDO::getRemark, reqVO.getRemark())
.orderByDesc(KeyboardTenantCommissionDO::getId));
}
}

View File

@@ -19,6 +19,7 @@ public interface KeyboardTenantWithdrawOrderMapper extends BaseMapperX<KeyboardT
default PageResult<KeyboardTenantWithdrawOrderDO> selectPage(KeyboardTenantWithdrawOrderPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<KeyboardTenantWithdrawOrderDO>()
.inIfPresent(KeyboardTenantWithdrawOrderDO::getTenantId, reqVO.getTenantIds())
.eqIfPresent(KeyboardTenantWithdrawOrderDO::getWithdrawNo, reqVO.getWithdrawNo())
.eqIfPresent(KeyboardTenantWithdrawOrderDO::getBizNo, reqVO.getBizNo())
.eqIfPresent(KeyboardTenantWithdrawOrderDO::getCurrency, reqVO.getCurrency())

View File

@@ -101,6 +101,8 @@ public class CommissionWithdrawableJob implements JobHandler {
.bizNo(bizNo)
.points(tenantTotalAmount)
.balance(balance.getBalance())
.frozenAmt(balance.getFrozenAmt() != null ? balance.getFrozenAmt() : BigDecimal.ZERO)
.withdrawableBalance(newWithdrawable)
.tenantId(tenantId)
.type(WITHDRAWABLE_TYPE)
.description("分成转可提现")

View File

@@ -30,6 +30,7 @@ import java.util.List;
/**
* 租户分成计算定时任务
* 每小时执行一次,计算邀请用户的内购分成
* 支持一级代理和二级代理的分成计算
*
* @author ziin
*/
@@ -110,101 +111,112 @@ public class TenantCommissionCalculateJob implements JobHandler {
continue;
}
// 获取收益归属租户
Long tenantId = invite.getProfitTenantId();
if (tenantId == null) {
tenantId = invite.getInviterTenantId();
// 获取收益归属租户(邀请人所属租户)
Long inviterTenantId = invite.getProfitTenantId();
if (inviterTenantId == null) {
inviterTenantId = invite.getInviterTenantId();
}
if (tenantId == null) {
if (inviterTenantId == null) {
log.warn("[TenantCommissionCalculateJob] 用户 {} 的邀请关系没有关联租户,跳过", record.getUserId());
continue;
}
// 获取租户信息和分成比例
TenantDO tenant = tenantMapper.selectById(tenantId);
if (tenant == null) {
log.warn("[TenantCommissionCalculateJob] 租户 {} 不存在,跳过", tenantId);
// 获取邀请人租户信息
TenantDO inviterTenant = tenantMapper.selectById(inviterTenantId);
if (inviterTenant == null) {
log.warn("[TenantCommissionCalculateJob] 租户 {} 不存在,跳过", inviterTenantId);
continue;
}
BigDecimal commissionRate = tenant.getProfitShareRatio();
if (commissionRate == null || commissionRate.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("[TenantCommissionCalculateJob] 租户 {} 没有设置分成比例,跳过", tenantId);
continue;
}
// 3. 计算分成金额
// 获取内购金额
BigDecimal purchaseAmount = record.getPrice();
if (purchaseAmount == null || purchaseAmount.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("[TenantCommissionCalculateJob] 内购记录 {} 金额无效,跳过", record.getId());
continue;
}
BigDecimal commissionAmount = purchaseAmount.multiply(commissionRate)
.setScale(2, RoundingMode.HALF_UP);
// 4. 创建分成记录
LocalDateTime now = LocalDateTime.now();
LocalDateTime withdrawableAt = now.plusDays(30); // 30天后可提现
KeyboardTenantCommissionDO commission = KeyboardTenantCommissionDO.builder()
.purchaseRecordId(record.getId())
.transactionId(record.getTransactionId())
.inviteeUserId(record.getUserId())
.inviterUserId(invite.getProfitEmployeeId())
.tenantId(tenantId)
.purchaseAmount(purchaseAmount)
.commissionRate(commissionRate)
.commissionAmount(commissionAmount)
.status("SETTLED")
.purchaseTime(record.getPurchaseTime())
.settledAt(now)
.withdrawableAt(withdrawableAt)
.withdrawableProcessed(false)
.createdAt(now)
.updatedAt(now)
.build();
LocalDateTime withdrawableAt = now.plusDays(30);
// 5. 更新租户余额分成金额先计入总余额30天后才可提现
TenantBalanceDO balance = tenantBalanceMapper.selectById(tenantId);
if (balance == null) {
// 如果租户余额记录不存在,创建一个
balance = new TenantBalanceDO();
balance.setId(tenantId);
balance.setBalance(commissionAmount);
balance.setWithdrawableBalance(BigDecimal.ZERO);
balance.setFrozenAmt(BigDecimal.ZERO);
balance.setVersion(0);
tenantBalanceMapper.insert(balance);
// 判断是否是二级代理(有上级租户
if (inviterTenant.getParentId() != null) {
// 二级代理场景:需要给一级代理和二级代理都分成
TenantDO parentTenant = tenantMapper.selectById(inviterTenant.getParentId());
if (parentTenant == null) {
log.warn("[TenantCommissionCalculateJob] 上级租户 {} 不存在,跳过", inviterTenant.getParentId());
continue;
}
// 使用一级代理的分成比例计算总分成
BigDecimal totalCommissionRate = parentTenant.getProfitShareRatio();
if (totalCommissionRate == null || totalCommissionRate.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("[TenantCommissionCalculateJob] 一级代理租户 {} 没有设置分成比例,跳过", parentTenant.getId());
continue;
}
BigDecimal totalCommissionAmount = purchaseAmount.multiply(totalCommissionRate)
.setScale(2, RoundingMode.HALF_UP);
// 获取二级代理的返点比例
BigDecimal rebateRatio = inviterTenant.getUpstreamRebateRatio();
if (rebateRatio == null || rebateRatio.compareTo(BigDecimal.ZERO) <= 0) {
// 如果没有设置返点比例,全部归一级代理
log.debug("[TenantCommissionCalculateJob] 二级代理租户 {} 没有设置返点比例,全部归一级代理", inviterTenantId);
int count = createCommissionRecord(record, invite, parentTenant.getId(), purchaseAmount,
totalCommissionRate, totalCommissionAmount, now, withdrawableAt,
"一级代理分成(二级代理无返点)");
commissionCount += count;
totalCommission = totalCommission.add(totalCommissionAmount);
} else {
// 计算二级代理分成
BigDecimal secondLevelAmount = totalCommissionAmount.multiply(rebateRatio)
.setScale(2, RoundingMode.HALF_UP);
// 计算一级代理分成
BigDecimal firstLevelAmount = totalCommissionAmount.subtract(secondLevelAmount);
// 为二级代理创建分成记录
if (secondLevelAmount.compareTo(BigDecimal.ZERO) > 0) {
int count = createCommissionRecord(record, invite, inviterTenantId, purchaseAmount,
rebateRatio, secondLevelAmount, now, withdrawableAt,
"二级代理分成");
commissionCount += count;
totalCommission = totalCommission.add(secondLevelAmount);
log.info("[TenantCommissionCalculateJob] 内购记录 {}, 二级代理 {}, 分成金额 {}",
record.getId(), inviterTenantId, secondLevelAmount);
}
// 为一级代理创建分成记录
if (firstLevelAmount.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal firstLevelRate = BigDecimal.ONE.subtract(rebateRatio);
int count = createCommissionRecord(record, invite, parentTenant.getId(), purchaseAmount,
firstLevelRate, firstLevelAmount, now, withdrawableAt,
"一级代理分成(扣除二级返点)");
commissionCount += count;
totalCommission = totalCommission.add(firstLevelAmount);
log.info("[TenantCommissionCalculateJob] 内购记录 {}, 一级代理 {}, 分成金额 {}",
record.getId(), parentTenant.getId(), firstLevelAmount);
}
}
} else {
BigDecimal newBalance = balance.getBalance().add(commissionAmount);
balance.setBalance(newBalance);
tenantBalanceMapper.updateById(balance);
// 一级代理场景:全部分成归一级代理
BigDecimal commissionRate = inviterTenant.getProfitShareRatio();
if (commissionRate == null || commissionRate.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("[TenantCommissionCalculateJob] 租户 {} 没有设置分成比例,跳过", inviterTenantId);
continue;
}
BigDecimal commissionAmount = purchaseAmount.multiply(commissionRate)
.setScale(2, RoundingMode.HALF_UP);
int count = createCommissionRecord(record, invite, inviterTenantId, purchaseAmount,
commissionRate, commissionAmount, now, withdrawableAt,
"一级代理分成");
commissionCount += count;
totalCommission = totalCommission.add(commissionAmount);
log.info("[TenantCommissionCalculateJob] 内购记录 {}, 一级代理 {}, 分成金额 {}",
record.getId(), inviterTenantId, commissionAmount);
}
// 6. 创建余额交易记录
String bizNo = BizNoGenerator.generate("COMM");
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
.bizNo(bizNo)
.points(commissionAmount)
.balance(balance.getBalance())
.tenantId(tenantId)
.type(COMMISSION_TYPE)
.description("邀请用户内购分成")
.orderId(record.getTransactionId())
.createdAt(now)
.remark("内购记录ID: " + record.getId() + ", 被邀请用户: " + record.getUserId())
.build();
balanceTransactionMapper.insert(transaction);
// 更新分成记录的关联交易ID
commission.setBalanceTransactionId(transaction.getId());
commissionMapper.insert(commission);
commissionCount++;
totalCommission = totalCommission.add(commissionAmount);
log.info("[TenantCommissionCalculateJob] 处理内购记录 {}, 租户 {}, 分成金额 {}",
record.getId(), tenantId, commissionAmount);
}
String result = String.format("处理内购记录 %d 条,生成分成 %d 条,总分成金额 %s",
@@ -212,4 +224,85 @@ public class TenantCommissionCalculateJob implements JobHandler {
log.info("[TenantCommissionCalculateJob] 任务执行完成: {}", result);
return result;
}
/**
* 创建分成记录并更新租户余额
*
* @return 创建的分成记录数量
*/
private int createCommissionRecord(KeyboardUserPurchaseRecordsDO record,
KeyboardUserInvitesDO invite,
Long tenantId,
BigDecimal purchaseAmount,
BigDecimal commissionRate,
BigDecimal commissionAmount,
LocalDateTime now,
LocalDateTime withdrawableAt,
String remark) {
// 0. 检查该租户是否已对该内购记录计算过分成(防止重复计算)
KeyboardTenantCommissionDO existingCommission = commissionMapper.selectByPurchaseRecordIdAndTenantId(record.getId(), tenantId);
if (existingCommission != null) {
log.debug("[TenantCommissionCalculateJob] 租户 {} 已对内购记录 {} 计算过分成,跳过", tenantId, record.getId());
return 0;
}
// 1. 创建分成记录
KeyboardTenantCommissionDO commission = KeyboardTenantCommissionDO.builder()
.purchaseRecordId(record.getId())
.transactionId(record.getTransactionId())
.inviteeUserId(record.getUserId())
.inviterUserId(invite.getProfitEmployeeId())
.tenantId(tenantId)
.purchaseAmount(purchaseAmount)
.commissionRate(commissionRate)
.commissionAmount(commissionAmount)
.status("SETTLED")
.purchaseTime(record.getPurchaseTime())
.settledAt(now)
.withdrawableAt(withdrawableAt)
.withdrawableProcessed(false)
.createdAt(now)
.updatedAt(now)
.remark(remark)
.build();
// 2. 更新租户余额分成金额先计入总余额30天后才可提现
TenantBalanceDO balance = tenantBalanceMapper.selectById(tenantId);
if (balance == null) {
balance = new TenantBalanceDO();
balance.setId(tenantId);
balance.setBalance(commissionAmount);
balance.setWithdrawableBalance(BigDecimal.ZERO);
balance.setFrozenAmt(BigDecimal.ZERO);
balance.setVersion(0);
tenantBalanceMapper.insert(balance);
} else {
BigDecimal newBalance = balance.getBalance().add(commissionAmount);
balance.setBalance(newBalance);
tenantBalanceMapper.updateById(balance);
}
// 3. 创建余额交易记录
String bizNo = BizNoGenerator.generate("COMM");
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
.bizNo(bizNo)
.points(commissionAmount)
.balance(balance.getBalance())
.frozenAmt(balance.getFrozenAmt() != null ? balance.getFrozenAmt() : BigDecimal.ZERO)
.withdrawableBalance(balance.getWithdrawableBalance() != null ? balance.getWithdrawableBalance() : BigDecimal.ZERO)
.tenantId(tenantId)
.type(COMMISSION_TYPE)
.description("邀请用户内购分成")
.orderId(record.getTransactionId())
.createdAt(now)
.remark("内购记录ID: " + record.getId() + ", 被邀请用户: " + record.getUserId() + ", " + remark)
.build();
balanceTransactionMapper.insert(transaction);
// 4. 更新分成记录的关联交易ID并保存
commission.setBalanceTransactionId(transaction.getId());
commissionMapper.insert(commission);
return 1;
}
}

View File

@@ -140,6 +140,8 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
.bizNo(BizNoGenerator.generate("RECHARGE")) // 生成充值业务编号
.points(new BigDecimal(String.valueOf(addReqVO.getAmount()))) // 充值金额
.balance(newBalance) // 充值后余额
.frozenAmt(balance.getFrozenAmt() != null ? balance.getFrozenAmt() : BigDecimal.ZERO) // 当前冻结金额
.withdrawableBalance(balance.getWithdrawableBalance() != null ? balance.getWithdrawableBalance() : BigDecimal.ZERO) // 当前可提现金额
.tenantId(addReqVO.getId())
.type("RECHARGE") // 交易类型:充值
.description("余额充值") // 交易描述
@@ -243,18 +245,18 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
throw exception(TENANT_BALANCE_NOT_EXISTS);
}
// 3. 校验可用余额是否充足(可用余额 = 余额 - 冻结金额)
// 3. 校验可提现金额是否充足
BigDecimal withdrawAmount = withdrawReqVO.getAmount();
BigDecimal frozenAmt = balance.getFrozenAmt() != null ? balance.getFrozenAmt() : BigDecimal.ZERO;
BigDecimal availableBalance = balance.getBalance().subtract(frozenAmt);
if (availableBalance.compareTo(withdrawAmount) < 0) {
BigDecimal withdrawableBalance = balance.getWithdrawableBalance() != null ? balance.getWithdrawableBalance() : BigDecimal.ZERO;
if (withdrawableBalance.compareTo(withdrawAmount) < 0) {
throw exception(TENANT_BALANCE_WITHDRAW_INSUFFICIENT);
}
// 4. 扣减余额并增加冻结金额
BigDecimal newBalance = balance.getBalance().subtract(withdrawAmount);
// 4. 从可提现金额中扣减并增加冻结金额
BigDecimal frozenAmt = balance.getFrozenAmt() != null ? balance.getFrozenAmt() : BigDecimal.ZERO;
BigDecimal newWithdrawableBalance = withdrawableBalance.subtract(withdrawAmount);
BigDecimal newFrozenAmt = frozenAmt.add(withdrawAmount);
balance.setBalance(newBalance);
balance.setWithdrawableBalance(newWithdrawableBalance);
balance.setFrozenAmt(newFrozenAmt);
int updateCount = tenantBalanceMapper.updateById(balance);
if (updateCount == 0) {
@@ -268,8 +270,10 @@ public class TenantBalanceServiceImpl implements TenantBalanceService {
// 6. 创建冻结交易记录
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
.bizNo(bizNo)
.points(withdrawAmount.negate()) // 冻结金额(负数表示冻结扣减)
.balance(newBalance) // 扣减后的余额
.points(withdrawAmount.negate()) // 冻结金额(负数表示冻结扣减)
.balance(balance.getBalance()) // 当前总余额
.frozenAmt(newFrozenAmt) // 冻结后的冻结金额
.withdrawableBalance(newWithdrawableBalance) // 扣减后的可提现余额
.tenantId(tenantId)
.type("FREEZE")
.description("提现冻结")

View File

@@ -59,4 +59,11 @@ public interface KeyboardTenantCommissionService {
*/
PageResult<KeyboardTenantCommissionDO> getTenantCommissionPage(KeyboardTenantCommissionPageReqVO pageReqVO);
/**
* 获得当前登录租户的分成记录分页
*
* @param pageReqVO 分页查询
* @return 租户内购分成记录分页
*/
}

View File

@@ -14,6 +14,9 @@ import com.yolo.keyboard.framework.common.pojo.PageParam;
import com.yolo.keyboard.framework.common.util.object.BeanUtils;
import com.yolo.keyboard.dal.mysql.tenantcommission.KeyboardTenantCommissionMapper;
import com.yolo.keyboard.framework.tenant.core.context.TenantContextHolder;
import com.yolo.keyboard.module.system.dal.dataobject.tenant.TenantDO;
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;
@@ -31,6 +34,9 @@ public class KeyboardTenantCommissionServiceImpl implements KeyboardTenantCommis
@Resource
private KeyboardTenantCommissionMapper tenantCommissionMapper;
@Resource
private TenantMapper tenantMapper;
@Override
public Long createTenantCommission(KeyboardTenantCommissionSaveReqVO createReqVO) {
// 插入
@@ -78,6 +84,15 @@ public class KeyboardTenantCommissionServiceImpl implements KeyboardTenantCommis
@Override
public PageResult<KeyboardTenantCommissionDO> getTenantCommissionPage(KeyboardTenantCommissionPageReqVO pageReqVO) {
// 如果当前租户的 tenantLevel 不等于 0只能查询属于自己的数据
Long currentTenantId = TenantContextHolder.getTenantId();
if (currentTenantId != null) {
TenantDO currentTenant = tenantMapper.selectById(currentTenantId);
if (currentTenant != null && currentTenant.getTenantLevel() != null
&& currentTenant.getTenantLevel() != 0) {
pageReqVO.setTenantId(currentTenantId);
}
}
return tenantCommissionMapper.selectPage(pageReqVO);
}

View File

@@ -57,6 +57,6 @@ public interface KeyboardTenantWithdrawOrderService {
* @param pageReqVO 分页查询
* @return 租户提现订单表(申请-审核-打款-完成/失败)分页
*/
PageResult<KeyboardTenantWithdrawOrderDO> getTenantWithdrawOrderPage(KeyboardTenantWithdrawOrderPageReqVO pageReqVO);
PageResult<KeyboardTenantWithdrawOrderRespVO> getTenantWithdrawOrderPage(KeyboardTenantWithdrawOrderPageReqVO pageReqVO);
}

View File

@@ -1,23 +1,34 @@
package com.yolo.keyboard.service.tenantwithdraworder;
import com.yolo.keyboard.controller.admin.tenantwithdraworder.vo.KeyboardTenantWithdrawOrderPageReqVO;
import com.yolo.keyboard.controller.admin.tenantwithdraworder.vo.KeyboardTenantWithdrawOrderRespVO;
import com.yolo.keyboard.controller.admin.tenantwithdraworder.vo.KeyboardTenantWithdrawOrderSaveReqVO;
import com.yolo.keyboard.dal.dataobject.tenantbalance.TenantBalanceDO;
import com.yolo.keyboard.dal.dataobject.tenantbalancetransaction.TenantBalanceTransactionDO;
import com.yolo.keyboard.dal.dataobject.tenantwithdraworder.KeyboardTenantWithdrawOrderDO;
import com.yolo.keyboard.dal.mysql.tenantbalance.TenantBalanceMapper;
import com.yolo.keyboard.dal.mysql.tenantbalancetransaction.TenantBalanceTransactionMapper;
import com.yolo.keyboard.dal.mysql.tenantwithdraworder.KeyboardTenantWithdrawOrderMapper;
import com.yolo.keyboard.framework.common.pojo.PageResult;
import com.yolo.keyboard.framework.common.util.object.BeanUtils;
import com.yolo.keyboard.framework.tenant.core.context.TenantContextHolder;
import com.yolo.keyboard.module.system.dal.dataobject.tenant.TenantDO;
import com.yolo.keyboard.module.system.dal.mysql.tenant.TenantMapper;
import cn.hutool.core.collection.CollUtil;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import com.yolo.keyboard.controller.admin.tenantwithdraworder.vo.*;
import com.yolo.keyboard.dal.dataobject.tenantwithdraworder.KeyboardTenantWithdrawOrderDO;
import com.yolo.keyboard.framework.common.pojo.PageResult;
import com.yolo.keyboard.framework.common.pojo.PageParam;
import com.yolo.keyboard.framework.common.util.object.BeanUtils;
import com.yolo.keyboard.dal.mysql.tenantwithdraworder.KeyboardTenantWithdrawOrderMapper;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
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_WITHDRAW_ORDER_NOT_EXISTS;
/**
@@ -32,6 +43,15 @@ public class KeyboardTenantWithdrawOrderServiceImpl implements KeyboardTenantWit
@Resource
private KeyboardTenantWithdrawOrderMapper tenantWithdrawOrderMapper;
@Resource
private TenantMapper tenantMapper;
@Resource
private TenantBalanceMapper tenantBalanceMapper;
@Resource
private TenantBalanceTransactionMapper tenantBalanceTransactionMapper;
@Override
public Long createTenantWithdrawOrder(KeyboardTenantWithdrawOrderSaveReqVO createReqVO) {
// 插入
@@ -43,12 +63,145 @@ public class KeyboardTenantWithdrawOrderServiceImpl implements KeyboardTenantWit
}
@Override
@Transactional
public void updateTenantWithdrawOrder(KeyboardTenantWithdrawOrderSaveReqVO updateReqVO) {
// 校验存在
validateTenantWithdrawOrderExists(updateReqVO.getId());
// 更新
KeyboardTenantWithdrawOrderDO existingOrder = tenantWithdrawOrderMapper.selectById(updateReqVO.getId());
if (existingOrder == null) {
throw exception(TENANT_WITHDRAW_ORDER_NOT_EXISTS);
}
// 更新订单
KeyboardTenantWithdrawOrderDO updateObj = BeanUtils.toBean(updateReqVO, KeyboardTenantWithdrawOrderDO.class);
tenantWithdrawOrderMapper.updateById(updateObj);
String newStatus = updateReqVO.getStatus();
String oldStatus = existingOrder.getStatus();
// 如果提现状态更新为成功PAID则扣除提现用户的冻结金额并创建流水记录
if ("PAID".equals(newStatus) && !"PAID".equals(oldStatus)) {
handleWithdrawSuccess(existingOrder);
}
// 如果提现状态更新为已拒绝、已取消、打款失败,则返还冻结金额到可提现余额并创建流水记录
else if (isRefundStatus(newStatus) && !isRefundStatus(oldStatus)) {
handleWithdrawRefund(existingOrder, newStatus, updateReqVO.getReason());
}
}
/**
* 判断是否为需要退还冻结金额的状态
*/
private boolean isRefundStatus(String status) {
return "REJECTED".equals(status) || "CANCELED".equals(status) || "FAILED".equals(status);
}
/**
* 处理提现成功:扣除冻结金额并记录流水
*/
private void handleWithdrawSuccess(KeyboardTenantWithdrawOrderDO order) {
TenantBalanceDO balance = tenantBalanceMapper.selectById(order.getTenantId());
if (balance == null) {
return;
}
// 扣除冻结金额
BigDecimal frozenAmt = balance.getFrozenAmt() != null ? balance.getFrozenAmt() : BigDecimal.ZERO;
BigDecimal withdrawAmount = order.getAmount();
BigDecimal newFrozenAmt = frozenAmt.subtract(withdrawAmount);
if (newFrozenAmt.compareTo(BigDecimal.ZERO) < 0) {
newFrozenAmt = BigDecimal.ZERO;
}
balance.setFrozenAmt(newFrozenAmt);
tenantBalanceMapper.updateById(balance);
// 创建提现成功的流水记录
BigDecimal currentWithdrawableBalance = balance.getWithdrawableBalance() != null ? balance.getWithdrawableBalance() : BigDecimal.ZERO;
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
.bizNo(order.getBizNo())
.points(withdrawAmount.negate()) // 提现金额(负数表示支出)
.balance(balance.getBalance()) // 当前总余额
.frozenAmt(newFrozenAmt) // 扣除后的冻结金额
.withdrawableBalance(currentWithdrawableBalance) // 当前可提现余额
.tenantId(order.getTenantId())
.type("WITHDRAW_SUCCESS")
.description("提现成功")
.orderId(order.getWithdrawNo())
.operatorId(TenantContextHolder.getTenantId())
.createdAt(LocalDateTime.now())
.build();
tenantBalanceTransactionMapper.insert(transaction);
}
/**
* 处理提现退还:返还冻结金额到可提现余额并记录流水
*
* @param order 提现订单
* @param newStatus 新状态
* @param reason 拒绝/失败/取消原因
*/
private void handleWithdrawRefund(KeyboardTenantWithdrawOrderDO order, String newStatus, String reason) {
TenantBalanceDO balance = tenantBalanceMapper.selectById(order.getTenantId());
if (balance == null) {
return;
}
BigDecimal withdrawAmount = order.getAmount();
// 扣除冻结金额
BigDecimal frozenAmt = balance.getFrozenAmt() != null ? balance.getFrozenAmt() : BigDecimal.ZERO;
BigDecimal newFrozenAmt = frozenAmt.subtract(withdrawAmount);
if (newFrozenAmt.compareTo(BigDecimal.ZERO) < 0) {
newFrozenAmt = BigDecimal.ZERO;
}
balance.setFrozenAmt(newFrozenAmt);
// 返还到可提现余额
BigDecimal withdrawableBalance = balance.getWithdrawableBalance() != null ? balance.getWithdrawableBalance() : BigDecimal.ZERO;
BigDecimal newWithdrawableBalance = withdrawableBalance.add(withdrawAmount);
balance.setWithdrawableBalance(newWithdrawableBalance);
tenantBalanceMapper.updateById(balance);
// 根据状态确定流水类型和描述
String type;
String description;
switch (newStatus) {
case "REJECTED":
type = "WITHDRAW_REJECTED";
description = "提现被拒绝,金额已退还";
break;
case "CANCELED":
type = "WITHDRAW_CANCELED";
description = "提现已取消,金额已退还";
break;
case "FAILED":
type = "WITHDRAW_FAILED";
description = "提现打款失败,金额已退还";
break;
default:
type = "WITHDRAW_REFUND";
description = "提现退还";
}
// 备注:优先使用传入的原因,如果没有则使用默认描述
String remark = (reason != null && !reason.trim().isEmpty()) ? reason : description;
// 创建退还流水记录
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
.bizNo(order.getBizNo())
.points(withdrawAmount) // 退还金额(正数表示收入)
.balance(balance.getBalance()) // 当前总余额
.frozenAmt(newFrozenAmt) // 扣除后的冻结金额
.withdrawableBalance(newWithdrawableBalance) // 退还后的可提现余额
.tenantId(order.getTenantId())
.type(type)
.description(description)
.remark(remark)
.orderId(order.getWithdrawNo())
.operatorId(TenantContextHolder.getTenantId())
.createdAt(LocalDateTime.now())
.build();
tenantBalanceTransactionMapper.insert(transaction);
}
@Override
@@ -78,8 +231,67 @@ public class KeyboardTenantWithdrawOrderServiceImpl implements KeyboardTenantWit
}
@Override
public PageResult<KeyboardTenantWithdrawOrderDO> getTenantWithdrawOrderPage(KeyboardTenantWithdrawOrderPageReqVO pageReqVO) {
return tenantWithdrawOrderMapper.selectPage(pageReqVO);
public PageResult<KeyboardTenantWithdrawOrderRespVO> getTenantWithdrawOrderPage(KeyboardTenantWithdrawOrderPageReqVO pageReqVO) {
// 根据当前租户级别过滤下级租户的提现申请
Long currentTenantId = TenantContextHolder.getTenantId();
if (currentTenantId != null) {
TenantDO currentTenant = tenantMapper.selectById(currentTenantId);
if (currentTenant != null && currentTenant.getTenantLevel() != null) {
if (currentTenant.getTenantLevel() == 0) {
// 系统管理员只能查看1级代理的提现申请
List<TenantDO> firstLevelAgents = tenantMapper.selectList(
new LambdaQueryWrapper<TenantDO>().eq(TenantDO::getTenantLevel, 1));
List<Long> firstLevelAgentIds = firstLevelAgents.stream()
.map(TenantDO::getId)
.collect(Collectors.toList());
if (CollUtil.isEmpty(firstLevelAgentIds)) {
// 没有1级代理返回空结果
return PageResult.empty(0L);
}
pageReqVO.setTenantIds(firstLevelAgentIds);
} else {
// 非系统管理员:只能查看直属下级租户的提现申请
List<TenantDO> subordinateTenants = tenantMapper.selectList(
new LambdaQueryWrapper<TenantDO>().eq(TenantDO::getParentId, currentTenantId));
List<Long> subordinateTenantIds = subordinateTenants.stream()
.map(TenantDO::getId)
.collect(Collectors.toList());
if (CollUtil.isEmpty(subordinateTenantIds)) {
// 没有下级租户,返回空结果
return PageResult.empty(0L);
}
pageReqVO.setTenantIds(subordinateTenantIds);
}
}
}
// 分页查询租户提现订单数据
PageResult<KeyboardTenantWithdrawOrderDO> pageResult = tenantWithdrawOrderMapper.selectPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return PageResult.empty(pageResult.getTotal());
}
// 批量获取租户名称 - 提升性能避免N+1查询
List<Long> tenantIds = pageResult.getList().stream()
.map(KeyboardTenantWithdrawOrderDO::getTenantId)
.distinct()
.collect(Collectors.toList());
List<TenantDO> tenants = tenantMapper.selectBatchIds(tenantIds);
Map<Long, String> tenantNameMap = CollUtil.isEmpty(tenants)
? new HashMap<>() // 如果没有查询到租户数据返回空map
: tenants.stream().collect(Collectors.toMap(TenantDO::getId, TenantDO::getName, (a, b) -> a)); // 构建租户ID到名称的映射
// 转换为 VO 并填充租户名称
List<KeyboardTenantWithdrawOrderRespVO> voList = pageResult.getList().stream().map(order -> {
// 将DO转换为VO
KeyboardTenantWithdrawOrderRespVO vo = BeanUtils.toBean(order, KeyboardTenantWithdrawOrderRespVO.class);
// 根据租户ID获取并设置租户名称
vo.setTenantName(tenantNameMap.get(order.getTenantId()));
return vo;
}).collect(Collectors.toList());
// 返回包含VO列表和总数的分页结果
return new PageResult<>(voList, pageResult.getTotal());
}
}

View File

@@ -11,6 +11,7 @@ import com.yolo.keyboard.framework.tenant.core.aop.TenantIgnore;
import com.yolo.keyboard.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
import com.yolo.keyboard.module.system.controller.admin.tenant.vo.tenant.TenantRespVO;
import com.yolo.keyboard.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
import com.yolo.keyboard.module.system.controller.admin.tenant.vo.tenant.TenantInfoRespVO;
import com.yolo.keyboard.module.system.dal.dataobject.tenant.TenantDO;
import com.yolo.keyboard.module.system.service.tenant.TenantService;
import io.swagger.v3.oas.annotations.Operation;
@@ -113,6 +114,13 @@ public class TenantController {
return success(BeanUtils.toBean(tenant, TenantRespVO.class));
}
@GetMapping("/current")
@Operation(summary = "获得当前登录租户信息")
public CommonResult<TenantInfoRespVO> getCurrentTenant() {
TenantDO tenant = tenantService.getCurrentTenant();
return success(BeanUtils.toBean(tenant, TenantInfoRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得租户分页")
@PreAuthorize("@ss.hasPermission('system:tenant:query')")

View File

@@ -0,0 +1,24 @@
package com.yolo.keyboard.module.system.controller.admin.tenant.vo.tenant;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - 当前租户信息 Response VO")
@Data
public class TenantInfoRespVO {
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "上级租户编号", example = "1")
private Long parentId;
@Schema(description = "代理级别", example = "1")
private Integer tenantLevel;
@Schema(description = "分润比例", example = "0.3")
private BigDecimal profitShareRatio;
}

View File

@@ -19,6 +19,7 @@ public interface TenantMapper extends BaseMapperX<TenantDO> {
.likeIfPresent(TenantDO::getContactName, reqVO.getContactName())
.likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile())
.eqIfPresent(TenantDO::getStatus, reqVO.getStatus())
.eqIfPresent(TenantDO::getParentId, reqVO.getParentId())
.betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(TenantDO::getId));
}

View File

@@ -142,4 +142,11 @@ public interface TenantService {
*/
void validTenant(Long id);
/**
* 获取当前登录租户信息
*
* @return 当前租户
*/
TenantDO getCurrentTenant();
}

View File

@@ -132,6 +132,11 @@ public class TenantServiceImpl implements TenantService {
}
tenant.setParentId(currentTenantId);
tenant.setTenantLevel(parentTenant != null && parentTenant.getTenantLevel() != null ? parentTenant.getTenantLevel() + 1 : 1);
// 如果当前用户是1级代理下级分成比例沿用自己的分成比例不允许单独设置
if (parentTenant != null && parentTenant.getTenantLevel() != null && parentTenant.getTenantLevel() == 1) {
tenant.setProfitShareRatio(parentTenant.getProfitShareRatio());
}
}
}
tenantMapper.insert(tenant);
@@ -285,6 +290,15 @@ public class TenantServiceImpl implements TenantService {
@Override
public PageResult<TenantDO> getTenantPage(TenantPageReqVO pageReqVO) {
// 如果当前租户是一级代理tenantLevel=1只能查询自己的下级租户
Long currentTenantId = TenantContextHolder.getTenantId();
if (currentTenantId != null) {
TenantDO currentTenant = tenantMapper.selectById(currentTenantId);
if (currentTenant != null && currentTenant.getTenantLevel() != null
&& currentTenant.getTenantLevel() == 1) {
pageReqVO.setParentId(currentTenantId);
}
}
return tenantMapper.selectPage(pageReqVO);
}
@@ -352,4 +366,10 @@ public class TenantServiceImpl implements TenantService {
return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable());
}
@Override
public TenantDO getCurrentTenant() {
Long tenantId = TenantContextHolder.getRequiredTenantId();
return getTenant(tenantId);
}
}