From 7f3081da3c0597cfbfc8f0fc4e342c4ceb901f6d Mon Sep 17 00:00:00 2001
From: kong <sunuping1@sina.com>
Date: 星期日, 12 四月 2026 18:04:00 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/1.0' into 1.0

---
 xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java |  242 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 242 insertions(+), 0 deletions(-)

diff --git a/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java b/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
index 933ec77..3abc61c 100644
--- a/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
+++ b/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
@@ -10,9 +10,12 @@
 import com.xiaopeng.api.dto.operation.PointMapQuery;
 import com.xiaopeng.api.dto.order.HistoryEndPointsDto;
 import com.xiaopeng.api.dto.order.HistoryStartPointsDto;
+import com.xiaopeng.api.feignClient.account.XpAppUserCollectAddrClient;
 import com.xiaopeng.api.model.operation.XpElectronicFenceArea;
+import com.xiaopeng.api.model.operation.XpElectronicFence;
 import com.xiaopeng.api.model.operation.XpPoint;
 import com.xiaopeng.api.model.operation.XpRegion;
+import com.xiaopeng.api.model.operation.XpStation;
 import com.xiaopeng.api.model.operation.XpSystemSetting;
 import com.xiaopeng.api.query.operation.PointListQuery;
 import com.xiaopeng.api.query.operation.PointPageListListQuery;
@@ -21,6 +24,7 @@
 import com.xiaopeng.api.vo.order.XpPointsListVo;
 import com.xiaopeng.api.vo.order.XpPointsSearchVo;
 import com.xiaopeng.common.core.constant.CacheConstants;
+import com.xiaopeng.common.core.domain.R;
 import com.xiaopeng.common.core.exception.ServiceException;
 import com.xiaopeng.common.core.utils.StringUtils;
 import com.xiaopeng.common.core.web.page.PageInfo;
@@ -28,18 +32,23 @@
 import com.xiaopeng.evi.oss.service.AliOssService;
 import com.xiaopeng.map.constant.I18nKeyConstant;
 import com.xiaopeng.map.dto.NearestDto;
+import com.xiaopeng.map.dto.StationDetailQueryDto;
 import com.xiaopeng.map.mapper.XpPointMapper;
 import com.xiaopeng.map.mapper.XpRegionMapper;
 import com.xiaopeng.map.mapper.XpSystemSettingMapper;
 import com.xiaopeng.map.query.PointByNameQuery;
+import com.xiaopeng.map.service.XpElectronicFenceService;
 import com.xiaopeng.map.service.XpElectronicFenceAreaService;
 import com.xiaopeng.map.service.XpPointService;
+import com.xiaopeng.map.service.XpStationService;
 import com.xiaopeng.map.utils.TimeRangeChecker;
 import com.xiaopeng.map.utils.holiday.HolidayUtil;
 import com.xiaopeng.map.utils.map.map.FastDistanceCalculator;
 import com.xiaopeng.map.vo.NearestNextVo;
 import com.xiaopeng.map.vo.NearestVo;
 import com.xiaopeng.map.vo.OrderDistanceVo;
+import com.xiaopeng.map.vo.StationDetailVo;
+import com.xiaopeng.map.vo.StationPointItemVo;
 import lombok.extern.slf4j.Slf4j;
 import net.sourceforge.pinyin4j.PinyinHelper;
 import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
@@ -53,6 +62,9 @@
 
 import javax.annotation.Resource;
 import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
@@ -87,6 +99,15 @@
 
     @Resource
     private XpElectronicFenceAreaService xpElectronicFenceAreaService;
+
+    @Resource
+    private XpElectronicFenceService xpElectronicFenceService;
+
+    @Resource
+    private XpStationService xpStationService;
+
+    @Resource
+    private XpAppUserCollectAddrClient xpAppUserCollectAddrClient;
 
     @Resource
     private XpRegionMapper xpRegionMapper;
@@ -370,6 +391,63 @@
     }
 
     @Override
+    public StationDetailVo queryStationDetail(StationDetailQueryDto dto, String uid) {
+        StationDetailVo stationDetailVo = new StationDetailVo();
+        if (dto == null || dto.getStationId() == null) {
+            return stationDetailVo;
+        }
+
+        XpStation station = xpStationService.getById(dto.getStationId());
+        if (station == null || !isStationAvailable(station)) {
+            return stationDetailVo;
+        }
+
+        // 仅查询场站下运营点位,最终结果还会叠加时段/围栏有效性过滤。
+        List<XpPoint> stationPoints = this.lambdaQuery()
+                .eq(XpPoint::getStationId, station.getId())
+                .eq(XpPoint::getPointType, 1)
+                .eq(XpPoint::getOperationStatus, 2)
+                .list();
+        if (CollUtil.isEmpty(stationPoints)) {
+            return stationDetailVo;
+        }
+
+        Map<Long, XpElectronicFenceArea> areaMap = buildAreaMap(stationPoints.stream()
+                .map(XpPoint::getElectronicAreaId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList()));
+        Map<Long, XpElectronicFence> fenceMap = buildFenceMap(areaMap.values().stream()
+                .map(XpElectronicFenceArea::getElectronicId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList()));
+        Set<Long> collectStationIds = getCollectStationIds(uid);
+        Set<Long> collectPointIds = getCollectPointIds(uid);
+
+        // 默认点位优先置顶,其余点位再按场站内排序和距离排序。
+        List<StationPointItemVo> points = stationPoints.stream()
+                .filter(point -> isPointAvailable(point, areaMap, fenceMap))
+                .map(point -> toStationPointItem(point, station.getDefaultPointId(), dto.getLatitude(), dto.getLongitude(), collectPointIds))
+                .sorted(buildStationPointComparator())
+                .collect(Collectors.toList());
+        if (points.isEmpty()) {
+            return new StationDetailVo();
+        }
+
+        stationDetailVo.setStationId(station.getId());
+        stationDetailVo.setStationName(station.getStationName());
+        stationDetailVo.setDefaultPointId(station.getDefaultPointId());
+        stationDetailVo.setCenterLon(station.getCenterLon());
+        stationDetailVo.setCenterLat(station.getCenterLat());
+        if (collectStationIds.contains(station.getId())) {
+            stationDetailVo.setIsCollect(1);
+        }
+        stationDetailVo.setPoints(points);
+        return stationDetailVo;
+    }
+
+    @Override
     public XpPointsSearchVo queryPointByName(PointByNameQuery query) {
         XpPointsSearchVo searchVo = new XpPointsSearchVo();
         List<XpPointsListVo> list = this.baseMapper.queryPointByName(query);
@@ -432,4 +510,168 @@
         return pageInfo;
     }
 
+    private Map<Long, XpElectronicFenceArea> buildAreaMap(List<Long> areaIds) {
+        if (CollUtil.isEmpty(areaIds)) {
+            return Collections.emptyMap();
+        }
+        return xpElectronicFenceAreaService.listByIds(areaIds).stream()
+                .collect(Collectors.toMap(XpElectronicFenceArea::getId, area -> area, (a, b) -> a));
+    }
+
+    /**
+     * 提前拉取电子围栏,避免逐点位重复查库。
+     */
+    private Map<Long, XpElectronicFence> buildFenceMap(List<Long> fenceIds) {
+        if (CollUtil.isEmpty(fenceIds)) {
+            return Collections.emptyMap();
+        }
+        return xpElectronicFenceService.listByIds(fenceIds).stream()
+                .collect(Collectors.toMap(XpElectronicFence::getId, fence -> fence, (a, b) -> a));
+    }
+
+    /**
+     * 场站可展示判断:
+     * 1. 状态必须为启用中;
+     * 2. 与运营区同步时,依赖运营区和围栏的当前有效性;
+     * 3. 自定义时段时,依赖场站自身时段配置。
+     */
+    private boolean isStationAvailable(XpStation station) {
+        if (station == null || !Integer.valueOf(6).equals(station.getStatus())) {
+            return false;
+        }
+        if (Integer.valueOf(1).equals(station.getOperationType())) {
+            if (station.getElectronicAreaId() == null) {
+                return false;
+            }
+            XpElectronicFenceArea area = xpElectronicFenceAreaService.getById(station.getElectronicAreaId());
+            if (area == null || !Integer.valueOf(1).equals(area.getAreaStatus()) || area.getElectronicId() == null) {
+                return false;
+            }
+            XpElectronicFence fence = xpElectronicFenceService.getById(area.getElectronicId());
+            return isFenceAvailable(fence);
+        }
+        if (Integer.valueOf(2).equals(station.getOperationType())
+                && station.getEffectivePeriod() != null
+                && StringUtils.isNotBlank(station.getEffectiveTimePeriod())) {
+            return getInTime(LocalTime.now(), station.getEffectivePeriod(), station.getEffectiveTimePeriod());
+        }
+        return false;
+    }
+
+    /**
+     * 子点位是否可展示与场站卡片中。
+     * 规则与现有点位有效性保持一致,避免地图页和其他点位查询出现口径偏差。
+     */
+    private boolean isPointAvailable(XpPoint point, Map<Long, XpElectronicFenceArea> areaMap, Map<Long, XpElectronicFence> fenceMap) {
+        if (point == null || !Integer.valueOf(2).equals(point.getOperationStatus())) {
+            return false;
+        }
+        if (Integer.valueOf(1).equals(point.getOperationType())) {
+            if (point.getElectronicAreaId() == null) {
+                return false;
+            }
+            XpElectronicFenceArea area = areaMap.get(point.getElectronicAreaId());
+            if (area == null || !Integer.valueOf(1).equals(area.getAreaStatus()) || area.getElectronicId() == null) {
+                return false;
+            }
+            return isFenceAvailable(fenceMap.get(area.getElectronicId()));
+        }
+        if (Integer.valueOf(2).equals(point.getOperationType())
+                && point.getEffectivePeriod() != null
+                && StringUtils.isNotBlank(point.getEffectiveTimePeriod())) {
+            return getInTime(LocalTime.now(), point.getEffectivePeriod(), point.getEffectiveTimePeriod());
+        }
+        return false;
+    }
+
+    /**
+     * 围栏生效判断:
+     * 启用中,且当前时间需要落在生效时间与失效时间窗口内。
+     */
+    private boolean isFenceAvailable(XpElectronicFence fence) {
+        if (fence == null || !Integer.valueOf(2).equals(fence.getStatus())) {
+            return false;
+        }
+        LocalDateTime now = LocalDateTime.now();
+        if (fence.getEffectTime() != null && now.isBefore(fence.getEffectTime())) {
+            return false;
+        }
+        return !Integer.valueOf(2).equals(fence.getFailureType()) || fence.getFailureTime() == null || !now.isAfter(fence.getFailureTime());
+    }
+
+    /**
+     * 组装单个场站子点位返回对象,并在有默认点位配置时打标。
+     */
+    private StationPointItemVo toStationPointItem(XpPoint point, Long defaultPointId, Double latitude, Double longitude, Set<Long> collectPointIds) {
+        StationPointItemVo itemVo = new StationPointItemVo();
+        itemVo.setPointId(point.getId());
+        itemVo.setPointName(point.getPointName());
+        itemVo.setPointAddress(point.getPointAddress());
+        itemVo.setLatitude(point.getPointLat());
+        itemVo.setLongitude(point.getPointLon());
+        itemVo.setStationSortNo(point.getStationSortNo());
+        if (collectPointIds.contains(point.getId())) {
+            itemVo.setIsCollect(1);
+        }
+        if (defaultPointId != null && defaultPointId.equals(point.getId())) {
+            itemVo.setIsDefault(1);
+        }
+        if (latitude != null && longitude != null && point.getPointLat() != null && point.getPointLon() != null) {
+            double distance = FastDistanceCalculator.distanceFast(latitude, longitude, point.getPointLat(), point.getPointLon());
+            itemVo.setDistance(BigDecimal.valueOf(distance).setScale(2, RoundingMode.HALF_UP).doubleValue());
+        }
+        return itemVo;
+    }
+
+    /**
+     * 排序规则:
+     * 默认点位优先,其次按场站内排序,再按与当前定位距离排序。
+     */
+    private Comparator<StationPointItemVo> buildStationPointComparator() {
+        return Comparator
+                .comparing((StationPointItemVo item) -> item.getIsDefault() == null ? 2 : item.getIsDefault())
+                .thenComparing(item -> item.getStationSortNo() == null ? Integer.MAX_VALUE : item.getStationSortNo())
+                .thenComparing(item -> item.getDistance() == null ? Double.MAX_VALUE : item.getDistance());
+    }
+
+    /**
+     * 查询用户收藏的场站ID。
+     * 收藏服务异常时降级为空集合,避免影响场站详情主流程。
+     */
+    private Set<Long> getCollectStationIds(String uid) {
+        if (!StringUtils.hasText(uid)) {
+            return Collections.emptySet();
+        }
+        try {
+            R<List<Long>> result = xpAppUserCollectAddrClient.getStationIdsByUserId(Long.valueOf(uid));
+            if (result == null || CollUtil.isEmpty(result.getData())) {
+                return Collections.emptySet();
+            }
+            return new HashSet<>(result.getData());
+        } catch (Exception e) {
+            log.warn("查询用户收藏场站失败 uid={}", uid, e);
+            return Collections.emptySet();
+        }
+    }
+
+    /**
+     * 查询用户收藏的点位ID。
+     * 子点位收藏态只用于回显,不影响场站筛选和排序主逻辑。
+     */
+    private Set<Long> getCollectPointIds(String uid) {
+        if (!StringUtils.hasText(uid)) {
+            return Collections.emptySet();
+        }
+        try {
+            R<List<Long>> result = xpAppUserCollectAddrClient.getPointIdsByUserId(Long.valueOf(uid));
+            if (result == null || CollUtil.isEmpty(result.getData())) {
+                return Collections.emptySet();
+            }
+            return new HashSet<>(result.getData());
+        } catch (Exception e) {
+            log.warn("查询用户收藏点位失败 uid={}", uid, e);
+            return Collections.emptySet();
+        }
+    }
+
 }

--
Gitblit v1.7.1