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 {
|
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;
|
||||||
|
|||||||
@@ -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("乐观锁版本号")
|
||||||
|
|||||||
@@ -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 = "乐观锁版本号不能为空")
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* 租户分成计算定时任务
|
* 租户分成计算定时任务
|
||||||
* 每小时执行一次,计算邀请用户的内购分成
|
* 每小时执行一次,计算邀请用户的内购分成
|
||||||
|
* 支持一级代理和二级代理的分成计算
|
||||||
*
|
*
|
||||||
* @author ziin
|
* @author ziin
|
||||||
*/
|
*/
|
||||||
@@ -110,42 +111,142 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
LocalDateTime withdrawableAt = now.plusDays(30);
|
||||||
|
|
||||||
|
// 判断是否是二级代理(有上级租户)
|
||||||
|
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 commissionRate = inviterTenant.getProfitShareRatio();
|
||||||
|
if (commissionRate == null || commissionRate.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
log.debug("[TenantCommissionCalculateJob] 租户 {} 没有设置分成比例,跳过", inviterTenantId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
BigDecimal commissionAmount = purchaseAmount.multiply(commissionRate)
|
BigDecimal commissionAmount = purchaseAmount.multiply(commissionRate)
|
||||||
.setScale(2, RoundingMode.HALF_UP);
|
.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
// 4. 创建分成记录
|
int count = createCommissionRecord(record, invite, inviterTenantId, purchaseAmount,
|
||||||
LocalDateTime now = LocalDateTime.now();
|
commissionRate, commissionAmount, now, withdrawableAt,
|
||||||
LocalDateTime withdrawableAt = now.plusDays(30); // 30天后可提现
|
"一级代理分成");
|
||||||
|
commissionCount += count;
|
||||||
|
totalCommission = totalCommission.add(commissionAmount);
|
||||||
|
|
||||||
|
log.info("[TenantCommissionCalculateJob] 内购记录 {}, 一级代理 {}, 分成金额 {}",
|
||||||
|
record.getId(), inviterTenantId, commissionAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String result = String.format("处理内购记录 %d 条,生成分成 %d 条,总分成金额 %s",
|
||||||
|
processedCount, commissionCount, totalCommission.toPlainString());
|
||||||
|
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()
|
KeyboardTenantCommissionDO commission = KeyboardTenantCommissionDO.builder()
|
||||||
.purchaseRecordId(record.getId())
|
.purchaseRecordId(record.getId())
|
||||||
.transactionId(record.getTransactionId())
|
.transactionId(record.getTransactionId())
|
||||||
@@ -162,12 +263,12 @@ public class TenantCommissionCalculateJob implements JobHandler {
|
|||||||
.withdrawableProcessed(false)
|
.withdrawableProcessed(false)
|
||||||
.createdAt(now)
|
.createdAt(now)
|
||||||
.updatedAt(now)
|
.updatedAt(now)
|
||||||
|
.remark(remark)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 5. 更新租户余额(分成金额先计入总余额,30天后才可提现)
|
// 2. 更新租户余额(分成金额先计入总余额,30天后才可提现)
|
||||||
TenantBalanceDO balance = tenantBalanceMapper.selectById(tenantId);
|
TenantBalanceDO balance = tenantBalanceMapper.selectById(tenantId);
|
||||||
if (balance == null) {
|
if (balance == null) {
|
||||||
// 如果租户余额记录不存在,创建一个
|
|
||||||
balance = new TenantBalanceDO();
|
balance = new TenantBalanceDO();
|
||||||
balance.setId(tenantId);
|
balance.setId(tenantId);
|
||||||
balance.setBalance(commissionAmount);
|
balance.setBalance(commissionAmount);
|
||||||
@@ -181,7 +282,7 @@ public class TenantCommissionCalculateJob implements JobHandler {
|
|||||||
tenantBalanceMapper.updateById(balance);
|
tenantBalanceMapper.updateById(balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 创建余额交易记录
|
// 3. 创建余额交易记录
|
||||||
String bizNo = BizNoGenerator.generate("COMM");
|
String bizNo = BizNoGenerator.generate("COMM");
|
||||||
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
|
TenantBalanceTransactionDO transaction = TenantBalanceTransactionDO.builder()
|
||||||
.bizNo(bizNo)
|
.bizNo(bizNo)
|
||||||
@@ -192,24 +293,14 @@ public class TenantCommissionCalculateJob implements JobHandler {
|
|||||||
.description("邀请用户内购分成")
|
.description("邀请用户内购分成")
|
||||||
.orderId(record.getTransactionId())
|
.orderId(record.getTransactionId())
|
||||||
.createdAt(now)
|
.createdAt(now)
|
||||||
.remark("内购记录ID: " + record.getId() + ", 被邀请用户: " + record.getUserId())
|
.remark("内购记录ID: " + record.getId() + ", 被邀请用户: " + record.getUserId() + ", " + remark)
|
||||||
.build();
|
.build();
|
||||||
balanceTransactionMapper.insert(transaction);
|
balanceTransactionMapper.insert(transaction);
|
||||||
|
|
||||||
// 更新分成记录的关联交易ID
|
// 4. 更新分成记录的关联交易ID并保存
|
||||||
commission.setBalanceTransactionId(transaction.getId());
|
commission.setBalanceTransactionId(transaction.getId());
|
||||||
commissionMapper.insert(commission);
|
commissionMapper.insert(commission);
|
||||||
|
|
||||||
commissionCount++;
|
return 1;
|
||||||
totalCommission = totalCommission.add(commissionAmount);
|
|
||||||
|
|
||||||
log.info("[TenantCommissionCalculateJob] 处理内购记录 {}, 租户 {}, 分成金额 {}",
|
|
||||||
record.getId(), tenantId, commissionAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
String result = String.format("处理内购记录 %d 条,生成分成 %d 条,总分成金额 %s",
|
|
||||||
processedCount, commissionCount, totalCommission.toPlainString());
|
|
||||||
log.info("[TenantCommissionCalculateJob] 任务执行完成: {}", result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user