feat(core): 增加租户提成计算功能并升级Quartz版本

- 新增KeyboardTenantCommissionDO、KeyboardTenantCommissionMapper及TenantCommissionCalculateJob,实现租户提成定时计算
- 升级Quartz至2.5.2,开启acquireTriggersWithinLock防并发
- 精简BannerApplicationRunner,移除模块启用提示
- 调整IDEA HTTP客户端端口至48081
This commit is contained in:
2025-12-30 14:15:17 +08:00
parent 2ed121926b
commit eb4b615ed6
10 changed files with 374 additions and 50 deletions

View File

@@ -0,0 +1,107 @@
package com.yolo.keyboard.dal.dataobject.tenantcommission;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yolo.keyboard.framework.tenant.core.aop.TenantIgnore;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 租户分成记录 DO
* 记录每笔内购订单的分成计算结果
*
* @author ziin
*/
@TableName("keyboard_tenant_commission")
@KeySequence("keyboard_tenant_commission_seq")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TenantIgnore
public class KeyboardTenantCommissionDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 内购记录ID
*/
private Integer purchaseRecordId;
/**
* 内购交易ID唯一标识
*/
private String transactionId;
/**
* 被邀请用户ID购买用户
*/
private Integer inviteeUserId;
/**
* 邀请人用户ID
*/
private Long inviterUserId;
/**
* 收益归属租户ID
*/
private Long tenantId;
/**
* 内购金额
*/
private BigDecimal purchaseAmount;
/**
* 分成比例
*/
private BigDecimal commissionRate;
/**
* 分成金额
*/
private BigDecimal commissionAmount;
/**
* 状态PENDING-待结算SETTLED-已结算REFUNDED-已退款
*/
private String status;
/**
* 内购时间
*/
private LocalDateTime purchaseTime;
/**
* 结算时间
*/
private LocalDateTime settledAt;
/**
* 关联的余额交易记录ID
*/
private Long balanceTransactionId;
/**
* 创建时间
*/
private LocalDateTime createdAt;
/**
* 更新时间
*/
private LocalDateTime updatedAt;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,28 @@
package com.yolo.keyboard.dal.mysql.tenantcommission;
import com.yolo.keyboard.dal.dataobject.tenantcommission.KeyboardTenantCommissionDO;
import com.yolo.keyboard.framework.mybatis.core.mapper.BaseMapperX;
import org.apache.ibatis.annotations.Mapper;
/**
* 租户分成记录 Mapper
*
* @author ziin
*/
@Mapper
public interface KeyboardTenantCommissionMapper extends BaseMapperX<KeyboardTenantCommissionDO> {
/**
* 根据交易ID查询分成记录
*/
default KeyboardTenantCommissionDO selectByTransactionId(String transactionId) {
return selectOne(KeyboardTenantCommissionDO::getTransactionId, transactionId);
}
/**
* 根据内购记录ID查询分成记录
*/
default KeyboardTenantCommissionDO selectByPurchaseRecordId(Integer purchaseRecordId) {
return selectOne(KeyboardTenantCommissionDO::getPurchaseRecordId, purchaseRecordId);
}
}

View File

@@ -0,0 +1,211 @@
package com.yolo.keyboard.job;
import cn.hutool.core.collection.CollUtil;
import com.yolo.keyboard.dal.dataobject.tenantbalance.TenantBalanceDO;
import com.yolo.keyboard.dal.dataobject.tenantbalancetransaction.TenantBalanceTransactionDO;
import com.yolo.keyboard.dal.dataobject.tenantcommission.KeyboardTenantCommissionDO;
import com.yolo.keyboard.dal.dataobject.userinvites.KeyboardUserInvitesDO;
import com.yolo.keyboard.dal.dataobject.userpurchaserecords.KeyboardUserPurchaseRecordsDO;
import com.yolo.keyboard.dal.mysql.tenantbalance.TenantBalanceMapper;
import com.yolo.keyboard.dal.mysql.tenantbalancetransaction.TenantBalanceTransactionMapper;
import com.yolo.keyboard.dal.mysql.tenantcommission.KeyboardTenantCommissionMapper;
import com.yolo.keyboard.dal.mysql.userinvites.KeyboardUserInvitesMapper;
import com.yolo.keyboard.dal.mysql.userpurchaserecords.KeyboardUserPurchaseRecordsMapper;
import com.yolo.keyboard.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.yolo.keyboard.framework.quartz.core.handler.JobHandler;
import com.yolo.keyboard.framework.tenant.core.aop.TenantIgnore;
import com.yolo.keyboard.module.system.dal.dataobject.tenant.TenantDO;
import com.yolo.keyboard.module.system.dal.mysql.tenant.TenantMapper;
import com.yolo.keyboard.utils.BizNoGenerator;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.List;
/**
* 租户分成计算定时任务
* 每小时执行一次,计算邀请用户的内购分成
*
* @author ziin
*/
@Component
@Slf4j
public class TenantCommissionCalculateJob implements JobHandler {
@Resource
private KeyboardUserPurchaseRecordsMapper purchaseRecordsMapper;
@Resource
private KeyboardUserInvitesMapper userInvitesMapper;
@Resource
private KeyboardTenantCommissionMapper commissionMapper;
@Resource
private TenantMapper tenantMapper;
@Resource
private TenantBalanceMapper tenantBalanceMapper;
@Resource
private TenantBalanceTransactionMapper balanceTransactionMapper;
private static final String COMMISSION_TYPE = "COMMISSION";
private static final String STATUS_PAID = "PAID";
private static final String INVITE_TYPE_AGENT = "AGENT";
@Override
@TenantIgnore
@Transactional(rollbackFor = Exception.class)
public String execute(String param) {
log.info("[TenantCommissionCalculateJob] 开始执行分成计算任务");
// 1. 查询最近一小时内已支付的内购记录
LocalDateTime endTime = LocalDateTime.now();
LocalDateTime startTime = endTime.minusHours(1);
List<KeyboardUserPurchaseRecordsDO> purchaseRecords = purchaseRecordsMapper.selectList(
new LambdaQueryWrapperX<KeyboardUserPurchaseRecordsDO>()
.eq(KeyboardUserPurchaseRecordsDO::getStatus, STATUS_PAID)
.between(KeyboardUserPurchaseRecordsDO::getPurchaseTime, startTime, endTime)
);
if (CollUtil.isEmpty(purchaseRecords)) {
log.info("[TenantCommissionCalculateJob] 最近一小时内没有已支付的内购记录");
return "没有需要处理的内购记录";
}
int processedCount = 0;
int commissionCount = 0;
BigDecimal totalCommission = BigDecimal.ZERO;
// 2. 遍历内购记录,检查是否有邀请关系
for (KeyboardUserPurchaseRecordsDO record : purchaseRecords) {
// 检查是否已经计算过分成
if (commissionMapper.selectByPurchaseRecordId(record.getId()) != null) {
log.debug("[TenantCommissionCalculateJob] 内购记录 {} 已计算过分成,跳过", record.getId());
continue;
}
processedCount++;
// 查询该用户的邀请关系
KeyboardUserInvitesDO invite = userInvitesMapper.selectOne(
KeyboardUserInvitesDO::getInviteeUserId, record.getUserId().longValue()
);
if (invite == null) {
log.debug("[TenantCommissionCalculateJob] 用户 {} 没有邀请关系,跳过", record.getUserId());
continue;
}
// 只处理代理邀请类型
if (!INVITE_TYPE_AGENT.equals(invite.getInviteType())) {
log.debug("[TenantCommissionCalculateJob] 用户 {} 的邀请类型不是代理,跳过", record.getUserId());
continue;
}
// 获取收益归属租户
Long tenantId = invite.getProfitTenantId();
if (tenantId == null) {
tenantId = invite.getInviterTenantId();
}
if (tenantId == null) {
log.warn("[TenantCommissionCalculateJob] 用户 {} 的邀请关系没有关联租户,跳过", record.getUserId());
continue;
}
// 获取租户信息和分成比例
TenantDO tenant = tenantMapper.selectById(tenantId);
if (tenant == null) {
log.warn("[TenantCommissionCalculateJob] 租户 {} 不存在,跳过", tenantId);
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();
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)
.createdAt(now)
.updatedAt(now)
.build();
// 5. 更新租户余额
TenantBalanceDO balance = tenantBalanceMapper.selectById(tenantId);
if (balance == null) {
// 如果租户余额记录不存在,创建一个
balance = new TenantBalanceDO();
balance.setId(tenantId);
balance.setBalance(commissionAmount);
balance.setFrozenAmt(BigDecimal.ZERO);
balance.setVersion(0);
tenantBalanceMapper.insert(balance);
} else {
BigDecimal newBalance = balance.getBalance().add(commissionAmount);
balance.setBalance(newBalance);
tenantBalanceMapper.updateById(balance);
}
// 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",
processedCount, commissionCount, totalCommission.toPlainString());
log.info("[TenantCommissionCalculateJob] 任务执行完成: {}", result);
return result;
}
}