refactor(task): 重构经销商订单结算任务中的上级用户查找逻辑

- 引入 UpstreamUserFinder 工具类来统一处理向上游用户链路的遍历逻辑
- 添加缓存机制减少数据库查询次数,提高性能
- 修改 settleOneOrder 方法签名以传递缓存对象
- 更新门店分红上级查找逻辑,从简单的链路取前两级改为精确查找门店角色用户
- 删除废弃的 ShopOrderSettlement10584Task 临时排查任务类
- 添加 UpstreamUserFinder 的单元测试确保逻辑正确性
This commit is contained in:
2026-01-26 12:34:56 +08:00
parent 803ac3301e
commit 3b63172012
4 changed files with 199 additions and 268 deletions

View File

@@ -15,6 +15,7 @@ import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.service.ShopDealerRefereeService;
import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopOrderService;
import com.gxwebsoft.shop.util.UpstreamUserFinder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -80,6 +81,10 @@ public class DealerOrderSettlement10584Task {
return;
}
// Per-run caches to reduce DB chatter across orders.
Map<Integer, Integer> level1ParentCache = new HashMap<>();
Map<Integer, Boolean> shopRoleCache = new HashMap<>();
log.info("租户{}待结算订单数: {}, orderNos(sample)={}",
TENANT_ID,
orders.size(),
@@ -92,7 +97,7 @@ public class DealerOrderSettlement10584Task {
if (!claimOrderToSettle(order.getOrderId())) {
return;
}
settleOneOrder(order);
settleOneOrder(order, level1ParentCache, shopRoleCache);
});
} catch (Exception e) {
log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e);
@@ -125,7 +130,7 @@ public class DealerOrderSettlement10584Task {
);
}
private void settleOneOrder(ShopOrder order) {
private void settleOneOrder(ShopOrder order, Map<Integer, Integer> level1ParentCache, Map<Integer, Boolean> shopRoleCache) {
if (order.getUserId() == null || order.getOrderNo() == null) {
throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId());
}
@@ -141,8 +146,8 @@ public class DealerOrderSettlement10584Task {
// 1) 直推/简推shop_dealer_referee
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount);
// 2) 门店分红上级(按 ShopDealerUser.type=1,在 shop_dealer_referee 链路向上查找)
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount);
// 2) 门店分红上级:从下单用户开始逐级向上找,命中 ShopDealerUser.type=1 的最近两级(直推门店/简推门店)。
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, level1ParentCache, shopRoleCache);
// 3) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
createDealerOrderRecord(order, baseAmount, dealerRefereeCommission, shopRoleCommission);
@@ -195,9 +200,14 @@ public class DealerOrderSettlement10584Task {
return rel != null ? rel.getDealerId() : null;
}
private ShopRoleCommission settleShopRoleRefereeCommission(ShopOrder order, BigDecimal baseAmount) {
List<Integer> shopRoleReferees = findFirstTwoUpstreamReferees(order.getUserId());
log.info("分红上级链路结果(level=1链路取前两级) - orderNo={}, buyerUserId={}, referees={}",
private ShopRoleCommission settleShopRoleRefereeCommission(
ShopOrder order,
BigDecimal baseAmount,
Map<Integer, Integer> level1ParentCache,
Map<Integer, Boolean> shopRoleCache
) {
List<Integer> shopRoleReferees = findFirstTwoShopRoleReferees(order.getUserId(), level1ParentCache, shopRoleCache);
log.info("门店分红命中结果(type=1门店角色取前两级) - orderNo={}, buyerUserId={}, shopRoleReferees={}",
order.getOrderNo(), order.getUserId(), shopRoleReferees);
if (shopRoleReferees.isEmpty()) {
return ShopRoleCommission.empty();
@@ -222,41 +232,71 @@ public class DealerOrderSettlement10584Task {
}
/**
* 沿 shop_dealer_referee(level=1) 链路向上找到最近两级上级。
* <p>
* 旧逻辑依赖扩展字段 isShopRole(已废弃) 来过滤门店角色;目前按链路取前两级,保证能回填/结算。
* 门店分红规则:
* - 门店角色为 ShopDealerUser.type=1
* - 从下单用户开始,沿 shop_dealer_referee(level=1) 链路逐级向上找;
* - 遇到第一个 type=1 用户命中为“直推门店用户”,继续向上找到第二个 type=1 用户命中为“简推门店用户”。
*/
private List<Integer> findFirstTwoUpstreamReferees(Integer buyerUserId) {
private List<Integer> findFirstTwoShopRoleReferees(
Integer buyerUserId,
Map<Integer, Integer> level1ParentCache,
Map<Integer, Boolean> shopRoleCache
) {
if (buyerUserId == null) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>(2);
Set<Integer> visited = new HashSet<>();
return UpstreamUserFinder.findFirstNMatchingUpstreamUsers(
buyerUserId,
2,
MAX_REFEREE_CHAIN_DEPTH,
childId -> getLevel1ParentCached(childId, level1ParentCache),
userId -> isShopRoleUserCached(userId, shopRoleCache)
);
}
// 仅依赖 level=1 的推荐关系一路向上查找(兼容“只维护 level=1”的数据形态
Integer current = buyerUserId;
for (int i = 0; i < MAX_REFEREE_CHAIN_DEPTH && current != null; i++) {
Integer parentId = getDealerRefereeId(current, 1);
if (parentId == null) {
break;
}
log.debug("分红链路向上 - buyerOrChildUserId={}, parentId={}", current, parentId);
if (!visited.add(parentId)) {
break;
}
result.add(parentId);
if (result.size() >= 2) {
break;
}
current = parentId;
private Integer getLevel1ParentCached(Integer userId, Map<Integer, Integer> cache) {
if (userId == null) {
return null;
}
if (cache != null) {
if (cache.containsKey(userId)) {
return cache.get(userId);
}
Integer parent = getDealerRefereeId(userId, 1);
cache.put(userId, parent);
return parent;
}
return getDealerRefereeId(userId, 1);
}
return result;
private boolean isShopRoleUserCached(Integer userId, Map<Integer, Boolean> cache) {
if (userId == null) {
return false;
}
if (cache != null) {
Boolean cached = cache.get(userId);
if (cached != null) {
return cached;
}
if (cache.containsKey(userId)) {
return false;
}
boolean val = isShopRoleUser(userId);
cache.put(userId, val);
return val;
}
return isShopRoleUser(userId);
}
private boolean isShopRoleUser(Integer userId) {
return shopDealerUserService.count(
new LambdaQueryWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getUserId, userId)
.eq(ShopDealerUser::getType, 1)
.and(w -> w.eq(ShopDealerUser::getIsDelete, 0).or().isNull(ShopDealerUser::getIsDelete))
) > 0;
}
private void creditDealerCommission(Integer dealerUserId, BigDecimal money, ShopOrder order, Integer toUserId, String comments) {

View File

@@ -1,234 +0,0 @@
package com.gxwebsoft.shop.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.common.core.context.TenantContext;
import com.gxwebsoft.shop.entity.ShopDealerReferee;
import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.mapper.ShopDealerRefereeMapper;
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.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 租户10584一次性排查任务
* <p>
* 应用启动后延迟10秒执行一次查询指定订单的下单用户上级推荐人/门店(type=1)链路。
*/
@Slf4j
@Component
public class ShopOrderSettlement10584Task {
private static final Integer TENANT_ID = 10584;
/**
* 目标订单编号(按用户要求写死;如需动态,可改成配置项或入参)。
*/
private static final String TARGET_ORDER_NO = "2015591043920007168";
private static final int MAX_REFEREE_CHAIN_DEPTH = 20;
private final AtomicBoolean executed = new AtomicBoolean(false);
@Resource
private ShopOrderService shopOrderService;
@Resource
private ShopDealerRefereeService shopDealerRefereeService;
@Resource
private ShopDealerRefereeMapper shopDealerRefereeMapper;
@Resource
private ShopDealerUserService shopDealerUserService;
/**
* 启动10秒后执行只执行一次。
*/
@EventListener(ApplicationReadyEvent.class)
public void scheduleOnceAfterStartup() {
log.info("ShopOrderSettlement10584Task 已注册10秒后执行一次 - tenantId={}, orderNo={}", TENANT_ID, TARGET_ORDER_NO);
CompletableFuture
.delayedExecutor(10, TimeUnit.SECONDS)
.execute(() -> TenantContext.runIgnoreTenant(this::runOnceIgnoreTenant));
}
private void runOnceIgnoreTenant() {
if (!executed.compareAndSet(false, true)) {
return;
}
ShopOrder order = shopOrderService.getOne(
new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getOrderNo, TARGET_ORDER_NO)
.eq(ShopOrder::getDeleted, 0)
.orderByDesc(ShopOrder::getOrderId)
.last("limit 1")
);
if (order == null) {
log.warn("未找到目标订单 - tenantId={}, orderNo={}", TENANT_ID, TARGET_ORDER_NO);
return;
}
Integer buyerUserId = order.getUserId();
Integer refereeId = getRefereeIdByLevel(buyerUserId, 1);
Integer refereeRefereeId = getRefereeIdByLevel(buyerUserId, 2);
List<Integer> upstreamReferees = findFirstTwoUpstreamReferees(buyerUserId);
Integer upstreamRefereeId = upstreamReferees.size() > 0 ? upstreamReferees.get(0) : null;
Integer upstreamRefereeRefereeId = upstreamReferees.size() > 1 ? upstreamReferees.get(1) : null;
List<Integer> shopRoleReferees = findFirstTwoShopRoleReferees(buyerUserId);
Integer shopRoleRefereeId = shopRoleReferees.size() > 0 ? shopRoleReferees.get(0) : null;
Integer shopRoleRefereeRefereeId = shopRoleReferees.size() > 1 ? shopRoleReferees.get(1) : null;
log.info("订单推荐关系查询结果 - orderNo={}, buyerUserId={}, refereeId(level=1)={}, refereeRefereeId(level=2)={}, upstreamRefereeId(level=1链路第1级)={}, upstreamRefereeRefereeId(level=1链路第2级)={}, shopRoleRefereeId(type=1)={}, shopRoleRefereeRefereeId(type=1)={}",
order.getOrderNo(), buyerUserId, refereeId, refereeRefereeId, upstreamRefereeId, upstreamRefereeRefereeId, shopRoleRefereeId, shopRoleRefereeRefereeId);
// 便于排查:从下单用户开始一路向上打印每一跳的门店(type=1)判定情况。
logShopRoleTraversal(buyerUserId);
}
private Integer getRefereeIdByLevel(Integer userId, int level) {
if (userId == null) {
return null;
}
ShopDealerReferee rel = shopDealerRefereeService.getOne(
new LambdaQueryWrapper<ShopDealerReferee>()
.eq(ShopDealerReferee::getTenantId, TENANT_ID)
.eq(ShopDealerReferee::getUserId, userId)
.eq(ShopDealerReferee::getLevel, level)
.orderByDesc(ShopDealerReferee::getId)
.last("limit 1")
);
return rel != null ? rel.getDealerId() : null;
}
/**
* 基于 shop_dealer_referee(level=1) 的链路,找出“最近的两级上级用户”。
* <p>
* 旧逻辑依赖扩展字段 isShopRole(已废弃) 过滤门店角色;目前仅按链路取前两级用于排查。
*/
private List<Integer> findFirstTwoUpstreamReferees(Integer buyerUserId) {
if (buyerUserId == null) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>(2);
Set<Integer> visited = new HashSet<>();
// 仅依赖 level=1 的推荐关系一路向上查找(可兼容“只维护 level=1”的数据形态
Integer current = buyerUserId;
for (int i = 0; i < MAX_REFEREE_CHAIN_DEPTH && current != null && result.size() < 2; i++) {
Integer parentId = getRefereeIdByLevel(current, 1);
if (parentId == null || !visited.add(parentId)) {
break;
}
result.add(parentId);
current = parentId;
}
return result;
}
/**
* 基于 shop_dealer_referee 的链路,找出“最近的两级门店角色用户”(ShopDealerUser.type=1)。
* <p>
* 判定逻辑由 SQL join shop_dealer_user(type=1) 得到 isShopRole。
*/
private List<Integer> findFirstTwoShopRoleReferees(Integer buyerUserId) {
if (buyerUserId == null) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>(2);
Set<Integer> visited = new HashSet<>();
// 优先:直接从该买家(userId)的多级关系(level=1/2/3/...)里,按 level 升序找到最近的两级门店(type=1)。
// List<ShopDealerReferee> relList = shopDealerRefereeMapper.selectRefereeChainWithShopRole(
// TENANT_ID, buyerUserId, MAX_REFEREE_CHAIN_DEPTH
// );
// for (ShopDealerReferee rel : relList) {
// Integer dealerId = rel != null ? rel.getDealerId() : null;
// if (dealerId == null || !visited.add(dealerId)) {
// continue;
// }
// if (Boolean.TRUE.equals(rel.getIsShopRole())) {
// result.add(dealerId);
// if (result.size() >= 2) {
// return result;
// }
// }
// }
// 兜底:若只维护了 level=1 的推荐关系,则尝试按“向上查找”的方式一路找门店(type=1)。
// Integer current = buyerUserId;
// for (int i = 0; i < MAX_REFEREE_CHAIN_DEPTH && current != null && result.size() < 2; i++) {
// ShopDealerReferee parentRel = shopDealerRefereeMapper.selectFirstLevelRefereeWithShopRole(TENANT_ID, current);
// Integer parentId = parentRel != null ? parentRel.getDealerId() : null;
// if (parentId == null || !visited.add(parentId)) {
// break;
// }
//
// if (Boolean.TRUE.equals(parentRel.getIsShopRole())) {
// result.add(parentId);
// }
// current = parentId;
// }
//
return result;
}
private void logShopRoleTraversal(Integer buyerUserId) {
if (buyerUserId == null) {
return;
}
Integer current = buyerUserId;
Set<Integer> visited = new HashSet<>();
/*for (int depth = 1; depth <= MAX_REFEREE_CHAIN_DEPTH && current != null; depth++) {
ShopDealerReferee parentRel = shopDealerRefereeMapper.selectFirstLevelRefereeWithShopRole(TENANT_ID, current);
Integer parentId = parentRel != null ? parentRel.getDealerId() : null;
if (parentId == null) {
log.info("type=1链路检查结束 - buyerUserId={}, depth={}, currentUserId={}, reason=no_parent", buyerUserId, depth, current);
return;
}
if (!visited.add(parentId)) {
log.info("type=1链路检查结束 - buyerUserId={}, depth={}, currentUserId={}, parentUserId={}, reason=cycle", buyerUserId, depth, current, parentId);
return;
}
ShopDealerUser du = shopDealerUserService.getOne(
new LambdaQueryWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getUserId, parentId)
.orderByDesc(ShopDealerUser::getId)
.last("limit 1")
);
Integer duType = du != null ? du.getType() : null;
Integer duIsDelete = du != null ? du.getIsDelete() : null;
log.info("type=1链路检查 - buyerUserId={}, depth={}, childUserId={}, parentUserId={}, isShopRole(sql)={}, shopDealerUser.type={}, shopDealerUser.is_delete={}",
buyerUserId, depth, current, parentId, parentRel != null ? parentRel.getIsShopRole() : null, duType, duIsDelete);
current = parentId;
}*/
log.info("type=1链路检查结束 - buyerUserId={}, depthReached={}, reason=max_depth", buyerUserId, MAX_REFEREE_CHAIN_DEPTH);
}
}

View File

@@ -0,0 +1,60 @@
package com.gxwebsoft.shop.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Utility to traverse an upstream "parent" chain and pick the nearest N users matching a predicate.
* <p>
* Typical usage: starting from buyerUserId, walk level-1 referee chain upwards and pick the first
* two store-role users (type=1).
*/
public final class UpstreamUserFinder {
private UpstreamUserFinder() {
}
/**
* Walk the upstream chain starting from {@code startUserId} (exclusive), repeatedly resolving
* the parent via {@code parentResolver}. Collect the first {@code needCount} userIds that match
* {@code matcher}, preserving encounter order (nearest first).
*/
public static List<Integer> findFirstNMatchingUpstreamUsers(
Integer startUserId,
int needCount,
int maxDepth,
Function<Integer, Integer> parentResolver,
Predicate<Integer> matcher
) {
if (startUserId == null || needCount <= 0 || maxDepth <= 0 || parentResolver == null || matcher == null) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>(Math.min(needCount, 4));
Set<Integer> visited = new HashSet<>();
Integer current = startUserId;
for (int depth = 0; depth < maxDepth && current != null && result.size() < needCount; depth++) {
Integer parentId = parentResolver.apply(current);
if (parentId == null) {
break;
}
if (!visited.add(parentId)) {
break;
}
if (matcher.test(parentId)) {
result.add(parentId);
}
current = parentId;
}
return result;
}
}

View File

@@ -0,0 +1,65 @@
package com.gxwebsoft.shop.util;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import static org.junit.jupiter.api.Assertions.assertEquals;
class UpstreamUserFinderTest {
@Test
void findFirstNMatchingUpstreamUsers_skipsNonMatchingAndKeepsOrder() {
// 100 -> 101 -> 102 -> 103 -> null
Map<Integer, Integer> parent = Map.of(
100, 101,
101, 102,
102, 103
);
Set<Integer> shopRoleUsers = Set.of(102, 103);
Function<Integer, Integer> parentResolver = parent::get;
Predicate<Integer> matcher = shopRoleUsers::contains;
List<Integer> got = UpstreamUserFinder.findFirstNMatchingUpstreamUsers(100, 2, 20, parentResolver, matcher);
assertEquals(List.of(102, 103), got);
}
@Test
void findFirstNMatchingUpstreamUsers_returnsSingleWhenOnlyOneMatchExists() {
// 200 -> 201 -> 202 -> null
Map<Integer, Integer> parent = Map.of(
200, 201,
201, 202
);
Set<Integer> shopRoleUsers = Set.of(201);
List<Integer> got = UpstreamUserFinder.findFirstNMatchingUpstreamUsers(200, 2, 20, parent::get, shopRoleUsers::contains);
assertEquals(List.of(201), got);
}
@Test
void findFirstNMatchingUpstreamUsers_stopsOnCycle() {
// 300 -> 301 -> 302 -> 301 (cycle)
Map<Integer, Integer> parent = Map.of(
300, 301,
301, 302,
302, 301
);
Set<Integer> shopRoleUsers = Set.of(301, 302);
List<Integer> got = UpstreamUserFinder.findFirstNMatchingUpstreamUsers(300, 2, 20, parent::get, shopRoleUsers::contains);
assertEquals(List.of(301, 302), got);
}
@Test
void findFirstNMatchingUpstreamUsers_returnsEmptyForNullStart() {
List<Integer> got = UpstreamUserFinder.findFirstNMatchingUpstreamUsers(null, 2, 20, x -> null, x -> true);
assertEquals(List.of(), got);
}
}