From 2207add1934861ee70151b77b44e64a5f99cca0e Mon Sep 17 00:00:00 2001 From: ziin Date: Tue, 30 Dec 2025 19:29:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(commission):=20=E6=94=AF=E6=8C=81=E4=BA=8C?= =?UTF-8?q?=E7=BA=A7=E4=BB=A3=E7=90=86=E5=88=86=E6=88=90=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E4=B8=8E=E4=BD=99=E9=A2=9D=E7=B2=BE=E5=BA=A6=E6=94=B9=E4=B8=BA?= =?UTF-8?q?BigDecimal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增Mapper方法:按内购记录ID+租户ID查重,避免重复分成 - 将租户余额字段从Integer升级为BigDecimal,保证金额精度 - 重构定时任务:拆分一级/二级代理逻辑,按返点比例分别创建分成记录 - 提取createCommissionRecord方法,复用余额更新与交易流水创建逻辑 --- .../vo/TenantBalancePageReqVO.java | 2 +- .../tenantbalance/vo/TenantBalanceRespVO.java | 2 +- .../vo/TenantBalanceSaveReqVO.java | 2 +- .../KeyboardTenantCommissionMapper.java | 10 + .../job/TenantCommissionCalculateJob.java | 245 ++++++++++++------ 5 files changed, 181 insertions(+), 80 deletions(-) diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalancePageReqVO.java b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalancePageReqVO.java index 9955ca3..4372fb2 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalancePageReqVO.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalancePageReqVO.java @@ -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; diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceRespVO.java b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceRespVO.java index 85d700e..6af0067 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceRespVO.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceRespVO.java @@ -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("乐观锁版本号") diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceSaveReqVO.java b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceSaveReqVO.java index 65da098..602532b 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceSaveReqVO.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/controller/admin/tenantbalance/vo/TenantBalanceSaveReqVO.java @@ -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 = "乐观锁版本号不能为空") diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/dal/mysql/tenantcommission/KeyboardTenantCommissionMapper.java b/keyboard-server/src/main/java/com/yolo/keyboard/dal/mysql/tenantcommission/KeyboardTenantCommissionMapper.java index a9d6f3f..ca8a48a 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/dal/mysql/tenantcommission/KeyboardTenantCommissionMapper.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/dal/mysql/tenantcommission/KeyboardTenantCommissionMapper.java @@ -29,6 +29,16 @@ public interface KeyboardTenantCommissionMapper extends BaseMapperX() + .eq(KeyboardTenantCommissionDO::getPurchaseRecordId, purchaseRecordId) + .eq(KeyboardTenantCommissionDO::getTenantId, tenantId)); + } + default PageResult selectPage(KeyboardTenantCommissionPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(KeyboardTenantCommissionDO::getPurchaseRecordId, reqVO.getPurchaseRecordId()) diff --git a/keyboard-server/src/main/java/com/yolo/keyboard/job/TenantCommissionCalculateJob.java b/keyboard-server/src/main/java/com/yolo/keyboard/job/TenantCommissionCalculateJob.java index 29319a8..dfed5f2 100644 --- a/keyboard-server/src/main/java/com/yolo/keyboard/job/TenantCommissionCalculateJob.java +++ b/keyboard-server/src/main/java/com/yolo/keyboard/job/TenantCommissionCalculateJob.java @@ -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,83 @@ 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()) + .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; + } }