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