package com.tourguide.modules.user.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tourguide.modules.order.domain.OrderStatus; import com.tourguide.modules.order.entity.TourOrder; import com.tourguide.modules.order.mapper.TourOrderMapper; import com.tourguide.modules.user.entity.GuideAutoDisableConfig; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.mapper.GuideAutoDisableConfigMapper; import com.tourguide.modules.user.mapper.GuideRealtimeStatusMapper; import com.tourguide.modules.user.mapper.GuideReceivingSessionMapper; import com.tourguide.modules.user.mapper.GuideUserMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; import java.util.List; @Service public class GuideAutoDisableService { private static final Logger log = LoggerFactory.getLogger(GuideAutoDisableService.class); private static final int BATCH_SIZE = 100; private final GuideAutoDisableConfigMapper configMapper; private final GuideUserMapper guideUserMapper; private final TourOrderMapper orderMapper; private final GuideRealtimeStatusMapper realtimeStatusMapper; private final GuideReceivingSessionMapper receivingSessionMapper; public GuideAutoDisableService(GuideAutoDisableConfigMapper configMapper, GuideUserMapper guideUserMapper, TourOrderMapper orderMapper, GuideRealtimeStatusMapper realtimeStatusMapper, GuideReceivingSessionMapper receivingSessionMapper) { this.configMapper = configMapper; this.guideUserMapper = guideUserMapper; this.orderMapper = orderMapper; this.realtimeStatusMapper = realtimeStatusMapper; this.receivingSessionMapper = receivingSessionMapper; } @Scheduled(fixedDelayString = "${tourguide.guide.auto-disable-scan-delay-millis:60000}") @Transactional public void disableEligibleGuides() { GuideAutoDisableConfig config = configMapper.selectById(1); if (!valid(config)) return; Long lastGuideId = null; do { List candidates = selectCandidates(config, lastGuideId); if (candidates.isEmpty()) return; for (GuideUser guide : candidates) { disableIfEligible(guide, config); } lastGuideId = candidates.get(candidates.size() - 1).getId(); if (candidates.size() < BATCH_SIZE) return; } while (true); } /** 导游移出白名单后立即复核,避免等待下一次定时扫描。 */ @Transactional public void disableGuideIfEligible(Long guideId) { if (guideId == null) return; GuideAutoDisableConfig config = configMapper.selectById(1); if (!valid(config)) return; GuideUser guide = guideUserMapper.selectById(guideId); if (!matchesRule(guide, config)) return; disableIfEligible(guide, config); } private List selectCandidates(GuideAutoDisableConfig config, Long lastGuideId) { List candidates = guideUserMapper.selectList( new LambdaQueryWrapper() .eq(GuideUser::getStatus, GuideUserService.STATUS_ENABLED) .and(wrapper -> wrapper.isNull(GuideUser::getAutoDisableWhitelisted) .or().eq(GuideUser::getAutoDisableWhitelisted, 0)) .ge(GuideUser::getAcceptedOrderCount, config.getOrderThreshold()) .gt(GuideUser::getRating, BigDecimal.ZERO) .le(GuideUser::getRating, config.getRatingThreshold()) .gt(lastGuideId != null, GuideUser::getId, lastGuideId) .orderByAsc(GuideUser::getId) .last("LIMIT " + BATCH_SIZE)); return candidates == null ? Collections.emptyList() : candidates; } void disableIfEligible(GuideUser guide, GuideAutoDisableConfig config) { if (guide == null || Integer.valueOf(1).equals(guide.getAutoDisableWhitelisted())) return; if (hasActiveServiceOrder(guide.getId())) return; int updated = guideUserMapper.autoDisableIfEligible(guide.getId(), config.getOrderThreshold(), config.getRatingThreshold()); if (updated != 1) return; realtimeStatusMapper.disableReceiving(guide.getId()); receivingSessionMapper.stopReceivingByGuide(guide.getId()); log.warn("Guide auto-disabled, guideId={}, acceptedOrderCount={}, rating={}", guide.getId(), guide.getAcceptedOrderCount(), guide.getRating()); } private boolean hasActiveServiceOrder(Long guideId) { return orderMapper.selectCount(new LambdaQueryWrapper() .eq(TourOrder::getGuideId, guideId) .in(TourOrder::getStatus, Arrays.asList( OrderStatus.PENDING_MEETING.getCode(), OrderStatus.IN_SERVICE.getCode()))) > 0; } private boolean matchesRule(GuideUser guide, GuideAutoDisableConfig config) { return guide != null && Integer.valueOf(GuideUserService.STATUS_ENABLED).equals(guide.getStatus()) && !Integer.valueOf(1).equals(guide.getAutoDisableWhitelisted()) && guide.getAcceptedOrderCount() != null && guide.getAcceptedOrderCount() >= config.getOrderThreshold() && guide.getRating() != null && guide.getRating().compareTo(BigDecimal.ZERO) > 0 && guide.getRating().compareTo(config.getRatingThreshold()) <= 0; } private boolean valid(GuideAutoDisableConfig config) { return config != null && config.getOrderThreshold() != null && config.getOrderThreshold() > 0 && config.getRatingThreshold() != null; } }