liujie
2026-06-09 c96fbe6b0aeb413a17c429133ee6d81d138144b9
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
@@ -2,44 +2,51 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiaopeng.api.dto.operation.PointAddDto;
import com.xiaopeng.api.dto.operation.PointMapQuery;
import com.xiaopeng.api.dto.operation.PointUpdateDto;
import com.xiaopeng.api.dto.order.HistoryEndPointsDto;
import com.xiaopeng.api.dto.order.HistoryStartPointsDto;
import com.xiaopeng.api.model.operation.XpElectronicFenceArea;
import com.xiaopeng.api.model.operation.XpPoint;
import com.xiaopeng.api.model.operation.XpRegion;
import com.xiaopeng.api.model.operation.XpSystemSetting;
import com.xiaopeng.api.feignClient.account.XpAppUserCollectAddrClient;
import com.xiaopeng.api.model.operation.*;
import com.xiaopeng.api.query.operation.FenceAndStationVo;
import com.xiaopeng.api.query.operation.PointListQuery;
import com.xiaopeng.api.query.operation.PointPageListListQuery;
import com.xiaopeng.api.query.operation.XpStationQuery;
import com.xiaopeng.api.vo.account.LoginUser;
import com.xiaopeng.api.vo.order.XpHistoryEndPointsVo;
import com.xiaopeng.api.vo.order.XpHistoryStartPointsVo;
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;
import com.xiaopeng.common.redis.service.RedisService;
import com.xiaopeng.common.security.service.TokenService;
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.XpElectronicFenceMapper;
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.XpElectronicFenceAreaService;
import com.xiaopeng.map.service.XpElectronicFenceService;
import com.xiaopeng.map.service.XpPointService;
import com.xiaopeng.map.utils.TimeRangeChecker;
import com.xiaopeng.map.service.XpStationService;
import com.xiaopeng.map.utils.GDMapGeocodingUtil;
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.*;
import lombok.extern.slf4j.Slf4j;
import net.sourceforge.pinyin4j.PinyinHelper;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
@@ -48,11 +55,14 @@
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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;
@@ -72,6 +82,7 @@
public class XpPointServiceImpl extends ServiceImpl<XpPointMapper, XpPoint> implements XpPointService {
    private static final String POINT_BIZ_TYPE = "PUD";
    private static final int MAX_POINTS_PER_FENCE = 15;
    private static final int SERIAL_WIDTH = 6;
    private static final int LOCK_WAIT_SECONDS = 5;
    private static final int LOCK_LEASE_SECONDS = 30;
@@ -89,6 +100,16 @@
    private XpElectronicFenceAreaService xpElectronicFenceAreaService;
    @Resource
    private XpElectronicFenceMapper xpElectronicFenceMapper;
    @Resource
    @Lazy
    private XpStationService xpStationService;
    @Resource
    private XpAppUserCollectAddrClient xpAppUserCollectAddrClient;
    @Resource
    private XpRegionMapper xpRegionMapper;
    @Autowired
@@ -96,6 +117,58 @@
    @Autowired
    private AliOssService aliOssService;
    @Lazy
    @Autowired
    private BootCommonService bootCommonService;
    @Lazy
    @Resource
    private XpElectronicFenceService xpElectronicFenceService;
    @Resource
    private GDMapGeocodingUtil gdMapGeocodingUtil;
    @Override
    public void validateAdd(PointAddDto dto) {
        XpElectronicFenceArea electronicFenceArea = xpElectronicFenceAreaService.getById(dto.getElectronicAreaId());
        if(electronicFenceArea == null){
            throw new ServiceException("所属运营区id错误");
        }
        XpElectronicFence electronicFence = xpElectronicFenceService.getById(electronicFenceArea.getElectronicId());
        //场站的自定义时间必须在 围栏运营时间内
        if(dto.getOperationType() != null && dto.getOperationType() == 2){
//            ["06:00-12:00", "13:00-21:00"]
            if(StrUtil.isBlank(dto.getEffectiveTimePeriod())){
                throw new ServiceException("自定义时间段不能为空");
            }
            boolean stationTimeFenceOperationStrategy = bootCommonService.getStationTime_fenceOperationStrategy(dto.getEffectivePeriod(), dto.getEffectiveTimePeriod(), electronicFence.getOperationStrategy());
            if(!stationTimeFenceOperationStrategy){
                throw new ServiceException("自定义时间必须在围栏运营时间内");
            }
        }
    }
    @Override
    public void validateUpdate(PointUpdateDto dto) {
        XpElectronicFenceArea electronicFenceArea = xpElectronicFenceAreaService.getById(dto.getElectronicAreaId());
        if(electronicFenceArea == null){
            throw new ServiceException("所属运营区id错误");
        }
        XpElectronicFence electronicFence = xpElectronicFenceService.getById(electronicFenceArea.getElectronicId());
        //场站的自定义时间必须在 围栏运营时间内
        if(dto.getOperationType() != null && dto.getOperationType() == 2){
//            ["06:00-12:00", "13:00-21:00"]
            if(StrUtil.isBlank(dto.getEffectiveTimePeriod())){
                throw new ServiceException("自定义时间段不能为空");
            }
            boolean stationTimeFenceOperationStrategy = bootCommonService.getStationTime_fenceOperationStrategy(dto.getEffectivePeriod(), dto.getEffectiveTimePeriod(), electronicFence.getOperationStrategy());
            if(!stationTimeFenceOperationStrategy){
                throw new ServiceException("自定义时间必须在围栏运营时间内");
            }
        }
    }
    @Override
    @Transactional
@@ -107,10 +180,58 @@
        if (pointAddDto.getElectronicAreaId() == null) {
            throw new ServiceException("运营区域ID不能为空", I18nKeyConstant.POINT_ELECTRONIC_AREA_ID_EMPTY);
        }
        validatePointNameUnique(pointAddDto.getPointName(), null);
        String cityPrefix = buildCityPrefix(pointAddDto.getElectronicAreaId());
        String pointNumber = getPointNumber(pointAddDto.getElectronicAreaId());
        XpPoint xpPoint = new XpPoint();
        BeanUtils.copyProperties(pointAddDto, xpPoint);
        xpPoint.setFpElectronicAreaId(pointAddDto.getElectronicAreaId());
        xpPoint.setPointNumber(pointNumber);
        xpPoint.setStationSortNo(0);
        Map<String, String> geocode = gdMapGeocodingUtil.geocode(xpPoint.getPointLon().toString(), xpPoint.getPointLat().toString());
        if(null == geocode){
            throw new ServiceException("点位定位解析失败");
        }
        xpPoint.setCityCode(geocode.get("cityCode"));
        xpPoint.setCityName(geocode.get("address"));
        this.save(xpPoint);
        //场站点位排序 后添加的点位 排第一个。
        this.setStationSortNo(xpPoint.getStationId(),0);
        return xpPoint;
    }
    /**
     * 场站 点位排序
     * @param stationId
     * @param stationSortNo
     */
    @Transactional
    @Override
    public void setStationSortNo(Long stationId, Integer stationSortNo){
        if(null != stationId  && stationSortNo != null && stationSortNo ==0){
            // 查询该场站下的所有点位,按 stationSortNo 排序
            List<XpPoint> points = this.lambdaQuery()
                    .eq(XpPoint::getStationId, stationId)
                    .orderByAsc(XpPoint::getStationSortNo)
                    .list();
            if (CollUtil.isNotEmpty(points)) {
                int newSortNo = 1;
                for (XpPoint point : points) {
                    point.setStationSortNo(newSortNo++);
                }
                this.updateBatchById(points);
            }
        }
    }
    @Override
    public String getPointNumber(Long electronicAreaId) {
        String cityPrefix = buildCityPrefix(electronicAreaId);
        String lockKey = buildPointNumberLockKey(cityPrefix, POINT_BIZ_TYPE);
        RLock lock = redissonClient.getLock(lockKey);
        boolean locked = false;
        String pointNumber;
@@ -119,6 +240,7 @@
            if (!locked) {
                throw new ServiceException("点位编号生成繁忙,请稍后重试", I18nKeyConstant.POINT_NUMBER_LOCK_FAIL);
            }
//            validateFencePointCountLimit(electronicAreaId);
            String maxPointNumber = this.baseMapper.selectMaxPointNumber(cityPrefix, POINT_BIZ_TYPE);
            int nextSerial = 1;
            if (StringUtils.hasText(maxPointNumber)) {
@@ -137,13 +259,21 @@
                log.warn("释放点位编号生成锁失败 lockKey={}", lockKey, e);
            }
        }
        return pointNumber;
    }
        XpPoint xpPoint = new XpPoint();
        BeanUtils.copyProperties(pointAddDto, xpPoint);
        xpPoint.setFpElectronicAreaId(pointAddDto.getElectronicAreaId());
        xpPoint.setPointNumber(pointNumber);
        this.save(xpPoint);
        return xpPoint;
    @Override
    public void validatePointNameUnique(String pointName, Long excludeId) {
        if (!StringUtils.hasText(pointName)) {
            return;
        }
        long count = this.lambdaQuery()
                .eq(XpPoint::getPointName, pointName)
                .ne(excludeId != null, XpPoint::getId, excludeId)
                .count();
        if (count > 0) {
            throw new ServiceException("点位名称已存在", I18nKeyConstant.POINT_NAME_EXISTS);
        }
    }
    @Override
@@ -158,10 +288,18 @@
            orderDistance = xpSystemSetting.getNearPointDistance();
        }
        // 获取所有的站点信息
        List<XpPoint> list = list(new LambdaQueryWrapper<XpPoint>().eq(XpPoint::getOperationStatus, 2).eq(XpPoint::getPointType,1).isNotNull(XpPoint::getElectronicAreaId)
                // 添加距离计算字段
                .apply("ST_Distance_Sphere(point({0}, {1}), point(point_lon, point_lat)) <= {2}",
                        dto.getLongitude(), dto.getLatitude(), orderDistance));
        PointPageListListQuery query =  new PointPageListListQuery();
        query.setOperationStatus(2);
        query.setPointType(1);
        query.setElectronicAreaIdNotNull(true);
        query.setLongitude(dto.getLongitude());
        query.setLatitude(dto.getLatitude());
        query.setOrderDistance(orderDistance);
        List<XpPoint> list = baseMapper.pageList(query);
        // 添加距离计算字段
//        List<XpPoint> list = list(new LambdaQueryWrapper<XpPoint>().eq(XpPoint::getOperationStatus, 2).eq(XpPoint::getPointType,1).isNotNull(XpPoint::getElectronicAreaId)
//                .apply("ST_Distance_Sphere(point({0}, {1}), point(point_lon, point_lat)) <= {2}",
//                        dto.getLongitude(), dto.getLatitude(), orderDistance));
        NearestVo nearestVo = new NearestVo();
        List<NearestNextVo> nearestNextVos = new ArrayList<>();
        LocalTime now = LocalTime.now();
@@ -169,9 +307,15 @@
        for (XpPoint xpPoint : list) {
            // 判断是否在时间段内
            boolean intime = getInTime(now, xpPoint.getEffectivePeriod(), xpPoint.getEffectiveTimePeriod());
            boolean intime = bootCommonService.getPointOperation(now, xpPoint);
            if (!intime) {
                continue;
            }
            Integer parkingTimeLimit = xpPoint.getParkingTimeLimit();
            //如果点位位于场站内,则使用场站的限停时间
            if(null != xpPoint.getStationId()){
                XpStation xpStation = xpStationService.getById(xpPoint.getStationId());
                parkingTimeLimit = xpStation.getParkingTimeLimit();
            }
            // 计算距离 小于的站点
            double v = FastDistanceCalculator.distanceFast(dto.getLatitude(), dto.getLongitude(), xpPoint.getPointLat(), xpPoint.getPointLon());
@@ -183,7 +327,7 @@
                nearestNextVo.setLongitude(xpPoint.getPointLon());
                nearestNextVo.setDistance(v);
                nearestNextVo.setAddress(xpPoint.getPointAddress());
                nearestNextVo.setTime(xpPoint.getParkingTimeLimit());
                nearestNextVo.setTime(parkingTimeLimit);
                nearestNextVo.setElectronicAreaId(xpPoint.getElectronicAreaId());
@@ -223,6 +367,36 @@
        return getPinyinInitials(prefix).toUpperCase();
    }
    private void validateFencePointCountLimit(Long electronicAreaId) {
        if (electronicAreaId == null || electronicAreaId == 0L) {
            return;
        }
        XpElectronicFenceArea currentArea = xpElectronicFenceAreaService.getById(electronicAreaId);
        if (currentArea == null) {
            throw new ServiceException("运营区域不存在", I18nKeyConstant.POINT_ELECTRONIC_AREA_NOT_EXIST);
        }
        Long fenceId = currentArea.getElectronicId();
        if (fenceId == null) {
            throw new ServiceException("运营区域不存在", I18nKeyConstant.POINT_ELECTRONIC_AREA_NOT_EXIST);
        }
        List<Long> areaIds = xpElectronicFenceAreaService.lambdaQuery()
                .eq(XpElectronicFenceArea::getElectronicId, fenceId)
                .list()
                .stream()
                .map(XpElectronicFenceArea::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (areaIds.isEmpty()) {
            return;
        }
        long pointCount = this.lambdaQuery()
                .in(XpPoint::getElectronicAreaId, areaIds)
                .count();
        if (pointCount >= MAX_POINTS_PER_FENCE) {
            throw new ServiceException("当前围栏最多只能添加15个点位", I18nKeyConstant.POINT_FENCE_POINT_LIMIT_EXCEEDED);
        }
    }
    private static String buildPointNumberLockKey(String cityPrefix, String bizType) {
        return "xp:point:number:" + cityPrefix + ":" + bizType;
    }
@@ -256,42 +430,6 @@
    public boolean getInTime(LocalTime now,  Integer effectivePeriod, String effectiveTimePeriod){
        //  2非工作日  0工作日  1节假日 3调休日
        Integer holidayToday = holidayUtil.isHolidayToday();
        if(effectivePeriod==1){
            boolean inTime = TimeRangeChecker.isCurrentTimeInRanges(now,effectiveTimePeriod);
            if (!inTime) {
                return false;
            }
        }else if(effectivePeriod==2){
            if(holidayToday!=0){
                return false;
            }
            boolean inTime = TimeRangeChecker.isCurrentTimeInRanges(now,effectiveTimePeriod);
            if (!inTime) {
                return false;
            }
        }else if(effectivePeriod==3){
            if(holidayToday!=2){
                return false;
            }
            boolean inTime = TimeRangeChecker.isCurrentTimeInRanges(now,effectiveTimePeriod);
            if (!inTime) {
                return false;
            }
        }else if(effectivePeriod==4){
            if(holidayToday!=1){
                return false;
            }
            boolean inTime = TimeRangeChecker.isCurrentTimeInRanges(now,effectiveTimePeriod);
            if (!inTime) {
                return false;
            }
        }
        return true;
    }
    @Override
    public List<XpPoint> pointMap(PointMapQuery query) {
        List<XpPoint> list = this.baseMapper.pointMap(query);
@@ -299,37 +437,18 @@
    }
    @Override
    public List<FenceAndStationVo> findListFenceAndStation(String cityCode){
        return baseMapper.findListFenceAndStation(cityCode);
    }
    @Override
    public List<XpPoint> findList(PointListQuery query) {
        return this.baseMapper.listAll(query);
    }
    @Override
    public List<XpPoint> listAll(PointListQuery query) {
        List<XpPoint> list = this.baseMapper.listAll(query);
        for (XpPoint xpPoint : list) {
            String pointPictures = xpPoint.getPointPictures();
            log.info("pointPictures:{}", pointPictures);
            if (StringUtils.isNotBlank(pointPictures)) {
                log.info("pointPictures进入处理:{}", pointPictures);
                JSONArray jsonArray = JSONArray.parseArray(pointPictures);
                for (int i = 0; i < jsonArray.size(); i++) {
                    JSONObject jsonObject = jsonArray.getJSONObject(i);
                    if (Objects.isNull(jsonObject)) {
                        continue;
                    }
                    String downloadUrl ="";
                    try {
                        downloadUrl = aliOssService.getDownloadUrl(jsonObject.getString("id"), jsonObject.getString("name"), true);
                        log.info("downloadUrl:{}", downloadUrl);
                    } catch (UnsupportedEncodingException e) {
                        log.error("获取图片下载地址失败", e);
                    }
                    jsonObject.put("url", downloadUrl);
                }
                log.info("jsonArray:{}", jsonArray);
                xpPoint.setPointPictures(JSONArray.toJSONString(jsonArray));
                log.info("xpPoint:{}", xpPoint.getPointPictures());
            }
        }
        return list;
        return this.baseMapper.listAll(query);
    }
    @Override
@@ -337,6 +456,12 @@
        List<XpPoint> xpPoints = this.baseMapper.selectBatchIds(historyStartPointsDto.getPointIds());
        ArrayList<XpHistoryStartPointsVo> resultList = new ArrayList<>(10);
        for (XpPoint xpPoint : xpPoints) {
            Integer parkingTimeLimit = xpPoint.getParkingTimeLimit();
            //如果点位位于场站内,则使用场站的限停时间
            if(null != xpPoint.getStationId()){
                XpStation xpStation = xpStationService.getById(xpPoint.getStationId());
                parkingTimeLimit = xpStation.getParkingTimeLimit();
            }
            XpHistoryStartPointsVo xpHistoryStartPointsVo = new XpHistoryStartPointsVo();
            xpHistoryStartPointsVo.setName(xpPoint.getPointName());
            xpHistoryStartPointsVo.setAddress(xpPoint.getPointAddress());
@@ -345,7 +470,7 @@
            xpHistoryStartPointsVo.setId(xpPoint.getId());
            double distanced = FastDistanceCalculator.distanceFastKm(historyStartPointsDto.getLat(), historyStartPointsDto.getLon(), xpPoint.getPointLat(), xpPoint.getPointLon());
            xpHistoryStartPointsVo.setDistance(distanced);
            xpHistoryStartPointsVo.setTime(xpPoint.getParkingTimeLimit());
            xpHistoryStartPointsVo.setTime(parkingTimeLimit);
            xpHistoryStartPointsVo.setElectronicAreaId(xpPoint.getElectronicAreaId());
            resultList.add(xpHistoryStartPointsVo);
@@ -359,6 +484,12 @@
        List<XpPoint> xpPoints = this.baseMapper.selectBatchIds(historyEndPointsDto.getPointIds());
        ArrayList<XpHistoryEndPointsVo> resultList = new ArrayList<>(10);
        for (XpPoint xpPoint : xpPoints) {
            Integer parkingTimeLimit = xpPoint.getParkingTimeLimit();
            //如果点位位于场站内,则使用场站的限停时间
            if(null != xpPoint.getStationId()){
                XpStation xpStation = xpStationService.getById(xpPoint.getStationId());
                parkingTimeLimit = xpStation.getParkingTimeLimit();
            }
            XpHistoryEndPointsVo xpHistoryEndPointsVo = new XpHistoryEndPointsVo();
            xpHistoryEndPointsVo.setName(xpPoint.getPointName());
            xpHistoryEndPointsVo.setAddress(xpPoint.getPointAddress());
@@ -367,12 +498,79 @@
            xpHistoryEndPointsVo.setId(xpPoint.getId());
            double distanced = FastDistanceCalculator.distanceFastKm(historyEndPointsDto.getLat(), historyEndPointsDto.getLon(), xpPoint.getPointLat(), xpPoint.getPointLon());
            xpHistoryEndPointsVo.setDistance(distanced);
            xpHistoryEndPointsVo.setTime(xpPoint.getParkingTimeLimit());
            xpHistoryEndPointsVo.setTime(parkingTimeLimit);
            xpHistoryEndPointsVo.setElectronicAreaId(xpPoint.getElectronicAreaId());
            resultList.add(xpHistoryEndPointsVo);
        }
        return resultList;
    }
    @Override
    public StationDetailVo queryStationDetail(StationDetailQueryDto dto, String uid) {
        StationDetailVo stationDetailVo = new StationDetailVo();
        if (dto == null || dto.getStationId() == null) {
            return stationDetailVo;
        }
        XpStationQuery query = new XpStationQuery();
        query.setId(dto.getStationId());
        query.setStatus(2);
        List<XpStation> list = xpStationService.findList(query);
        if(CollUtil.isEmpty(list)){
            return stationDetailVo;
        }
        XpStation station = list.get(0);
        LocalTime localTime = LocalTime.now();
        boolean stationOperation = bootCommonService.getStationOperation(localTime, station.getOperationType(), station.getEffectivePeriod(), station.getEffectiveTimePeriod(), station.getFenceOperationStrategy());
        if(!stationOperation){
            return stationDetailVo;
        }
        // 仅查询场站下运营点位,最终结果还会叠加时段/围栏有效性过滤。
        PointPageListListQuery queryPoint = new PointPageListListQuery();
        queryPoint.setStationId( station.getId());
        queryPoint.setPointType(1);
        queryPoint.setOperationStatus(2);
        List<XpPoint> stationPoints = this.baseMapper.pageList(queryPoint);
        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(localTime, point))
                .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
@@ -382,10 +580,10 @@
        if (list.isEmpty()) {
            return searchVo;
        }
        LocalTime now = LocalTime.now();
        for (XpPointsListVo xpPointsListVo : list) {
            boolean inTime = getInTime(now, xpPointsListVo.getEffectivePeriod(), xpPointsListVo.getEffectiveTimePeriod());
            boolean inTime = bootCommonService.getPointOperation_vo(now, xpPointsListVo);
            if (!inTime) {
                // 不在运营段
                xpPointsListVo.setState(0);
@@ -420,8 +618,77 @@
        return orderDistanceVo;
    }
    @Override
    public List<XpPoint> pageList(PageInfo<XpPoint> pageInfo, PointPageListListQuery query) {
        List<XpPoint> list = null;
        if (pageInfo == null) {
            list = this.baseMapper.pageList(query);
        }else{
            list = this.baseMapper.pageList(pageInfo,query);
        }
        if(CollUtil.isNotEmpty(list)){
            /**
             * hover在状态标签时,展示信息
             * - 已停用:展示停用原因;比如“因所属场站 [白云机场] 已关闭,该点位同步失效”
             * - 测试不通过:展示不通过原因
             * - 待生效:展示生效时间
             */
            for (XpPoint point : list) {
                String hoverStatusDes = "";
                if(point.getVerificationStatus()==4){//验证状态 1=待验证、2=验证中、3=已验证 4验证失败 5待生效
                    hoverStatusDes = (point.getRemarks() == null ?"":point.getRemarks());
                }
                if(point.getOperationStatus() == 3){//运营状态 1=待启用 2=启用中 3=已停用 4=已失效
                    hoverStatusDes = (point.getRemarks()==null?"":point.getRemarks());
                }
                if(point.getVerificationStatus()==5 && point.getOperationStatus() == 1){
                    if(point.getOperationType() ==  1){//运营类型 1=与运营区同步 2=自定义时间段
                        ;
                        //[{"name": "策略组1", "time": ["06:00-12:00", "13:00-22:59"], "type": 0, "typeStr": []},
                        // {"name": "策略组2", "time": ["05:00-12:00", "12:00-16:00"], "type": 2, "typeStr": ""},
                        // {"name": "策略组3", "time": ["09:00-21:59"], "type": 1, "typeStr": ""}]
                        JSONArray electronicOperationStrategys = JSONArray.parseArray(point.getFenceOperationStrategy());
                        if(!electronicOperationStrategys.isEmpty()){
                            StringJoiner joiner = new StringJoiner(";");
                            for (Object electronicOperationStrategy : electronicOperationStrategys) {
                                JSONObject obj = (JSONObject) electronicOperationStrategy;
                                List<String> times = obj.getList("time",String.class);
                                joiner.add((String.join(";", times)));
                            }
                            hoverStatusDes = joiner.toString()  ;
                        }
                    }else{
                        if(StrUtil.isNotBlank(point.getEffectiveTimePeriod()) ){
                            List<String> effectiveTimePeriods = JSONArray.parseArray(point.getEffectiveTimePeriod(), String.class);
                            hoverStatusDes = String.join(";", effectiveTimePeriods);
                        }
                    }
                }
                point.setHoverStatusDes(hoverStatusDes);
            }
        }
        return list;
    }
    @Override
    public PageInfo<XpPoint> pageList(PointPageListListQuery query) {
        if(CollUtil.isNotEmpty(query.getFenceAndStationList())){
            List<String> fenceAreaIds = new ArrayList<>();
            List<String> stationIds = new ArrayList<>();
            for (FenceAndStationVo bean : query.getFenceAndStationList()) {
                if(bean.getType()==1){
                    fenceAreaIds.addAll(StrUtil.split(bean.getIds(),",") );
                }else{
                    stationIds.addAll(StrUtil.split(bean.getIds(),",") );
                }
            }
            query.setFenceAreaIds(fenceAreaIds);
            query.setStationIds(stationIds);
        }
        PageInfo<XpPoint> pageInfo = new PageInfo<>(query.getPageNum(),query.getPageSize());
        if (StringUtils.isNotBlank(query.getPointLonAndLat())) {
            List<String> lonAndLat = query.getPointLonAndLat().trim().contains(",") ? Arrays.asList(query.getPointLonAndLat().split(",")) :
@@ -434,8 +701,229 @@
            }
        }
        List<XpPoint> list = this.baseMapper.pageList(pageInfo,query);
        list.forEach(s->s.setStrId(s.getId().toString()));
        pageInfo.setRecords(list);
        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 xpElectronicFenceMapper.selectBatchIds(fenceIds).stream()
                .collect(Collectors.toMap(XpElectronicFence::getId, fence -> fence, (a, b) -> a));
    }
    /**
     * 子点位是否可展示与场站卡片中。
     * 规则与现有点位有效性保持一致,避免地图页和其他点位查询出现口径偏差。
     */
    private boolean isPointAvailable(LocalTime localTime,  XpPoint point) {
        return bootCommonService.getPointOperation(localTime, point);
    }
    /**
     * 组装单个场站子点位返回对象,并在有默认点位配置时打标。
     */
    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。
     * 收藏服务异常时降级为空集合,避免影响场站详情主流程。
     */
    @Override
    public 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();
        }
    }
    @Override
    public List<XpPoint> listByStationId(Long stationId) {
        if (stationId == null) {
            return Collections.emptyList();
        }
        return this.lambdaQuery()
                .eq(XpPoint::getStationId, stationId)
                .list();
    }
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void clearStationInfoByStationId(Long stationId) {
        if (stationId == null) {
            return;
        }
        this.lambdaUpdate()
                .eq(XpPoint::getStationId, stationId)
                .set(XpPoint::getStationId, null)
                .set(XpPoint::getStationSortNo, 0)
                .update();
    }
    /**
     * 查询用户收藏的点位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();
        }
    }
    /**
     * 场站停用
     *     -  操作项展示为 [停用]。点击后弹出二次确认框,“当前场站包含12个点位,确认一起停用?
     *       - 是:确认后场站以及子点位状态流转至“已停用”。
     *       - 否:子点位维持当前状态,但自动退回为“独立点位”。
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void statusStopByStationId(Long stationId, Boolean pointStop) {
        if (stationId == null) {
            return;
        }
        if(pointStop){
            this.lambdaUpdate()
                    .eq(XpPoint::getStationId, stationId)
                    .set(XpPoint::getOperationStatus, 3)//运营状态 1=待启用 2=启用中 3=已停用 4=已失效
                    .update();
        }else{
            clearStationInfoByStationId(stationId);
        }
    }
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void statusEnableByStationId(Long stationId) {
        if (stationId == null) {
            return;
        }
        this.lambdaUpdate()
                .eq(XpPoint::getStationId, stationId)
                .set(XpPoint::getOperationStatus, 2)//运营状态 1=待启用 2=启用中 3=已停用 4=已失效
                .update();
    }
    /**
     * 场站删除
     *       - 删除拦截逻辑:
     *         - 测试阶段(待测试、测试中等):点击直接删除。
     *         - 历史运营阶段(已停用、失效):点击弹出二次确认框“当前场站包含12个点位,确认一起删除?”
     *           - 是:点位统一一起删除
     *           - 否:不自动删除点位,点位自动退回为[待测试]的“独立点位”,并自动继承父级运营区的运营时段
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void pointDeleteByStationId(Long stationId, Boolean pointDelete) {
        if (stationId == null) {
            return;
        }
        if(pointDelete){
            this.lambdaUpdate()
                    .eq(XpPoint::getStationId, stationId)
                    .set(XpPoint::getDeleted, 1)
                    .update();
        }else{
            clearStationInfoByStationId(stationId);
        }
    }
    @Resource
    private TokenService tokenService;
    /**
     * 场站 失效-围栏失效 场站失效
     * @param electronicAreaIds
     */
    @Override
    public void setStatusFailure(List<Long> electronicAreaIds){
        if(CollUtil.isNotEmpty(electronicAreaIds)){
            LambdaQueryWrapper<XpPoint> query = new LambdaQueryWrapper<>();
            query.in(XpPoint::getElectronicAreaId, electronicAreaIds);
            query.eq(XpPoint::getDeleted, 0);
            List<XpPoint> list = super.list(query);
            if(!list.isEmpty()){
                LoginUser loginUser = tokenService.getLoginUser();
                for (XpPoint bean : list) {
                    bean.setUpdateBy(loginUser.getUsername());
                    bean.setUpdateTime(LocalDateTime.now());
                    bean.setOperationStatus(4);//场站状态 0=草稿 1=待启用 2=启用中 3=已停用 4=失效
                    boolean b = updateById(bean);
                }
            }
        }
    }
}