feat(commission): 支持二级代理分成计算与余额精度改为BigDecimal

- 新增Mapper方法:按内购记录ID+租户ID查重,避免重复分成
- 将租户余额字段从Integer升级为BigDecimal,保证金额精度
- 重构定时任务:拆分一级/二级代理逻辑,按返点比例分别创建分成记录
- 提取createCommissionRecord方法,复用余额更新与交易流水创建逻辑
This commit is contained in:
2025-12-30 19:29:49 +08:00
parent 98e427c65a
commit 2207add193
5 changed files with 181 additions and 80 deletions

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,16 @@ public interface KeyboardTenantCommissionMapper extends BaseMapperX<KeyboardTena
return selectOne(KeyboardTenantCommissionDO::getPurchaseRecordId, purchaseRecordId); 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) { default PageResult<KeyboardTenantCommissionDO> selectPage(KeyboardTenantCommissionPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<KeyboardTenantCommissionDO>() return selectPage(reqVO, new LambdaQueryWrapperX<KeyboardTenantCommissionDO>()
.eqIfPresent(KeyboardTenantCommissionDO::getPurchaseRecordId, reqVO.getPurchaseRecordId()) .eqIfPresent(KeyboardTenantCommissionDO::getPurchaseRecordId, reqVO.getPurchaseRecordId())

View File

@@ -30,6 +30,7 @@ import java.util.List;
/** /**
* 租户分成计算定时任务 * 租户分成计算定时任务
* 每小时执行一次,计算邀请用户的内购分成 * 每小时执行一次,计算邀请用户的内购分成
* 支持一级代理和二级代理的分成计算
* *
* @author ziin * @author ziin
*/ */
@@ -110,101 +111,112 @@ public class TenantCommissionCalculateJob implements JobHandler {
continue; continue;
} }
// 获取收益归属租户 // 获取收益归属租户(邀请人所属租户)
Long tenantId = invite.getProfitTenantId(); Long inviterTenantId = invite.getProfitTenantId();
if (tenantId == null) { if (inviterTenantId == null) {
tenantId = invite.getInviterTenantId(); inviterTenantId = invite.getInviterTenantId();
} }
if (tenantId == null) { if (inviterTenantId == null) {
log.warn("[TenantCommissionCalculateJob] 用户 {} 的邀请关系没有关联租户,跳过", record.getUserId()); log.warn("[TenantCommissionCalculateJob] 用户 {} 的邀请关系没有关联租户,跳过", record.getUserId());
continue; continue;
} }
// 获取租户信息和分成比例 // 获取邀请人租户信息
TenantDO tenant = tenantMapper.selectById(tenantId); TenantDO inviterTenant = tenantMapper.selectById(inviterTenantId);
if (tenant == null) { if (inviterTenant == null) {
log.warn("[TenantCommissionCalculateJob] 租户 {} 不存在,跳过", tenantId); log.warn("[TenantCommissionCalculateJob] 租户 {} 不存在,跳过", inviterTenantId);
continue; continue;
} }
BigDecimal commissionRate = tenant.getProfitShareRatio(); // 获取内购金额
if (commissionRate == null || commissionRate.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("[TenantCommissionCalculateJob] 租户 {} 没有设置分成比例,跳过", tenantId);
continue;
}
// 3. 计算分成金额
BigDecimal purchaseAmount = record.getPrice(); BigDecimal purchaseAmount = record.getPrice();
if (purchaseAmount == null || purchaseAmount.compareTo(BigDecimal.ZERO) <= 0) { if (purchaseAmount == null || purchaseAmount.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("[TenantCommissionCalculateJob] 内购记录 {} 金额无效,跳过", record.getId()); log.debug("[TenantCommissionCalculateJob] 内购记录 {} 金额无效,跳过", record.getId());
continue; continue;
} }
BigDecimal commissionAmount = purchaseAmount.multiply(commissionRate)
.setScale(2, RoundingMode.HALF_UP);
// 4. 创建分成记录
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
LocalDateTime withdrawableAt = now.plusDays(30); // 30天后可提现 LocalDateTime withdrawableAt = now.plusDays(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();
// 5. 更新租户余额分成金额先计入总余额30天后才可提现 // 判断是否是二级代理(有上级租户
TenantBalanceDO balance = tenantBalanceMapper.selectById(tenantId); if (inviterTenant.getParentId() != null) {
if (balance == null) { // 二级代理场景:需要给一级代理和二级代理都分成
// 如果租户余额记录不存在,创建一个 TenantDO parentTenant = tenantMapper.selectById(inviterTenant.getParentId());
balance = new TenantBalanceDO(); if (parentTenant == null) {
balance.setId(tenantId); log.warn("[TenantCommissionCalculateJob] 上级租户 {} 不存在,跳过", inviterTenant.getParentId());
balance.setBalance(commissionAmount); continue;
balance.setWithdrawableBalance(BigDecimal.ZERO); }
balance.setFrozenAmt(BigDecimal.ZERO);
balance.setVersion(0); // 使用一级代理的分成比例计算总分成
tenantBalanceMapper.insert(balance); 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 { } else {
BigDecimal newBalance = balance.getBalance().add(commissionAmount); // 一级代理场景:全部分成归一级代理
balance.setBalance(newBalance); BigDecimal commissionRate = inviterTenant.getProfitShareRatio();
tenantBalanceMapper.updateById(balance); 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", String result = String.format("处理内购记录 %d 条,生成分成 %d 条,总分成金额 %s",
@@ -212,4 +224,83 @@ public class TenantCommissionCalculateJob implements JobHandler {
log.info("[TenantCommissionCalculateJob] 任务执行完成: {}", result); log.info("[TenantCommissionCalculateJob] 任务执行完成: {}", result);
return 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;
}
} }