feat(task): 添加租户10584分销订单结算定时任务
- 实现每20秒执行一次的定时任务,处理已付款且未结算的订单 - 添加直推和简推佣金计算逻辑,按10%比例发放 - 实现shop角色推荐人佣金分配,支持最多两级推荐 - 添加订单认领机制防止重复结算,并使用事务模板确保数据一致性 - 实现分销商账户余额累加和资金流水记录功能 - 添加缓存机制减少重复角色查询,提升性能
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user