kong
2026-04-12 6e1d45b41deffba06e9c4092db07630d4fbd5078
add 路线选择
9个文件已添加
6个文件已修改
708 ■■■■■ 已修改文件
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/RouteAndFareDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/RoutePreferenceSaveDto.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/RouteStaleCheckDto.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/SelectNavigationRouteDto.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/enums/RouteStrategyKind.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/MultiRouteAndFareVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/RouteOptionVo.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/RoutePreferenceItemVo.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/RouteStaleCheckVo.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-common/xp-common-core/src/main/java/com/xiaopeng/common/core/constant/CacheConstants.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/controller/XpPricingController.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/dto/XpOrderDto.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/navigation/MultiRouteSnapshotPayload.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/XpPricingService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/impl/XpPricingServiceImpl.java 366 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/RouteAndFareDto.java
@@ -36,6 +36,4 @@
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/RoutePreferenceSaveDto.java
New file
@@ -0,0 +1,16 @@
package com.xiaopeng.api.dto.map;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
@ApiModel("保存用户路线偏好")
public class RoutePreferenceSaveDto {
    @ApiModelProperty(value = "SMART / TIME / PRICE", required = true)
    @NotBlank
    private String code;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/RouteStaleCheckDto.java
New file
@@ -0,0 +1,26 @@
package com.xiaopeng.api.dto.map;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
@ApiModel("路线/价格过期校验")
public class RouteStaleCheckDto {
    @NotNull
    @ApiModelProperty(value = "multi-route 返回的 snapshotId", required = true)
    private String snapshotId;
    @ApiModelProperty("当前选中的路径标识")
    private String pathFingerprint;
    @NotNull
    @ApiModelProperty(value = "快照生成时间(与 multi 返回的 generatedAtMillis 一致)", required = true)
    private Long clientSnapshotMillis;
    @ApiModelProperty("客户端展示的预估最终价(用于对比波动)")
    private Double clientFinalFare;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/SelectNavigationRouteDto.java
New file
@@ -0,0 +1,20 @@
package com.xiaopeng.api.dto.map;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
@ApiModel("选中某条规划路线")
public class SelectNavigationRouteDto {
    @ApiModelProperty(value = "multi-route-and-fare 返回的 snapshotId", required = true)
    @NotBlank
    private String snapshotId;
    @ApiModelProperty(value = "路线 pathFingerprint(高德 routeID)", required = true)
    @NotBlank
    private String pathFingerprint;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/enums/RouteStrategyKind.java
New file
@@ -0,0 +1,36 @@
package com.xiaopeng.api.enums;
import lombok.Getter;
/**
 * 路线策略(与高德 strategy 参数对应,见 xp_route_plan 注释)
 */
@Getter
public enum RouteStrategyKind {
    /** 智能推荐 / 高德默认 */
    SMART(32, "智能推荐", 1),
    /** 时间优先 → 速度最快 */
    TIME(38, "时间优先", 2),
    /** 价格优先 → 少收费 */
    PRICE(36, "价格优先", 3);
    private final int amapStrategy;
    private final String displayName;
    private final int priority;
    RouteStrategyKind(int amapStrategy, String displayName, int priority) {
        this.amapStrategy = amapStrategy;
        this.displayName = displayName;
        this.priority = priority;
    }
    /**
     * 主标签优先级:数值越小越优先(智能 > 时间 > 低价)
     */
    public static RouteStrategyKind primaryOf(java.util.Collection<RouteStrategyKind> kinds) {
        if (kinds == null || kinds.isEmpty()) {
            return SMART;
        }
        return kinds.stream().min((a, b) -> Integer.compare(a.priority, b.priority)).orElse(SMART);
    }
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/MultiRouteAndFareVo.java
New file
@@ -0,0 +1,33 @@
package com.xiaopeng.api.vo.operation;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("多路线规划与费用")
public class MultiRouteAndFareVo {
    @ApiModelProperty("本次规划快照 ID,选路与下单需回传")
    private String snapshotId;
    @ApiModelProperty("服务端生成快照的时间戳(毫秒)")
    private Long generatedAtMillis;
    @ApiModelProperty("是否展示「选路线」入口:去重后有效路径数≥2")
    private Boolean showRouteSelector;
    @ApiModelProperty("去重后的路线列表")
    private List<RouteOptionVo> routes;
    @ApiModelProperty("路线偏好可选项(后台配置)")
    private List<RoutePreferenceItemVo> routePreferences;
    @ApiModelProperty("停留超过该秒数后,呼叫前需做静默校验")
    private Integer staleCheckSeconds;
    @ApiModelProperty("价格变动超过该百分比则提示路况变化(如 5 表示 5%)")
    private Double priceWaveThresholdPercent;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/RouteOptionVo.java
New file
@@ -0,0 +1,40 @@
package com.xiaopeng.api.vo.operation;
import com.xiaopeng.api.vo.order.XpPricingCardVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
 * 去重后的单条可选路线(同一路径多策略合并)
 */
@Data
@ApiModel("合并后的路线选项")
public class RouteOptionVo {
    @ApiModelProperty("路径唯一标识(高德 routeID)")
    private String pathFingerprint;
    @ApiModelProperty("策略标签文案,如:智能推荐、时间优先、低价优先")
    private List<String> strategyLabels;
    @ApiModelProperty("主标签文案(色块)")
    private String primaryStrategyLabel;
    @ApiModelProperty("主策略对应的高德 strategy,用于下单/队列")
    private Integer primaryRoutePlanRule;
    @ApiModelProperty("路线几何与耗时距离")
    private RouteVo route;
    @ApiModelProperty("费用预估")
    private FareVo fareEstimate;
    @ApiModelProperty("计价明细")
    private XpPricingCardVo xpPricingCard;
    @ApiModelProperty("优惠券不可用时的提示(无可用券时)")
    private String couponMessage;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/RoutePreferenceItemVo.java
New file
@@ -0,0 +1,25 @@
package com.xiaopeng.api.vo.operation;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("路线偏好选项")
public class RoutePreferenceItemVo {
    @ApiModelProperty("策略编码 SMART/TIME/PRICE")
    private String code;
    @ApiModelProperty("展示名称")
    private String name;
    @ApiModelProperty("是否启用")
    private Boolean enabled;
    @ApiModelProperty("排序,越小越靠前")
    private Integer sortOrder;
    @ApiModelProperty("对应高德 strategy")
    private Integer amapStrategy;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/RouteStaleCheckVo.java
New file
@@ -0,0 +1,22 @@
package com.xiaopeng.api.vo.operation;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("路线过期校验结果")
public class RouteStaleCheckVo {
    @ApiModelProperty("是否需要弹窗提示路况/价格变化")
    private Boolean needRefreshNotice;
    @ApiModelProperty("提示文案")
    private String noticeMessage;
    @ApiModelProperty("若为 true,客户端应使用 latestMultiRoute 刷新 UI")
    private Boolean replacedSnapshot;
    @ApiModelProperty("最新规划快照(可选,避免客户端再请求一次)")
    private MultiRouteAndFareVo latestMultiRoute;
}
xp-robotaxi-common/xp-common-core/src/main/java/com/xiaopeng/common/core/constant/CacheConstants.java
@@ -136,4 +136,14 @@
     * 车辆挡位
     */
    public static final String GEAR_LEVEL = "gear_level:";
    /**
     * 多路线规划快照(选路、下单校验)
     */
    public static final String ORDER_MULTI_ROUTE_SNAPSHOT = "ORDER_MULTI_ROUTE_SNAPSHOT:";
    /**
     * 用户默认路线偏好(高德 strategy)
     */
    public static final String USER_ROUTE_PREFERENCE = "USER_ROUTE_PREFERENCE:";
}
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/controller/XpPricingController.java
@@ -3,6 +3,12 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.dto.map.RouteAndFareDto;
import com.xiaopeng.api.dto.map.RoutePreferenceSaveDto;
import com.xiaopeng.api.dto.map.RouteStaleCheckDto;
import com.xiaopeng.api.dto.map.SelectNavigationRouteDto;
import com.xiaopeng.api.vo.operation.MultiRouteAndFareVo;
import com.xiaopeng.api.vo.operation.RoutePreferenceItemVo;
import com.xiaopeng.api.vo.operation.RouteStaleCheckVo;
import com.xiaopeng.api.dto.operation.LonLatDto;
import com.xiaopeng.api.model.operation.XpPricing;
import com.xiaopeng.api.vo.operation.RouteAndFareVo;
@@ -24,6 +30,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.List;
/**
 * <p>
@@ -145,4 +152,47 @@
        hotFixInfoVo.setHotfixStatus(0);
        return R.ok(hotFixInfoVo);
    }
    @PostMapping("/base/v1/mini-program/navigation/multi-route-and-fare")
    @ApiOperation(value = "导航-多策略路线合并与费用", tags = {"小程序-路线选择"}, response = MultiRouteAndFareVo.class)
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<MultiRouteAndFareVo> getMultiRouteAndFare(@RequestBody @Valid RouteAndFareDto routeAndFareDto, HttpServletRequest request) {
        String uid = request.getHeader("XP-Uid");
        return R.ok(xpPricingService.getMultiRouteAndFare(routeAndFareDto, uid));
    }
    @PostMapping("/base/v1/mini-program/navigation/select-route")
    @ApiOperation(value = "选中某条路线并写入导航缓存", tags = {"小程序-路线选择"})
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<Void> selectNavigationRoute(@RequestBody @Valid SelectNavigationRouteDto dto, HttpServletRequest request) {
        String uid = request.getHeader("XP-Uid");
        xpPricingService.selectNavigationRoute(dto, uid);
        return R.ok();
    }
    @PostMapping("/base/v1/mini-program/navigation/route-stale-check")
    @ApiOperation(value = "路线/价格过期静默校验", tags = {"小程序-路线选择"}, response = RouteStaleCheckVo.class)
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<RouteStaleCheckVo> checkRouteStale(@RequestBody @Valid RouteStaleCheckDto dto, HttpServletRequest request) {
        String uid = request.getHeader("XP-Uid");
        return R.ok(xpPricingService.checkRouteStale(dto, uid));
    }
    @GetMapping("/base/v1/mini-program/navigation/route-preferences")
    @ApiOperation(value = "路线偏好可选项", tags = {"小程序-路线选择"}, response = RoutePreferenceItemVo.class)
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<List<RoutePreferenceItemVo>> listRoutePreferences(@RequestParam Long openCityId) {
        return R.ok(xpPricingService.listRoutePreferences(openCityId));
    }
    @PostMapping("/base/v1/mini-program/navigation/route-preference")
    @ApiOperation(value = "保存用户路线偏好", tags = {"小程序-路线选择"})
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<Void> saveUserRoutePreference(@RequestBody @Valid RoutePreferenceSaveDto dto, HttpServletRequest request) {
        String uid = request.getHeader("XP-Uid");
        xpPricingService.saveUserRoutePreference(dto.getCode(), uid);
        return R.ok();
    }
}
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/dto/XpOrderDto.java
@@ -52,4 +52,13 @@
    @ApiModelProperty("用户优惠券id")
    private Long userCouponId;
    @ApiModelProperty("多路线快照 ID(选路线下单时与 selectedRouteId 同时传)")
    private String navigationSnapshotId;
    @ApiModelProperty("选中的路径唯一标识(高德 routeID)")
    private String selectedRouteId;
    @ApiModelProperty("选中路线对应的高德 strategy,用于推单队列;多路线下单时由服务端写入")
    private Integer selectedRoutePlanRule;
}
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/navigation/MultiRouteSnapshotPayload.java
New file
@@ -0,0 +1,22 @@
package com.xiaopeng.itinerary.navigation;
import com.xiaopeng.api.dto.operation.LonLatDto;
import com.xiaopeng.api.vo.operation.RouteOptionVo;
import lombok.Data;
import java.util.List;
/**
 * 存入 Redis 的多路线快照
 */
@Data
public class MultiRouteSnapshotPayload {
    private String snapshotId;
    private Long generatedAtMillis;
    private String userId;
    private Long openCityId;
    private LonLatDto origin;
    private LonLatDto destination;
    private Long userCouponId;
    private List<RouteOptionVo> routes;
}
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/XpPricingService.java
@@ -3,9 +3,16 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.xiaopeng.api.dto.map.RouteAndFareDto;
import com.xiaopeng.api.dto.map.RouteStaleCheckDto;
import com.xiaopeng.api.dto.map.SelectNavigationRouteDto;
import com.xiaopeng.api.model.operation.XpPricing;
import com.xiaopeng.api.vo.operation.MultiRouteAndFareVo;
import com.xiaopeng.api.vo.operation.RouteAndFareVo;
import com.xiaopeng.api.vo.operation.RoutePreferenceItemVo;
import com.xiaopeng.api.vo.operation.RouteStaleCheckVo;
import com.xiaopeng.itinerary.dto.XpOrderDto;
import java.util.List;
/**
 * <p>
@@ -45,5 +52,29 @@
     */
    String saveOrder(XpOrderDto dto, String uid);
    /**
     * 多策略路径规划合并、计价与快照
     */
    MultiRouteAndFareVo getMultiRouteAndFare(RouteAndFareDto routeAndFareDto, String userId);
    /**
     * 用户选中某条路径后,写入导航缓存(与单路线下单一致)
     */
    void selectNavigationRoute(SelectNavigationRouteDto dto, String userId);
    /**
     * 停留超时后的静默校验
     */
    RouteStaleCheckVo checkRouteStale(RouteStaleCheckDto dto, String userId);
    /**
     * 路线偏好可选项(后台可扩展为配置表)
     */
    List<RoutePreferenceItemVo> listRoutePreferences(Long openCityId);
    /**
     * 保存用户默认路线偏好(影响后续起终点规划策略)
     */
    void saveUserRoutePreference(String code, String userId);
}
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/impl/XpPricingServiceImpl.java
@@ -9,7 +9,10 @@
import com.xiaopeng.api.dto.account.GetUserCouponDto;
import com.xiaopeng.api.dto.map.OrderRouteAndFareDto;
import com.xiaopeng.api.dto.map.RouteAndFareDto;
import com.xiaopeng.api.dto.map.RouteStaleCheckDto;
import com.xiaopeng.api.dto.map.SelectNavigationRouteDto;
import com.xiaopeng.api.dto.operation.LonLatDto;
import com.xiaopeng.api.enums.RouteStrategyKind;
import com.xiaopeng.api.dto.pushMessage.SendMessageDTO;
import com.xiaopeng.api.feignClient.account.XpAppUserClient;
import com.xiaopeng.api.feignClient.account.XpAppUserCouponClient;
@@ -21,7 +24,11 @@
import com.xiaopeng.api.model.order.XpOrder;
import com.xiaopeng.api.vo.account.UserCouponVo;
import com.xiaopeng.api.vo.operation.FareVo;
import com.xiaopeng.api.vo.operation.MultiRouteAndFareVo;
import com.xiaopeng.api.vo.operation.RouteAndFareVo;
import com.xiaopeng.api.vo.operation.RouteOptionVo;
import com.xiaopeng.api.vo.operation.RoutePreferenceItemVo;
import com.xiaopeng.api.vo.operation.RouteStaleCheckVo;
import com.xiaopeng.api.vo.operation.RouteVo;
import com.xiaopeng.api.vo.order.XpPricingCardVo;
import com.xiaopeng.common.core.constant.CacheConstants;
@@ -31,6 +38,7 @@
import com.xiaopeng.common.redis.service.RedisService;
import com.xiaopeng.itinerary.constant.I18nKeyConstant;
import com.xiaopeng.itinerary.dto.XpOrderDto;
import com.xiaopeng.itinerary.navigation.MultiRouteSnapshotPayload;
import com.xiaopeng.itinerary.mapper.*;
import com.xiaopeng.itinerary.service.IXpGeoQueryEntityService;
import com.xiaopeng.itinerary.service.XpPointService;
@@ -41,6 +49,7 @@
import com.xiaopeng.itinerary.utils.map.GDMapGeocodingUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.stereotype.Service;
@@ -124,6 +133,11 @@
    @Resource
    private XpPointClient xpPointClient;
    @Value("${navigation.route.stale-seconds:60}")
    private int navigationStaleSeconds;
    @Value("${navigation.route.price-wave-percent:5}")
    private double navigationPriceWavePercent;
    // 自定义线程池用于并行执行任务
    private final ExecutorService executorService;
@@ -208,6 +222,14 @@
        XpRoutePlan xpRoutePlan = xpRoutePlanMapper.selectOne(new LambdaQueryWrapper<XpRoutePlan>().eq(XpRoutePlan::getAuditStatus, 2).eq(XpRoutePlan::getCityCode, cityCode));
        if (xpRoutePlan != null) {
            routeAndFareDto.setRoutePlanRule(xpRoutePlan.getRoutePlanRule());
        }
        Object userPref = redisService.getCacheObject(CacheConstants.USER_ROUTE_PREFERENCE + userId);
        if (userPref != null) {
            try {
                routeAndFareDto.setRoutePlanRule(Integer.valueOf(userPref.toString()));
            } catch (Exception ignored) {
                // keep city default
            }
        }
        XpPricing xpPricing = this.baseMapper.selectOne(new LambdaQueryWrapper<XpPricing>().eq(XpPricing::getCityId, routeAndFareDto.getOpenCityId()));
@@ -561,12 +583,34 @@
        Integer distance = 0;
        Integer duration = 0;
        Object cacheObject = redisService.getCacheObject("ORDER_NAVIGATION_END_DATA:" + uid);
        if (cacheObject != null) {
            RouteVo routeVo = JSON.parseObject(cacheObject.toString(), RouteVo.class);
            distance = routeVo.getDistance();
            duration = routeVo.getDuration();
        if (dto.getNavigationSnapshotId() != null && dto.getSelectedRouteId() != null) {
            String snap = redisService.getCacheObject(CacheConstants.ORDER_MULTI_ROUTE_SNAPSHOT + uid + ":" + dto.getNavigationSnapshotId());
            if (snap != null) {
                MultiRouteSnapshotPayload snapPayload = JSON.parseObject(snap, MultiRouteSnapshotPayload.class);
                RouteOptionVo opt = snapPayload.getRoutes().stream()
                        .filter(r2 -> dto.getSelectedRouteId().equals(r2.getPathFingerprint()))
                        .findFirst()
                        .orElse(null);
                if (opt != null && opt.getRoute() != null) {
                    distance = opt.getRoute().getDistance();
                    duration = opt.getRoute().getDuration();
                    redisService.setCacheObject("ORDER_NAVIGATION_END_DATA:" + uid, JSON.toJSONString(opt.getRoute()), 24L, TimeUnit.HOURS);
                    if (dto.getSelectedRoutePlanRule() == null) {
                        dto.setSelectedRoutePlanRule(opt.getPrimaryRoutePlanRule());
                    }
                }
            }
        }
        if (distance == 0 && duration == 0) {
            Object cacheObject = redisService.getCacheObject("ORDER_NAVIGATION_END_DATA:" + uid);
            if (cacheObject != null) {
                RouteVo routeVo = JSON.parseObject(cacheObject.toString(), RouteVo.class);
                distance = routeVo.getDistance();
                duration = routeVo.getDuration();
            }
        }
        final Integer planRuleForQueue = dto.getSelectedRoutePlanRule() != null ? dto.getSelectedRoutePlanRule()
                : (xpRoutePlan != null ? xpRoutePlan.getRoutePlanRule() : null);
        xpOrder.setCouponId(dto.getUserCouponId());
        // 产生订单
        R<Long> addOrderR = xpOrderClient.addOrder(xpOrder);
@@ -600,7 +644,7 @@
            // 和上车点进行判断是否<maxPushDistance
            double v = FastDistanceCalculator.distanceFast(Double.parseDouble(lonLat.split(",")[1]), Double.parseDouble(lonLat.split(",")[0]), xpPoint.getPointLat(), xpPoint.getPointLon());
            if (v < maxPushDistance) {
                String value = orderId + "_" + xpPoint.getPointLon() + "," + xpPoint.getPointLat() + "_0" + "_" + uid + "_" + xpOrder.getOrderNumber() + "_" + distance + "_" + duration + "_" + xpRoutePlan.getRoutePlanRule();
                String value = orderId + "_" + xpPoint.getPointLon() + "," + xpPoint.getPointLat() + "_0" + "_" + uid + "_" + xpOrder.getOrderNumber() + "_" + distance + "_" + duration + "_" + planRuleForQueue;
                if (isPriorityDispatch) {
                    // 头部  value 订单_经纬度_状态_用户id_订单编号_距离_时间
                    redisService.addToHead(s, value);
@@ -628,11 +672,10 @@
        }
        String key = CacheConstants.QUEUE_POINT_ID + "-" + xpPoint.getId() + "-" + xpPoint.getPointLon() + "," + xpPoint.getPointLat();
        if(xpRoutePlan==null){
        if (planRuleForQueue == null) {
            redisService.setCacheList(key, Arrays.asList(orderId + "_" + xpPoint.getPointLon() + "," + xpPoint.getPointLat() + "_0" + "_" + uid + "_" + xpOrder.getOrderNumber() + "_" + distance + "_" + duration + "_" + null));
        }else {
            redisService.setCacheList(key, Arrays.asList(orderId + "_" + xpPoint.getPointLon() + "," + xpPoint.getPointLat() + "_0" + "_" + uid + "_" + xpOrder.getOrderNumber() + "_" + distance + "_" + duration + "_" + xpRoutePlan.getRoutePlanRule()));
        } else {
            redisService.setCacheList(key, Arrays.asList(orderId + "_" + xpPoint.getPointLon() + "," + xpPoint.getPointLat() + "_0" + "_" + uid + "_" + xpOrder.getOrderNumber() + "_" + distance + "_" + duration + "_" + planRuleForQueue));
        }
        log.info("订单推单:{}   {}", key, redisService.getCacheList(key));
        xpOrder.setQueueName(key);
@@ -696,5 +739,308 @@
    }
    /**
     * 单次路径计价(不含区域诊断、不写导航缓存),供多路线合并调用。
     */
    private RouteAndFareVo pricingForRoute(RouteAndFareDto routeAndFareDto, RouteVo routeVo, XpPricing xpPricing, String userId) {
        XpPricingCardVo xpPricingCardVo;
        if (xpPricing.getPricingType() == 1) {
            xpPricingCardVo = xpPricingUtil.calculationPrice(userId,
                    JSON.parseObject(xpPricing.getBasicPricingJson()),
                    JSON.parseArray(xpPricing.getStartingFareJson()),
                    routeVo.getDistance(), routeVo.getDuration(), 0, 0.0, xpPricing.getIsDynamicPrice(), routeAndFareDto.getOpenCityId(), xpPricing.getDynamicPricingJson()
            );
        } else {
            xpPricingCardVo = xpPricingUtil.calculationFixedPrice(userId,
                    JSON.parseObject(xpPricing.getBasicPricingJson()),
                    JSON.parseArray(xpPricing.getStartingFareJson()),
                    routeVo.getDistance(), routeVo.getDuration(), 0, 0.0, xpPricing.getIsDynamicPrice(), routeAndFareDto.getOpenCityId(), xpPricing.getDynamicPricingJson()
            );
        }
        R<UserCouponVo> coupon = null;
        CompletableFuture<R<UserCouponVo>> couponFuture = null;
        if (routeAndFareDto.getUserCouponId() != null) {
            coupon = xpAppUserCouponClient.getUserCoupon(
                    new GetUserCouponDto(Long.valueOf(userId), xpPricingCardVo.getTotalPrice(), routeAndFareDto.getOpenCityId(), routeAndFareDto.getUserCouponId()));
            if (coupon.getCode() == 200 && coupon.getData().getStatus() == 2) {
                throw new ServiceException("优惠券不满足条件", I18nKeyConstant.COUPON_NOT_SATISFY);
            }
        } else {
            couponFuture = CompletableFuture.supplyAsync(
                    () -> xpAppUserCouponClient.getUserCoupon(
                            new GetUserCouponDto(Long.valueOf(userId), xpPricingCardVo.getTotalPrice(), routeAndFareDto.getOpenCityId(), null)),
                    executorService);
        }
        R<UserCouponVo> couponResult;
        if (routeAndFareDto.getUserCouponId() != null) {
            couponResult = coupon;
        } else {
            couponResult = couponFuture.join();
        }
        Integer minOrderDistance;
        Object minOrderDistanceObj = redisService.getCacheObject(CacheConstants.MIN_ORDER_DISTANCE);
        if (minOrderDistanceObj != null) {
            minOrderDistance = Integer.valueOf(minOrderDistanceObj.toString());
        } else {
            minOrderDistance = systemSettingMapper.selectOne(null).getMinOrderDistance();
        }
        if (routeVo.getDistance() < minOrderDistance) {
            throw new ServiceException("当前上下车点距离过近,建议步行前往", I18nKeyConstant.TO_WORK);
        }
        if (couponResult.getCode() != R.SUCCESS) {
            throw new RuntimeException(couponResult.getMsg());
        }
        UserCouponVo userCouponVo = couponResult.getData();
        if (userCouponVo != null) {
            Double money = userCouponVo.getMoney();
            money = new BigDecimal(money).setScale(2, RoundingMode.HALF_UP).doubleValue();
            xpPricingCardVo.setCoupon(String.valueOf(money));
            if (xpPricingCardVo.getTotalPrice() > money) {
                xpPricingCardVo.setSystemDiscount(xpPricingCardVo.getTotalPrice() - money);
            }
        }
        RouteAndFareVo routeAndFareVo = new RouteAndFareVo();
        routeAndFareVo.setRoute(routeVo);
        xpPricingCardVo.setTotalPriceStr(new BigDecimal(xpPricingCardVo.getTotalPrice()).setScale(2, RoundingMode.HALF_UP).toString());
        xpPricingCardVo.setStartPriceStr(new BigDecimal(xpPricingCardVo.getStartPrice()).setScale(2, RoundingMode.HALF_UP).toString());
        xpPricingCardVo.setDistancePriceStr(new BigDecimal(xpPricingCardVo.getDistancePrice()).setScale(2, RoundingMode.HALF_UP).toString());
        xpPricingCardVo.setDurationPriceStr(new BigDecimal(xpPricingCardVo.getDurationPrice()).setScale(2, RoundingMode.HALF_UP).toString());
        xpPricingCardVo.setSystemDiscountStr(new BigDecimal(xpPricingCardVo.getSystemDiscount()).setScale(2, RoundingMode.HALF_UP).toString());
        routeAndFareVo.setXpPricingCard(xpPricingCardVo);
        FareVo fareVo = new FareVo();
        fareVo.setEstimatedDistanceKm(routeVo.getDistance().doubleValue() / 1000);
        fareVo.setEstimatedDurationMin(routeVo.getDuration() / 60);
        fareVo.setOriginalFare(xpPricingCardVo.getTotalPrice());
        fareVo.setOriginalFareStr(new BigDecimal(fareVo.getOriginalFare()).setScale(2, RoundingMode.HALF_UP).toString());
        if (userCouponVo != null) {
            if (userCouponVo.getMoney() > fareVo.getOriginalFare()) {
                fareVo.setDiscountAmount(fareVo.getOriginalFare());
                fareVo.setFinalFare(0.00);
            } else {
                fareVo.setDiscountAmount(userCouponVo.getMoney());
                fareVo.setFinalFare(fareVo.getOriginalFare() - fareVo.getDiscountAmount());
            }
            if (fareVo.getFinalFare() > 0) {
                fareVo.setSystemDiscount(fareVo.getFinalFare());
                fareVo.setFinalFare(0.00);
            }
            fareVo.setCouponId(userCouponVo.getId());
        } else {
            fareVo.setDiscountAmount(0.00);
            fareVo.setFinalFare(fareVo.getOriginalFare());
            fareVo.setSystemDiscount(fareVo.getFinalFare());
            fareVo.setFinalFare(0.00);
        }
        Integer waitTime = getWaitTime(routeAndFareDto.getOrigin().getLongitude(), routeAndFareDto.getOrigin().getLatitude());
        fareVo.setWaitingTime(waitTime);
        fareVo.setFinalFareStr(new BigDecimal(fareVo.getFinalFare()).setScale(2, RoundingMode.HALF_UP).toString());
        fareVo.setOriginalFare(new BigDecimal(fareVo.getOriginalFare()).setScale(2, RoundingMode.HALF_UP).doubleValue());
        fareVo.setSystemDiscount(new BigDecimal(fareVo.getSystemDiscount() == null ? 0 : fareVo.getSystemDiscount()).setScale(2, RoundingMode.HALF_UP).doubleValue());
        routeAndFareVo.setFareEstimate(fareVo);
        routeAndFareVo.getXpPricingCard().setFinalFareStr(fareVo.getFinalFareStr());
        fareVo.setId(null);
        return routeAndFareVo;
    }
    @Override
    public MultiRouteAndFareVo getMultiRouteAndFare(RouteAndFareDto routeAndFareDto, String userId) {
        Map<String, String> geocode = gdMapGeocodingUtil.geocode(routeAndFareDto.getOrigin().getLongitude().toString(), routeAndFareDto.getOrigin().getLatitude().toString());
        if (null == geocode) {
            throw new ServiceException("获取城市信息失败", I18nKeyConstant.CITY_NOT_FOUND);
        }
        Long cityCode = routeAndFareDto.getOpenCityId();
        XpOpenCity openCities = xpOpenCityMapper.selectById(cityCode);
        if (openCities == null) {
            throw new ServiceException("获取城市信息失败", I18nKeyConstant.CITY_NOT_FOUND);
        }
        routeAndFareDto.setOpenCityId(openCities.getId());
        XpPricing xpPricing = this.baseMapper.selectOne(new LambdaQueryWrapper<XpPricing>().eq(XpPricing::getCityId, routeAndFareDto.getOpenCityId()));
        if (xpPricing == null) {
            throw new ServiceException("当前区域未开通服务", I18nKeyConstant.NO_CURRENT_REGION);
        }
        R<Boolean> diagnosisResult = xpMapClient.diagnosisArea(routeAndFareDto);
        if (diagnosisResult.getCode() != R.SUCCESS) {
            throw new RuntimeException(diagnosisResult.getMsg());
        }
        if (!Boolean.TRUE.equals(diagnosisResult.getData())) {
            throw new ServiceException("当前区域未开通服务", I18nKeyConstant.NO_CURRENT_REGION);
        }
        List<RouteStrategyKind> kinds = Arrays.asList(RouteStrategyKind.SMART, RouteStrategyKind.TIME, RouteStrategyKind.PRICE);
        Map<String, RouteVo> routeById = new LinkedHashMap<>();
        Map<String, Set<RouteStrategyKind>> strategiesById = new LinkedHashMap<>();
        for (RouteStrategyKind kind : kinds) {
            RouteAndFareDto copy = new RouteAndFareDto();
            copy.setOrigin(routeAndFareDto.getOrigin());
            copy.setDestination(routeAndFareDto.getDestination());
            copy.setOpenCityId(routeAndFareDto.getOpenCityId());
            copy.setUserCouponId(routeAndFareDto.getUserCouponId());
            copy.setVehicleId(routeAndFareDto.getVehicleId());
            copy.setRoutePlanRule(kind.getAmapStrategy());
            R<RouteVo> r = xpMapClient.getDistanceAndDuration(copy);
            if (r.getCode() != 200 || r.getData() == null || r.getData().getRouteID() == null) {
                continue;
            }
            String fid = r.getData().getRouteID();
            routeById.putIfAbsent(fid, r.getData());
            strategiesById.computeIfAbsent(fid, k -> new LinkedHashSet<>()).add(kind);
        }
        if (routeById.isEmpty()) {
            throw new ServiceException("获取导肮信息失败", I18nKeyConstant.PATH_NOT_FOUND);
        }
        List<RouteOptionVo> options = new ArrayList<>();
        for (Map.Entry<String, RouteVo> e : routeById.entrySet()) {
            String fid = e.getKey();
            RouteVo rv = e.getValue();
            Set<RouteStrategyKind> kindsOnRoute = strategiesById.get(fid);
            RouteStrategyKind primary = RouteStrategyKind.primaryOf(kindsOnRoute);
            RouteAndFareDto pricingDto = new RouteAndFareDto();
            pricingDto.setOrigin(routeAndFareDto.getOrigin());
            pricingDto.setDestination(routeAndFareDto.getDestination());
            pricingDto.setOpenCityId(routeAndFareDto.getOpenCityId());
            pricingDto.setUserCouponId(routeAndFareDto.getUserCouponId());
            pricingDto.setVehicleId(routeAndFareDto.getVehicleId());
            pricingDto.setRoutePlanRule(primary.getAmapStrategy());
            RouteAndFareVo part = pricingForRoute(pricingDto, rv, xpPricing, userId);
            RouteOptionVo opt = new RouteOptionVo();
            opt.setPathFingerprint(fid);
            List<String> labels = new ArrayList<>();
            for (RouteStrategyKind k : Arrays.asList(RouteStrategyKind.SMART, RouteStrategyKind.TIME, RouteStrategyKind.PRICE)) {
                if (kindsOnRoute.contains(k)) {
                    labels.add(k.getDisplayName());
                }
            }
            opt.setStrategyLabels(labels);
            opt.setPrimaryStrategyLabel(primary.getDisplayName());
            opt.setPrimaryRoutePlanRule(primary.getAmapStrategy());
            opt.setRoute(part.getRoute());
            opt.setFareEstimate(part.getFareEstimate());
            opt.setXpPricingCard(part.getXpPricingCard());
            if (part.getFareEstimate() != null && part.getFareEstimate().getCouponId() == null) {
                opt.setCouponMessage("当前路线不满足优惠券使用条件");
            }
            options.add(opt);
        }
        String snapshotId = UUID.randomUUID().toString().replace("-", "");
        long now = System.currentTimeMillis();
        MultiRouteSnapshotPayload payload = new MultiRouteSnapshotPayload();
        payload.setSnapshotId(snapshotId);
        payload.setGeneratedAtMillis(now);
        payload.setUserId(userId);
        payload.setOpenCityId(routeAndFareDto.getOpenCityId());
        payload.setOrigin(routeAndFareDto.getOrigin());
        payload.setDestination(routeAndFareDto.getDestination());
        payload.setUserCouponId(routeAndFareDto.getUserCouponId());
        payload.setRoutes(options);
        redisService.setCacheObject(CacheConstants.ORDER_MULTI_ROUTE_SNAPSHOT + userId + ":" + snapshotId, JSON.toJSONString(payload), 24L, TimeUnit.HOURS);
        MultiRouteAndFareVo vo = new MultiRouteAndFareVo();
        vo.setSnapshotId(snapshotId);
        vo.setGeneratedAtMillis(now);
        vo.setShowRouteSelector(options.size() >= 2);
        vo.setRoutes(options);
        vo.setRoutePreferences(listRoutePreferences(routeAndFareDto.getOpenCityId()));
        vo.setStaleCheckSeconds(navigationStaleSeconds);
        vo.setPriceWaveThresholdPercent(navigationPriceWavePercent);
        RouteOptionVo first = options.get(0);
        redisService.setCacheObject("ORDER_NAVIGATION_END_DATA:" + userId, JSON.toJSONString(first.getRoute()), 24L, TimeUnit.HOURS);
        return vo;
    }
    @Override
    public void selectNavigationRoute(SelectNavigationRouteDto dto, String userId) {
        String json = redisService.getCacheObject(CacheConstants.ORDER_MULTI_ROUTE_SNAPSHOT + userId + ":" + dto.getSnapshotId());
        if (json == null) {
            throw new ServiceException("路线已失效,请重新规划", I18nKeyConstant.PATH_NOT_FOUND);
        }
        MultiRouteSnapshotPayload payload = JSON.parseObject(json, MultiRouteSnapshotPayload.class);
        RouteOptionVo selected = payload.getRoutes().stream()
                .filter(r -> dto.getPathFingerprint().equals(r.getPathFingerprint()))
                .findFirst()
                .orElseThrow(() -> new ServiceException("未找到对应路线", I18nKeyConstant.PATH_NOT_FOUND));
        redisService.setCacheObject("ORDER_NAVIGATION_END_DATA:" + userId, JSON.toJSONString(selected.getRoute()), 24L, TimeUnit.HOURS);
        String id = CacheConstants.ORD + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8);
        if (selected.getXpPricingCard() != null) {
            redisService.setCacheObject(CacheConstants.ORD + id + userId, selected.getXpPricingCard(), 1L, TimeUnit.DAYS);
        }
        if (selected.getFareEstimate() != null) {
            selected.getFareEstimate().setId(id);
        }
    }
    @Override
    public RouteStaleCheckVo checkRouteStale(RouteStaleCheckDto dto, String userId) {
        RouteStaleCheckVo out = new RouteStaleCheckVo();
        out.setNeedRefreshNotice(false);
        long elapsed = System.currentTimeMillis() - dto.getClientSnapshotMillis();
        if (elapsed <= navigationStaleSeconds * 1000L) {
            return out;
        }
        String snapJson = redisService.getCacheObject(CacheConstants.ORDER_MULTI_ROUTE_SNAPSHOT + userId + ":" + dto.getSnapshotId());
        if (snapJson == null) {
            out.setNeedRefreshNotice(true);
            out.setNoticeMessage("路况已变,价格已更新");
            out.setReplacedSnapshot(true);
            return out;
        }
        MultiRouteSnapshotPayload oldSnap = JSON.parseObject(snapJson, MultiRouteSnapshotPayload.class);
        RouteAndFareDto refreshDto = new RouteAndFareDto();
        refreshDto.setOrigin(oldSnap.getOrigin());
        refreshDto.setDestination(oldSnap.getDestination());
        refreshDto.setOpenCityId(oldSnap.getOpenCityId());
        refreshDto.setUserCouponId(oldSnap.getUserCouponId());
        MultiRouteAndFareVo fresh = getMultiRouteAndFare(refreshDto, userId);
        out.setLatestMultiRoute(fresh);
        if (dto.getClientFinalFare() == null || fresh.getRoutes() == null) {
            return out;
        }
        String fp = dto.getPathFingerprint();
        RouteOptionVo newOpt = fresh.getRoutes().stream()
                .filter(r -> fp == null || fp.equals(r.getPathFingerprint()))
                .findFirst()
                .orElse(fresh.getRoutes().get(0));
        Double nf = newOpt.getFareEstimate() != null ? newOpt.getFareEstimate().getFinalFare() : null;
        if (nf == null) {
            return out;
        }
        double diff = Math.abs(nf - dto.getClientFinalFare()) / Math.max(1e-6, dto.getClientFinalFare()) * 100.0;
        if (diff > navigationPriceWavePercent) {
            out.setNeedRefreshNotice(true);
            out.setNoticeMessage("路况已变,价格已更新");
            out.setReplacedSnapshot(true);
        }
        return out;
    }
    @Override
    public List<RoutePreferenceItemVo> listRoutePreferences(Long openCityId) {
        List<RoutePreferenceItemVo> list = new ArrayList<>();
        int i = 0;
        for (RouteStrategyKind k : Arrays.asList(RouteStrategyKind.SMART, RouteStrategyKind.TIME, RouteStrategyKind.PRICE)) {
            RoutePreferenceItemVo v = new RoutePreferenceItemVo();
            v.setCode(k.name());
            v.setName(k.getDisplayName());
            v.setEnabled(true);
            v.setSortOrder(i++);
            v.setAmapStrategy(k.getAmapStrategy());
            list.add(v);
        }
        return list;
    }
    @Override
    public void saveUserRoutePreference(String code, String userId) {
        RouteStrategyKind k;
        try {
            k = RouteStrategyKind.valueOf(code.trim().toUpperCase(Locale.ROOT));
        } catch (Exception e) {
            throw new ServiceException("无效的路线偏好编码", I18nKeyConstant.PATH_NOT_FOUND);
        }
        redisService.setCacheObject(CacheConstants.USER_ROUTE_PREFERENCE + userId, String.valueOf(k.getAmapStrategy()), 365L, TimeUnit.DAYS);
    }
}