feat(task): 添加租户10584分销订单结算定时任务

- 实现每20秒执行一次的定时任务,处理已付款且未结算的订单
- 添加直推和简推佣金计算逻辑,按10%比例发放
- 实现shop角色推荐人佣金分配,支持最多两级推荐
- 添加订单认领机制防止重复结算,并使用事务模板确保数据一致性
- 实现分销商账户余额累加和资金流水记录功能
- 添加缓存机制减少重复角色查询,提升性能
This commit is contained in:
2026-01-23 00:20:36 +08:00
parent 757291f256
commit 4ffc62fef1

View File

@@ -0,0 +1,271 @@
package com.gxwebsoft.shop.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.common.system.entity.Role;
import com.gxwebsoft.common.system.service.UserRoleService;
import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerReferee;
import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerRefereeService;
import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 租户10584分销订单结算任务
* <p>
* 每20秒执行一次查询“已付款且未结算”的订单按指定规则发放佣金并将订单置为已结算。
*/
@Slf4j
@Component
public class DealerOrderSettlement10584Task {
private static final Integer TENANT_ID = 10584;
private static final BigDecimal RATE_0_10 = new BigDecimal("0.10");
private static final BigDecimal RATE_0_08 = new BigDecimal("0.08");
private static final BigDecimal RATE_0_02 = new BigDecimal("0.02");
private static final int MAX_ORDERS_PER_RUN = 50;
private static final int MAX_REFEREE_CHAIN_DEPTH = 20;
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private ShopOrderService shopOrderService;
@Resource
private ShopDealerRefereeService shopDealerRefereeService;
@Resource
private ShopDealerUserService shopDealerUserService;
@Resource
private ShopDealerCapitalService shopDealerCapitalService;
@Resource
private UserRoleService userRoleService;
/**
* 每20秒执行一次。
*/
@Scheduled(cron = "0/20 * * * * ?")
@IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常")
public void settleTenant10584Orders() {
try {
List<ShopOrder> orders = findUnsettledPaidOrders();
if (orders.isEmpty()) {
return;
}
// 缓存:减少同一批次内重复查角色
Map<Integer, Boolean> shopRoleCache = new HashMap<>();
for (ShopOrder order : orders) {
try {
transactionTemplate.executeWithoutResult(status -> {
// 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理)
if (!claimOrderToSettle(order.getOrderId())) {
return;
}
settleOneOrder(order, shopRoleCache);
});
} catch (Exception e) {
log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e);
}
}
} catch (Exception e) {
log.error("租户{}分销订单结算任务执行失败", TENANT_ID, e);
}
}
private List<ShopOrder> findUnsettledPaidOrders() {
return shopOrderService.list(
new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getPayStatus, true)
.eq(ShopOrder::getIsSettled, 0)
.orderByAsc(ShopOrder::getOrderId)
.last("limit " + MAX_ORDERS_PER_RUN)
);
}
private boolean claimOrderToSettle(Integer orderId) {
return shopOrderService.update(
new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, orderId)
.eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getIsSettled, 0)
.set(ShopOrder::getIsSettled, 1)
);
}
private void settleOneOrder(ShopOrder order, Map<Integer, Boolean> shopRoleCache) {
if (order.getUserId() == null || order.getOrderNo() == null) {
throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId());
}
BigDecimal baseAmount = getOrderBaseAmount(order);
if (baseAmount == null || baseAmount.signum() <= 0) {
throw new IllegalStateException("订单金额为空或<=0无法结算 - orderId=" + order.getOrderId() + ", orderNo=" + order.getOrderNo());
}
// 1) 直推/简推shop_dealer_referee
settleDealerRefereeCommission(order, baseAmount);
// 2) 角色为shop的推荐人shop_dealer_referee 链路向上查找 role=shop 的上级)
settleShopRoleRefereeCommission(order, baseAmount, shopRoleCache);
log.info("订单结算完成 - orderId={}, orderNo={}, baseAmount={}", order.getOrderId(), order.getOrderNo(), baseAmount);
}
private void settleDealerRefereeCommission(ShopOrder order, BigDecimal baseAmount) {
// level=2 可能不可靠:按“查两次”方式获取简推(直推的上级)。
Integer directDealerId = getDealerRefereeId(order.getUserId());
Integer simpleDealerId = directDealerId != null ? getDealerRefereeId(directDealerId) : null;
BigDecimal directMoney = calcMoney(baseAmount, RATE_0_10);
BigDecimal simpleMoney = calcMoney(baseAmount, RATE_0_10);
creditDealerCommission(directDealerId, directMoney, order, "直推佣金(10%)");
if (simpleDealerId != null && !simpleDealerId.equals(directDealerId)) {
creditDealerCommission(simpleDealerId, simpleMoney, order, "简推佣金(10%)");
}
}
private Integer getDealerRefereeId(Integer userId) {
if (userId == null) {
return null;
}
ShopDealerReferee rel = shopDealerRefereeService.getOne(
new LambdaQueryWrapper<ShopDealerReferee>()
.eq(ShopDealerReferee::getTenantId, TENANT_ID)
.eq(ShopDealerReferee::getUserId, userId)
.eq(ShopDealerReferee::getLevel, 1)
.orderByDesc(ShopDealerReferee::getId)
.last("limit 1")
);
return rel != null ? rel.getDealerId() : null;
}
private void settleShopRoleRefereeCommission(ShopOrder order, BigDecimal baseAmount, Map<Integer, Boolean> shopRoleCache) {
List<Integer> shopRoleReferees = findFirstTwoShopRoleRefereesByDealerReferee(order.getUserId(), shopRoleCache);
if (shopRoleReferees.isEmpty()) {
return;
}
if (shopRoleReferees.size() == 1) {
creditDealerCommission(shopRoleReferees.get(0), calcMoney(baseAmount, RATE_0_10), order, "shop角色推荐人佣金(仅1人10%)");
return;
}
// 两个或以上第一个0.02第二个0.08
creditDealerCommission(shopRoleReferees.get(0), calcMoney(baseAmount, RATE_0_02), order, "shop角色推荐人佣金(第1个2%)");
creditDealerCommission(shopRoleReferees.get(1), calcMoney(baseAmount, RATE_0_08), order, "shop角色推荐人佣金(第2个8%)");
}
private List<Integer> findFirstTwoShopRoleRefereesByDealerReferee(Integer buyerUserId, Map<Integer, Boolean> shopRoleCache) {
if (buyerUserId == null) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>(2);
Set<Integer> visited = new HashSet<>();
Integer current = buyerUserId;
for (int i = 0; i < MAX_REFEREE_CHAIN_DEPTH && current != null; i++) {
Integer parentId = getDealerRefereeId(current);
if (parentId == null) {
break;
}
if (!visited.add(parentId)) {
break;
}
if (hasShopRole(parentId, shopRoleCache)) {
result.add(parentId);
if (result.size() >= 2) {
break;
}
}
current = parentId;
}
return result;
}
private boolean hasShopRole(Integer userId, Map<Integer, Boolean> shopRoleCache) {
Boolean cached = shopRoleCache.get(userId);
if (cached != null) {
return cached;
}
List<Role> roles = userRoleService.listByUserId(userId);
boolean isShop = roles != null && roles.stream().anyMatch(r -> "shop".equalsIgnoreCase(r.getRoleCode()));
shopRoleCache.put(userId, isShop);
return isShop;
}
private void creditDealerCommission(Integer dealerUserId, BigDecimal money, ShopOrder order, String comments) {
if (dealerUserId == null || money == null || money.signum() <= 0) {
return;
}
// 先累加佣金到分销商账户避免并发下丢失更新用SQL自增
boolean updated = shopDealerUserService.update(
new LambdaUpdateWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getUserId, dealerUserId)
.setSql("money = IFNULL(money,0) + " + money.toPlainString())
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
);
if (!updated) {
log.warn("发放佣金失败:未找到分销商账户 - tenantId={}, dealerUserId={}, orderNo={}", TENANT_ID, dealerUserId, order.getOrderNo());
return;
}
ShopDealerCapital cap = new ShopDealerCapital();
cap.setUserId(dealerUserId);
cap.setOrderNo(order.getOrderNo());
cap.setFlowType(10);
cap.setMoney(money);
cap.setComments(comments);
cap.setToUserId(order.getUserId());
cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
cap.setTenantId(TENANT_ID);
shopDealerCapitalService.save(cap);
}
private BigDecimal getOrderBaseAmount(ShopOrder order) {
if (order == null) {
return null;
}
return order.getPayPrice();
}
private BigDecimal calcMoney(BigDecimal base, BigDecimal rate) {
if (base == null || rate == null) {
return BigDecimal.ZERO;
}
return base.multiply(rate).setScale(2, RoundingMode.HALF_UP);
}
}