“mitao”
2026-03-14 164f20125c9da1d8992bd2752a3d8307446ae435
bugfix
3个文件已添加
3个文件已修改
641 ■■■■■ 已修改文件
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpTask.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/AccidentTaskNotificationSupport.java 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/AccidentTaskTimeoutCheckScheduleTask.java 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/AccidentTaskWillTimeoutScheduleTask.java 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpTask.java
@@ -12,9 +12,6 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * 任务管理
@@ -91,6 +88,9 @@
    @ApiModelProperty(value = "是否已发送过期通知 0=未发送 1=已发送")
    private Integer expiredNoticeSent;
    @ApiModelProperty(value = "事故即将超时通知是否已发送 0=未发送 1=已发送")
    private Integer willTimeoutNoticeSent;
    @ApiModelProperty(value = "状态 0=待到达 1=待处理 2=已完成 3=已取消")
    private Integer status;
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/AccidentTaskNotificationSupport.java
New file
@@ -0,0 +1,199 @@
package com.xiaopeng.maintenance.schedule;
import com.alibaba.fastjson2.JSONObject;
import com.xiaopeng.api.dto.pushMessage.AliPushMessageDTO;
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.pushMessage.XpAliPushMessageClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.account.XpOperationsUser;
import com.xiaopeng.api.model.account.XpOperationsUserNotification;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.common.core.constant.AliPushTypeConstant;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.enums.TaskTypeNotificationEnum;
import com.xiaopeng.common.core.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
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;
/**
 * 事故任务通知支撑类
 *
 * <p>用于事故任务相关定时通知的通用方法抽取。</p>
 *
 * @author mitao
 * @since 2026-03-14
 */
@Component
public class AccidentTaskNotificationSupport {
    private static final Logger log = LoggerFactory.getLogger(AccidentTaskNotificationSupport.class);
    @Autowired
    private XpOperationUserClient xpOperationUserClient;
    @Autowired
    private XpVehicleClient xpVehicleClient;
    @Autowired
    private XpAliPushMessageClient xpAliPushMessageClient;
    /**
     * 解析 handleUserId(逗号分隔)
     */
    public Set<Long> parseUserIds(String handleUserId) {
        if (StringUtils.isBlank(handleUserId)) {
            return Collections.emptySet();
        }
        String[] parts = handleUserId.split(",");
        Set<Long> userIds = new HashSet<>();
        for (String part : parts) {
            if (StringUtils.isBlank(part)) {
                continue;
            }
            try {
                userIds.add(Long.valueOf(part.trim()));
            } catch (NumberFormatException e) {
                log.warn("任务接收人ID非法,handleUserId={}", handleUserId);
            }
        }
        return userIds;
    }
    /**
     * 获取运维人员信息
     */
    public Map<Long, XpOperationsUser> fetchOperationUsers(Set<Long> userIds) {
        if (CollectionUtils.isEmpty(userIds)) {
            return Collections.emptyMap();
        }
        try {
            R<List<XpOperationsUser>> result = xpOperationUserClient.listByIds(new ArrayList<>(userIds));
            if (result == null || CollectionUtils.isEmpty(result.getData())) {
                return Collections.emptyMap();
            }
            return result.getData().stream()
                    .filter(Objects::nonNull)
                    .filter(u -> u.getId() != null)
                    .collect(Collectors.toMap(XpOperationsUser::getId, u -> u, (a, b) -> a));
        } catch (Exception e) {
            log.error("获取运维人员信息失败", e);
            return Collections.emptyMap();
        }
    }
    /**
     * 批量获取车辆信息
     */
    public Map<Long, XpVehicle> fetchVehicles(Set<Long> vehicleIds) {
        if (CollectionUtils.isEmpty(vehicleIds)) {
            return Collections.emptyMap();
        }
        try {
            R<List<XpVehicle>> result = xpVehicleClient.listByIds(new ArrayList<>(vehicleIds));
            if (result == null || CollectionUtils.isEmpty(result.getData())) {
                return Collections.emptyMap();
            }
            return result.getData().stream()
                    .filter(Objects::nonNull)
                    .filter(v -> v.getId() != null)
                    .collect(Collectors.toMap(XpVehicle::getId, v -> v, (a, b) -> a));
        } catch (Exception e) {
            log.error("获取车辆信息失败", e);
            return Collections.emptyMap();
        }
    }
    /**
     * 构建推送 body JSON
     */
    public String buildPushBodyJson(String content, Integer strategyCode, Long taskId) {
        Map<String, Object> param = new HashMap<>();
        param.put("content", content);
        param.put("strategy", strategyCode);
        param.put("type", TaskTypeNotificationEnum.ACCIDENT_TASK.getCode());
        param.put("id", taskId == null ? "0" : taskId.toString());
        return JSONObject.toJSONString(param);
    }
    /**
     * 批量构建通知记录(包含 notificationExt)
     */
    public List<XpOperationsUserNotification> buildNotifications(List<XpOperationsUser> users,
                                                                 Integer notificationType,
                                                                 String content,
                                                                 Long taskId,
                                                                 String bodyJson) {
        if (CollectionUtils.isEmpty(users)) {
            return Collections.emptyList();
        }
        List<XpOperationsUserNotification> notifications = new ArrayList<>();
        for (XpOperationsUser user : users) {
            if (user == null || user.getId() == null) {
                continue;
            }
            XpOperationsUserNotification notification = new XpOperationsUserNotification();
            notification.setNotificationType(notificationType);
            notification.setNotificationContent(content);
            notification.setOperationsUserId(user.getId());
            notification.setDataId(taskId == null ? 0L : taskId);
            notification.setReadStatus(0);
            notification.setNotificationExt(bodyJson);
            notifications.add(notification);
        }
        return notifications;
    }
    /**
     * 推送通知(按 deviceType 分组)
     */
    public int pushToUsersByDeviceType(List<XpOperationsUser> users, String title, String bodyJson) {
        if (CollectionUtils.isEmpty(users)) {
            return 0;
        }
        Map<Integer, List<XpOperationsUser>> operationUserMap = users.stream()
                .filter(u -> u != null && u.getDeviceType() != null && StringUtils.isNotBlank(u.getPhoneNumber()))
                .collect(Collectors.groupingBy(XpOperationsUser::getDeviceType));
        int pushCount = 0;
        for (Map.Entry<Integer, List<XpOperationsUser>> entry : operationUserMap.entrySet()) {
            List<XpOperationsUser> groupUsers = entry.getValue();
            if (CollectionUtils.isEmpty(groupUsers)) {
                continue;
            }
            String account = groupUsers.stream()
                    .map(XpOperationsUser::getPhoneNumber)
                    .filter(StringUtils::isNotBlank)
                    .collect(Collectors.joining(","));
            if (StringUtils.isBlank(account)) {
                continue;
            }
            try {
                xpAliPushMessageClient.pushMessage(AliPushMessageDTO.builder()
                        .body(bodyJson)
                        .account(account)
                        .title(title)
                        .pushType(AliPushTypeConstant.MESSAGE)
                        .deviceType(entry.getKey())
                        .build());
                pushCount++;
            } catch (Exception e) {
                log.error("事故任务推送失败,deviceType={}", entry.getKey(), e);
            }
        }
        return pushCount;
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/AccidentTaskTimeoutCheckScheduleTask.java
New file
@@ -0,0 +1,187 @@
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.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 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.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;
/**
 * 事故任务超时检查定时任务
 *
 * <p>每分钟执行一次,判断事故任务是否超时并发送通知。</p>
 *
 * @author mitao
 * @since 2026-03-14
 */
@Component
public class AccidentTaskTimeoutCheckScheduleTask {
    private static final Logger log = LoggerFactory.getLogger(AccidentTaskTimeoutCheckScheduleTask.class);
    private static final String TASK_NAME = "事故任务超时检查定时任务";
    private static final String PUSH_TITLE = "事故超时警示";
    private static final String CONTENT_TEMPLATE =
            "车辆%s在%s发生的紧急事故已超时!";
    @Autowired
    private XpTaskMapper taskMapper;
    @Autowired
    private XpOperationUserNotificationClient xpOperationUserNotificationClient;
    @Autowired
    private AccidentTaskNotificationSupport notificationSupport;
    /**
     * 每分钟执行一次
     */
    @Scheduled(cron = "0 * * * * ?")
    public void executeAccidentTaskTimeoutCheck() {
        log.info("========== 开始执行{} ==========", TASK_NAME);
        try {
            LocalDateTime now = LocalDateTime.now();
            List<XpTask> tasks = queryTimeoutAccidentTasks(now);
            if (CollectionUtils.isEmpty(tasks)) {
                log.info("未找到超时事故任务,跳过执行");
                return;
            }
            Set<Long> vehicleIds = tasks.stream()
                    .map(XpTask::getVehicleId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());
            Map<Long, XpVehicle> vehicleMap = notificationSupport.fetchVehicles(vehicleIds);
            Set<Long> allUserIds = new HashSet<>();
            for (XpTask task : tasks) {
                allUserIds.addAll(notificationSupport.parseUserIds(task.getHandleUserId()));
            }
            Map<Long, XpOperationsUser> userMap = notificationSupport.fetchOperationUsers(allUserIds);
            int notifiedTaskCount = 0;
            int notificationCount = 0;
            int pushCount = 0;
            List<XpOperationsUserNotification> allNotifications = new ArrayList<>();
            for (XpTask task : tasks) {
                try {
                    if (task == null || task.getId() == null) {
                        continue;
                    }
                    Set<Long> userIds = notificationSupport.parseUserIds(task.getHandleUserId());
                    if (CollectionUtils.isEmpty(userIds)) {
                        log.warn("事故任务ID={} 未配置有效处理人,跳过通知", task.getId());
                        continue;
                    }
                    int affectedRows = markTimeoutNoticeSent(task.getId(), now);
                    if (affectedRows <= 0) {
                        continue;
                    }
                    List<XpOperationsUser> 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 content = String.format(CONTENT_TEMPLATE, plateNumber, vehicleLocation);
                    String bodyJson = notificationSupport.buildPushBodyJson(
                            content,
                            NotificationTypeEnum.ACCIDENT_TIMEOUT.getCode(),
                            task.getId()
                    );
                    allNotifications.addAll(notificationSupport.buildNotifications(
                            users,
                            NotificationTypeEnum.ACCIDENT_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);
        }
    }
    /**
     * 查询超时事故任务:endTime < now 且未发送超时通知
     */
    private List<XpTask> queryTimeoutAccidentTasks(LocalDateTime now) {
        return taskMapper.selectList(new LambdaQueryWrapper<XpTask>()
                .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));
    }
    /**
     * 原子更新(抢占式)标记超时通知已发送
     */
    private int markTimeoutNoticeSent(Long taskId, LocalDateTime now) {
        return 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));
    }
    private String resolvePlateNumber(XpVehicle vehicle) {
        return vehicle == null ? "" : StringUtils.defaultString(vehicle.getPlateNumber());
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/AccidentTaskWillTimeoutScheduleTask.java
New file
@@ -0,0 +1,237 @@
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;
/**
 * 事故任务即将超时检查定时任务
 *
 * <p>每分钟执行一次,判断事故任务在即将超时窗口内并发送通知。</p>
 *
 * @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<XpTask> tasks = queryWillTimeoutAccidentTasks(now, windowEnd);
            if (CollectionUtils.isEmpty(tasks)) {
                log.info("未找到即将超时事故任务,跳过执行");
                return;
            }
            Set<Long> vehicleIds = tasks.stream()
                    .map(XpTask::getVehicleId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());
            Map<Long, XpVehicle> vehicleMap = notificationSupport.fetchVehicles(vehicleIds);
            Set<Long> allUserIds = new HashSet<>();
            for (XpTask task : tasks) {
                allUserIds.addAll(notificationSupport.parseUserIds(task.getHandleUserId()));
            }
            Map<Long, XpOperationsUser> userMap = notificationSupport.fetchOperationUsers(allUserIds);
            int notifiedTaskCount = 0;
            int notificationCount = 0;
            int pushCount = 0;
            List<XpOperationsUserNotification> allNotifications = new ArrayList<>();
            for (XpTask task : tasks) {
                try {
                    if (task == null || task.getId() == null) {
                        continue;
                    }
                    Set<Long> 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<XpOperationsUser> 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<XpTaskRule>().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<XpTask> queryWillTimeoutAccidentTasks(LocalDateTime now, LocalDateTime windowEnd) {
        return taskMapper.selectList(new LambdaQueryWrapper<XpTask>()
                .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<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)
                .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());
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java
@@ -251,6 +251,8 @@
        // 根据事故处理限时配置设置任务截止时间
        LocalDateTime endTime = LocalDateTime.now().plusMinutes(taskRule.getProcessingTimeLimit());
        xpTask.setEndTime(endTime);
        xpTask.setExpiredNoticeSent(0);
        xpTask.setWillTimeoutNoticeSent(0);
        this.save(xpTask);
        // 异步发送消息通知
@@ -1168,6 +1170,7 @@
        task.setReportTime(LocalDateTime.now());
        task.setEndTime(LocalDateTime.now().plusMinutes(taskRule.getProcessingTimeLimit()));
        task.setExpiredNoticeSent(0);
        task.setWillTimeoutNoticeSent(0);
        task.setStatus(TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE);
        task.setCreateBy("系统自动生成");
        task.setCreateTime(LocalDateTime.now());
@@ -1217,6 +1220,8 @@
            task.setVehicleLat(dto.getVehicleLat());
            task.setReportUserId(reportUserId);
            task.setReportTime(LocalDateTime.now());
            task.setExpiredNoticeSent(0);
            task.setWillTimeoutNoticeSent(0);
            saveTask.add(task);
        }
        this.saveBatch(saveTask);
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java
@@ -216,13 +216,14 @@
            if (userByMobile == null || userByMobile.getId() == null) {
                return buildEmptyOrderPage(query);
            }
            query.setUid(userByMobile.getId());
            query.setUid(userByMobile.getUid());
        }
        // 1. 构建查询条件
        LambdaQueryWrapper<XpOrder> wrapper = buildQueryWrapper(query);
        if (CollUtil.isNotEmpty(vehicleIdsFilter)) {
            wrapper.in(XpOrder::getVehicleId, vehicleIdsFilter);
        }
        wrapper.eq(Objects.nonNull(query.getUid()), XpOrder::getAppUserId, query.getUid());
        wrapper.orderByDesc(XpOrder::getCreateTime);
        // 2. 分页查询
@@ -366,8 +367,10 @@
            if (userByMobile == null || userByMobile.getId() == null) {
                return new ArrayList<>();
            }
            query.setUid(userByMobile.getId());
            query.setUid(userByMobile.getUid());
        }
        wrapper.eq(Objects.nonNull(query.getUid()), XpOrder::getAppUserId, query.getUid());
        wrapper.orderByDesc(XpOrder::getCreateTime);
        // 2. 查询订单列表
        List<XpOrder> orderList = this.list(wrapper);