“mitao”
2026-03-02 7a394e5bf750464ef5cdfe4034847fe354fcfe20
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
package com.xiaopeng.maintenance.schedule;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.account.XpOperationsRole;
import com.xiaopeng.api.model.maintenance.XpTask;
import com.xiaopeng.api.model.maintenance.XpTaskRule;
import com.xiaopeng.api.model.maintenance.XpTaskRuleRole;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.maintenance.constant.TaskScheduleConstants;
import com.xiaopeng.maintenance.mapper.XpTaskMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleRoleMapper;
import com.xiaopeng.maintenance.utils.UUIDUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
 
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
 
/**
 * 年检/保险策略定时任务
 * <p>
 * 根据任务规则配置,在车辆保险、年检、运营资质即将到期时自动创建提醒任务,
 * 并下发给关联角色的运维人员
 * </p>
 *
 * @author mitao
 * @since 2026-02-27
 */
@Component
public class InsuranceInspectionScheduleTask {
 
    private static final Logger log = LoggerFactory.getLogger(InsuranceInspectionScheduleTask.class);
 
    /**
     * 任务名称
     */
    private static final String TASK_NAME = "年检/保险策略定时任务";
 
    @Autowired
    private XpTaskRuleMapper taskRuleMapper;
 
    @Autowired
    private XpTaskRuleRoleMapper taskRuleRoleMapper;
 
    @Autowired
    private XpTaskMapper taskMapper;
 
    @Autowired
    private XpVehicleClient xpVehicleClient;
 
    @Autowired
    private XpOperationUserClient xpOperationUserClient;
 
    /**
     * 每天凌晨1点执行一次
     * <p>
     * 检查逻辑:
     * 1. 获取任务规则配置
     * 2. 判断年检/保险策略功能是否启用
     * 3. 获取所有运营中的车辆
     * 4. 分别检查三种到期情况并创建任务:
     *    - 保险到期
     *    - 年检到期
     *    - 运营资质到期
     * </p>
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void executeInsuranceInspectionTask() {
        log.info("========== 开始执行年检/保险策略定时任务检查 ==========");
 
        // 1. 获取任务规则配置
        XpTaskRule taskRule = getTaskRule();
        if (taskRule == null) {
            log.info("未找到任务规则配置,跳过执行");
            return;
        }
 
        // 2. 判断年检/保险策略功能是否启用
        if (!Boolean.TRUE.equals(taskRule.getInsurance())) {
            log.info("年检/保险策略功能未启用,跳过执行");
            return;
        }
 
        log.info("开始执行年检/保险策略任务下发,保险提前{}天, 年检提前{}天, 运营资质提前{}天, 紧急程度: P{}",
                taskRule.getInsuranceExpires(),
                taskRule.getInsuranceInspectionExpires(),
                taskRule.getOperationQualificationExpires(),
                taskRule.getInsuranceLevel());
 
        try {
            // 执行任务创建
            int createdCount = createExpireTasks(taskRule);
 
            log.info("========== 年检/保险策略定时任务执行完成,创建任务数: {} ==========", createdCount);
 
        } catch (Exception e) {
            log.error("年检/保险策略定时任务执行失败", e);
        }
    }
 
    /**
     * 获取任务规则配置
     *
     * @return 任务规则
     */
    private XpTaskRule getTaskRule() {
        return taskRuleMapper.selectOne(new LambdaQueryWrapper<XpTaskRule>().last("limit 1"));
    }
 
    /**
     * 创建到期提醒任务
     *
     * @param taskRule 任务规则配置
     * @return 创建的任务数量
     */
    private int createExpireTasks(XpTaskRule taskRule) {
        // 1. 获取任务接收角色的运维人员
        List<Long> operationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_INSURANCE);
        if (CollectionUtils.isEmpty(operationUserIds)) {
            log.warn("未找到年检/保险任务接收角色的运维人员");
            return 0;
        }
 
        log.info("找到 {} 个运维人员可接收年检/保险任务", operationUserIds.size());
 
        // 2. 获取所有运营中的车辆
        List<XpVehicle> vehicles = getOperatingVehicles();
        if (CollectionUtils.isEmpty(vehicles)) {
            log.info("没有运营中的车辆,无需创建到期提醒任务");
            return 0;
        }
 
        log.info("找到 {} 辆运营中的车辆", vehicles.size());
 
        int totalCreatedCount = 0;
 
        // 3. 检查保险到期
        totalCreatedCount += checkAndCreateInsuranceExpireTasks(vehicles, operationUserIds, taskRule);
 
        // 4. 检查年检到期
        totalCreatedCount += checkAndCreateInspectionExpireTasks(vehicles, operationUserIds, taskRule);
 
        // 5. 检查运营资质到期
        totalCreatedCount += checkAndCreateQualificationExpireTasks(vehicles, operationUserIds, taskRule);
 
        return totalCreatedCount;
    }
 
    /**
     * 检查并创建保险到期任务
     *
     * @param vehicles         车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int checkAndCreateInsuranceExpireTasks(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        if (taskRule.getInsuranceExpires() == null || taskRule.getInsuranceExpires() <= 0) {
            log.info("保险到期提醒天数未配置,跳过保险到期检查");
            return 0;
        }
 
        List<XpVehicle> needWarnVehicles = vehicles.stream()
                .filter(v -> isWithinExpireWarningDays(v.getInsuranceEndTime(), taskRule.getInsuranceExpires()))
                .filter(v -> !hasUnfinishedTask(v.getId(), TaskScheduleConstants.TASK_TYPE_INSURANCE_EXPIRE))
                .collect(Collectors.toList());
 
        if (CollectionUtils.isEmpty(needWarnVehicles)) {
            log.info("没有需要保险到期提醒的车辆");
            return 0;
        }
 
        log.info("找到 {} 辆车辆需要保险到期提醒", needWarnVehicles.size());
 
        return createTasksForExpireType(
                needWarnVehicles,
                operationUserIds,
                taskRule,
                TaskScheduleConstants.TASK_TYPE_INSURANCE_EXPIRE,
                "保险即将到期"
        );
    }
 
    /**
     * 检查并创建年检到期任务
     *
     * @param vehicles         车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int checkAndCreateInspectionExpireTasks(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        if (taskRule.getInsuranceInspectionExpires() == null || taskRule.getInsuranceInspectionExpires() <= 0) {
            log.info("年检到期提醒天数未配置,跳过年检到期检查");
            return 0;
        }
 
        List<XpVehicle> needWarnVehicles = vehicles.stream()
                .filter(v -> isWithinExpireWarningDays(v.getAnnualInspectionDate(), taskRule.getInsuranceInspectionExpires()))
                .filter(v -> !hasUnfinishedTask(v.getId(), TaskScheduleConstants.TASK_TYPE_INSPECTION_EXPIRE))
                .collect(Collectors.toList());
 
        if (CollectionUtils.isEmpty(needWarnVehicles)) {
            log.info("没有需要年检到期提醒的车辆");
            return 0;
        }
 
        log.info("找到 {} 辆车辆需要年检到期提醒", needWarnVehicles.size());
 
        return createTasksForExpireType(
                needWarnVehicles,
                operationUserIds,
                taskRule,
                TaskScheduleConstants.TASK_TYPE_INSPECTION_EXPIRE,
                "年检即将到期"
        );
    }
 
    /**
     * 检查并创建运营资质到期任务
     *
     * @param vehicles         车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int checkAndCreateQualificationExpireTasks(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        if (taskRule.getOperationQualificationExpires() == null || taskRule.getOperationQualificationExpires() <= 0) {
            log.info("运营资质到期提醒天数未配置,跳过运营资质到期检查");
            return 0;
        }
 
        List<XpVehicle> needWarnVehicles = vehicles.stream()
                .filter(v -> isWithinExpireWarningDays(v.getQualificationExpireTime(), taskRule.getOperationQualificationExpires()))
                .filter(v -> !hasUnfinishedTask(v.getId(), TaskScheduleConstants.TASK_TYPE_QUALIFICATION_EXPIRE))
                .collect(Collectors.toList());
 
        if (CollectionUtils.isEmpty(needWarnVehicles)) {
            log.info("没有需要运营资质到期提醒的车辆");
            return 0;
        }
 
        log.info("找到 {} 辆车辆需要运营资质到期提醒", needWarnVehicles.size());
 
        return createTasksForExpireType(
                needWarnVehicles,
                operationUserIds,
                taskRule,
                TaskScheduleConstants.TASK_TYPE_QUALIFICATION_EXPIRE,
                "运营资质即将到期"
        );
    }
 
    /**
     * 判断是否在到期提醒范围内
     *
     * @param expireTime        到期时间
     * @param daysBeforeExpire 提前X天提醒
     * @return 是否需要提醒
     */
    private boolean isWithinExpireWarningDays(LocalDateTime expireTime, Integer daysBeforeExpire) {
        if (expireTime == null || daysBeforeExpire == null || daysBeforeExpire <= 0) {
            return false;
        }
 
        LocalDate today = LocalDate.now();
        LocalDate expireDate = expireTime.toLocalDate();
        long daysUntilExpire = ChronoUnit.DAYS.between(today, expireDate);
 
        // 距离到期天数 <= 配置的提前天数,且未过期(daysUntilExpire >= 0)
        return daysUntilExpire >= 0 && daysUntilExpire <= daysBeforeExpire;
    }
 
    /**
     * 检查是否已存在未完成的同类型任务
     * <p>
     * 避免重复创建到期提醒任务
     * </p>
     *
     * @param vehicleId  车辆ID
     * @param taskTypeId 任务类型ID
     * @return 是否存在未完成任务
     */
    private boolean hasUnfinishedTask(Long vehicleId, String taskTypeId) {
        XpTask existingTask = taskMapper.selectOne(
                new LambdaQueryWrapper<XpTask>()
                        .eq(XpTask::getVehicleId, vehicleId)
                        .eq(XpTask::getTaskTypeId, taskTypeId)
                        .in(XpTask::getStatus,
                                TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE,
                                TaskScheduleConstants.TASK_STATUS_PENDING)
                        .last("limit 1")
        );
        return existingTask != null;
    }
 
    /**
     * 为指定到期类型创建任务
     *
     * @param vehicles         需要提醒的车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @param taskTypeId       任务类型ID
     * @param taskDescription  任务描述
     * @return 创建的任务数量
     */
    private int createTasksForExpireType(List<XpVehicle> vehicles, List<Long> operationUserIds,
                                          XpTaskRule taskRule, String taskTypeId, String taskDescription) {
        List<XpTask> tasksToSave = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
 
        // 将所有配置角色的运维人员ID拼接,任务分配给该角色下的所有运维人员
        String belongUserIds = operationUserIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
 
        for (XpVehicle vehicle : vehicles) {
            XpTask task = new XpTask();
 
            // 生成任务编号
            String taskCode = sdf.format(new Date()) + UUIDUtil.getNumberRandom(5);
            task.setTaskCode(taskCode);
 
            // 设置任务基本信息
            task.setTaskCategory(TaskScheduleConstants.TASK_CATEGORY_ROUTINE);
            task.setTaskTypeId(taskTypeId);
            task.setTaskUrgencyLevel(taskRule.getInsuranceLevel() != null
                    ? taskRule.getInsuranceLevel()
                    : TaskScheduleConstants.URGENCY_LEVEL_P2);
            task.setTaskDescription("系统自动下发的" + taskDescription + "提醒任务");
 
            // 设置车辆信息
            task.setVehicleId(vehicle.getId());
 
            // 设置任务时限 常规任务限时3天
            task.setEndTime(LocalDateTime.now().plusDays(3));
 
            // 设置任务状态为待到达
            task.setStatus(TaskScheduleConstants.TASK_STATUS_PENDING);
 
            // 设置任务接收人(配置角色下的所有运维人员都可以看到)
            task.setHandleUserId(belongUserIds);
            task.setReportTime(LocalDateTime.now());
            task.setReportUserId(0L);
            // 设置创建信息
            task.setReportTime(LocalDateTime.now());
            task.setCreateBy(TaskScheduleConstants.SCHEDULE_CREATOR);
            tasksToSave.add(task);
        }
 
        // 批量保存任务
        if (!CollectionUtils.isEmpty(tasksToSave)) {
            for (XpTask task : tasksToSave) {
                taskMapper.insert(task);
            }
            log.info("成功创建 {} 个{}提醒任务", tasksToSave.size(), taskDescription);
        }
 
        return tasksToSave.size();
    }
 
    /**
     * 根据角色类型获取运维人员ID列表
     *
     * @param roleType 角色类型
     * @return 运维人员ID列表
     */
    private List<Long> getOperationUserIdsByRoleType(Integer roleType) {
        // 1. 获取该类型对应的角色ID
        List<XpTaskRuleRole> ruleRoles = taskRuleRoleMapper.selectList(
                new LambdaQueryWrapper<XpTaskRuleRole>()
                        .eq(XpTaskRuleRole::getRuleType, roleType)
        );
 
        if (CollectionUtils.isEmpty(ruleRoles)) {
            return Collections.emptyList();
        }
 
        // 2. 获取角色对应的运维人员
        Set<Long> userIds = new HashSet<>();
        for (XpTaskRuleRole ruleRole : ruleRoles) {
            try {
                R<List<XpOperationsRole>> result = xpOperationUserClient.getXpOperationsRoleByRoleId(ruleRole.getRoleId());
                if (result != null && result.getData() != null) {
                    userIds.addAll(
                            result.getData().stream()
                                    .map(XpOperationsRole::getOperationsUserId)
                                    .collect(Collectors.toList())
                    );
                }
            } catch (Exception e) {
                log.error("获取角色[{}]对应的运维人员失败", ruleRole.getRoleId(), e);
            }
        }
 
        return new ArrayList<>(userIds);
    }
 
    /**
     * 获取所有运营中的车辆
     *
     * @return 车辆列表
     */
    private List<XpVehicle> getOperatingVehicles() {
        try {
            R<List<XpVehicle>> result = xpVehicleClient.listVehicle();
            if (result != null && result.getData() != null) {
                // 过滤运营中的车辆 (vehicleType=2 表示运营, operatingStatus=1 表示运营中)
                return result.getData().stream()
                        .filter(v -> v.getVehicleType() != null && v.getVehicleType() == 2)
                        .filter(v -> v.getOperatingStatus() != null && v.getOperatingStatus() == 1)
                        .collect(Collectors.toList());
            }
        } catch (Exception e) {
            log.error("获取车辆列表失败", e);
        }
        return Collections.emptyList();
    }
}