xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/maintenance/XpVehicleRepairClientFallbackFactory.java
New file @@ -0,0 +1,30 @@ package com.xiaopeng.api.factory.maintenance; import com.xiaopeng.api.feignClient.maintenance.XpVehicleRepairClient; import com.xiaopeng.common.core.domain.R; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.stereotype.Component; /** * 车辆维修Feign客户端降级工厂 * * @author mitao * @since 2026-02-11 */ @Slf4j @Component public class XpVehicleRepairClientFallbackFactory implements FallbackFactory<XpVehicleRepairClient> { @Override public XpVehicleRepairClient create(Throwable cause) { log.error("车辆维修服务调用失败: {}", cause.getMessage()); return new XpVehicleRepairClient() { @Override public R<Integer> getMaintenanceStatus(Long vehicleId) { return R.fail("获取车辆维修状态失败:"+cause.getMessage()); } }; } } xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/maintenance/XpVehicleRepairClient.java
New file @@ -0,0 +1,29 @@ package com.xiaopeng.api.feignClient.maintenance; import com.xiaopeng.api.factory.maintenance.XpVehicleRepairClientFallbackFactory; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.dto.ServiceNameConstantsEnum; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; /** * 车辆维修Feign客户端 * * @author mitao * @since 2026-02-11 */ @FeignClient(contextId = "XpVehicleRepairClient", value = ServiceNameConstantsEnum.XP_MAINTENANCE_SERVICE, fallbackFactory = XpVehicleRepairClientFallbackFactory.class) public interface XpVehicleRepairClient { /** * 获取车辆维修状态 * @param vehicleId * @return */ @GetMapping("/inner/xpVehicleRepair/getMaintenanceStatus") R<Integer> getMaintenanceStatus(@RequestParam Long vehicleId); } xp-robotaxi-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -42,4 +42,5 @@ com.xiaopeng.api.factory.maintenance.XpTaskClientFallbackFactory com.xiaopeng.api.factory.maintenance.XpTaskRuleClientFallbackFactory com.xiaopeng.api.factory.maintenance.XpWorkOrderClientFallbackFactory com.xiaopeng.api.factory.pushMessage.XpAliPushMessageClientFallbackFactory com.xiaopeng.api.factory.pushMessage.XpAliPushMessageClientFallbackFactory com.xiaopeng.api.factory.maintenance.XpVehicleRepairClientFallbackFactory xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/controller/XpVehicleRepairController.java
@@ -1,6 +1,7 @@ package com.xiaopeng.maintenance.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.xiaopeng.api.model.maintenance.XpVehicleRepair; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.exception.ServiceException; @@ -22,6 +23,7 @@ import javax.validation.Valid; import java.time.LocalDateTime; import java.util.Objects; /** * <p> @@ -97,5 +99,23 @@ PageInfo<XpVehicleRepairVO> pageInfo = xpVehicleRepairService.queryDetectionByVehicleId(query); return R.ok(pageInfo); } @GetMapping("/inner/xpVehicleRepair/getMaintenanceStatus") public R<Integer> getMaintenanceStatus(@RequestParam Long vehicleId){ XpVehicleRepair xpVehicleRepair = xpVehicleRepairService.getOne(new LambdaQueryWrapper<XpVehicleRepair>() .eq(XpVehicleRepair::getVehicleId, vehicleId) .isNull(XpVehicleRepair::getReturnTime) .isNull(XpVehicleRepair::getTaskId) .orderByDesc(XpVehicleRepair::getCreateTime) .last("LIMIT 1")); if(Objects.nonNull(xpVehicleRepair)){ return R.ok(2); }else { return R.ok(1); } } } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/ClockInScheduleTask.java
@@ -158,46 +158,47 @@ } } // if (!CollectionUtils.isEmpty(clockOutPlanListLast)){ // List<Long> fleetIds = clockOutPlanListLast.stream().map(XpSchedulingDetail::getFleetId).distinct().collect(Collectors.toList()); // // 查询所有车队的绑定人员 // List<XpFleetOperations> xpFleetOperations = xpFleetOperationsClient.getOperationsIdsByFleetIds(fleetIds).getData(); // // 查询班次 // List<Long> shiftIds = clockOutPlanListLast.stream().map(XpSchedulingDetail::getShiftId).collect(Collectors.toList()); // List<XpShift> xpShifts = xpShiftService.list(Wrappers.lambdaQuery(XpShift.class) // .in(XpShift::getId, shiftIds)); // log.info("========== 查询到{}个车队 ==========", xpFleetOperations.size()); // // // 处理下班卡缺卡情况 // // 过滤当前时间在班次下班打卡时间之后5小时的数据 今日已过下班打卡时间5小时后的班次 // List<XpShift> shiftListFive = xpShifts.stream().filter(xpShift -> // DateUtils.stringToLocalDateTime(LocalDate.now().minusDays(1) + " " + xpShift.getEndTime() + ":00").plusHours(5).isBefore(LocalDateTime.now())).collect(Collectors.toList()); // List<Long> fiveIds = shiftListFive.stream().map(XpShift::getId).collect(Collectors.toList()); // if(!CollectionUtils.isEmpty(fiveIds)){ // log.info("========== 进入下班过期的班次 =========="); // // 获取过期当前班次的排班明细 覆盖 // List<XpSchedulingDetail> fiveClockOutPlans = clockOutPlanListLast.stream().filter(xpSchedulingDetail -> fiveIds.contains(xpSchedulingDetail.getShiftId())).collect(Collectors.toList()); // List<Long> fiveFleetIdIn = fiveClockOutPlans.stream().map(XpSchedulingDetail::getFleetId).collect(Collectors.toList()); // // 查询所有车队的绑定人员 // if(!CollectionUtils.isEmpty(xpFleetOperations)){ // List<Long> operationsUserIdsFive = xpFleetOperations.stream().filter(x -> fiveFleetIdIn.contains(x.getFleetId())) // .map(XpFleetOperations::getOperationsUserId).distinct().collect(Collectors.toList()); // // 批量查询人员打卡信息 // List<XpClockIn> clockInListFive = xpClockInService.list(Wrappers.lambdaQuery(XpClockIn.class) // .in(XpClockIn::getAppUserId, operationsUserIdsFive) // .isNull(XpClockIn::getClockOutTime)); // log.info("========== 获取到{}个人员没有下班打卡信息 ==========", clockInListFive.size()); // if(!CollectionUtils.isEmpty(clockInListFive)){ // for (XpClockIn xpClockIn : clockInListFive) { // xpClockIn.setClockOutTime(LocalDateTime.now()); // xpClockIn.setClockOutResult(3); // } // xpClockInService.updateBatchById(clockInListFive); // log.info("========== 修改为下班缺卡 =========="); // } // } // } // } if (!CollectionUtils.isEmpty(clockOutPlanListLast)){ List<Long> fleetIds = clockOutPlanListLast.stream().map(XpSchedulingDetail::getFleetId).distinct().collect(Collectors.toList()); // 查询所有车队的绑定人员 List<XpFleetOperations> xpFleetOperations = xpFleetOperationsClient.getOperationsIdsByFleetIds(fleetIds).getData(); // 查询班次 List<Long> shiftIds = clockOutPlanListLast.stream().map(XpSchedulingDetail::getShiftId).collect(Collectors.toList()); List<XpShift> xpShifts = xpShiftService.list(Wrappers.lambdaQuery(XpShift.class) .in(XpShift::getId, shiftIds)); log.info("========== 查询到{}个车队 ==========", xpFleetOperations.size()); // 处理下班卡缺卡情况 // 过滤当前时间在班次下班打卡时间之后5小时的数据 今日已过下班打卡时间5小时后的班次 List<XpShift> shiftListFive = xpShifts.stream().filter(xpShift -> DateUtils.stringToLocalDateTime(LocalDate.now().minusDays(1) + " " + xpShift.getEndTime() + ":00").plusHours(5).isBefore(LocalDateTime.now())).collect(Collectors.toList()); List<Long> fiveIds = shiftListFive.stream().map(XpShift::getId).collect(Collectors.toList()); if(!CollectionUtils.isEmpty(fiveIds)){ log.info("========== 进入下班过期的班次 =========="); // 获取过期当前班次的排班明细 覆盖 List<XpSchedulingDetail> fiveClockOutPlans = clockOutPlanListLast.stream().filter(xpSchedulingDetail -> fiveIds.contains(xpSchedulingDetail.getShiftId())).collect(Collectors.toList()); List<Long> fiveFleetIdIn = fiveClockOutPlans.stream().map(XpSchedulingDetail::getFleetId).collect(Collectors.toList()); // 查询所有车队的绑定人员 if(!CollectionUtils.isEmpty(xpFleetOperations)){ List<Long> operationsUserIdsFive = xpFleetOperations.stream().filter(x -> fiveFleetIdIn.contains(x.getFleetId())) .map(XpFleetOperations::getOperationsUserId).distinct().collect(Collectors.toList()); // 批量查询人员打卡信息 List<XpClockIn> clockInListFive = xpClockInService.list(Wrappers.lambdaQuery(XpClockIn.class) .in(XpClockIn::getAppUserId, operationsUserIdsFive) .isNull(XpClockIn::getClockOutTime)); log.info("========== 获取到{}个人员没有下班打卡信息 ==========", clockInListFive.size()); if(!CollectionUtils.isEmpty(clockInListFive)){ for (XpClockIn xpClockIn : clockInListFive) { xpClockIn.setAppUserId(null); xpClockIn.setClockOutTime(LocalDateTime.now()); xpClockIn.setClockOutResult(3); } xpClockInService.updateBatchById(clockInListFive); log.info("========== 修改为下班缺卡 =========="); } } } } } catch (Exception e) { log.error("执行打卡检查任务执行失败", e); xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java
@@ -1,21 +1,28 @@ package com.xiaopeng.maintenance.service.impl; import cn.hutool.core.collection.CollUtil; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.xiaopeng.api.dto.pushMessage.AliPushMessageDTO; import com.xiaopeng.api.feignClient.account.XpOperationUserClient; import com.xiaopeng.api.feignClient.account.XpOperationUserNotificationClient; import com.xiaopeng.api.feignClient.pushMessage.XpAliPushMessageClient; import com.xiaopeng.api.feignClient.vehicleConvoy.XpFleetOperationsClient; import com.xiaopeng.api.model.account.SysRole; import com.xiaopeng.api.model.account.XpOperationsRole;import com.xiaopeng.api.model.account.XpOperationsUser;import com.xiaopeng.api.model.maintenance.XpCheckLocation; import com.xiaopeng.api.model.account.XpOperationsRole;import com.xiaopeng.api.model.account.XpOperationsUser; import com.xiaopeng.api.model.account.XpOperationsUserNotification; import com.xiaopeng.api.model.maintenance.XpCheckLocation; import com.xiaopeng.api.model.maintenance.XpClockIn; import com.xiaopeng.api.model.maintenance.XpSchedulingDetail; import com.xiaopeng.api.model.maintenance.XpShift; import com.xiaopeng.api.vo.account.XpOperationsUserVO; import com.xiaopeng.api.vo.account.XpOperationsUserWithRoleVO;import com.xiaopeng.common.core.constant.Constants; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.enums.NotificationTypeEnum; import com.xiaopeng.common.core.utils.DateUtils; import com.xiaopeng.common.core.utils.GeodesyUtil; import com.xiaopeng.common.core.utils.StringUtils; @@ -36,6 +43,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import lombok.extern.slf4j.Slf4j; @@ -46,6 +54,9 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -76,6 +87,10 @@ @Resource private XpFleetOperationsClient fleetOperationsClient; @Resource private XpOperationUserNotificationClient xpOperationUserNotificationClient; @Resource private XpAliPushMessageClient xpAliPushMessageClient; @@ -132,7 +147,8 @@ XpClockIn xpClockIn = new XpClockIn(); xpClockIn.setAppUserId(dto.getOperationsUserId()); xpClockIn.setClockInTime(LocalDateTime.now()); LocalDateTime now = LocalDateTime.now(); xpClockIn.setClockInTime(now); xpClockIn.setClockInLon(dto.getUserLon()); xpClockIn.setClockInLat(dto.getUserLat()); xpClockIn.setShiftId(dto.getShiftId()); @@ -143,6 +159,7 @@ xpClockIn.setClockInResult(1); } this.save(xpClockIn); sendAgreementUpdateNotification(now); return R.ok(); } @@ -184,7 +201,8 @@ String endTime = xpShift.getEndTime(); LocalDateTime endLocalDateTime = DateUtils.stringToLocalDateTime(LocalDate.now() + " " + endTime + ":00"); xpClockIn.setClockOutTime(LocalDateTime.now()); LocalDateTime now = LocalDateTime.now(); xpClockIn.setClockOutTime(now); xpClockIn.setClockOutLon(dto.getUserLon()); xpClockIn.setClockOutLat(dto.getUserLat()); xpClockIn.setClockOutAddress(dto.getClockOutAddress()); @@ -195,6 +213,7 @@ } xpClockIn.setAppUserId(null); this.updateById(xpClockIn); sendAgreementUpdateNotification(now); return R.ok(); } @@ -441,4 +460,154 @@ ExcelUtil<ClockInStatisticsDetailsDto> excel = new ExcelUtil<ClockInStatisticsDetailsDto>(ClockInStatisticsDetailsDto.class); excel.exportExcel(response, clockInStatisticsDetails, "打卡统计明细"); } /** * 发送协议更新通知 * @param clockTime 打卡时间 */ private void sendAgreementUpdateNotification(LocalDateTime clockTime) { // 异步处理,避免阻塞主流程 ThreadPoolExecutor executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() ); executor.execute(() -> { try { // 1. 获取所有启用状态的运维用户 R<List<XpOperationsUser>> result = xpOperationUserClient.listAllEnabled(); if (result == null || result.getCode() != 200 || result.getData() == null || result.getData().isEmpty()) { log.warn("没有需要通知的启用状态用户"); return; } List<XpOperationsUser> enabledUsers = result.getData(); log.info("打卡成功通知,共需通知 {} 位用户", enabledUsers.size()); // 2. 构建通知内容 String message = String.format("你已于%s打卡成功!", clockTime); String bodyJson = buildAgreementPushBodyJson(message); // 3. 构建并保存通知记录 List<XpOperationsUserNotification> notifications = enabledUsers.stream() .map(user -> buildAgreementNotification(message, bodyJson, user)) .collect(Collectors.toList()); // 批量保存通知记录 xpOperationUserNotificationClient.addNotificationBatch(notifications); log.info("协议更新通知记录保存成功,共 {} 条", notifications.size()); // 4. 按设备类型分组并批量推送 pushMessageByDeviceType(enabledUsers, message); } catch (Exception e) { log.error("发送协议更新通知失败", e); } }); executor.shutdown(); } /** * 构建推送 body JSON * @param message 消息内容 * @return JSON 字符串 */ private String buildAgreementPushBodyJson(String message) { JSONObject body = new JSONObject(); body.put("content", message); body.put("strategy", NotificationTypeEnum.CLOCK_IN_SUCCESS.getCode()); // 11 body.put("type", 0); body.put("id", 0); return body.toJSONString(); } /** * 构建通知实体 * @param message 消息内容 * @param bodyJson 推送body内容 * @param user 运维用户 * @return 通知实体 */ private XpOperationsUserNotification buildAgreementNotification(String message, String bodyJson, XpOperationsUser user) { XpOperationsUserNotification notification = new XpOperationsUserNotification(); notification.setNotificationType(NotificationTypeEnum.CLOCK_IN_SUCCESS.getCode()); // 11 notification.setNotificationContent(message); notification.setOperationsUserId(user.getId()); notification.setDataId(0L); notification.setReadStatus(0); // 未读 notification.setNotificationExt(bodyJson); return notification; } /** * 按设备类型分组推送消息 * @param users 用户列表 * @param message 消息内容 */ private void pushMessageByDeviceType(List<XpOperationsUser> users, String message) { // 按设备类型分组 (1=Android, 2=iOS) Map<Integer, List<XpOperationsUser>> usersByDeviceType = users.stream() .filter(u -> u.getDeviceType() != null && u.getPhoneNumber() != null) .collect(Collectors.groupingBy(XpOperationsUser::getDeviceType)); // 分别推送 Android 和 iOS for (Map.Entry<Integer, List<XpOperationsUser>> entry : usersByDeviceType.entrySet()) { Integer deviceType = entry.getKey(); List<XpOperationsUser> deviceUsers = entry.getValue(); // 按 1000 个手机号一批进行推送 List<List<XpOperationsUser>> batches = partition(deviceUsers, 1000); for (List<XpOperationsUser> batch : batches) { String accounts = batch.stream() .map(XpOperationsUser::getPhoneNumber) .collect(Collectors.joining(",")); AliPushMessageDTO pushDTO = buildPushMessageDTO(deviceType, accounts, message); try { xpAliPushMessageClient.pushMessage(pushDTO); log.info("打卡成功推送成功,设备类型={},用户数={}", deviceType == 1 ? "Android" : "iOS", batch.size()); } catch (Exception e) { log.error("打卡成功推送失败,设备类型={}", deviceType, e); } } } } /** * 将列表按指定大小分批 * @param list 原始列表 * @param batchSize 批次大小 * @return 分批后的列表 */ private <T> List<List<T>> partition(List<T> list, int batchSize) { List<List<T>> batches = new ArrayList<>(); for (int i = 0; i < list.size(); i += batchSize) { batches.add(list.subList(i, Math.min(i + batchSize, list.size()))); } return batches; } /** * 构建推送消息 DTO * @param deviceType 设备类型 * @param accounts 手机号(逗号分隔) * @param message 消息内容 * @return 推送 DTO */ private AliPushMessageDTO buildPushMessageDTO(Integer deviceType, String accounts, String message) { AliPushMessageDTO dto = new AliPushMessageDTO(); dto.setDeviceType(deviceType); dto.setAccount(accounts); dto.setTitle("打卡成功通知"); dto.setBody(buildAgreementPushBodyJson(message)); dto.setPushType("NOTICE"); return dto; } } xp-robotaxi-maintenance-boot/src/main/resources/mapper/maintenance/XpTaskMapper.xml
@@ -70,6 +70,10 @@ and t1.task_urgency_level = #{query.taskUrgencyLevel} </if> <if test="query.status != null"> -- 事故不在已处理中展示 <if test="query.status == 2"> and t1.task_category = 1 </if> and t1.status = #{query.status} </if> <if test="query.plateNumber != null and query.plateNumber != ''"> xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpElectronicFenceController.java
@@ -2,6 +2,7 @@ import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.xiaopeng.api.dto.operation.ElectronicFenceAddDto; import com.xiaopeng.api.dto.operation.ElectronicFenceAreaUpdateStatusDto; import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto; @@ -74,9 +75,9 @@ * @return 电子围栏列表 */ @ApiOperation(value = "电子围栏列表接口", tags = {"运维APP-车辆管理"}) @GetMapping("/api/v1/electronicFence/list") public R<List<XpElectronicFence>> vehicleMap() { List<XpElectronicFence> list = xpElectronicFenceService.list(); @GetMapping("/operationsFenceList") public R<List<XpElectronicFence>> operationsFenceList() { List<XpElectronicFence> list = xpElectronicFenceService.list(Wrappers.lambdaQuery(XpElectronicFence.class).eq(XpElectronicFence::getStatus, 2)); return R.ok(list); } /** xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/controller/XpVehicleController.java
@@ -13,12 +13,11 @@ import com.mongodb.client.MongoCollection;import com.xiaopeng.api.dto.operation.VehiclePositionByVinDto; import com.xiaopeng.api.dto.vehicleConvoy.UpdateVehicleStateDto; import com.xiaopeng.api.dto.vehicleConvoy.VehicleTemperatureDto; import com.xiaopeng.api.feignClient.maintenance.XpChargingRecordClient; import com.xiaopeng.api.feignClient.maintenance.XpTaskClient; import com.xiaopeng.api.feignClient.maintenance.XpTaskRuleClient; import com.xiaopeng.api.feignClient.maintenance.XpVehiclePreparationClient; import com.xiaopeng.api.feignClient.maintenance.*; import com.xiaopeng.api.model.account.XpOperationsUser; import com.xiaopeng.api.model.maintenance.XpTask; import com.xiaopeng.api.model.maintenance.XpTaskRule; import com.xiaopeng.api.model.maintenance.XpVehicleRepair; import com.xiaopeng.api.model.vehicleConvoy.XpFleetVehicle; import com.xiaopeng.api.model.vehicleConvoy.XpVehicle; import com.xiaopeng.api.model.vehicleConvoy.XpVehicleAnnualRecord; @@ -105,6 +104,7 @@ private final MongoTemplate mongoTemplate; private final MessageSource messageSource; private final TokenService tokenService; private final XpVehicleRepairClient xpVehicleRepairClient; /** * 根据ID列表批量查询车辆信息(内部接口) * @@ -853,6 +853,9 @@ xpVehicleVO.setMileageRange(0F); xpVehicleVO.setCurrentElectricity(0); } // 判断车辆维修状态 Integer maintenanceStatus = xpVehicleRepairClient.getMaintenanceStatus(xpVehicleVO.getId()).getData(); xpVehicleVO.setMaintenanceStatus(maintenanceStatus); return R.ok(xpVehicleVO); } xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/model/vo/XpVehicleVO.java
@@ -33,4 +33,7 @@ @ApiModelProperty(value = "车辆纬度") private Double latitude=30.586688; @ApiModelProperty(value = "车辆维修状态 1=取回 2=送修") private Integer maintenanceStatus; }