feat(core): 增加租户提成计算功能并升级Quartz版本
- 新增KeyboardTenantCommissionDO、KeyboardTenantCommissionMapper及TenantCommissionCalculateJob,实现租户提成定时计算 - 升级Quartz至2.5.2,开启acquireTriggersWithinLock防并发 - 精简BannerApplicationRunner,移除模块启用提示 - 调整IDEA HTTP客户端端口至48081
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user