“mitao”
2026-04-12 a06900135aeca18447f99167bcf7749f5a3cf9ab
feat:APP地图选点-场站
4个文件已添加
2个文件已修改
383 ■■■■■ 已修改文件
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/dto/StationDetailQueryDto.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpPointService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java 242 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/vo/StationDetailVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/vo/StationPointItemVo.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointController.java
New file
@@ -0,0 +1,40 @@
package com.xiaopeng.map.controller;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.map.dto.StationDetailQueryDto;
import com.xiaopeng.map.service.XpPointService;
import com.xiaopeng.map.vo.StationDetailVo;
import com.xiaopeng.unifiedverification.interceptor.XpClientCheck;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@RestController
@RequestMapping("")
@Api(tags = "点位模块")
public class XpPointController {
    @Resource
    private XpPointService xpPointService;
    /**
     * 地图选点页场站卡片查询。
     * 根据场站ID返回场站基础信息和当前可展示的子点位列表。
     */
    @ApiOperation(value = "根据场站查询场站详情及所属点位", tags = {"小程序-地图选点页场站详情"})
    @PostMapping("/base/v1/mini-program/xp-point/queryStationDetail")
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<StationDetailVo> queryStationDetail(@Validated @RequestBody StationDetailQueryDto dto, HttpServletRequest request) {
        String uid = request.getHeader("XP-Uid");
        return R.ok(xpPointService.queryStationDetail(dto, uid));
    }
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/dto/StationDetailQueryDto.java
New file
@@ -0,0 +1,22 @@
package com.xiaopeng.map.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
@ApiModel(value = "StationDetailQueryDto", description = "场站详情查询")
public class StationDetailQueryDto {
    @ApiModelProperty(value = "场站id")
    @NotNull(message = "场站id不能为空")
    private Long stationId;
    @ApiModelProperty(value = "当前纬度")
    private Double latitude;
    @ApiModelProperty(value = "当前经度")
    private Double longitude;
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpPointService.java
@@ -14,9 +14,11 @@
import com.xiaopeng.api.vo.order.XpPointsSearchVo;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.map.dto.NearestDto;
import com.xiaopeng.map.dto.StationDetailQueryDto;
import com.xiaopeng.map.query.PointByNameQuery;
import com.xiaopeng.map.vo.NearestVo;
import com.xiaopeng.map.vo.OrderDistanceVo;
import com.xiaopeng.map.vo.StationDetailVo;
import javax.validation.Valid;
import java.time.LocalTime;
@@ -43,6 +45,12 @@
     */
    NearestVo nearest(@Valid NearestDto dto);
    /**
     * 根据场站查询场站详情及所属点位。
     * 仅返回当前时段、当前运营状态下可展示的场站与子点位数据。
     */
    StationDetailVo queryStationDetail(@Valid StationDetailQueryDto dto, String uid);
    /**
     * 判断点位是否时间段内
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();
        }
    }
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/vo/StationDetailVo.java
New file
@@ -0,0 +1,34 @@
package com.xiaopeng.map.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@ApiModel(value = "StationDetailVo", description = "场站详情")
public class StationDetailVo {
    @ApiModelProperty(value = "场站id")
    private Long stationId;
    @ApiModelProperty(value = "场站名称")
    private String stationName;
    @ApiModelProperty(value = "默认展示点位id")
    private Long defaultPointId;
    @ApiModelProperty(value = "场站中心点经度")
    private Double centerLon;
    @ApiModelProperty(value = "场站中心点纬度")
    private Double centerLat;
    @ApiModelProperty(value = "是否收藏 1是 2否")
    private Integer isCollect = 2;
    @ApiModelProperty(value = "场站子点位列表")
    private List<StationPointItemVo> points = new ArrayList<>();
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/vo/StationPointItemVo.java
New file
@@ -0,0 +1,37 @@
package com.xiaopeng.map.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(value = "StationPointItemVo", description = "场站子点位")
public class StationPointItemVo {
    @ApiModelProperty(value = "点位id")
    private Long pointId;
    @ApiModelProperty(value = "点位名称")
    private String pointName;
    @ApiModelProperty(value = "点位地址")
    private String pointAddress;
    @ApiModelProperty(value = "点位纬度")
    private Double latitude;
    @ApiModelProperty(value = "点位经度")
    private Double longitude;
    @ApiModelProperty(value = "距离,单位米")
    private Double distance;
    @ApiModelProperty(value = "点位在场站内排序")
    private Integer stationSortNo;
    @ApiModelProperty(value = "是否默认点位 1是 2否")
    private Integer isDefault = 2;
    @ApiModelProperty(value = "是否收藏 1是 2否")
    private Integer isCollect = 2;
}