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.XpFleetOperationsClient; 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.api.vo.automaticPilot.VehiclePosition; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.api.vo.maintenance.TaskScheduleConstants; import com.xiaopeng.common.core.utils.SensitiveDataCryptoUtil; import com.xiaopeng.maintenance.mapper.XpTaskMapper; import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper; import com.xiaopeng.maintenance.mapper.XpTaskRuleRoleMapper; import com.xiaopeng.maintenance.service.VehicleLocationFillService; 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 org.springframework.util.StringUtils; 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; /** * 年检/保险策略定时任务 *

* 根据任务规则配置,在车辆保险、年检、运营资质即将到期时自动创建提醒任务, * 并下发给关联角色的运维人员 *

* * @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; @Autowired private XpFleetOperationsClient xpFleetOperationsClient; @Autowired private VehicleLocationFillService vehicleLocationFillService; /** * 每天凌晨1点执行一次 *

* 检查逻辑: * 1. 获取任务规则配置 * 2. 判断年检/保险策略功能是否启用 * 3. 获取所有运营中的车辆 * 4. 分别检查三种到期情况并创建任务: * - 保险到期 * - 年检到期 * - 运营资质到期 *

*/ @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().last("limit 1")); } /** * 创建到期提醒任务 * * @param taskRule 任务规则配置 * @return 创建的任务数量 */ private int createExpireTasks(XpTaskRule taskRule) { // 1. 获取任务接收角色的运维人员 List roleOperationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_INSURANCE); if (CollectionUtils.isEmpty(roleOperationUserIds)) { log.warn("未找到年检/保险任务接收角色的运维人员"); return 0; } Set roleOperationUserIdSet = new HashSet<>(roleOperationUserIds); log.info("找到 {} 个运维人员可接收年检/保险任务", roleOperationUserIdSet.size()); // 2. 获取所有运营中的车辆 List vehicles = getOperatingVehicles(); if (CollectionUtils.isEmpty(vehicles)) { log.info("没有运营中的车辆,无需创建到期提醒任务"); return 0; } log.info("找到 {} 辆运营中的车辆", vehicles.size()); Map positionByVin = vehicleLocationFillService.loadPositionByVin( vehicles.stream() .map(XpVehicle::getVehicleVin) .filter(StringUtils::hasLength) .distinct() .collect(Collectors.toList()) ); Map vehicleIdToFleetIdCache = new HashMap<>(); Map> fleetIdToOperationUserIdsCache = new HashMap<>(); int totalCreatedCount = 0; // 3. 检查保险到期 totalCreatedCount += checkAndCreateInsuranceExpireTasks(vehicles, roleOperationUserIdSet, taskRule, vehicleIdToFleetIdCache, fleetIdToOperationUserIdsCache, positionByVin); // 4. 检查年检到期 totalCreatedCount += checkAndCreateInspectionExpireTasks(vehicles, roleOperationUserIdSet, taskRule, vehicleIdToFleetIdCache, fleetIdToOperationUserIdsCache, positionByVin); // 5. 检查运营资质到期 totalCreatedCount += checkAndCreateQualificationExpireTasks(vehicles, roleOperationUserIdSet, taskRule, vehicleIdToFleetIdCache, fleetIdToOperationUserIdsCache, positionByVin); return totalCreatedCount; } /** * 检查并创建保险到期任务 * * @param vehicles 车辆列表 * @param roleOperationUserIds 配置角色下的运维人员ID集合 * @param taskRule 任务规则 * @param vehicleIdToFleetIdCache 车辆->车队缓存 * @param fleetIdToOperationUserIdsCache 车队->人员缓存 * @return 创建的任务数量 */ private int checkAndCreateInsuranceExpireTasks(List vehicles, Set roleOperationUserIds, XpTaskRule taskRule, Map vehicleIdToFleetIdCache, Map> fleetIdToOperationUserIdsCache, Map positionByVin) { if (taskRule.getInsuranceExpires() == null || taskRule.getInsuranceExpires() <= 0) { log.info("保险到期提醒天数未配置,跳过保险到期检查"); return 0; } List 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, roleOperationUserIds, taskRule, TaskScheduleConstants.TASK_TYPE_INSURANCE_EXPIRE, "保险即将到期", vehicleIdToFleetIdCache, fleetIdToOperationUserIdsCache, positionByVin ); } /** * 检查并创建年检到期任务 * * @param vehicles 车辆列表 * @param roleOperationUserIds 配置角色下的运维人员ID集合 * @param taskRule 任务规则 * @param vehicleIdToFleetIdCache 车辆->车队缓存 * @param fleetIdToOperationUserIdsCache 车队->人员缓存 * @return 创建的任务数量 */ private int checkAndCreateInspectionExpireTasks(List vehicles, Set roleOperationUserIds, XpTaskRule taskRule, Map vehicleIdToFleetIdCache, Map> fleetIdToOperationUserIdsCache, Map positionByVin) { if (taskRule.getInsuranceInspectionExpires() == null || taskRule.getInsuranceInspectionExpires() <= 0) { log.info("年检到期提醒天数未配置,跳过年检到期检查"); return 0; } List 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, roleOperationUserIds, taskRule, TaskScheduleConstants.TASK_TYPE_INSPECTION_EXPIRE, "年检即将到期", vehicleIdToFleetIdCache, fleetIdToOperationUserIdsCache, positionByVin ); } /** * 检查并创建运营资质到期任务 * * @param vehicles 车辆列表 * @param roleOperationUserIds 配置角色下的运维人员ID集合 * @param taskRule 任务规则 * @param vehicleIdToFleetIdCache 车辆->车队缓存 * @param fleetIdToOperationUserIdsCache 车队->人员缓存 * @return 创建的任务数量 */ private int checkAndCreateQualificationExpireTasks(List vehicles, Set roleOperationUserIds, XpTaskRule taskRule, Map vehicleIdToFleetIdCache, Map> fleetIdToOperationUserIdsCache, Map positionByVin) { if (taskRule.getOperationQualificationExpires() == null || taskRule.getOperationQualificationExpires() <= 0) { log.info("运营资质到期提醒天数未配置,跳过运营资质到期检查"); return 0; } List 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, roleOperationUserIds, taskRule, TaskScheduleConstants.TASK_TYPE_QUALIFICATION_EXPIRE, "运营资质即将到期", vehicleIdToFleetIdCache, fleetIdToOperationUserIdsCache, positionByVin ); } /** * 判断是否在到期提醒范围内 * * @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; } /** * 检查是否已存在未完成的同类型任务 *

* 避免重复创建到期提醒任务 *

* * @param vehicleId 车辆ID * @param taskTypeId 任务类型ID * @return 是否存在未完成任务 */ private boolean hasUnfinishedTask(Long vehicleId, String taskTypeId) { XpTask existingTask = taskMapper.selectOne( new LambdaQueryWrapper() .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 roleOperationUserIds 配置角色下的运维人员ID集合 * @param taskRule 任务规则 * @param taskTypeId 任务类型ID * @param taskDescription 任务描述 * @param vehicleIdToFleetIdCache 车辆->车队缓存 * @param fleetIdToOperationUserIdsCache 车队->人员缓存 * @return 创建的任务数量 */ private int createTasksForExpireType(List vehicles, Set roleOperationUserIds, XpTaskRule taskRule, String taskTypeId, String taskDescription, Map vehicleIdToFleetIdCache, Map> fleetIdToOperationUserIdsCache, Map positionByVin) { List tasksToSave = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS"); int skippedCount = 0; for (XpVehicle vehicle : vehicles) { List handleUserIds = resolveHandleUserIds(vehicle.getId(), taskTypeId, roleOperationUserIds, vehicleIdToFleetIdCache, fleetIdToOperationUserIdsCache); if (CollectionUtils.isEmpty(handleUserIds)) { skippedCount++; continue; } String belongUserIds = handleUserIds.stream().map(String::valueOf).collect(Collectors.joining(",")); 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()); String vin = SensitiveDataCryptoUtil.decryptIfPossible(vehicle.getVehicleVin()); vehicleLocationFillService.fillFromVin(task, vin, positionByVin); // 设置任务时限 常规任务限时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); } if (skippedCount > 0) { log.warn("{}提醒任务:跳过 {} 辆车(无可分配处理人)", taskDescription, skippedCount); } return tasksToSave.size(); } private List resolveHandleUserIds(Long vehicleId, String taskTypeId, Set roleOperationUserIds, Map vehicleIdToFleetIdCache, Map> fleetIdToOperationUserIdsCache) { if (vehicleId == null) { log.warn("定时任务分配处理人失败:vehicleId为空,taskTypeId={}", taskTypeId); return Collections.emptyList(); } Long fleetId = vehicleIdToFleetIdCache.get(vehicleId); if (!vehicleIdToFleetIdCache.containsKey(vehicleId)) { try { R fleetIdResult = xpVehicleClient.findFleetIdByVehicleId(vehicleId); fleetId = fleetIdResult != null ? fleetIdResult.getData() : null; } catch (Exception e) { log.error("根据车辆查询车队失败,vehicleId={}, taskTypeId={}", vehicleId, taskTypeId, e); fleetId = null; } vehicleIdToFleetIdCache.put(vehicleId, fleetId); } if (fleetId == null) { log.warn("定时任务跳过:车辆未关联车队,vehicleId={}, taskTypeId={}", vehicleId, taskTypeId); return Collections.emptyList(); } Set fleetOperationUserIds = fleetIdToOperationUserIdsCache.get(fleetId); if (!fleetIdToOperationUserIdsCache.containsKey(fleetId)) { try { R> opsResult = xpFleetOperationsClient.getOperationsIdsByFleetId(fleetId); List opsIds = opsResult != null ? opsResult.getData() : null; fleetOperationUserIds = CollectionUtils.isEmpty(opsIds) ? Collections.emptySet() : new HashSet<>(opsIds); } catch (Exception e) { log.error("根据车队查询关联人员失败,fleetId={}, vehicleId={}, taskTypeId={}", fleetId, vehicleId, taskTypeId, e); fleetOperationUserIds = Collections.emptySet(); } fleetIdToOperationUserIdsCache.put(fleetId, fleetOperationUserIds); } if (CollectionUtils.isEmpty(fleetOperationUserIds)) { log.warn("定时任务跳过:车队未关联人员,fleetId={}, vehicleId={}, taskTypeId={}", fleetId, vehicleId, taskTypeId); return Collections.emptyList(); } List finalUserIds = fleetOperationUserIds.stream() .filter(roleOperationUserIds::contains) .sorted() .collect(Collectors.toList()); if (CollectionUtils.isEmpty(finalUserIds)) { log.warn("定时任务跳过:车队人员与角色人员无交集,fleetId={}, vehicleId={}, taskTypeId={}", fleetId, vehicleId, taskTypeId); return Collections.emptyList(); } return finalUserIds; } /** * 根据角色类型获取运维人员ID列表 * * @param roleType 角色类型 * @return 运维人员ID列表 */ private List getOperationUserIdsByRoleType(Integer roleType) { // 1. 获取该类型对应的角色ID List ruleRoles = taskRuleRoleMapper.selectList( new LambdaQueryWrapper() .eq(XpTaskRuleRole::getRuleType, roleType) ); if (CollectionUtils.isEmpty(ruleRoles)) { return Collections.emptyList(); } // 2. 获取角色对应的运维人员 Set userIds = new HashSet<>(); for (XpTaskRuleRole ruleRole : ruleRoles) { try { R> 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 getOperatingVehicles() { try { R> 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(); } }