From 3b63172012a6ad84e366f5c9efd82e233c5976b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 26 Jan 2026 12:34:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor(task):=20=E9=87=8D=E6=9E=84=E7=BB=8F?= =?UTF-8?q?=E9=94=80=E5=95=86=E8=AE=A2=E5=8D=95=E7=BB=93=E7=AE=97=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E4=B8=AD=E7=9A=84=E4=B8=8A=E7=BA=A7=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=9F=A5=E6=89=BE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 UpstreamUserFinder 工具类来统一处理向上游用户链路的遍历逻辑 - 添加缓存机制减少数据库查询次数,提高性能 - 修改 settleOneOrder 方法签名以传递缓存对象 - 更新门店分红上级查找逻辑,从简单的链路取前两级改为精确查找门店角色用户 - 删除废弃的 ShopOrderSettlement10584Task 临时排查任务类 - 添加 UpstreamUserFinder 的单元测试确保逻辑正确性 --- .../task/DealerOrderSettlement10584Task.java | 108 +++++--- .../task/ShopOrderSettlement10584Task.java | 234 ------------------ .../shop/util/UpstreamUserFinder.java | 60 +++++ .../shop/util/UpstreamUserFinderTest.java | 65 +++++ 4 files changed, 199 insertions(+), 268 deletions(-) delete mode 100644 src/main/java/com/gxwebsoft/shop/task/ShopOrderSettlement10584Task.java create mode 100644 src/main/java/com/gxwebsoft/shop/util/UpstreamUserFinder.java create mode 100644 src/test/java/com/gxwebsoft/shop/util/UpstreamUserFinderTest.java diff --git a/src/main/java/com/gxwebsoft/shop/task/DealerOrderSettlement10584Task.java b/src/main/java/com/gxwebsoft/shop/task/DealerOrderSettlement10584Task.java index 19a39e8..be4d8df 100644 --- a/src/main/java/com/gxwebsoft/shop/task/DealerOrderSettlement10584Task.java +++ b/src/main/java/com/gxwebsoft/shop/task/DealerOrderSettlement10584Task.java @@ -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 level1ParentCache = new HashMap<>(); + Map 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 level1ParentCache, Map 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 shopRoleReferees = findFirstTwoUpstreamReferees(order.getUserId()); - log.info("分红上级链路结果(level=1链路取前两级) - orderNo={}, buyerUserId={}, referees={}", + private ShopRoleCommission settleShopRoleRefereeCommission( + ShopOrder order, + BigDecimal baseAmount, + Map level1ParentCache, + Map shopRoleCache + ) { + List 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) 链路向上找到最近两级上级。 - *

- * 旧逻辑依赖扩展字段 isShopRole(已废弃) 来过滤门店角色;目前按链路取前两级,保证能回填/结算。 + * 门店分红规则: + * - 门店角色为 ShopDealerUser.type=1; + * - 从下单用户开始,沿 shop_dealer_referee(level=1) 链路逐级向上找; + * - 遇到第一个 type=1 用户命中为“直推门店用户”,继续向上找到第二个 type=1 用户命中为“简推门店用户”。 */ - private List findFirstTwoUpstreamReferees(Integer buyerUserId) { + private List findFirstTwoShopRoleReferees( + Integer buyerUserId, + Map level1ParentCache, + Map shopRoleCache + ) { if (buyerUserId == null) { return Collections.emptyList(); } - List result = new ArrayList<>(2); - Set 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 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 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() + .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) { diff --git a/src/main/java/com/gxwebsoft/shop/task/ShopOrderSettlement10584Task.java b/src/main/java/com/gxwebsoft/shop/task/ShopOrderSettlement10584Task.java deleted file mode 100644 index ae1afed..0000000 --- a/src/main/java/com/gxwebsoft/shop/task/ShopOrderSettlement10584Task.java +++ /dev/null @@ -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:一次性排查任务 - *

- * 应用启动后延迟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() - .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 upstreamReferees = findFirstTwoUpstreamReferees(buyerUserId); - Integer upstreamRefereeId = upstreamReferees.size() > 0 ? upstreamReferees.get(0) : null; - Integer upstreamRefereeRefereeId = upstreamReferees.size() > 1 ? upstreamReferees.get(1) : null; - - List 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() - .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) 的链路,找出“最近的两级上级用户”。 - *

- * 旧逻辑依赖扩展字段 isShopRole(已废弃) 过滤门店角色;目前仅按链路取前两级用于排查。 - */ - private List findFirstTwoUpstreamReferees(Integer buyerUserId) { - if (buyerUserId == null) { - return Collections.emptyList(); - } - - List result = new ArrayList<>(2); - Set 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)。 - *

- * 判定逻辑由 SQL join shop_dealer_user(type=1) 得到 isShopRole。 - */ - private List findFirstTwoShopRoleReferees(Integer buyerUserId) { - if (buyerUserId == null) { - return Collections.emptyList(); - } - - List result = new ArrayList<>(2); - Set visited = new HashSet<>(); - - // 优先:直接从该买家(userId)的多级关系(level=1/2/3/...)里,按 level 升序找到最近的两级门店(type=1)。 -// List 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 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() - .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); - } -} diff --git a/src/main/java/com/gxwebsoft/shop/util/UpstreamUserFinder.java b/src/main/java/com/gxwebsoft/shop/util/UpstreamUserFinder.java new file mode 100644 index 0000000..b26fd7a --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/util/UpstreamUserFinder.java @@ -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. + *

+ * 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 findFirstNMatchingUpstreamUsers( + Integer startUserId, + int needCount, + int maxDepth, + Function parentResolver, + Predicate matcher + ) { + if (startUserId == null || needCount <= 0 || maxDepth <= 0 || parentResolver == null || matcher == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(Math.min(needCount, 4)); + Set 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; + } +} + diff --git a/src/test/java/com/gxwebsoft/shop/util/UpstreamUserFinderTest.java b/src/test/java/com/gxwebsoft/shop/util/UpstreamUserFinderTest.java new file mode 100644 index 0000000..5147a43 --- /dev/null +++ b/src/test/java/com/gxwebsoft/shop/util/UpstreamUserFinderTest.java @@ -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 parent = Map.of( + 100, 101, + 101, 102, + 102, 103 + ); + Set shopRoleUsers = Set.of(102, 103); + + Function parentResolver = parent::get; + Predicate matcher = shopRoleUsers::contains; + + List got = UpstreamUserFinder.findFirstNMatchingUpstreamUsers(100, 2, 20, parentResolver, matcher); + assertEquals(List.of(102, 103), got); + } + + @Test + void findFirstNMatchingUpstreamUsers_returnsSingleWhenOnlyOneMatchExists() { + // 200 -> 201 -> 202 -> null + Map parent = Map.of( + 200, 201, + 201, 202 + ); + Set shopRoleUsers = Set.of(201); + + List 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 parent = Map.of( + 300, 301, + 301, 302, + 302, 301 + ); + Set shopRoleUsers = Set.of(301, 302); + + List got = UpstreamUserFinder.findFirstNMatchingUpstreamUsers(300, 2, 20, parent::get, shopRoleUsers::contains); + assertEquals(List.of(301, 302), got); + } + + @Test + void findFirstNMatchingUpstreamUsers_returnsEmptyForNullStart() { + List got = UpstreamUserFinder.findFirstNMatchingUpstreamUsers(null, 2, 20, x -> null, x -> true); + assertEquals(List.of(), got); + } +} +