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.XpChargingRecord; 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.maintenance.mapper.XpChargingRecordMapper; 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.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; /** * 运营车辆电量检测定时任务 *

* 根据任务规则配置的充电阈值,从MongoDB读取低电量车辆位置数据, * 创建充电类型任务并下发给配置角色对应的运维人员。 *

* * @author mitao * @since 2026-03-10 */ @Component public class VehicleChargingScheduleTask { private static final Logger log = LoggerFactory.getLogger(VehicleChargingScheduleTask.class); /** * 任务名称 */ private static final String TASK_NAME = "运营车辆电量检测定时任务"; @Autowired private XpTaskRuleMapper taskRuleMapper; @Autowired private XpTaskRuleRoleMapper taskRuleRoleMapper; @Autowired private XpTaskMapper taskMapper; @Autowired private XpChargingRecordMapper chargingRecordMapper; @Autowired private XpVehicleClient xpVehicleClient; @Autowired private XpOperationUserClient xpOperationUserClient; @Autowired private XpFleetOperationsClient xpFleetOperationsClient; @Autowired private MongoTemplate mongoTemplate; @Autowired private VehicleLocationFillService vehicleLocationFillService; /** * 每分钟执行一次电量检测 */ @Scheduled(cron = "0 * * * * ?") public void executeChargingDetectionTask() { log.info("========== 开始执行{}检查 ==========", TASK_NAME); // 1. 获取任务规则配置 XpTaskRule taskRule = getTaskRule(); if (taskRule == null) { log.info("未找到任务规则配置,跳过执行"); return; } // 2. 判断充电策略功能是否启用 if (taskRule.getChargingStrategy() == null || taskRule.getChargingStrategy() != 1) { log.info("充电策略未启用,跳过执行"); return; } // 3. 获取电量阈值 BigDecimal thresholdDecimal = taskRule.getChargingStrategyBatteryl(); if (thresholdDecimal == null) { log.warn("充电策略电量阈值未配置,跳过执行"); return; } int threshold = thresholdDecimal.intValue(); if (threshold <= 0) { log.warn("充电策略电量阈值不合法:{},跳过执行", thresholdDecimal); return; } // 4. 获取任务接收角色的运维人员 List roleOperationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_CHARGING); if (CollectionUtils.isEmpty(roleOperationUserIds)) { log.warn("未找到充电策略任务接收角色的运维人员"); return; } Set roleOperationUserIdSet = new HashSet<>(roleOperationUserIds); Map vehicleIdToFleetIdCache = new HashMap<>(); Map> fleetIdToOperationUserIdsCache = new HashMap<>(); // 5. 获取所有运营中的车辆,并构建 VIN -> Vehicle 映射 List operatingVehicles = getOperatingVehicles(); if (CollectionUtils.isEmpty(operatingVehicles)) { log.info("没有运营中的车辆,跳过执行"); return; } Map operatingVehicleByVin = operatingVehicles.stream() .filter(v -> StringUtils.hasLength(v.getVehicleVin())) .collect(Collectors.toMap(XpVehicle::getVehicleVin, v -> v, (a, b) -> a)); if (operatingVehicleByVin.isEmpty()) { log.info("运营车辆VIN为空,跳过执行"); return; } // 6. 从MongoDB查询电量低于阈值的车辆位置数据(只查运营车辆VIN) List vins = new ArrayList<>(operatingVehicleByVin.keySet()); Query query = new Query(Criteria.where("vin").in(vins).and("currentElectricity").lt(threshold)); List lowBatteryPositions = mongoTemplate.find(query, VehiclePosition.class); if (CollectionUtils.isEmpty(lowBatteryPositions)) { log.info("未找到低电量车辆(阈值={}),跳过执行", threshold); return; } int createdCount = 0; int skippedUnfinishedTaskCount = 0; int skippedUnfinishedChargingRecordCount = 0; int skippedNoAssigneeCount = 0; for (VehiclePosition position : lowBatteryPositions) { if (position == null || !StringUtils.hasLength(position.getVin())) { continue; } XpVehicle vehicle = operatingVehicleByVin.get(position.getVin()); if (vehicle == null || vehicle.getId() == null) { continue; } // 避免重复创建:若存在未完成的充电任务则跳过 if (hasUnfinishedTask(vehicle.getId(), TaskScheduleConstants.TASK_TYPE_CHARGING)) { skippedUnfinishedTaskCount++; continue; } // 若车辆最新一条充电记录未完成,则不再创建新的充电任务 if (hasUnfinishedLatestChargingRecord(vehicle.getId())) { skippedUnfinishedChargingRecordCount++; continue; } List handleUserIds = resolveHandleUserIds(vehicle.getId(), roleOperationUserIdSet, vehicleIdToFleetIdCache, fleetIdToOperationUserIdsCache); if (CollectionUtils.isEmpty(handleUserIds)) { skippedNoAssigneeCount++; continue; } String belongUserIds = handleUserIds.stream().map(String::valueOf).collect(Collectors.joining(",")); XpTask task = buildChargingTask(vehicle, position, belongUserIds, taskRule, threshold); taskMapper.insert(task); createdCount++; } log.info("========== {}执行完成,阈值={},命中={},创建任务数={},跳过(已存在未完成任务)={},跳过(最新充电记录未完成)={},跳过(无可分配处理人)={} ==========", TASK_NAME, threshold, lowBatteryPositions.size(), createdCount, skippedUnfinishedTaskCount, skippedUnfinishedChargingRecordCount, skippedNoAssigneeCount); } private XpTaskRule getTaskRule() { return taskRuleMapper.selectOne(new LambdaQueryWrapper().last("limit 1")); } /** * 获取所有运营中的车辆 */ private List getOperatingVehicles() { try { R> result = xpVehicleClient.listVehicle(); if (result != null && result.getData() != null) { 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(); } /** * 根据角色类型获取运维人员ID列表 */ private List getOperationUserIdsByRoleType(Integer roleType) { List ruleRoles = taskRuleRoleMapper.selectList( new LambdaQueryWrapper() .eq(XpTaskRuleRole::getRuleType, roleType) ); if (CollectionUtils.isEmpty(ruleRoles)) { return Collections.emptyList(); } 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) .filter(Objects::nonNull) .collect(Collectors.toList()) ); } } catch (Exception e) { log.error("获取角色[{}]对应的运维人员失败", ruleRole.getRoleId(), e); } } return new ArrayList<>(userIds); } /** * 检查是否已存在未完成的同类型任务 */ 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; } /** * 检查车辆最新一条充电记录是否未完成 */ private boolean hasUnfinishedLatestChargingRecord(Long vehicleId) { XpChargingRecord latestChargingRecord = chargingRecordMapper.selectOne( new LambdaQueryWrapper() .eq(XpChargingRecord::getVehicleId, vehicleId) .orderByDesc(XpChargingRecord::getCreateTime) .last("limit 1") ); return latestChargingRecord != null && (latestChargingRecord.getEndTime() == null || latestChargingRecord.getEndRemainingBattery() == null); } private List resolveHandleUserIds(Long vehicleId, Set roleOperationUserIds, Map vehicleIdToFleetIdCache, Map> fleetIdToOperationUserIdsCache) { if (vehicleId == null) { log.warn("充电策略定时任务分配处理人失败:vehicleId为空"); 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={}", vehicleId, e); fleetId = null; } vehicleIdToFleetIdCache.put(vehicleId, fleetId); } if (fleetId == null) { log.warn("充电策略定时任务跳过:车辆未关联车队,vehicleId={}", vehicleId); 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={}", fleetId, vehicleId, e); fleetOperationUserIds = Collections.emptySet(); } fleetIdToOperationUserIdsCache.put(fleetId, fleetOperationUserIds); } if (CollectionUtils.isEmpty(fleetOperationUserIds)) { log.warn("充电策略定时任务跳过:车队未关联人员,fleetId={}, vehicleId={}", fleetId, vehicleId); return Collections.emptyList(); } List finalUserIds = fleetOperationUserIds.stream() .filter(roleOperationUserIds::contains) .sorted() .collect(Collectors.toList()); if (CollectionUtils.isEmpty(finalUserIds)) { log.warn("充电策略定时任务跳过:车队人员与角色人员无交集,fleetId={}, vehicleId={}", fleetId, vehicleId); return Collections.emptyList(); } return finalUserIds; } private XpTask buildChargingTask(XpVehicle vehicle, VehiclePosition position, String belongUserIds, XpTaskRule taskRule, int threshold) { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS"); String taskCode = sdf.format(new Date()) + UUIDUtil.getNumberRandom(5); Integer currentElectricity = position.getCurrentElectricity(); String desc = String.format("系统自动下发的车辆充电任务,当前电量%s%%,阈值%d%%", currentElectricity == null ? "-" : String.valueOf(currentElectricity), threshold); XpTask task = new XpTask(); task.setTaskCode(taskCode); task.setTaskCategory(TaskScheduleConstants.TASK_CATEGORY_ROUTINE); task.setTaskTypeId(TaskScheduleConstants.TASK_TYPE_CHARGING); task.setTaskUrgencyLevel(taskRule.getChargingStrategyLevel() != null ? taskRule.getChargingStrategyLevel() : TaskScheduleConstants.URGENCY_LEVEL_P2); task.setTaskDescription(desc); task.setVehicleId(vehicle.getId()); vehicleLocationFillService.fillFromPosition(task, vehicle.getVehicleVin(), position); // 常规任务默认限时3天 task.setEndTime(LocalDateTime.now().plusDays(3)); task.setStatus(TaskScheduleConstants.TASK_STATUS_PENDING); task.setHandleUserId(belongUserIds); task.setReportUserId(0L); task.setReportTime(LocalDateTime.now()); task.setCreateBy(TaskScheduleConstants.SCHEDULE_CREATOR); return task; } }