xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/operation/XpElectronicClientFallbackFactory.java
@@ -5,6 +5,7 @@ import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto; import com.xiaopeng.api.feignClient.operation.XpElectronicClient; import com.xiaopeng.api.model.operation.XpElectronicFence; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.query.operation.ElectronicPageListQuery; import com.xiaopeng.api.vo.operation.ElectronicFenceDetailVo; import com.xiaopeng.common.core.domain.R; @@ -67,6 +68,12 @@ log.error("根据ID列表批量查询电子围栏失败:{}", cause.getMessage()); return R.fail("根据ID列表批量查询电子围栏失败"); } @Override public R<XpElectronicFenceArea> queryElectronicFenceAreaById(Long id) { log.error("通过区域ID查询围栏失败:{}", cause.getMessage()); return R.fail("通过区域ID查询围栏失败"); } }; } } xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/operation/XpElectronicClient.java
@@ -5,6 +5,7 @@ import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto; import com.xiaopeng.api.factory.operation.XpPointClientFallbackFactory; import com.xiaopeng.api.model.operation.XpElectronicFence; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.query.operation.ElectronicPageListQuery; import com.xiaopeng.api.vo.operation.ElectronicFenceDetailVo; import com.xiaopeng.common.core.domain.R; @@ -57,4 +58,13 @@ */ @PostMapping("/api/v1/electronicFence/listByIds") R<List<XpElectronicFence>> listByIds(@RequestBody Collection<Long> electronicIdSet); /** * 根据区域ID查询电子围栏区域信息 * * @param id 区域ID * @return 区域信息 */ @PostMapping("/inner/v1/xp-electronic-fence-area/queryById") R<XpElectronicFenceArea> queryElectronicFenceAreaById(@RequestParam("id") Long id); } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleCollectionScheduleTask.java
@@ -1,14 +1,24 @@ package com.xiaopeng.maintenance.schedule; import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.xiaopeng.api.dto.automaticPilot.PushOrderDto; import com.xiaopeng.api.feignClient.automaticPilot.OrderMissionClient; import com.xiaopeng.api.feignClient.operation.XpCruisePlanClient; import com.xiaopeng.api.feignClient.order.XpCruiseOrderClient; import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient; 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.automaticPilot.VehiclePosition; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper; 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; @@ -42,6 +52,18 @@ @Autowired private XpVehicleClient xpVehicleClient; @Autowired private XpCruiseOrderClient xpCruiseOrderClient; @Autowired private MongoTemplate mongoTemplate; @Autowired private OrderMissionClient orderMissionClient; @Autowired private XpCruisePlanClient xpCruisePlanClient; /** * 每分钟检查一次是否需要执行收车任务 @@ -162,6 +184,7 @@ return result.getData().stream() .filter(v -> v.getVehicleType() != null && v.getVehicleType() == 2) .filter(v -> v.getOperatingStatus() != null && v.getOperatingStatus() == 1) .filter(v -> v.getState() != null && v.getState() == 0) .collect(Collectors.toList()); } } catch (Exception e) { @@ -179,10 +202,37 @@ * 2. 更新车辆状态为"已收车" * </p> * * @param vehicle 车辆信息 * @param xpVehicle 车辆信息 */ private void sendCollectionCommand(XpVehicle vehicle) { // TODO: 具体发送收车指令的实现 log.info("向车辆[{}]发送收车指令", vehicle.getPlateNumber()); private void sendCollectionCommand(XpVehicle xpVehicle) { // 收车 关闭当前巡游单子 生成新的返程单子 找到收车站点 R<String> r = xpCruiseOrderClient.updateCruiseOrder(xpVehicle.getVehicleVin()); // redis 获取当前最新经纬度位置 VehiclePosition vehiclePosition = mongoTemplate.findOne(new Query(Criteria.where("vin").is(xpVehicle.getVehicleVin())), VehiclePosition.class); XpCruiseOrder xpCruiseOrder = new XpCruiseOrder(); if(vehiclePosition!=null){ double x = vehiclePosition.getPoint().getX(); double y = vehiclePosition.getPoint().getY(); xpCruiseOrder.setStartLon(x); xpCruiseOrder.setStartLat(y); } xpCruiseOrder.setId("XU"+ IdUtil.fastUUID()); xpCruiseOrder.setVehicleId(xpVehicle.getId()); xpCruiseOrder.setFleetId(xpVehicle.getFleetId()); xpCruiseOrder.setOrderType(2); xpCruiseOrder.setStatus(1); xpCruiseOrder.setOrderReverse(1); xpCruiseOrder.setLastOrderId(r.getData()); // 找到收车站点 R<XpCruiseOrder> returnCruisePoint = xpCruisePlanClient.findReturnCruisePoint(xpCruiseOrder); if(returnCruisePoint!=null){ xpCruiseOrderClient.addCruiseOrder(returnCruisePoint.getData()); // 下发命令 orderMissionClient.pushOrder(new PushOrderDto( xpCruiseOrder.getVehicleVin(),xpCruiseOrder.getId(),null)); } log.info("向车辆[{}]发送收车指令", xpVehicle.getPlateNumber()); } } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpElectronicFenceAreaController.java
@@ -4,11 +4,12 @@ import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.operation.service.IXpElectronicFenceAreaService; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @@ -20,8 +21,21 @@ * @author mitao * @since 2026-02-03 */ @Controller @RequestMapping("/xp-electronic-fence-area") @RestController public class XpElectronicFenceAreaController { @Resource private IXpElectronicFenceAreaService xpElectronicFenceAreaService; /** * 根据区域ID查询区域信息 * * @param id 区域ID * @return 区域信息 */ @ApiOperation(value = "根据区域ID查询区域信息", notes = "根据区域ID查询区域信息", tags = {"内部接口"}) @PostMapping("/inner/v1/xp-electronic-fence-area/queryById/queryById") public R<XpElectronicFenceArea> queryById(@RequestParam("id") Long id) { return R.ok(xpElectronicFenceAreaService.getById(id)); } } xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/controller/XpOrderController.java
@@ -270,7 +270,7 @@ * @param dto 改派请求参数 * @return 操作结果 */ @ApiOperation(value = "改派订单", tags = {"管理后台-交易管理-订单管理"},notes = "车辆列表在maintenance服务") @ApiOperation(value = "改派订单", tags = {"管理后台-交易管理-订单管理"},notes = "车辆列表在vehicleConvoy服务") @PostMapping("/api/v1/order/reassignById") public R<?> reassignById(@Validated @RequestBody XpOrderReassignDTO dto) { xpOrderService.reassignOrder( xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java
@@ -511,7 +511,20 @@ orderCancel.setCancelReason("运营人员取消订单"); orderCancel.setCreateBy(SecurityUtils.getLoginUser().getSysUser().getNickName()); xpOrderCancelService.save(orderCancel); //查询车辆信息 R<XpVehicle> xpVehicleR = xpVehicleClient.queryById(order.getId()); if (xpVehicleR.getCode() == HttpStatus.SUCCESS && Objects.nonNull(xpVehicleR.getData())) { // 下发命令 if (order.getStatus() > 1) { PushOrderDto pushOrderDto = new PushOrderDto(); pushOrderDto.setOrderId(order.getId().toString()); pushOrderDto.setVin(xpVehicleR.getData().getVehicleVin()); pushOrderDto.setOperation_type(OrderOperationType.CLOUD_CANCEL_ORDER); orderMissionClient.cancelOrder(pushOrderDto); } } else { log.info("查询车辆信息失败,订单ID: {}, 下发取消订单命令失败", order.getId()); } log.info("订单取消成功,订单ID: {}, 操作人: {}", order.getId(), SecurityUtils.getUsername()); } @@ -538,10 +551,10 @@ throw new ServiceException("订单不存在"); } // 2. 校验订单状态(只有待接单、待出发、接驾中状态可以改派) // 2. 校验订单状态(只有待出发、接驾中、待乘客上车状态可以改派) Integer currentStatus = order.getStatus(); if (currentStatus != 1 && currentStatus != 2 && currentStatus != 3) { throw new ServiceException("当前订单状态不允许改派,只有待接单、待出发、接驾中状态的订单可以改派", I18nKeyConstant.ORDER_REASSIGN_FAILED); if (currentStatus != 2 && currentStatus != 3 && currentStatus != 4) { throw new ServiceException("当前订单状态不允许改派,只有待出发、接驾中、待乘客上车状态的订单可以改派", I18nKeyConstant.ORDER_REASSIGN_FAILED); } // 3. 校验新旧车辆是否相同 @@ -580,6 +593,47 @@ // 6. 保存原车辆和车队信息 Long oldVehicleId = order.getVehicleId(); Long oldFleetId = order.getFleetId(); // 6.1 处理原车辆:取消订单指令、解绑乘客、更新状态、创建返程巡游订单 if (Objects.nonNull(oldVehicleId)) { try { R<XpVehicle> oldVehicleResult = xpVehicleClient.queryById(oldVehicleId); if (oldVehicleResult != null && oldVehicleResult.getCode() == HttpStatus.SUCCESS && oldVehicleResult.getData() != null) { XpVehicle oldVehicle = oldVehicleResult.getData(); // 6.1.1 取消原订单指令 PushOrderDto cancelDto = new PushOrderDto(); cancelDto.setOrderId(order.getId().toString()); cancelDto.setVin(oldVehicle.getVehicleVin()); cancelDto.setOperation_type(OrderOperationType.CLOUD_CANCEL_ORDER); orderMissionClient.cancelOrder(cancelDto); log.info("已取消原车辆订单指令,订单ID: {}, VIN: {}", orderId, oldVehicle.getVehicleVin()); // 6.1.2 解绑乘客与原车辆关系 if (Objects.nonNull(order.getAppUserId())) { CancelTaxiPassengerVo cancelVo = new CancelTaxiPassengerVo(); cancelVo.setVin(oldVehicle.getVehicleVin()); cancelVo.setUid(order.getAppUserId().toString()); cancelVo.setOrderNo(order.getOrderNumber()); cancelVo.setAppId(""); userVehicleClient.cancelTaxiPassenger(cancelVo); log.info("已解绑乘客与原车辆关系,用户ID: {}, VIN: {}", order.getAppUserId(), oldVehicle.getVehicleVin()); } // 6.1.3 修改原车辆状态为空闲 oldVehicle.setState(0); xpVehicleClient.updateVehicleById(oldVehicle); log.info("已将原车辆状态设为空闲,车辆ID: {}", oldVehicleId); // 6.1.4 为原车辆创建返程巡游订单 createReturnCruiseOrder(oldVehicle, order); } else { log.warn("查询原车辆信息失败,车辆ID: {}", oldVehicleId); } } catch (Exception e) { log.error("处理原车辆失败,车辆ID: {},错误: {}", oldVehicleId, e.getMessage()); } } // 7. 查询新车辆关联的车队ID Long newFleetId = null; @@ -634,7 +688,118 @@ log.info("订单改派成功,订单ID: {}, 原车辆ID: {}, 新车辆ID: {}, 原车队ID: {}, 新车队ID: {}, 操作人: {}", orderId, oldVehicleId, newVehicleId, oldFleetId, newFleetId, SecurityUtils.getUsername()); //推送消息 // 11. 处理新车辆:绑定乘客、下发订单、更新状态 try { // 11.1 绑定乘客与新车辆关系 if (Objects.nonNull(order.getAppUserId())) { AddTaxiPassengerVo addVo = new AddTaxiPassengerVo(); addVo.setVin(newVehicle.getVehicleVin()); addVo.setUid(order.getAppUserId().toString()); addVo.setOrderNo(order.getOrderNumber()); addVo.setAppId(""); // TODO 获取appId userVehicleClient.addTaxiPassenger(addVo); log.info("已绑定乘客与新车辆关系,用户ID: {}, VIN: {}", order.getAppUserId(), newVehicle.getVehicleVin()); } // 11.2 下发订单到新车辆 PushOrderDto pushDto = new PushOrderDto(); pushDto.setOrderId(order.getId().toString()); pushDto.setVin(newVehicle.getVehicleVin()); orderMissionClient.pushOrder(pushDto); log.info("已下发订单到新车辆,订单ID: {}, VIN: {}", orderId, newVehicle.getVehicleVin()); // 11.3 更新新车辆状态为已接单 newVehicle.setState(1); xpVehicleClient.updateVehicleById(newVehicle); log.info("已将新车辆状态设为已接单,车辆ID: {}", newVehicleId); } catch (Exception e) { log.error("处理新车辆失败,车辆ID: {},错误: {}", newVehicleId, e.getMessage()); } } /** * 为原车辆创建返程巡游订单 * <p> * 改派时,原车辆需要返回巡游路线: * - 若最新巡游订单状态为已终止(status=2),返回原出发点 * - 若最新巡游订单状态为已完成(status=3),返回原终点 * - 其他状态,默认返回原出发点 * </p> * * @param oldVehicle 原车辆信息 * @param order 当前订单信息 */ private void createReturnCruiseOrder(XpVehicle oldVehicle, XpOrder order) { try { // 查询该车辆最新的一条巡游订单 XpCruiseOrder lastCruise = xpCruiseOrderMapper.selectOne( new LambdaQueryWrapper<XpCruiseOrder>() .eq(XpCruiseOrder::getVehicleId, oldVehicle.getId()) .orderByDesc(BaseModel::getCreateTime) .last("limit 1") ); if (lastCruise == null) { log.warn("原车辆没有巡游订单记录,跳过创建返程订单,车辆ID: {}", oldVehicle.getId()); return; } XpCruiseOrder returnOrder = new XpCruiseOrder(); returnOrder.setId("XU" + IdUtil.fastUUID()); returnOrder.setVehicleId(oldVehicle.getId()); returnOrder.setVehicleVin(oldVehicle.getVehicleVin()); returnOrder.setFleetId(oldVehicle.getFleetId()); returnOrder.setOrderType(2); // 返程单 returnOrder.setStatus(1); // 进行中 returnOrder.setLastOrderId(lastCruise.getId()); returnOrder.setCreateTime(LocalDateTime.now()); // 返程起点 = 当前订单的终点(车辆当前位置附近) returnOrder.setStartPointId(order.getEndPointId()); returnOrder.setStartPointName(order.getEndPointName()); returnOrder.setStartLon(order.getEndLon()); returnOrder.setStartLat(order.getEndLat()); // 根据最新巡游订单状态决定返程终点 if (lastCruise.getStatus() != null && lastCruise.getStatus() == 2) { // 已终止(status=2) -> 返回原出发点 returnOrder.setEndPointId(lastCruise.getStartPointId()); returnOrder.setEndPointName(lastCruise.getStartPointName()); returnOrder.setEndLon(lastCruise.getStartLon()); returnOrder.setEndLat(lastCruise.getStartLat()); log.info("巡游订单状态为已终止,返回原出发点"); } else if (lastCruise.getStatus() != null && lastCruise.getStatus() == 3) { // 已完成(status=3) -> 返回原终点 returnOrder.setEndPointId(lastCruise.getEndPointId()); returnOrder.setEndPointName(lastCruise.getEndPointName()); returnOrder.setEndLon(lastCruise.getEndLon()); returnOrder.setEndLat(lastCruise.getEndLat()); log.info("巡游订单状态为已完成,返回原终点"); } else { // 其他状态(进行中等),默认返回原出发点 returnOrder.setEndPointId(lastCruise.getStartPointId()); returnOrder.setEndPointName(lastCruise.getStartPointName()); returnOrder.setEndLon(lastCruise.getStartLon()); returnOrder.setEndLat(lastCruise.getStartLat()); log.info("巡游订单状态为{},默认返回原出发点", lastCruise.getStatus()); } xpCruiseOrderMapper.insert(returnOrder); // 下发巡游订单到原车辆 PushOrderDto pushDto = new PushOrderDto(); pushDto.setVin(oldVehicle.getVehicleVin()); pushDto.setOrderId(returnOrder.getId()); orderMissionClient.pushOrder(pushDto); log.info("为原车辆创建返程巡游订单成功,订单ID: {}, 车辆ID: {}", returnOrder.getId(), oldVehicle.getId()); } catch (Exception e) { log.error("创建返程巡游订单失败,车辆ID: {},错误: {}", oldVehicle.getId(), e.getMessage()); } } @Override xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/controller/XpVehicleController.java
@@ -479,8 +479,9 @@ */ @ApiOperation(value = "查询可改派的车辆列表", tags = {"管理后台-订单管理"}) @GetMapping("/api/v1/vehicle/availableList") public R<List<XpVehicle>> listAvailableVehicles() { List<XpVehicle> list = xpVehicleService.listAvailableVehicles(); @ApiImplicitParam(name = "orderId", value = "订单ID", required = true, dataTypeClass = Long.class) public R<List<XpVehicle>> listAvailableVehicles(@RequestParam Long orderId) { List<XpVehicle> list = xpVehicleService.listAvailableVehicles(orderId); return R.ok(list); } xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/service/IXpVehicleService.java
@@ -33,7 +33,7 @@ * * @return 车辆列表 */ List<XpVehicle> listAvailableVehicles(); List<XpVehicle> listAvailableVehicles(Long orderId); /** * 查询车辆列表接口-地图 xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/service/impl/XpVehicleServiceImpl.java
@@ -11,6 +11,7 @@ import com.xiaopeng.api.feignClient.operation.XpElectronicClient; import com.xiaopeng.api.feignClient.order.XpOrderClient; import com.xiaopeng.api.model.operation.XpElectronicFence; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.model.order.XpOrder; import com.xiaopeng.api.model.vehicleConvoy.XpVehicle; import com.xiaopeng.api.model.vehicleConvoy.XpVehicleAnnualRecord; @@ -83,22 +84,45 @@ } @Override public List<XpVehicle> listAvailableVehicles() { // 构建查询条件 LambdaQueryWrapper<XpVehicle> wrapper = new LambdaQueryWrapper<>(); public List<XpVehicle> listAvailableVehicles(Long orderId) { // 1. 通过订单ID查询订单信息,获取区域ID R<XpOrder> orderResult = xpOrderClient.queryOrderById(orderId); if (orderResult == null || orderResult.getData() == null) { log.warn("查询订单失败,orderId: {}", orderId); return Collections.emptyList(); } // 必要条件:可改派车辆的4个核心条件 wrapper.ne(XpVehicle::getStatus, 3) // 状态不是保险/年检过期 .isNotNull(XpVehicle::getStatus) .eq(XpVehicle::getOperatingStatus, 1) // 运营中 .eq(XpVehicle::getAcceptOrTest, 1) // 接单模式 .eq(XpVehicle::getAcceptOrTestStatus, 1); // 开始接单 XpOrder order = orderResult.getData(); Long areaId = order.getXpElectronicFenceAreaId(); if (areaId == null) { log.warn("订单区域ID为空,orderId: {}", orderId); return Collections.emptyList(); } // 2. 通过区域ID查询围栏ID R<XpElectronicFenceArea> areaResult = xpElectronicClient.queryElectronicFenceAreaById(areaId); if (areaResult == null || areaResult.getData() == null) { log.warn("查询围栏区域失败,areaId: {}", areaId); return Collections.emptyList(); } Long electronicId = areaResult.getData().getElectronicId(); if (electronicId == null) { log.warn("围栏ID为空,areaId: {}", areaId); return Collections.emptyList(); } // 3. 根据围栏ID查询车辆列表 (operatingStatus=1 且 state=0) LambdaQueryWrapper<XpVehicle> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(XpVehicle::getElectronicId, electronicId) .eq(XpVehicle::getOperatingStatus, 1) .eq(XpVehicle::getState, 0); // 排序:按电量降序、入库时间降序 wrapper.orderByDesc(XpVehicle::getCurrentElectricity) .orderByDesc(XpVehicle::getWarehousingTime); // 执行查询 return this.list(wrapper); } xp-robotaxi-vehicle-convoy-boot/src/main/resources/i18n/messages.properties
@@ -1,7 +1,7 @@ # 中文 fleet.operation.user.bound.v2=当前车队已和该人员关联 policy.application.audit.ids.notEmpty=保单申请ID列表不能为空 fleet.user.bound=运维人员[{0}]已绑定车队[{1}] fleet.operation.user.bound.v1=运维人员[{0}]已绑定车队[{1}] fleet.vehicle.bound.v2=当前车队已和该车辆关联 fleet.vehicle.bound.v1=车辆[{0}]已绑定车队[{1}] policy.application.audit.status.notNull=保单审核状态不能为空 xp-robotaxi-vehicle-convoy-boot/src/main/resources/i18n/messages_zh_CN.properties
@@ -2,6 +2,7 @@ fleet.operation.user.bound.v2=当前车队已和该人员关联 policy.application.audit.ids.notEmpty=保单申请ID列表不能为空 fleet.user.bound=运维人员[{0}]已绑定车队[{1}] fleet.operation.user.bound.v1=运维人员[{0}]已绑定车队[{1}] fleet.vehicle.bound.v2=当前车队已和该车辆关联 policy.application.audit.status.notNull=保单审核状态不能为空 welcome.message=欢迎来到我们的应用程序!