From 0eaa65fad3a106078defa731acc1da1b329f60e9 Mon Sep 17 00:00:00 2001
From: yyy <1083599556@qq.com>
Date: 星期二, 21 四月 2026 15:03:45 +0800
Subject: [PATCH] 1.场站功能开发

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

diff --git a/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpStationServiceImpl.java b/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpStationServiceImpl.java
index 48545cb..204e04e 100644
--- a/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpStationServiceImpl.java
+++ b/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpStationServiceImpl.java
@@ -1,10 +1,41 @@
 package com.xiaopeng.map.service.serviceimpl;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.xiaopeng.api.model.operation.XpStation;
+import com.xiaopeng.api.model.map.XpGeoFencingPolygon;
+import com.xiaopeng.api.model.operation.*;
+import com.xiaopeng.api.query.operation.XpStationQuery;
+import com.xiaopeng.api.vo.operation.XpStationVo;
+import com.xiaopeng.common.core.domain.R;
+import com.xiaopeng.common.core.exception.ServiceException;
+import com.xiaopeng.common.core.web.page.PageInfo;
+import com.xiaopeng.map.constant.I18nKeyConstant;
+import com.xiaopeng.map.mapper.XpRegionMapper;
 import com.xiaopeng.map.mapper.XpStationMapper;
+import com.xiaopeng.map.service.XpElectronicFenceAreaService;
+import com.xiaopeng.map.service.XpElectronicFenceService;
+import com.xiaopeng.map.service.XpPointService;
 import com.xiaopeng.map.service.XpStationService;
+import com.xiaopeng.map.utils.GDMapGeocodingUtil;
+import com.xiaopeng.map.utils.PolygonOverlapService;
+import net.sourceforge.pinyin4j.PinyinHelper;
+import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
+import org.apache.shardingsphere.transaction.core.TransactionType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 /**
  * <p>
@@ -17,4 +48,451 @@
 @Service
 public class XpStationServiceImpl extends ServiceImpl<XpStationMapper, XpStation> implements XpStationService {
 
+
+
+    @Autowired
+    private MongoTemplate mongoTemplate;
+    @Resource
+    private PolygonOverlapService polygonOverlapService;
+    @Resource
+    private XpElectronicFenceService xpElectronicFenceService;
+    @Resource
+    private XpElectronicFenceAreaService xpElectronicFenceAreaService;
+    @Autowired
+    private GDMapGeocodingUtil gdMapGeocodingUtil;
+    @Autowired
+    private XpRegionMapper xpRegionMapper;
+    @Resource
+    private XpPointService xpPointService;
+
+    @Override
+    public List<XpStation> findList(XpStationQuery query) {
+        return this.baseMapper.pageList( query);
+    }
+
+    @Override
+    public PageInfo<XpStation> pageList(XpStationQuery query) {
+        PageInfo<XpStation> pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize());
+        if (null == query.getCreateSort() && null == query.getUpdateSort()){
+            query.setCreateSort(2);
+        }
+        List<XpStation> list = this.baseMapper.pageList(pageInfo, query);
+        if (list.isEmpty()) {
+            return new PageInfo<>();
+        }
+//        List<Long> electronicIds = list.stream().map(XpElectronicFence::getId).collect(Collectors.toList());
+//        Map<Long, List<XpElectronicFenceArea>> electronicFenceAreaMap = xpElectronicFenceAreaMapper.selectList(new LambdaQueryWrapper<XpElectronicFenceArea>()
+//                        .in(XpElectronicFenceArea::getElectronicId, electronicIds)).stream()
+//                .collect(Collectors.groupingBy(XpElectronicFenceArea::getElectronicId));
+        for (XpStation bean : list) {
+
+            if(bean.getFencePoints() !=null){
+                bean.setPointCount(bean.getFencePoints().split(",").length); //点位总数
+            }
+            bean.setOperatingPointCount(0);//运营中点位数
+
+        }
+        pageInfo.setRecords(list);
+        return pageInfo;
+    }
+
+
+    public List<XpStation> findList() {
+        // 获取现有的运营区域(areaType=1)
+        LambdaQueryWrapper<XpStation> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(XpStation::getDeleted, 0);
+        //status = 场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
+        queryWrapper.in(XpStation::getStatus, 1,2,3,4,5,6,7);
+        return super.list(queryWrapper);
+    }
+
+
+    /**
+     * 判断一个坐标点位是否在一个坐标区域内
+     * @param longitude 经度
+     * @param latitude 纬度
+     * @param fencePoints 围栏点位字符串,格式为 "lng1_lat1,lng2_lat2,..."
+     * @return 是否在区域内 true在 false不在
+     */
+    public boolean isPointInPolygon(double longitude, double latitude, String fencePoints) {
+        if (fencePoints == null || fencePoints.trim().isEmpty()) {
+            return false;
+        }
+
+        // 解析围栏点位
+        String[] points = fencePoints.split(",");
+        if (points.length < 3) {
+            return false;
+        }
+
+        int n = points.length;
+        boolean inside = false;
+
+        double x = longitude, y = latitude;
+        double[] xs = new double[n];
+        double[] ys = new double[n];
+
+        for (int i = 0; i < n; i++) {
+            String[] coords = points[i].split("_");
+            if (coords.length != 2) {
+                continue;
+            }
+            try {
+                xs[i] = Double.parseDouble(coords[0]);
+                ys[i] = Double.parseDouble(coords[1]);
+            } catch (NumberFormatException e) {
+                return false;
+            }
+        }
+
+        // 使用射线法判断点是否在多边形内
+        for (int i = 0, j = n - 1; i < n; j = i++) {
+            if (((ys[i] > y) != (ys[j] > y)) &&
+                    (x < (xs[j] - xs[i]) * (y - ys[i]) / (ys[j] - ys[i]) + xs[i])) {
+                inside = !inside;
+            }
+        }
+
+        return inside;
+    }
+
+
+    /**
+     * 判断一个坐标区域(多边形A)是否完全在另一个坐标区域(多边形B)内
+     * 原理:如果多边形A的所有顶点都在多边形B内,则认为A在B内
+     * @param innerFencePoints 内部围栏点位字符串(待判断是否在外的围栏),格式为 "lng1_lat1,lng2_lat2,..."
+     * @param outerFencePoints 外部围栏点位字符串(参考围栏),格式为 "lng1_lat1,lng2_lat2,..."
+     * @return 是否在内 true在内 false不在
+     */
+    public boolean isPolygonInPolygon(String innerFencePoints, String outerFencePoints) {
+        if (innerFencePoints == null || innerFencePoints.trim().isEmpty()
+                || outerFencePoints == null || outerFencePoints.trim().isEmpty()) {
+            return false;
+        }
+
+        // 解析内部围栏点位
+        String[] innerPoints = innerFencePoints.split(",");
+        if (innerPoints.length < 3) {
+            return false;
+        }
+
+        // 检查内部围栏的每一个顶点是否都在外部围栏内
+        for (String pointStr : innerPoints) {
+            String[] coords = pointStr.split("_");
+            if (coords.length != 2) {
+                continue;
+            }
+            try {
+                double lon = Double.parseDouble(coords[0]);
+                double lat = Double.parseDouble(coords[1]);
+
+                // 如果有一个顶点不在外部围栏内,则整个区域不在内部
+                if (!isPointInPolygon(lon, lat, outerFencePoints)) {
+                    return false;
+                }
+            } catch (NumberFormatException e) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+
+    /**
+     * 根据围栏点位字符串计算中心点坐标
+     * @param fencePoints 围栏点位字符串,格式为 "lng1_lat1,lng2_lat2,..."
+     * @return double[0] 经度, double[1] 纬度
+     */
+    public double[] calculateCenterPoint(String fencePoints) {
+        if (fencePoints == null || fencePoints.trim().isEmpty()) {
+            return new double[]{0.0, 0.0};
+        }
+
+        String[] points = fencePoints.split(",");
+        if (points.length == 0) {
+            return new double[]{0.0, 0.0};
+        }
+
+        double sumLon = 0.0;
+        double sumLat = 0.0;
+        int count = 0;
+
+        for (String point : points) {
+            String[] coords = point.split("_");
+            if (coords.length == 2) {
+                try {
+                    double lon = Double.parseDouble(coords[0]);
+                    double lat = Double.parseDouble(coords[1]);
+                    sumLon += lon;
+                    sumLat += lat;
+                    count++;
+                } catch (NumberFormatException e) {
+                    // 忽略解析错误的点
+                }
+            }
+        }
+
+        if (count == 0) {
+            return new double[]{0.0, 0.0};
+        }
+
+        double centerLon = sumLon / count;
+        double centerLat = sumLat / count;
+
+        return new double[]{centerLon, centerLat};
+    }
+
+
+    @Override
+    public void validate(XpStationVo dto) {
+        if (dto.getElectronicAreaId() == null) {
+            throw new ServiceException("运营区域ID不能为空");
+        }
+        if (dto.getElectronicId() == null) {
+            throw new ServiceException("围栏ID不能为空");
+        }
+//        if(dto.getXpPointList().isEmpty()){
+//            throw new ServiceException("场站点位不能为空");
+//        }
+
+        for (XpPoint xpPoint : dto.getXpPointList()) {
+            if(StrUtil.isBlank(xpPoint.getPointName()) ){
+                throw new ServiceException("点位名称不能为空");
+            }
+            if(StrUtil.isBlank(xpPoint.getPointPoi()) ){
+                throw new ServiceException("点位poi不能为空");
+            }
+            if(StrUtil.isBlank(xpPoint.getPointAddress())){
+                throw new ServiceException("点位地址不能为空");
+            }
+            if(xpPoint.getPointLon() == null || xpPoint.getPointLat() == null){
+                throw new ServiceException("点位经纬度不能为空");
+            }
+        }
+
+        if (dto.getEffectTime().isBefore(LocalDateTime.now())) {
+            throw new ServiceException("计划生效时间不能小于当前时间");
+        }
+        XpElectronicFence electronicFence = xpElectronicFenceService.getById(dto.getElectronicId());
+        if(electronicFence == null){
+            throw new ServiceException("场站id错误");
+        }
+        XpElectronicFenceArea electronicFenceArea = xpElectronicFenceAreaService.getById(dto.getElectronicAreaId());
+        if(electronicFenceArea == null){
+            throw new ServiceException("所属运营区id错误");
+        }
+
+        dto.setCityCode(electronicFence.getCityCode());
+
+        double[] centerPoints = calculateCenterPoint(dto.getFencePoints());//场站中心点位
+        dto.setCenterLon(centerPoints[0]);
+        dto.setCenterLat(centerPoints[1]);
+        //判断场站是否在 围栏里
+        boolean polygonInPolygon = isPolygonInPolygon(electronicFenceArea.getFencePoints(), dto.getFencePoints());
+        if(!polygonInPolygon){
+            throw new ServiceException("场站不在 "+electronicFence.getElectronicName()+"围栏内");
+
+        }
+        //判断点位是否在 场站里
+
+        if(!dto.getXpPointList().isEmpty()) {
+            for (XpPoint xpPoint : dto.getXpPointList()) {
+                if(!isPointInPolygon(xpPoint.getPointLon(),xpPoint.getPointLat(),dto.getFencePoints())){
+                    throw new ServiceException(xpPoint.getPointName()+" 点位不在 "+dto.getStationName()+"场站内");
+                }
+            }
+        }
+
+        //获取现有的场站区域
+        List<XpStation> listAll = findList();
+        // 检查新区域与现有运营区域是否有重叠
+        for (XpStation existingArea : listAll) {
+            if (polygonOverlapService.checkPolygonOverlap(dto.getFencePoints(),existingArea.getFencePoints())) {
+                throw new ServiceException("场站区域{}与现有场站区域{}发生重叠",I18nKeyConstant.EDIT_ELECTRONIC_ERROR,
+                        new Object[]{dto.getStationName(), existingArea.getStationName()}
+                );
+            }
+        }
+    }
+
+    public static String getPinyinInitials(String chinese) {
+        StringBuilder pinyinInitials = new StringBuilder();
+        for (char c : chinese.toCharArray()) {
+            // 获取拼音数组(多音字会返回多个拼音)
+            String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c);
+            if (pinyinArray != null && pinyinArray.length > 0) {
+                // 取第一个拼音的首字母
+                pinyinInitials.append(pinyinArray[0].charAt(0));
+            }
+        }
+        return pinyinInitials.toString();
+    }
+
+    @Transactional
+    @ShardingSphereTransactionType(TransactionType.BASE)
+    @Override
+    public R add(XpStationVo dto) {
+
+        try {
+            // 生成围栏id 规则:城市首字母-类型简写-4位自增流水号(如 SZ-OP-001),支持复制;数据停用或删除后,流水号不回收
+            XpRegion xpRegion = xpRegionMapper.selectOne(new LambdaQueryWrapper<XpRegion>()
+                    .eq(XpRegion::getCode, dto.getCityCode()));
+            String name = xpRegion.getName();
+            dto.setCityName(name);
+
+            dto.setStatus(0); //status = 场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
+            this.save(dto);
+
+            //场站编号唯一 规则:城市首字母-HUB-4位流水号
+            // 根据城市中文名,截取前两位的首字母并且大写
+            // 截取前两个字符
+            String prefix = name.substring(0, 2);
+            // 获取拼音首字母并大写
+            String result = getPinyinInitials(prefix).toUpperCase();
+            dto.setStationNumber(result + "-HUB-" + String.format("%04d", dto.getId()));
+
+            this.updateById(dto);
+
+            save_operatingArea(dto);
+
+            savePoint(dto);
+            return R.ok();
+        } catch (Exception e) {
+            log.error("新增场站失败", e);
+            throw new ServiceException("新增场站失败");
+        }
+    }
+
+
+    @Transactional
+    @ShardingSphereTransactionType(TransactionType.BASE)
+    @Override
+    public R edit(XpStationVo dto) {
+
+        try {
+//            dto.setStatus(0); //status = 场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
+            this.updateById(dto);
+            // 删除旧的区域数据,然后保存新的区域数据
+            remove_operatingArea(dto);
+            save_operatingArea(dto);
+            xpPointService.clearStationInfoByStationId(dto.getId());
+            savePoint(dto);
+            return R.ok();
+        } catch (Exception e) {
+            log.error("新增场站失败", e);
+            throw new ServiceException("新增场站失败");
+        }
+    }
+    @Transactional
+    public void savePoint(XpStationVo dto) {
+        if(CollUtil.isNotEmpty(dto.getXpPointList())){
+            //保存点位
+            List<XpPoint> xpPointListAdd = new ArrayList<>();
+            List<XpPoint> xpPointListUpdate = new ArrayList<>();
+            int stationSortNo = 1;
+            for (XpPoint xpPoint : dto.getXpPointList()) {
+                xpPoint.setStationSortNo(stationSortNo++);
+                xpPoint.setStationId(dto.getId());
+                if(xpPoint.getId() == null){
+                    xpPointService.getPointNumber(dto.getElectronicAreaId());
+                    xpPoint.setFpElectronicAreaId(dto.getElectronicAreaId());
+                    xpPoint.setElectronicAreaId(dto.getElectronicAreaId());
+                    xpPointListAdd.add(xpPoint);
+                }else{
+                    xpPointListUpdate.add(xpPoint);
+                }
+            }
+            if(CollUtil.isNotEmpty(xpPointListAdd)){
+                xpPointService.saveBatch(xpPointListAdd);
+            }
+            if(CollUtil.isNotEmpty(xpPointListUpdate)){
+                xpPointService.updateBatchById(xpPointListUpdate);
+            }
+        }
+    }
+
+    @Override
+    public boolean setStatus(XpStation bean, Integer status){
+
+        bean.setStatus(status);
+        boolean b = updateById(bean);
+        if(b){
+            //场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
+            List<Integer> statusList = Arrays.asList(4, 5, 6, 7) ;
+            if(statusList.contains(status) ){
+                save_operatingArea(bean);
+            }
+            if(status == 7 ){
+                remove_operatingArea(bean);
+            }
+        }
+        return b;
+    }
+
+    private void remove_operatingArea(XpStation bean) {
+        // 同步删除Mongo电子围栏多边形
+        Query delMongoQuery = new Query();
+        delMongoQuery.addCriteria(Criteria.where("stationId").is(String.valueOf(bean.getId())));
+        mongoTemplate.remove(delMongoQuery, XpGeoFencingPolygon.class);
+    }
+
+    private void save_operatingArea (XpStation bean) {
+        XpGeoFencingPolygon xpGeoFencingPolygon = new XpGeoFencingPolygon();
+        xpGeoFencingPolygon.setFenceId(String.valueOf(bean.getElectronicId()));
+        xpGeoFencingPolygon.setName(bean.getStationName());
+        xpGeoFencingPolygon.setAreaId(String.valueOf(bean.getElectronicAreaId()));
+        xpGeoFencingPolygon.setCityCode(bean.getCityCode());
+        xpGeoFencingPolygon.setStationId(String.valueOf(bean.getId()));
+        xpGeoFencingPolygon.setOperationStrategy(null);//运营策略
+        // 将围栏点转换为GeoJSON多边形
+        GeoJsonPolygon geoJsonPolygon = xpElectronicFenceService.convertToGeoJsonPolygon(bean.getFencePoints());
+        xpGeoFencingPolygon.setPoints(geoJsonPolygon);
+        mongoTemplate.insert(xpGeoFencingPolygon);
+    }
+
+
+    /**
+     * 停用
+     *     -  操作项展示为 [停用]。点击后弹出二次确认框,“当前场站包含12个点位,确认一起停用?
+     *       - 是:确认后场站以及子点位状态流转至“已停用”。
+     *       - 否:子点位维持当前状态,但自动退回为“独立点位”。
+     */
+    @Override
+    public boolean updateStatusStop(XpStation bean, Boolean pointStop){
+
+        xpPointService.statusStopByStationId(bean.getId(),pointStop);
+        return setStatus(bean, 7);
+    }
+
+    /**
+     *       - 删除拦截逻辑:
+     *         - 测试阶段(待测试、测试中等):点击直接删除。
+     *         - 历史运营阶段(已停用、失效):点击弹出二次确认框“当前场站包含12个点位,确认一起删除?”
+     *           - 是:点位统一一起删除
+     *           - 否:不自动删除点位,点位自动退回为[待测试]的“独立点位”,并自动继承父级运营区的运营时段
+     * @param bean 实体
+     * @return
+     */
+    @Transactional
+    @Override
+    public boolean removeById(XpStation bean, Boolean pointDelete){
+        boolean b = true;
+        //场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
+        if(bean.getStatus() == 1 || bean.getStatus() == 2 || bean.getStatus() == 3 || bean.getStatus() == 7 || bean.getStatus() == 8){
+            b = super.removeById(bean.getId());
+        }
+        if(bean.getStatus() == 7 || bean.getStatus() == 8){
+             xpPointService.pointDeleteByStationId(bean.getId(),pointDelete);
+        }
+        if(b){
+            // 同步删除Mongo电子围栏多边形
+            Query delMongoQuery = new Query();
+            delMongoQuery.addCriteria(Criteria.where("stationId").is(String.valueOf(bean.getId())));
+            mongoTemplate.remove(delMongoQuery, XpGeoFencingPolygon.class);
+        }
+        return b;
+
+    }
+
 }

--
Gitblit v1.7.1