refactor(task): 重构经销商订单结算任务中的上级用户查找逻辑
- 引入 UpstreamUserFinder 工具类来统一处理向上游用户链路的遍历逻辑 - 添加缓存机制减少数据库查询次数,提高性能 - 修改 settleOneOrder 方法签名以传递缓存对象 - 更新门店分红上级查找逻辑,从简单的链路取前两级改为精确查找门店角色用户 - 删除废弃的 ShopOrderSettlement10584Task 临时排查任务类 - 添加 UpstreamUserFinder 的单元测试确保逻辑正确性
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user