xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/order/XpOrderClientFallbackFactory.java
@@ -79,6 +79,12 @@ } @Override public R<Integer> countActiveOrdersByCityId(Long cityId) { log.error("查询进行中订单数量失败,原因: {}", cause.getMessage()); return R.ok(0); } @Override public R<?> popOrder(PopOrderDto dto) { log.error("订单出队失败,原因: {}", cause.getMessage()); return R.fail(); xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/order/XpOrderClient.java
@@ -85,6 +85,15 @@ @PostMapping("/inner/order/getOrderNum") R<Integer> getOrderNum(@RequestBody GetOrderNumDto dto); /** * 查询指定城市进行中的订单数量(status=1) * * @param cityId 开通城市ID * @return 进行中订单数量 */ @GetMapping("/inner/order/countActiveOrders") R<Integer> countActiveOrdersByCityId(@RequestParam("cityId") Long cityId); /** * 成功操作下发命令后调用 用于弹出redis队列的获取成功的数据 xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpChargingRecord.java
@@ -48,7 +48,7 @@ private String startChargingPicture; @ApiModelProperty(value = "开始充电时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime startTime; @ApiModelProperty(value = "结束剩余电量") @@ -59,7 +59,7 @@ private String endChargingPicture; @ApiModelProperty(value = "结束充电时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime endTime; @ApiModelProperty(value = "开始充电操作人") xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpVehicleRepair.java
@@ -66,14 +66,14 @@ private Long sendUserId; @ApiModelProperty(value = "送修时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime sendTime; @ApiModelProperty(value = "取回人") private Long returnUserId; @ApiModelProperty(value = "取回时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime returnTime; xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/model/vo/OperationRecordsVO.java
@@ -34,7 +34,7 @@ private Long operationsUserId; @ApiModelProperty(value = "操作时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime operationTime; @ApiModelProperty(value = "开始剩余电量") xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/model/vo/XpChargingRecordVO.java
@@ -31,7 +31,7 @@ private String operatorEndName; @ApiModelProperty(value = "操作时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime operationTime; } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/model/vo/XpVehicleCleanVO.java
@@ -31,7 +31,7 @@ private String operatorEndName; @ApiModelProperty(value = "操作时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime operationTime; } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/model/vo/XpVehicleDetectionVO.java
@@ -34,7 +34,7 @@ private String operatorEndName; @ApiModelProperty(value = "操作时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime operationTime; } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/model/vo/XpVehiclePreparationVO.java
@@ -31,7 +31,7 @@ private String operatorEndName; @ApiModelProperty(value = "操作时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime operationTime; } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/model/vo/XpVehicleRepairVO.java
@@ -31,7 +31,7 @@ private String operatorEndName; @ApiModelProperty(value = "操作时间") @JsonFormat(pattern = "yyyy年MM月dd日 HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime operationTime; } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/job/base/TimeJobType.java
@@ -6,7 +6,8 @@ */ public enum TimeJobType { ELECTRONIC_EFFECT("electronic_effect","电子围栏生效"), ELECTRONIC_FAILURE("electronic_failure","电子围栏失效"); ELECTRONIC_FAILURE("electronic_failure","电子围栏失效"), DYNAMIC_PRICING_CHECK("dynamic_pricing_check","动态调价供需检测"); private String type; xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/job/jobs/DynamicPricingCheckJob.java
New file @@ -0,0 +1,216 @@ package com.xiaopeng.operation.job.jobs; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.xiaopeng.api.feignClient.order.XpOrderClient; import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient; import com.xiaopeng.api.model.operation.XpElectronicFence; import com.xiaopeng.api.model.operation.XpOpenCity; import com.xiaopeng.api.model.operation.XpPricing; import com.xiaopeng.api.model.vehicleConvoy.XpVehicle; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.utils.DateUtils; import com.xiaopeng.common.core.utils.StringUtils; import com.xiaopeng.common.redis.service.RedisService; import com.xiaopeng.operation.job.base.AbstractJob; import com.xiaopeng.operation.job.base.TimeJobType; import com.xiaopeng.operation.job.base.QuartzManager; import com.xiaopeng.operation.job.utils.SpringContextsUtil; import com.xiaopeng.operation.mapper.XpElectronicFenceMapper; import com.xiaopeng.operation.mapper.XpOpenCityMapper; import com.xiaopeng.operation.service.XpPricingService; import lombok.extern.slf4j.Slf4j; import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import java.time.LocalDateTime; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 动态调价供需检测定时任务 * <p> * 根据配置的供需计算周期(dynamicPricingJson.num1)定期执行: * 检测城市订单需求(status=1 进行中)与空闲车辆供给, * 满足调价条件时向 Redis 写入 DYNAMIC_PRICE_{openCityId} 触发标志。 * 任务执行完后若 isDynamicPrice 仍为 1,自动注册下一次任务。 * </p> * * @author mitao */ @Slf4j public class DynamicPricingCheckJob extends AbstractJob { /** * 任务名称前缀 */ public static final String JOB_PREFIX = "dynamicPricingCheck_"; private XpPricingService xpPricingService; private XpOrderClient xpOrderClient; private XpVehicleClient xpVehicleClient; private XpOpenCityMapper xpOpenCityMapper; private XpElectronicFenceMapper xpElectronicFenceMapper; private RedisService redisService; public DynamicPricingCheckJob() { super(); this.xpPricingService = SpringContextsUtil.getBean(XpPricingService.class); this.xpOrderClient = SpringContextsUtil.getBean(XpOrderClient.class); this.xpVehicleClient = SpringContextsUtil.getBean(XpVehicleClient.class); this.xpOpenCityMapper = SpringContextsUtil.getBean(XpOpenCityMapper.class); this.xpElectronicFenceMapper = SpringContextsUtil.getBean(XpElectronicFenceMapper.class); this.redisService = SpringContextsUtil.getBean(RedisService.class); } @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap maps = context.getMergedJobDataMap(); Long pricingId = maps.getLong("pricingId"); log.info("========== 开始执行动态调价供需检测任务 pricingId={} ==========", pricingId); try { // 1. 查询定价配置,验证 isDynamicPrice=1 XpPricing pricing = xpPricingService.getById(pricingId); if (pricing == null) { log.warn("动态调价任务跳过:定价配置不存在 pricingId={}", pricingId); return; } if (!Integer.valueOf(1).equals(pricing.getIsDynamicPrice())) { log.info("动态调价任务跳过:isDynamicPrice 已关闭 pricingId={}", pricingId); return; } if (StringUtils.isBlank(pricing.getDynamicPricingJson())) { log.warn("动态调价任务跳过:dynamicPricingJson 为空 pricingId={}", pricingId); return; } // 2. 解析 dynamicPricingJson 获取 num1/num2/num3 JSONObject dynamicJson = JSONObject.parseObject(pricing.getDynamicPricingJson()); Integer periodMinutes = dynamicJson.getInteger("num1"); // 供需计算周期(分钟) Integer orderThreshold = dynamicJson.getInteger("num2"); // 订单需求阈值 Integer vehicleThreshold = dynamicJson.getInteger("num3"); // 车辆供给阈值 if (periodMinutes == null || periodMinutes <= 0 || orderThreshold == null || vehicleThreshold == null) { log.warn("动态调价任务跳过:dynamicPricingJson 参数不完整 pricingId={}, num1={}, num2={}, num3={}", pricingId, periodMinutes, orderThreshold, vehicleThreshold); return; } Long cityId = pricing.getCityId(); if (cityId == null) { log.warn("动态调价任务跳过:定价配置 cityId 为空 pricingId={}", pricingId); return; } // 3. 通过 cityId 查询 XpOpenCity,获取 cityCode XpOpenCity openCity = xpOpenCityMapper.selectById(cityId); if (openCity == null) { log.warn("动态调价任务跳过:开通城市信息不存在 cityId={}", cityId); return; } String cityCode = openCity.getCityCode(); if (StringUtils.isBlank(cityCode)) { log.warn("动态调价任务跳过:无法获取城市 code cityId={}", cityId); return; } // 4. 查询该城市下启用中(status=2)的电子围栏 List<XpElectronicFence> fences = xpElectronicFenceMapper.selectList( new LambdaQueryWrapper<XpElectronicFence>() .eq(XpElectronicFence::getCityCode, cityCode) .eq(XpElectronicFence::getStatus, 2)); if (fences == null || fences.isEmpty()) { log.warn("动态调价任务跳过:未找到启用中的围栏 cityCode={}", cityCode); return; } // 5. 查询进行中订单数(status=1) int activeOrderCount = 0; try { R<Integer> orderResult = xpOrderClient.countActiveOrdersByCityId(cityId); if (orderResult != null && orderResult.getData() != null) { activeOrderCount = orderResult.getData(); } } catch (Exception e) { log.error("动态调价任务:查询进行中订单数失败 cityId={}", cityId, e); } // 6. 查询空闲车辆数(汇总所有围栏下 state=0 的车辆) int idleVehicleCount = 0; for (XpElectronicFence fence : fences) { if (fence.getId() == null) { continue; } try { R<List<XpVehicle>> vehicleResult = xpVehicleClient.listByFenceId(fence.getId()); if (vehicleResult != null && vehicleResult.getData() != null) { // listByFenceId 内部已过滤 state=0 且 operatingStatus=1 的空闲车辆 idleVehicleCount += vehicleResult.getData().size(); } } catch (Exception e) { log.error("动态调价任务:查询围栏车辆失败 fenceId={}", fence.getId(), e); } } log.info("动态调价供需检测 pricingId={}, cityId={}, activeOrderCount={}, idleVehicleCount={}, orderThreshold={}, vehicleThreshold={}", pricingId, cityId, activeOrderCount, idleVehicleCount, orderThreshold, vehicleThreshold); // 7. 判断是否满足调价条件:订单数 > 订单需求阈值 && 空闲车辆数 < 车辆供给阈值 if (activeOrderCount > orderThreshold && idleVehicleCount < vehicleThreshold) { String redisKey = "DYNAMIC_PRICE_" + cityId; // 写入当前时间字符串,与现有 XpPricingUtil 消费侧兼容 redisService.setCacheObject(redisKey, DateUtil.format(new Date(), DatePattern.NORM_DATETIME_FORMAT)); log.info("动态调价触发:已写入 Redis key={} pricingId={}", redisKey, pricingId); } else { log.info("动态调价未触发:供需条件不满足 pricingId={}", pricingId); } } catch (Exception e) { log.error("动态调价供需检测任务执行失败 pricingId={}", pricingId, e); } finally { // 8. 无论是否触发调价,若 isDynamicPrice 仍为 1,自动注册下一次任务 scheduleNextRound(pricingId); } log.info("========== 动态调价供需检测任务执行完成 pricingId={} ==========", pricingId); } /** * 注册下一次动态调价检测任务(自我循环调度) * * @param pricingId 定价配置ID */ private void scheduleNextRound(Long pricingId) { try { XpPricing pricing = xpPricingService.getById(pricingId); if (pricing == null || !Integer.valueOf(1).equals(pricing.getIsDynamicPrice())) { log.info("动态调价循环调度停止:isDynamicPrice 已关闭 pricingId={}", pricingId); return; } if (StringUtils.isBlank(pricing.getDynamicPricingJson())) { return; } JSONObject dynamicJson = JSONObject.parseObject(pricing.getDynamicPricingJson()); Integer periodMinutes = dynamicJson.getInteger("num1"); if (periodMinutes == null || periodMinutes <= 0) { return; } String jobName = JOB_PREFIX + pricingId; Date nextTriggerTime = new Date(System.currentTimeMillis() + periodMinutes * 60_000L); Map<String, Object> jobData = new HashMap<>(); jobData.put("pricingId", pricingId); QuartzManager.addJob(DynamicPricingCheckJob.class, jobName, TimeJobType.DYNAMIC_PRICING_CHECK, nextTriggerTime, jobData); log.info("动态调价下一次任务已注册 pricingId={}, 间隔={}分钟", pricingId, periodMinutes); } catch (Exception e) { log.error("动态调价循环调度注册失败 pricingId={}", pricingId, e); } } } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpPricingServiceImpl.java
@@ -38,10 +38,14 @@ import com.xiaopeng.operation.service.IXpGeoQueryEntityService; import com.xiaopeng.operation.service.XpPointService; import com.xiaopeng.operation.service.XpPricingService; import com.xiaopeng.operation.job.base.QuartzManager; import com.xiaopeng.operation.job.base.TimeJobType; import com.xiaopeng.operation.job.jobs.DynamicPricingCheckJob; import com.xiaopeng.operation.utils.XpPricingUtil; import com.xiaopeng.operation.utils.holiday.HolidayUtil; import com.xiaopeng.operation.utils.map.FastDistanceCalculator; import com.xiaopeng.operation.vo.PricingPageListVO; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.stereotype.Service; @@ -63,6 +67,7 @@ * @author liu * @since 2026-02-03 */ @Slf4j @Service public class XpPricingServiceImpl extends ServiceImpl<XpPricingMapper, XpPricing> implements XpPricingService { @@ -762,6 +767,28 @@ update.setDynamicPricingJson(dto.getDynamicPricingJson()); update.setIsDynamicPrice(dto.getIsDynamicPrice()); this.updateById(update); // 动态调价 Quartz 任务调度 String jobName = DynamicPricingCheckJob.JOB_PREFIX + dto.getId(); if (Integer.valueOf(1).equals(dto.getIsDynamicPrice()) && StringUtils.isNotBlank(dto.getDynamicPricingJson())) { JSONObject dynamicJson = JSONObject.parseObject(dto.getDynamicPricingJson()); Integer periodMinutes = dynamicJson.getInteger("num1"); if (periodMinutes != null && periodMinutes > 0) { // 先移除可能存在的旧任务,再注册新任务(num1 分钟后首次执行) QuartzManager.removeJob(jobName, TimeJobType.DYNAMIC_PRICING_CHECK); Date triggerTime = new Date(System.currentTimeMillis() + periodMinutes * 60_000L); Map<String, Object> jobData = new HashMap<>(); jobData.put("pricingId", dto.getId()); QuartzManager.addJob(DynamicPricingCheckJob.class, jobName, TimeJobType.DYNAMIC_PRICING_CHECK, triggerTime, jobData); log.info("动态调价任务已注册 pricingId={}, 供需计算周期={}分钟", dto.getId(), periodMinutes); } } else { // 关闭动态调价时,移除已存在的任务 QuartzManager.removeJob(jobName, TimeJobType.DYNAMIC_PRICING_CHECK); log.info("动态调价任务已移除 pricingId={}", dto.getId()); } } @Override xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/controller/XpOrderController.java
@@ -141,6 +141,17 @@ return R.ok(xpOrderService.getOrderNum(dto)); } /** * 查询指定城市进行中的订单数量(status=1) * * @param cityId 开通城市ID * @return 进行中订单数量 */ @GetMapping("/inner/order/countActiveOrders") public R<Integer> countActiveOrdersByCityId(@RequestParam("cityId") Long cityId) { return R.ok(xpOrderService.countActiveOrdersByCityId(cityId)); } /** * 成功操作下发命令后调用 用于弹出redis队列的获取成功的数据 xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/IXpOrderService.java
@@ -200,6 +200,14 @@ Integer getOrderNum(GetOrderNumDto dto); /** * 查询指定城市进行中的订单数量(status=1) * * @param cityId 开通城市ID * @return 进行中订单数量 */ Integer countActiveOrdersByCityId(Long cityId); /** * 保存车辆位置信息 * @param orderTrajectory */ xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java
@@ -2547,6 +2547,14 @@ } @Override public Integer countActiveOrdersByCityId(Long cityId) { Long num = this.baseMapper.selectCount(new LambdaQueryWrapper<XpOrder>() .eq(XpOrder::getCityId, cityId) .eq(XpOrder::getStatus, 1)); return num.intValue(); } @Override public void saveVehicleLocation(OrderTrajectory orderTrajectory) { // 查询订单信息 XpOrder xpOrder = this.getById(orderTrajectory.getOrderId());