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.common.core.utils.SensitiveDataCryptoUtil;
|
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;
|
|
/**
|
* 运营车辆电量检测定时任务
|
* <p>
|
* 根据任务规则配置的充电阈值,从MongoDB读取低电量车辆位置数据,
|
* 创建充电类型任务并下发给配置角色对应的运维人员。
|
* </p>
|
*
|
* @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<Long> roleOperationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_CHARGING);
|
if (CollectionUtils.isEmpty(roleOperationUserIds)) {
|
log.warn("未找到充电策略任务接收角色的运维人员");
|
return;
|
}
|
Set<Long> roleOperationUserIdSet = new HashSet<>(roleOperationUserIds);
|
Map<Long, Long> vehicleIdToFleetIdCache = new HashMap<>();
|
Map<Long, Set<Long>> fleetIdToOperationUserIdsCache = new HashMap<>();
|
|
// 5. 获取所有运营中的车辆,并构建 VIN -> Vehicle 映射
|
List<XpVehicle> operatingVehicles = getOperatingVehicles();
|
if (CollectionUtils.isEmpty(operatingVehicles)) {
|
log.info("没有运营中的车辆,跳过执行");
|
return;
|
}
|
Map<String, XpVehicle> 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<String> vins = new ArrayList<>(operatingVehicleByVin.keySet());
|
vins.forEach(SensitiveDataCryptoUtil::decryptIfPossible);
|
Query query = new Query(Criteria.where("vin").in(vins).and("currentElectricity").lt(threshold));
|
List<VehiclePosition> 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<Long> 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<XpTaskRule>().last("limit 1"));
|
}
|
|
/**
|
* 获取所有运营中的车辆
|
*/
|
private List<XpVehicle> getOperatingVehicles() {
|
try {
|
R<List<XpVehicle>> 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<Long> getOperationUserIdsByRoleType(Integer roleType) {
|
List<XpTaskRuleRole> ruleRoles = taskRuleRoleMapper.selectList(
|
new LambdaQueryWrapper<XpTaskRuleRole>()
|
.eq(XpTaskRuleRole::getRuleType, roleType)
|
);
|
if (CollectionUtils.isEmpty(ruleRoles)) {
|
return Collections.emptyList();
|
}
|
|
Set<Long> userIds = new HashSet<>();
|
for (XpTaskRuleRole ruleRole : ruleRoles) {
|
try {
|
R<List<XpOperationsRole>> 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<XpTask>()
|
.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<XpChargingRecord>()
|
.eq(XpChargingRecord::getVehicleId, vehicleId)
|
.orderByDesc(XpChargingRecord::getCreateTime)
|
.last("limit 1")
|
);
|
return latestChargingRecord != null
|
&& (latestChargingRecord.getEndTime() == null
|
|| latestChargingRecord.getEndRemainingBattery() == null);
|
}
|
|
private List<Long> resolveHandleUserIds(Long vehicleId, Set<Long> roleOperationUserIds,
|
Map<Long, Long> vehicleIdToFleetIdCache,
|
Map<Long, Set<Long>> fleetIdToOperationUserIdsCache) {
|
if (vehicleId == null) {
|
log.warn("充电策略定时任务分配处理人失败:vehicleId为空");
|
return Collections.emptyList();
|
}
|
|
Long fleetId = vehicleIdToFleetIdCache.get(vehicleId);
|
if (!vehicleIdToFleetIdCache.containsKey(vehicleId)) {
|
try {
|
R<Long> 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<Long> fleetOperationUserIds = fleetIdToOperationUserIdsCache.get(fleetId);
|
if (!fleetIdToOperationUserIdsCache.containsKey(fleetId)) {
|
try {
|
R<List<Long>> opsResult = xpFleetOperationsClient.getOperationsIdsByFleetId(fleetId);
|
List<Long> 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<Long> 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());
|
String vin = SensitiveDataCryptoUtil.decryptIfPossible(vehicle.getVehicleVin());
|
vehicleLocationFillService.fillFromPosition(task, vin, 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;
|
}
|
}
|