feat(commission): 支持二级代理分成计算与余额精度改为BigDecimal
- 新增Mapper方法:按内购记录ID+租户ID查重,避免重复分成 - 将租户余额字段从Integer升级为BigDecimal,保证金额精度 - 重构定时任务:拆分一级/二级代理逻辑,按返点比例分别创建分成记录 - 提取createCommissionRecord方法,复用余额更新与交易流水创建逻辑
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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("乐观锁版本号")
|
||||
|
||||
@@ -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 = "乐观锁版本号不能为空")
|
||||
|
||||
@@ -29,6 +29,16 @@ 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::getPurchaseRecordId, reqVO.getPurchaseRecordId())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user