liujie
2026-09-15 ad491191ab50a259da548863d805c57d8a95d083
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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() {
        synchronizeGuideStatuses();
    }
 
    @Transactional
    public void synchronizeGuideStatuses() {
        GuideAutoDisableConfig config = configMapper.selectById(1);
        if (!valid(config)) return;
        restoreGuidesNoLongerEligible(config);
 
        Long lastGuideId = null;
        do {
            List<GuideUser> 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<GuideUser> selectCandidates(GuideAutoDisableConfig config, Long lastGuideId) {
        List<GuideUser> candidates = guideUserMapper.selectList(
                new LambdaQueryWrapper<GuideUser>()
                        .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 void restoreGuidesNoLongerEligible(GuideAutoDisableConfig config) {
        int restored = guideUserMapper.restoreAutoDisabledGuidesNoLongerEligible(
                config.getOrderThreshold(), config.getRatingThreshold());
        if (restored > 0) {
            log.info("Restored automatically disabled guides after rule reconciliation, count={}", restored);
        }
    }
 
    private boolean hasActiveServiceOrder(Long guideId) {
        return orderMapper.selectCount(new LambdaQueryWrapper<TourOrder>()
                .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;
    }
}