“mitao”
2026-04-12 f924bf7ef0a34724c12058d32e2e28fd73e1fa9f
feat:APP快捷胶囊、场站基础模型代码
5个文件已添加
15个文件已修改
550 ■■■■■ 已修改文件
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/PointAddDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/PointUpdateDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/operation/XpSystemSettingClientFallbackFactory.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/operation/XpSystemSettingClient.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpPoint.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpStation.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpStationMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpStationService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpStationServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/mapper/XpPointMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/mapper/XpStationMapper.xml 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpSystemSettingController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/XpSystemSettingService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpSystemSettingServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/controller/XpOrderController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/mapper/XpOrderMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/IXpOrderService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/resources/mapper/order/XpOrderMapper.xml 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/PointAddDto.java
@@ -90,4 +90,10 @@
    @ApiModelProperty(value = "区县code")
    private String districtCode;
    @ApiModelProperty(value = "所属场站id,为空表示运营区直属点位")
    private Long stationId;
    @ApiModelProperty(value = "点位在场站内排序")
    private Integer stationSortNo;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/PointUpdateDto.java
@@ -90,4 +90,10 @@
    @ApiModelProperty(value = "区县code")
    private String districtCode;
    @ApiModelProperty(value = "所属场站id,为空表示运营区直属点位")
    private Long stationId;
    @ApiModelProperty(value = "点位在场站内排序")
    private Integer stationSortNo;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/operation/XpSystemSettingClientFallbackFactory.java
@@ -1,8 +1,6 @@
package com.xiaopeng.api.factory.operation;
import com.xiaopeng.api.feignClient.operation.SysLogClient;
import com.xiaopeng.api.feignClient.operation.XpSystemSettingClient;
import com.xiaopeng.api.model.operation.SysOperLog;
import com.xiaopeng.api.model.operation.XpFeedbackType;
import com.xiaopeng.common.core.domain.R;
import lombok.extern.slf4j.Slf4j;
@@ -22,6 +20,12 @@
            }
            @Override
            public R<Integer> getMinOrderDistance() {
                log.error("获取最小下单距离失败:{}", cause.getMessage());
                return R.fail("获取最小下单距离失败");
            }
            @Override
            public R<Integer> getOrderFinishTime() {
                log.error("获取自动结束行程时间失败:{}", cause.getMessage());
                return R.fail("获取自动结束行程时间失败,"+cause.getMessage());
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/operation/XpSystemSettingClient.java
@@ -6,9 +6,7 @@
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.Mapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@FeignClient(contextId = "XpSystemSettingClient", value = ServiceNameConstantsEnum.XP_OPERATION_SERVICE, fallbackFactory = XpSystemSettingClientFallbackFactory.class)
public interface XpSystemSettingClient {
@@ -17,6 +15,9 @@
    @GetMapping("/base/v1/support/getOrderDistance")
    R getOrderDistance();
    @GetMapping("/logan/rt/v1/getMinOrderDistance")
    R<Integer> getMinOrderDistance();
    /**
     * 获取自动结束行程时间
     * @return
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpPoint.java
@@ -120,4 +120,12 @@
    @TableField("district_code")
    private String districtCode;
    @ApiModelProperty(value = "所属场站id,为空表示运营区直属点位")
    @TableField("station_id")
    private Long stationId;
    @ApiModelProperty(value = "点位在场站内排序")
    @TableField("station_sort_no")
    private Integer stationSortNo;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpStation.java
New file
@@ -0,0 +1,112 @@
package com.xiaopeng.api.model.operation;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xiaopeng.common.core.web.domain.BaseModel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * 场站
 * </p>
 *
 * @author mitao
 * @since 2026-04-12
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("xp_station")
@ApiModel(value = "XpStation对象", description = "场站")
public class XpStation extends BaseModel {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "场站名称,C端展示主标题")
    private String stationName;
    @ApiModelProperty(value = "场站编号唯一 规则:城市首字母-HUB-4位流水号")
    private String stationNumber;
    @ApiModelProperty(value = "场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效")
    private Integer status;
    @ApiModelProperty(value = "城市code")
    private String cityCode;
    @ApiModelProperty(value = "城市名称")
    private String cityName;
    @ApiModelProperty(value = "所属电子围栏id")
    private Long electronicId;
    @ApiModelProperty(value = "所属运营区id")
    private Long electronicAreaId;
    @ApiModelProperty(value = "默认展示点位id")
    private Long defaultPointId;
    @ApiModelProperty(value = "场站面积")
    private BigDecimal stationArea;
    @ApiModelProperty(value = "场站围栏点位 经度_纬度,经度_纬度")
    private String fencePoints;
    @ApiModelProperty(value = "中心点经度")
    private Double centerLon;
    @ApiModelProperty(value = "中心点纬度")
    private Double centerLat;
    @ApiModelProperty(value = "限停时长")
    private Integer parkingTimeLimit;
    @ApiModelProperty(value = "运营类型 1=与运营区同步 2=自定义时间段")
    private Integer operationType;
    @ApiModelProperty(value = "生效周期 1=每日 2=仅工作日 3=仅非工作日 4=仅节假日")
    private Integer effectivePeriod;
    @ApiModelProperty(value = "生效时间段")
    private String effectiveTimePeriod;
    @ApiModelProperty(value = "是否同步运营信息到子点位 0=否 1=是")
    private Integer syncToPoints;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "停用原因")
    private String stopReason;
    @ApiModelProperty(value = "测试不通过原因")
    private String testFailReason;
    @ApiModelProperty(value = "计划生效时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime effectTime;
    @ApiModelProperty(value = "场站内排序")
    private Integer sortNo;
    @ApiModelProperty(value = "点位总数")
    @TableField(exist = false)
    private Integer pointCount;
    @ApiModelProperty(value = "运营中点位数")
    @TableField(exist = false)
    private Integer operatingPointCount;
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java
@@ -251,6 +251,8 @@
                .set(XpPoint::getCityName, xpPoint.getCityName())
                .set(XpPoint::getDistrictName, xpPoint.getDistrictName())
                .set(XpPoint::getDistrictCode, xpPoint.getDistrictCode())
                .set(XpPoint::getStationId, xpPoint.getStationId())
                .set(XpPoint::getStationSortNo, xpPoint.getStationSortNo())
                .update();
        return R.ok();
    }
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpStationMapper.java
New file
@@ -0,0 +1,16 @@
package com.xiaopeng.map.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiaopeng.api.model.operation.XpStation;
/**
 * <p>
 * 场站 Mapper 接口
 * </p>
 *
 * @author mitao
 * @since 2026-04-12
 */
public interface XpStationMapper extends BaseMapper<XpStation> {
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpStationService.java
New file
@@ -0,0 +1,16 @@
package com.xiaopeng.map.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xiaopeng.api.model.operation.XpStation;
/**
 * <p>
 * 场站 服务类
 * </p>
 *
 * @author mitao
 * @since 2026-04-12
 */
public interface XpStationService extends IService<XpStation> {
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpStationServiceImpl.java
New file
@@ -0,0 +1,20 @@
package com.xiaopeng.map.service.serviceimpl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiaopeng.api.model.operation.XpStation;
import com.xiaopeng.map.mapper.XpStationMapper;
import com.xiaopeng.map.service.XpStationService;
import org.springframework.stereotype.Service;
/**
 * <p>
 * 场站 服务实现类
 * </p>
 *
 * @author mitao
 * @since 2026-04-12
 */
@Service
public class XpStationServiceImpl extends ServiceImpl<XpStationMapper, XpStation> implements XpStationService {
}
xp-robotaxi-map-boot/src/main/resources/mapper/XpPointMapper.xml
@@ -24,11 +24,13 @@
        <result column="update_time" property="updateTime" />
        <result column="create_by" property="createBy" />
        <result column="update_by" property="updateBy" />
        <result column="station_id" property="stationId" />
        <result column="station_sort_no" property="stationSortNo" />
    </resultMap>
    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id,electronic_area_id, point_name, point_poi, point_number, operation_status, point_type, verification_status, parking_time_limit, operation_type, effective_period, effective_time_period, remarks, point_pictures, point_lon, point_lat, is_deleted, create_time, update_time, create_by, update_by
        id,electronic_area_id, point_name, point_poi, point_number, operation_status, point_type, verification_status, parking_time_limit, operation_type, effective_period, effective_time_period, remarks, point_pictures, point_lon, point_lat, is_deleted, create_time, update_time, create_by, update_by, station_id, station_sort_no
    </sql>
    <select id="pointMap" resultType="com.xiaopeng.api.model.operation.XpPoint">
        select t1.*,
@@ -121,6 +123,7 @@
               xp.point_address as pointAddress,
               xp.point_lon as pointLon,xp.point_lat as pointLat,xp.create_time as createTime,xp.create_by as createBy
        ,xp.parking_time_limit as parkingTimeLimit,xp.verification_status as verificationStatus,xp.operation_status as operationStatus
        ,xp.station_id as stationId,xp.station_sort_no as stationSortNo
        from xp_point xp
        where xp.is_deleted = 0
        <if test="query.pointName != null and query.pointName != ''">
xp-robotaxi-map-boot/src/main/resources/mapper/XpStationMapper.xml
New file
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaopeng.map.mapper.XpStationMapper">
    <resultMap id="BaseResultMap" type="com.xiaopeng.api.model.operation.XpStation">
        <id column="id" property="id"/>
        <result column="station_name" property="stationName"/>
        <result column="station_number" property="stationNumber"/>
        <result column="status" property="status"/>
        <result column="city_code" property="cityCode"/>
        <result column="city_name" property="cityName"/>
        <result column="electronic_id" property="electronicId"/>
        <result column="electronic_area_id" property="electronicAreaId"/>
        <result column="default_point_id" property="defaultPointId"/>
        <result column="station_area" property="stationArea"/>
        <result column="fence_points" property="fencePoints"/>
        <result column="center_lon" property="centerLon"/>
        <result column="center_lat" property="centerLat"/>
        <result column="parking_time_limit" property="parkingTimeLimit"/>
        <result column="operation_type" property="operationType"/>
        <result column="effective_period" property="effectivePeriod"/>
        <result column="effective_time_period" property="effectiveTimePeriod"/>
        <result column="sync_to_points" property="syncToPoints"/>
        <result column="remark" property="remark"/>
        <result column="stop_reason" property="stopReason"/>
        <result column="test_fail_reason" property="testFailReason"/>
        <result column="effect_time" property="effectTime"/>
        <result column="sort_no" property="sortNo"/>
        <result column="is_deleted" property="deleted"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
        <result column="create_by" property="createBy"/>
        <result column="update_by" property="updateBy"/>
    </resultMap>
    <sql id="Base_Column_List">
        id, station_name, station_number, status, city_code, city_name, electronic_id, electronic_area_id,
        default_point_id, station_area, fence_points, center_lon, center_lat, parking_time_limit, operation_type,
        effective_period, effective_time_period, sync_to_points, remark, stop_reason, test_fail_reason, effect_time,
        sort_no, is_deleted, create_time, update_time, create_by, update_by
    </sql>
</mapper>
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpSystemSettingController.java
@@ -70,6 +70,12 @@
        return R.ok(xpSystemSettingService.getOrderDistance());
    }
    @GetMapping("/logan/rt/v1/getMinOrderDistance")
    @ApiOperation(value = "获取最小下单距离", notes = "内部服务调用,返回起终点允许发单的最小距离阈值,单位米", tags = {"内部接口-获取最小下单距离"}, response = Integer.class)
    public R<Integer> getMinOrderDistance() {
        return R.ok(xpSystemSettingService.getMinOrderDistance());
    }
    @ApiOperation(value = "获取自动结束行程时间",tags = {"小程序-业务流程"},response = Integer.class)
    @GetMapping("/api/v1/mini-program/support/getOrderFinishTime")
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/XpSystemSettingService.java
@@ -26,6 +26,12 @@
     */
    Integer getOrderDistance();
    /**
     * 获取起终点最小下单距离
     * @return
     */
    Integer getMinOrderDistance();
    /**
     * 获取自动结束行程时间
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpSystemSettingServiceImpl.java
@@ -46,6 +46,15 @@
    }
    @Override
    public Integer getMinOrderDistance() {
        XpSystemSetting xpSystemSetting = this.getOne(null);
        if (xpSystemSetting != null) {
            return xpSystemSetting.getMinOrderDistance();
        }
        return 0;
    }
    @Override
    public Integer getOrderFinishTime() {
        XpSystemSetting xpSystemSetting = this.getOne(null);
        if (xpSystemSetting != null) {
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/controller/XpOrderController.java
@@ -321,13 +321,16 @@
    /**
     * 获取用户快捷胶囊 历史 时段 终点高点
     */
    @ApiOperation(value = "获取用户快捷胶囊 历史 时段 终点高点", tags = {"小程序-获取用户快捷胶囊 历史 时段 终点高点"})
    @ApiOperation(value = "获取首页快捷胶囊推荐站点", notes = "根据当前起点和用户近30天有效订单,按场景优先推荐目的地;无匹配时返回空数据", tags = {"小程序-首页快捷胶囊"}, response = XpPointInfoVo.class)
    @ApiImplicitParams({
            @ApiImplicitParam(name = "startPointId", value = "当前首页起点站点ID", required = true, dataType = "Long", paramType = "query")
    })
    @GetMapping("/api/v1/mini-program/order/getUserQuickCapsule")
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<XpPointInfoVo> getUserQuickCapsule(HttpServletRequest request) {
    public R<XpPointInfoVo> getUserQuickCapsule(@RequestParam Long startPointId, HttpServletRequest request) {
        // uid已通过拦截器otp校验,可以直接使用
        String uid = request.getHeader("XP-Uid");
        return R.ok(xpOrderService.getUserQuickCapsule(uid));
        return R.ok(xpOrderService.getUserQuickCapsule(uid, startPointId));
    }
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/mapper/XpOrderMapper.java
@@ -11,6 +11,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -69,5 +70,8 @@
    List<XpOrder> getHistoryEndPoints(@Param("uid") Long uid, @Param("areaId") Long areaId);
    Long countUserValidHistoryOrders(@Param("uid") Long uid);
    List<XpOrder> listUserRecentValidOrders(@Param("uid") Long uid, @Param("startTime") LocalDateTime startTime);
}
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/IXpOrderService.java
@@ -99,7 +99,7 @@
     * @param uid
     * @return
     */
    XpPointInfoVo getUserQuickCapsule(String uid);
    XpPointInfoVo getUserQuickCapsule(String uid, Long startPointId);
    /**
     * 获取历史下单成功起点10条数据列表
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java
@@ -3,7 +3,6 @@
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
@@ -1209,16 +1208,41 @@
    }
    @Override
    public XpPointInfoVo getUserQuickCapsule(String uid) {
    public XpPointInfoVo getUserQuickCapsule(String uid, Long startPointId) {
        try {
            // 使用Redis的sorted set(排行榜)获取第一个元素
            // 获取分数最高的元素(默认按分数从高到低排序)
            Set<Object> result = redisService.redisTemplate.opsForZSet().reverseRange("history_order_" + uid, 0, 0);
            if (result != null && !result.isEmpty()) {
                // 排行榜中存储的是JSON字符串
                String jsonString = result.iterator().next().toString();
                // 使用Hutool的JSON工具解析为XpPointInfoVo对象
                return JSONUtil.toBean(jsonString, XpPointInfoVo.class);
            Long userId = Long.valueOf(uid);
            // 新用户或历史样本过少时直接隐藏胶囊,避免偶发订单干扰推荐。
            Long validHistoryCount = this.baseMapper.countUserValidHistoryOrders(userId);
            if (validHistoryCount == null || validHistoryCount < 3) {
                return null;
            }
            XpPoint startPoint = getPointById(startPointId);
            if (startPoint == null) {
                log.warn("获取用户快捷胶囊失败,起点不存在,uid:{}, startPointId:{}", uid, startPointId);
                return null;
            }
            Integer minOrderDistance = getMinOrderDistanceSafely(uid, startPointId);
            if (minOrderDistance == null) {
                return null;
            }
            LocalDateTime now = LocalDateTime.now();
            List<XpOrder> recentOrders = this.baseMapper.listUserRecentValidOrders(userId, now.minusDays(30));
            if (CollectionUtils.isEmpty(recentOrders)) {
                return null;
            }
            // 先走“同日别 + 当前时间前后1小时”的场景推荐,命不中再退化到30天高频。
            RecommendationCandidate sceneCandidate = buildSceneCandidate(recentOrders, now, startPoint, minOrderDistance);
            if (sceneCandidate != null) {
                return sceneCandidate.getPointInfo();
            }
            RecommendationCandidate fallbackCandidate = buildHighFrequencyCandidate(recentOrders, startPoint, minOrderDistance);
            if (fallbackCandidate != null) {
                return fallbackCandidate.getPointInfo();
            }
        } catch (Exception e) {
            log.error("获取用户快捷胶囊失败: {}", e.getMessage(), e);
@@ -1226,6 +1250,201 @@
        return null;
    }
    private RecommendationCandidate buildSceneCandidate(List<XpOrder> recentOrders, LocalDateTime now, XpPoint startPoint, Integer minOrderDistance) {
        DaySceneType currentSceneType = resolveDaySceneType(now.toLocalDate());
        Set<Integer> targetHours = buildTargetHours(now.getHour());
        List<XpOrder> sceneOrders = recentOrders.stream()
                .filter(order -> isSameScene(order, currentSceneType, targetHours))
                .collect(Collectors.toList());
        List<RecommendationCandidate> candidates = buildCandidates(sceneOrders, startPoint, minOrderDistance);
        if (candidates.isEmpty()) {
            return null;
        }
        RecommendationCandidate top1 = candidates.get(0);
        int secondCount = candidates.size() > 1 ? candidates.get(1).getCount() : 0;
        // 只有 top1 至少命中2次且严格领先第二名,才认为“场景规律”足够明显。
        if (top1.getCount() >= 2 && top1.getCount() > secondCount) {
            return top1;
        }
        return null;
    }
    private RecommendationCandidate buildHighFrequencyCandidate(List<XpOrder> recentOrders, XpPoint startPoint, Integer minOrderDistance) {
        List<RecommendationCandidate> candidates = buildCandidates(recentOrders, startPoint, minOrderDistance);
        if (candidates.isEmpty()) {
            return null;
        }
        return candidates.get(0);
    }
    private List<RecommendationCandidate> buildCandidates(List<XpOrder> orders, XpPoint startPoint, Integer minOrderDistance) {
        if (CollectionUtils.isEmpty(orders)) {
            return Collections.emptyList();
        }
        Map<Long, RecommendationCandidate> candidateMap = new HashMap<>();
        for (XpOrder order : orders) {
            Long endPointId = order.getEndPointId();
            if (endPointId == null || endPointId.equals(startPoint.getId())) {
                continue;
            }
            // 候选点位在创建阶段就完成“点位存在 + 距离阈值”过滤,后续只累加频次。
            RecommendationCandidate candidate = candidateMap.computeIfAbsent(endPointId, id -> createCandidate(id, startPoint, minOrderDistance));
            if (candidate == null || !candidate.isAvailable()) {
                continue;
            }
            candidate.increment(order.getCreateTime());
        }
        return candidateMap.values().stream()
                .filter(Objects::nonNull)
                .filter(RecommendationCandidate::isAvailable)
                .filter(candidate -> candidate.getCount() > 0)
                .sorted(Comparator.comparingInt(RecommendationCandidate::getCount).reversed()
                        .thenComparing(RecommendationCandidate::getLatestCreateTime, Comparator.nullsLast(Comparator.reverseOrder())))
                .collect(Collectors.toList());
    }
    private RecommendationCandidate createCandidate(Long pointId, XpPoint startPoint, Integer minOrderDistance) {
        XpPoint point = getPointById(pointId);
        if (point == null) {
            return null;
        }
        Double distance = getStraightDistance(startPoint, point);
        if (distance == null || distance <= minOrderDistance) {
            return RecommendationCandidate.unavailable();
        }
        XpPointInfoVo pointInfoVo = new XpPointInfoVo();
        pointInfoVo.setId(point.getId());
        pointInfoVo.setPointName(point.getPointName());
        pointInfoVo.setPointLon(point.getPointLon());
        pointInfoVo.setPointLat(point.getPointLat());
        return RecommendationCandidate.available(pointInfoVo);
    }
    private Integer getMinOrderDistanceSafely(String uid, Long startPointId) {
        R<Integer> result = xpSystemSettingClient.getMinOrderDistance();
        if (result == null || result.getCode() != R.SUCCESS || result.getData() == null) {
            log.error("获取用户快捷胶囊失败,最小下单距离配置异常,uid:{}, startPointId:{}, result:{}", uid, startPointId, JSON.toJSONString(result));
            return null;
        }
        return result.getData();
    }
    private XpPoint getPointById(Long pointId) {
        if (pointId == null) {
            return null;
        }
        R<XpPoint> pointResult = xpPointClient.queryById(pointId);
        if (pointResult == null || pointResult.getCode() != R.SUCCESS) {
            return null;
        }
        return pointResult.getData();
    }
    private Double getStraightDistance(XpPoint startPoint, XpPoint endPoint) {
        if (startPoint == null || endPoint == null || startPoint.getPointLon() == null || startPoint.getPointLat() == null
                || endPoint.getPointLon() == null || endPoint.getPointLat() == null) {
            return null;
        }
        Map<String, Double> distanceMap = GeodesyUtil.getDistance(startPoint.getPointLon() + "," + startPoint.getPointLat(),
                endPoint.getPointLon() + "," + endPoint.getPointLat());
        if (distanceMap == null) {
            return null;
        }
        return distanceMap.get("WGS84");
    }
    private boolean isSameScene(XpOrder order, DaySceneType currentSceneType, Set<Integer> targetHours) {
        if (order.getCreateTime() == null || order.getTimeSlot() == null) {
            return false;
        }
        // 场景命中必须同时满足“同类日期”和“当前小时前后1小时窗口”。
        if (resolveDaySceneType(order.getCreateTime().toLocalDate()) != currentSceneType) {
            return false;
        }
        Integer slotHour = parseTimeSlotStartHour(order.getTimeSlot());
        return slotHour != null && targetHours.contains(slotHour);
    }
    private Integer parseTimeSlotStartHour(String timeSlot) {
        if (StringUtils.isBlank(timeSlot) || !timeSlot.contains("-")) {
            return null;
        }
        try {
            return Integer.valueOf(timeSlot.split("-")[0]);
        } catch (Exception ex) {
            log.warn("解析时段失败,timeSlot:{}", timeSlot);
            return null;
        }
    }
    private Set<Integer> buildTargetHours(int currentHour) {
        Set<Integer> targetHours = new HashSet<>();
        targetHours.add((currentHour + 23) % 24);
        targetHours.add(currentHour);
        targetHours.add((currentHour + 1) % 24);
        return targetHours;
    }
    private DaySceneType resolveDaySceneType(LocalDate date) {
        if (holidayUtil.isHoliday(date)) {
            return DaySceneType.HOLIDAY;
        }
        DayOfWeek dayOfWeek = date.getDayOfWeek();
        if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) {
            return DaySceneType.NON_WORKDAY;
        }
        return DaySceneType.WORKDAY;
    }
    private enum DaySceneType {
        WORKDAY,
        NON_WORKDAY,
        HOLIDAY
    }
    private static class RecommendationCandidate {
        private final XpPointInfoVo pointInfo;
        private final boolean available;
        private int count;
        private LocalDateTime latestCreateTime;
        private RecommendationCandidate(XpPointInfoVo pointInfo, boolean available) {
            this.pointInfo = pointInfo;
            this.available = available;
        }
        public static RecommendationCandidate available(XpPointInfoVo pointInfo) {
            return new RecommendationCandidate(pointInfo, true);
        }
        public static RecommendationCandidate unavailable() {
            return new RecommendationCandidate(null, false);
        }
        public void increment(LocalDateTime createTime) {
            this.count++;
            if (createTime != null && (this.latestCreateTime == null || createTime.isAfter(this.latestCreateTime))) {
                this.latestCreateTime = createTime;
            }
        }
        public XpPointInfoVo getPointInfo() {
            return pointInfo;
        }
        public boolean isAvailable() {
            return available;
        }
        public int getCount() {
            return count;
        }
        public LocalDateTime getLatestCreateTime() {
            return latestCreateTime;
        }
    }
    @Override
    public List<XpHistoryStartPointsVo> getHistoryStartPoints(String uid, double lon, double lat,Long electronicAreaId) {
        //
xp-robotaxi-order-boot/src/main/resources/mapper/order/XpOrderMapper.xml
@@ -72,4 +72,32 @@
        ORDER BY num DESC,create_time desc
    </select>
    <select id="countUserValidHistoryOrders" resultType="java.lang.Long">
        SELECT COUNT(*)
        FROM xp_order
        WHERE app_user_id = #{uid}
          AND is_deleted = 0
          AND status IN (9, 10)
    </select>
    <select id="listUserRecentValidOrders" resultType="com.xiaopeng.api.model.order.XpOrder">
        SELECT
            app_user_id,
            start_point_id,
            end_point_id,
            start_point_name,
            end_point_name,
            time_slot,
            create_time,
            end_lon,
            end_lat
        FROM xp_order
        WHERE app_user_id = #{uid}
          AND is_deleted = 0
          AND status IN (9, 10)
          AND create_time <![CDATA[>=]]> #{startTime}
          AND end_point_id IS NOT NULL
        ORDER BY create_time DESC
    </select>
</mapper>