xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/controller/XpTaskController.java
@@ -7,6 +7,7 @@ import com.xiaopeng.api.model.maintenance.XpErrorType; import com.xiaopeng.api.model.maintenance.XpTask; import com.xiaopeng.api.model.maintenance.XpTaskHandleDetailRoutine; import com.xiaopeng.api.model.maintenance.XpTaskRule; import com.xiaopeng.api.model.order.XpCruiseOrder; import com.xiaopeng.api.model.vehicleConvoy.XpVehicle; import com.xiaopeng.api.vo.account.LoginOperationUser; @@ -17,9 +18,12 @@ import com.xiaopeng.maintenance.dto.HandleVehicleAccidentDto; import com.xiaopeng.maintenance.dto.SaveRoutineDetailDto; import com.xiaopeng.maintenance.dto.SaveVehicleAccidentDto; import com.xiaopeng.maintenance.job.AccidentTaskQuartzScheduler; import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper; import com.xiaopeng.maintenance.model.query.TaskPageListQuery; import com.xiaopeng.maintenance.model.query.XpTaskRecordPageQuery; import com.xiaopeng.maintenance.model.vo.*; import com.xiaopeng.maintenance.schedule.InsuranceInspectionScheduleTask; import com.xiaopeng.maintenance.service.IXpErrorTypeService; import com.xiaopeng.maintenance.service.XpTaskService; import com.xiaopeng.maintenance.vo.TaskCleanDetailVO; @@ -30,6 +34,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.query.Criteria; @@ -65,6 +70,14 @@ private XpFleetOperationsClient xpFleetOperationsClient; @Resource private XpVehicleClient xpVehicleClient; @Autowired private InsuranceInspectionScheduleTask insuranceInspectionScheduleTask; @Resource private XpTaskRuleMapper xpTaskRuleMapper; @Resource private AccidentTaskQuartzScheduler accidentTaskQuartzScheduler; @ApiOperation(value = "事故类型列表", response = XpErrorType.class, tags = "运维APP-任务中心") @GetMapping("/api/v1/task/errorTypeList") @@ -131,7 +144,15 @@ public R saveVehicleAccident(@RequestBody SaveVehicleAccidentDto dto) { LoginOperationUser loginOperationUser = tokenService.getLoginOperationUser(); XpOperationsUser xpOperationsUser = loginOperationUser.getXpOperationsUser(); xpTaskService.saveVehicleAccident(dto, xpOperationsUser); XpTask task = xpTaskService.saveVehicleAccident(dto, xpOperationsUser); try { XpTaskRule taskRule = xpTaskRuleMapper.selectOne(null); Integer processingTimeLimit = taskRule == null ? null : taskRule.getProcessingTimeLimit(); accidentTaskQuartzScheduler.scheduleAccidentTimeoutJobs(task.getId(), task.getEndTime(), processingTimeLimit); } catch (Exception e) { log.error("事故任务Quartz调度失败,taskId={}", task == null ? null : task.getId(), e); } return R.ok(); } @@ -235,7 +256,12 @@ XpTask xpTask = xpTaskService.queryInfoByVehicleId(vehicleId); return R.ok(xpTask); } @ApiOperation(value = "执行保险年检检测定时任务", tags = {"定时任务"}) @GetMapping("/base/task/insurance/execute") public R<?> executeInsuranceTask() { insuranceInspectionScheduleTask.executeInsuranceInspectionTask(); return R.ok(); } } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/job/AccidentTaskQuartzScheduler.java
New file @@ -0,0 +1,78 @@ package com.xiaopeng.maintenance.job; import com.xiaopeng.maintenance.job.base.QuartzManager; import com.xiaopeng.maintenance.job.base.TimeJobType; import com.xiaopeng.maintenance.job.jobs.AccidentTimeoutJob; import com.xiaopeng.maintenance.job.jobs.AccidentWillTimeoutJob; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 事故任务 Quartz 调度器 * * <p>用于为单条事故任务调度“即将超时/超时”单次触发 Job。</p> * * @author mitao * @since 2026-03-15 */ @Component public class AccidentTaskQuartzScheduler { private static final String JOB_NAME_WILL_TIMEOUT_PREFIX = "ACCIDENT_WILL_TIMEOUT_"; private static final String JOB_NAME_TIMEOUT_PREFIX = "ACCIDENT_TIMEOUT_"; /** * 调度事故任务通知 * * @param taskId 事故任务ID * @param endTime 截止时间 * @param processingTimeLimit 处理时限(分钟) */ public void scheduleAccidentTimeoutJobs(Long taskId, LocalDateTime endTime, Integer processingTimeLimit) { if (taskId == null || taskId <= 0 || endTime == null) { return; } Map<String, Object> jobParam = new HashMap<>(); jobParam.put("taskId", taskId); Integer factorMinutes = calcWillTimeoutFactorMinutes(processingTimeLimit); if (factorMinutes != null && factorMinutes > 0) { LocalDateTime willTimeoutTime = endTime.minusMinutes(factorMinutes); QuartzManager.addJob( AccidentWillTimeoutJob.class, JOB_NAME_WILL_TIMEOUT_PREFIX + taskId, TimeJobType.ACCIDENT_WILL_TIMEOUT, toDate(willTimeoutTime), jobParam ); } QuartzManager.addJob( AccidentTimeoutJob.class, JOB_NAME_TIMEOUT_PREFIX + taskId, TimeJobType.ACCIDENT_TIMEOUT, toDate(endTime), jobParam ); } /** * 即将超时因子(分钟):floor(processingTimeLimit * 0.2) */ private Integer calcWillTimeoutFactorMinutes(Integer processingTimeLimit) { if (processingTimeLimit == null || processingTimeLimit <= 0) { return 0; } return (int) Math.floor(processingTimeLimit * 0.2d); } private Date toDate(LocalDateTime time) { return Date.from(time.atZone(ZoneId.systemDefault()).toInstant()); } } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/job/jobs/AccidentTimeoutJob.java
New file @@ -0,0 +1,132 @@ package com.xiaopeng.maintenance.job.jobs; 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.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.job.base.AbstractJob; import com.xiaopeng.maintenance.job.utils.SpringContextsUtil; import com.xiaopeng.maintenance.mapper.XpTaskMapper; import com.xiaopeng.maintenance.schedule.AccidentTaskNotificationSupport; import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.util.CollectionUtils; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * 事故任务超时通知Job(单次触发) * * <p>触发后按任务处理人推送“超时”消息,并保存通知记录。</p> * * @author mitao * @since 2026-03-15 */ public class AccidentTimeoutJob extends AbstractJob { private static final String PUSH_TITLE = "事故超时提醒"; private static final String CONTENT_TEMPLATE = "车辆%s在%s发生的紧急事故已超时!"; @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap maps = context.getMergedJobDataMap(); long taskIdValue = maps == null ? 0L : maps.getLongValue("taskId"); if (taskIdValue <= 0) { logger.warn("AccidentTimeoutJob 缺少有效taskId"); return; } Long taskId = taskIdValue; XpTaskMapper taskMapper = SpringContextsUtil.getBean(XpTaskMapper.class); AccidentTaskNotificationSupport support = SpringContextsUtil.getBean(AccidentTaskNotificationSupport.class); XpOperationUserNotificationClient notificationClient = SpringContextsUtil.getBean(XpOperationUserNotificationClient.class); XpTask task = taskMapper.selectById(taskId); if (!isValidAccidentTask(task)) { return; } LocalDateTime now = LocalDateTime.now(); if (task.getEndTime() == null || !task.getEndTime().isBefore(now)) { return; } int affectedRows = taskMapper.update(null, new LambdaUpdateWrapper<XpTask>() .set(XpTask::getExpiredNoticeSent, 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) .lt(XpTask::getEndTime, now) .eq(XpTask::getExpiredNoticeSent, 0)); if (affectedRows <= 0) { return; } Set<Long> userIds = support.parseUserIds(task.getHandleUserId()); if (CollectionUtils.isEmpty(userIds)) { logger.warn("AccidentTimeoutJob 事故任务未配置有效处理人,taskId={}", taskId); return; } Map<Long, XpOperationsUser> userMap = support.fetchOperationUsers(userIds); List<XpOperationsUser> users = userIds.stream() .map(userMap::get) .filter(Objects::nonNull) .collect(Collectors.toList()); if (CollectionUtils.isEmpty(users)) { logger.warn("AccidentTimeoutJob 未匹配到有效运维人员,taskId={}", taskId); return; } String plateNumber = ""; if (task.getVehicleId() != null) { Map<Long, XpVehicle> vehicleMap = support.fetchVehicles(Collections.singleton(task.getVehicleId())); XpVehicle vehicle = vehicleMap.get(task.getVehicleId()); plateNumber = vehicle == null ? "" : StringUtils.defaultString(vehicle.getPlateNumber()); } String vehicleLocation = StringUtils.defaultString(task.getVehicleLocation()); String content = String.format(CONTENT_TEMPLATE, plateNumber, vehicleLocation); String bodyJson = support.buildPushBodyJson(content, NotificationTypeEnum.ACCIDENT_TIMEOUT.getCode(), taskId); List<XpOperationsUserNotification> notifications = support.buildNotifications( users, NotificationTypeEnum.ACCIDENT_TIMEOUT.getCode(), content, taskId, bodyJson ); if (!CollectionUtils.isEmpty(notifications)) { notificationClient.addNotificationBatch(notifications); } support.pushToUsersByDeviceType(users, PUSH_TITLE, bodyJson); } private boolean isValidAccidentTask(XpTask task) { if (task == null || task.getId() == null) { return false; } if (!Objects.equals(task.getTaskCategory(), TaskScheduleConstants.TASK_CATEGORY_ACCIDENT)) { return false; } return Objects.equals(task.getStatus(), TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE) || Objects.equals(task.getStatus(), TaskScheduleConstants.TASK_STATUS_PENDING); } } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/job/jobs/AccidentWillTimeoutJob.java
New file @@ -0,0 +1,148 @@ package com.xiaopeng.maintenance.job.jobs; 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.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.job.base.AbstractJob; import com.xiaopeng.maintenance.job.utils.SpringContextsUtil; import com.xiaopeng.maintenance.mapper.XpTaskMapper; import com.xiaopeng.maintenance.schedule.AccidentTaskNotificationSupport; import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.util.CollectionUtils; import java.time.Duration; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * 事故任务即将超时通知Job(单次触发) * * <p>触发后按任务处理人推送“即将超时”消息,并保存通知记录。</p> * * @author mitao * @since 2026-03-15 */ public class AccidentWillTimeoutJob extends AbstractJob { private static final String PUSH_TITLE = "事故即将超时提醒"; private static final String CONTENT_TEMPLATE = "车辆%s在%s发生的紧急事故即将超时,剩余时间%s分钟,请及时到达现场!"; @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap maps = context.getMergedJobDataMap(); long taskIdValue = maps == null ? 0L : maps.getLongValue("taskId"); if (taskIdValue <= 0) { logger.warn("AccidentWillTimeoutJob 缺少有效taskId"); return; } Long taskId = taskIdValue; XpTaskMapper taskMapper = SpringContextsUtil.getBean(XpTaskMapper.class); AccidentTaskNotificationSupport support = SpringContextsUtil.getBean(AccidentTaskNotificationSupport.class); XpOperationUserNotificationClient notificationClient = SpringContextsUtil.getBean(XpOperationUserNotificationClient.class); XpTask task = taskMapper.selectById(taskId); if (!isValidAccidentTask(task)) { return; } LocalDateTime now = LocalDateTime.now(); if (task.getEndTime() == null || !task.getEndTime().isAfter(now)) { return; } int affectedRows = taskMapper.update(null, new LambdaUpdateWrapper<XpTask>() .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) .eq(XpTask::getWillTimeoutNoticeSent, 0)); if (affectedRows <= 0) { return; } Set<Long> userIds = support.parseUserIds(task.getHandleUserId()); if (CollectionUtils.isEmpty(userIds)) { logger.warn("AccidentWillTimeoutJob 事故任务未配置有效处理人,taskId={}", taskId); return; } Map<Long, XpOperationsUser> userMap = support.fetchOperationUsers(userIds); List<XpOperationsUser> users = userIds.stream() .map(userMap::get) .filter(Objects::nonNull) .collect(Collectors.toList()); if (CollectionUtils.isEmpty(users)) { logger.warn("AccidentWillTimeoutJob 未匹配到有效运维人员,taskId={}", taskId); return; } String plateNumber = ""; if (task.getVehicleId() != null) { Map<Long, XpVehicle> vehicleMap = support.fetchVehicles(Collections.singleton(task.getVehicleId())); XpVehicle vehicle = vehicleMap.get(task.getVehicleId()); plateNumber = vehicle == null ? "" : StringUtils.defaultString(vehicle.getPlateNumber()); } 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 = support.buildPushBodyJson(content, NotificationTypeEnum.ACCIDENT_WILL_TIMEOUT.getCode(), taskId); List<XpOperationsUserNotification> notifications = support.buildNotifications( users, NotificationTypeEnum.ACCIDENT_WILL_TIMEOUT.getCode(), content, taskId, bodyJson ); if (!CollectionUtils.isEmpty(notifications)) { notificationClient.addNotificationBatch(notifications); } support.pushToUsersByDeviceType(users, PUSH_TITLE, bodyJson); } private boolean isValidAccidentTask(XpTask task) { if (task == null || task.getId() == null) { return false; } if (!Objects.equals(task.getTaskCategory(), TaskScheduleConstants.TASK_CATEGORY_ACCIDENT)) { return false; } return Objects.equals(task.getStatus(), TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE) || Objects.equals(task.getStatus(), TaskScheduleConstants.TASK_STATUS_PENDING); } /** * 计算剩余分钟数(向上取整,避免出现 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; } } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/model/vo/TaskCleanDetailVO.java
@@ -54,4 +54,11 @@ @ApiModelProperty(value = "操作人名称") private String operationsUserName; @ApiModelProperty(value = "处理人姓名") private String handleUserName; @ApiModelProperty(value = "处理时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime handleTime; } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java
@@ -778,7 +778,10 @@ xpTaskHandleDetailRoutine.setOperationsUserId(id); xpTaskHandleDetailRoutineMapper.insert(xpTaskHandleDetailRoutine); //更新任务状态为已完成 lambdaUpdate().set(XpTask::getStatus, 2).eq(XpTask::getId, dto.getId()).update(); lambdaUpdate().set(XpTask::getStatus, 2).set(XpTask::getHandleUserId, id) .set(XpTask::getHandleTime, LocalDateTime.now()) .eq(XpTask::getId, dto.getId()) .update(); return R.ok(); } @@ -812,6 +815,7 @@ res.setTaskCategory(task.getTaskCategory()); res.setTaskTypeId(task.getTaskTypeId()); res.setStatus(task.getStatus()); res.setHandleTime(task.getHandleTime()); XpVehicleClean vehicleDetection = vehicleCleanMapper.selectOne(new LambdaQueryWrapper<XpVehicleClean>() .eq(XpVehicleClean::getTaskId, id).last("limit 1")); if (vehicleDetection != null) { @@ -821,6 +825,7 @@ XpOperationsUser operationsUser = xpOperationUserClient.queryById(vehicleDetection.getOperationsUserId()).getData(); if (operationsUser != null) { res.setOperationsUserName(operationsUser.getNickName()); res.setHandleUserName(operationsUser.getNickName()); } } } @@ -851,12 +856,59 @@ return R.fail("任务已被处理"); } Long vehicleId = task.getVehicleId(); String plateNumber = ""; try { if (vehicleId != null) { R<XpVehicle> vehicleResult = xpVehicleClient.queryById(vehicleId); XpVehicle vehicle = vehicleResult == null ? null : vehicleResult.getData(); if (vehicle != null && org.apache.commons.lang3.StringUtils.isNotBlank(vehicle.getPlateNumber())) { plateNumber = vehicle.getPlateNumber(); } } } catch (Exception e) { log.error("confirmArrive查询车辆信息失败,taskId={}, vehicleId={}", id, vehicleId, e); } String taskCode = org.apache.commons.lang3.StringUtils.isNotBlank(task.getTaskCode()) ? task.getTaskCode() : String.valueOf(task.getId()); // 更新任务 task.setStatus(1); task.setVehicleId(null); task.setHandleUserId(userId.toString()); this.updateById(task); // 保存通知记录 + 推送阿里云 NOTICE 消息(不影响主流程) try { String content = String.format("你处理的%s关联车辆:%s状态由待到达变更为待处理", taskCode, plateNumber); String bodyJson = buildTaskStatusChangedPushBodyJson(content, id); XpOperationsUserNotification notification = new XpOperationsUserNotification(); notification.setNotificationType(NotificationTypeEnum.ACCIDENT_STATUS_CHANGED.getCode()); notification.setNotificationContent(content); notification.setOperationsUserId(userId); notification.setDataId(id); notification.setReadStatus(0); notification.setNotificationExt(bodyJson); xpOperationUserNotificationClient.addNotificationBatch(Collections.singletonList(notification)); XpOperationsUser operationsUser = xpOperationUserClient.queryById(userId).getData(); if (operationsUser != null && operationsUser.getDeviceType() != null && StringUtils.isNotBlank(operationsUser.getPhoneNumber())) { xpAliPushMessageClient.pushMessage(AliPushMessageDTO.builder() .deviceType(operationsUser.getDeviceType()) .account(operationsUser.getPhoneNumber()) .title("任务状态变更") .body(bodyJson) .pushType(AliPushTypeConstant.NOTICE) .build()); } } catch (Exception e) { log.error("confirmArrive消息通知失败,taskId={}, userId={}", id, userId, e); } return R.ok(); } catch (InterruptedException e) { @@ -870,6 +922,18 @@ } } /** * 构建任务状态变更推送 body JSON */ private String buildTaskStatusChangedPushBodyJson(String content, Long taskId) { Map<String, Object> param = new HashMap<>(); param.put("content", content); param.put("strategy", NotificationTypeEnum.ACCIDENT_STATUS_CHANGED.getCode()); param.put("type", TaskTypeNotificationEnum.ACCIDENT_TASK.getCode()); param.put("id", taskId == null ? "0" : taskId.toString()); return JSONObject.toJSONString(param); } @Override public PageInfo<XpTaskRecordVO> queryTaskByVehicleId(XpTaskRecordPageQuery query) { // 1. 构建查询条件 xp-robotaxi-maintenance-boot/src/main/resources/mapper/maintenance/XpTaskMapper.xml
@@ -86,13 +86,13 @@ <if test="query.sortType != null"> <if test="query.sortType == 1"> order by t1.task_urgency_level order by t1.create_time desc, t1.task_urgency_level <if test="query.sortRule == 2"> desc </if> </if> <if test="query.sortType == 2"> order by t2.plate_number order by t1.create_time desc, t2.plate_number <if test="query.sortRule == 2"> desc </if>