package com.xiaopeng.maintenance.schedule; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.xiaopeng.api.feignClient.account.XpOperationUserNotificationClient; import com.xiaopeng.api.model.account.XpOperationsUser; import com.xiaopeng.api.model.account.XpOperationsUserNotification; import com.xiaopeng.api.model.maintenance.XpTask; import com.xiaopeng.api.model.maintenance.XpTaskRule; import com.xiaopeng.api.model.vehicleConvoy.XpVehicle; import com.xiaopeng.api.vo.maintenance.TaskScheduleConstants; import com.xiaopeng.common.core.enums.NotificationTypeEnum; import com.xiaopeng.common.core.utils.StringUtils; import com.xiaopeng.maintenance.mapper.XpTaskMapper; import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper; 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.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * 事故任务即将超时检查定时任务 * *

每分钟执行一次,判断事故任务在即将超时窗口内并发送通知。

* * @author mitao * @since 2026-03-14 */ @Component public class AccidentTaskWillTimeoutScheduleTask { private static final Logger log = LoggerFactory.getLogger(AccidentTaskWillTimeoutScheduleTask.class); private static final String TASK_NAME = "事故任务即将超时检查定时任务"; private static final String PUSH_TITLE = "事故即将超时提示"; private static final String CONTENT_TEMPLATE = "车辆%s在%s发生的紧急事故即将超时,剩余时间%s分钟,请及时到达现场!"; @Autowired private XpTaskMapper taskMapper; @Autowired private XpTaskRuleMapper taskRuleMapper; @Autowired private XpOperationUserNotificationClient xpOperationUserNotificationClient; @Autowired private AccidentTaskNotificationSupport notificationSupport; /** * 每分钟执行一次 */ @Scheduled(cron = "0 * * * * ?") public void executeAccidentTaskWillTimeoutCheck() { log.info("========== 开始执行{} ==========", TASK_NAME); try { XpTaskRule taskRule = getTaskRule(); Integer processingTimeLimit = taskRule == null ? null : taskRule.getProcessingTimeLimit(); Integer factorMinutes = calcWillTimeoutFactorMinutes(processingTimeLimit); if (factorMinutes == null || factorMinutes <= 0) { log.info("未配置有效处理时限或即将超时因子<=0,跳过执行,processingTimeLimit={}", processingTimeLimit); return; } LocalDateTime now = LocalDateTime.now(); LocalDateTime windowEnd = now.plusMinutes(factorMinutes); List tasks = queryWillTimeoutAccidentTasks(now, windowEnd); if (CollectionUtils.isEmpty(tasks)) { log.info("未找到即将超时事故任务,跳过执行"); return; } Set vehicleIds = tasks.stream() .map(XpTask::getVehicleId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map vehicleMap = notificationSupport.fetchVehicles(vehicleIds); Set allUserIds = new HashSet<>(); for (XpTask task : tasks) { allUserIds.addAll(notificationSupport.parseUserIds(task.getHandleUserId())); } Map userMap = notificationSupport.fetchOperationUsers(allUserIds); int notifiedTaskCount = 0; int notificationCount = 0; int pushCount = 0; List allNotifications = new ArrayList<>(); for (XpTask task : tasks) { try { if (task == null || task.getId() == null) { continue; } Set userIds = notificationSupport.parseUserIds(task.getHandleUserId()); if (CollectionUtils.isEmpty(userIds)) { log.warn("事故任务ID={} 未配置有效处理人,跳过通知", task.getId()); continue; } int affectedRows = markWillTimeoutNoticeSent(task.getId(), now, windowEnd); if (affectedRows <= 0) { continue; } List users = userIds.stream() .map(userMap::get) .filter(Objects::nonNull) .collect(Collectors.toList()); if (CollectionUtils.isEmpty(users)) { log.warn("事故任务ID={} 未匹配到有效运维人员,跳过通知", task.getId()); continue; } String plateNumber = resolvePlateNumber(vehicleMap.get(task.getVehicleId())); String vehicleLocation = StringUtils.defaultString(task.getVehicleLocation()); String remainMinutes = String.valueOf(calcRemainMinutes(now, task.getEndTime())); String content = String.format(CONTENT_TEMPLATE, plateNumber, vehicleLocation, remainMinutes); String bodyJson = notificationSupport.buildPushBodyJson( content, NotificationTypeEnum.ACCIDENT_WILL_TIMEOUT.getCode(), task.getId() ); allNotifications.addAll(notificationSupport.buildNotifications( users, NotificationTypeEnum.ACCIDENT_WILL_TIMEOUT.getCode(), content, task.getId(), bodyJson )); pushCount += notificationSupport.pushToUsersByDeviceType(users, PUSH_TITLE, bodyJson); notifiedTaskCount++; } catch (Exception e) { log.error("处理即将超时事故任务通知失败,任务ID={}", task == null ? null : task.getId(), e); } } if (!CollectionUtils.isEmpty(allNotifications)) { xpOperationUserNotificationClient.addNotificationBatch(allNotifications); notificationCount = allNotifications.size(); } log.info("========== {} 执行完成,任务数: {},通知记录数: {},推送次数: {} ==========", TASK_NAME, notifiedTaskCount, notificationCount, pushCount); } catch (Exception e) { log.error("{}执行失败", TASK_NAME, e); } } /** * 获取任务规则配置 */ private XpTaskRule getTaskRule() { return taskRuleMapper.selectOne(new LambdaQueryWrapper().last("limit 1")); } /** * 计算即将超时窗口(分钟):floor(processingTimeLimit * 0.2) */ private Integer calcWillTimeoutFactorMinutes(Integer processingTimeLimit) { if (processingTimeLimit == null || processingTimeLimit <= 0) { return 0; } return (int) Math.floor(processingTimeLimit * 0.2d); } /** * 查询即将超时事故任务:now < endTime <= windowEnd 且未发送即将超时通知 */ private List queryWillTimeoutAccidentTasks(LocalDateTime now, LocalDateTime windowEnd) { return taskMapper.selectList(new LambdaQueryWrapper() .eq(XpTask::getTaskCategory, TaskScheduleConstants.TASK_CATEGORY_ACCIDENT) .in(XpTask::getStatus, TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE, TaskScheduleConstants.TASK_STATUS_PENDING) .isNotNull(XpTask::getEndTime) .gt(XpTask::getEndTime, now) .le(XpTask::getEndTime, windowEnd) .eq(XpTask::getWillTimeoutNoticeSent, 0)); } /** * 原子更新(抢占式)标记即将超时通知已发送 */ private int markWillTimeoutNoticeSent(Long taskId, LocalDateTime now, LocalDateTime windowEnd) { return taskMapper.update(null, new LambdaUpdateWrapper() .set(XpTask::getWillTimeoutNoticeSent, 1) .eq(XpTask::getId, taskId) .eq(XpTask::getTaskCategory, TaskScheduleConstants.TASK_CATEGORY_ACCIDENT) .in(XpTask::getStatus, TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE, TaskScheduleConstants.TASK_STATUS_PENDING) .isNotNull(XpTask::getEndTime) .gt(XpTask::getEndTime, now) .le(XpTask::getEndTime, windowEnd) .eq(XpTask::getWillTimeoutNoticeSent, 0)); } /** * 计算剩余分钟数(向上取整,避免出现 0 分钟) */ private long calcRemainMinutes(LocalDateTime now, LocalDateTime endTime) { if (endTime == null) { return 0; } long seconds = Duration.between(now, endTime).getSeconds(); if (seconds <= 0) { return 0; } return (seconds + 59) / 60; } private String resolvePlateNumber(XpVehicle vehicle) { return vehicle == null ? "" : StringUtils.defaultString(vehicle.getPlateNumber()); } }