kong
2026-04-12 7f3081da3c0597cfbfc8f0fc4e342c4ceb901f6d
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,21 +1208,241 @@
    }
    @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);
        }
        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
@@ -1237,7 +1456,7 @@
        // 获取点位信息
        List<Long> startPointsIds = xpOrders.stream().map(XpOrder::getStartPointId).collect(Collectors.toList());
        R<List<XpHistoryStartPointsVo>> r = xpPointClient.getHistoryStartPoints(new HistoryStartPointsDto(startPointsIds, lon, lat));
        R<List<XpHistoryStartPointsVo>> r = xpPointClient.getHistoryStartPoints(new HistoryStartPointsDto(startPointsIds, lon, lat,Long.valueOf(uid)));
        List<XpHistoryStartPointsVo> data = r.getData();
@@ -1275,7 +1494,7 @@
        // 获取点位信息
        List<Long> endPointsIds = xpOrders.stream().map(XpOrder::getEndPointId).collect(Collectors.toList());
        R<List<XpHistoryEndPointsVo>> r = xpPointClient.getHistoryEndPoints(new HistoryEndPointsDto(endPointsIds, lon, lat));
        R<List<XpHistoryEndPointsVo>> r = xpPointClient.getHistoryEndPoints(new HistoryEndPointsDto(endPointsIds, lon, lat,Long.valueOf(uid)));
        List<XpHistoryEndPointsVo> data = r.getData();
@@ -3248,4 +3467,20 @@
        }
        log.info("订单修改价格推送消息完毕");
    }
    /**
     * 获取待支付订单数量
     * @param uid
     * @return
     */
    @Override
    public Integer getWaitPayOrderNum(String uid) {
        Long num = this.baseMapper.selectCount(
                new LambdaQueryWrapper<XpOrder>()
                        .eq(XpOrder::getAppUserId, uid)
                        .eq(XpOrder::getStatus, 8)
        );
        return num.intValue();
    }
}