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.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; /** *

* 场站 服务实现类 *

* * @author mitao * @since 2026-04-12 */ @Service public class XpStationServiceImpl extends ServiceImpl 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 findList(XpStationQuery query) { return this.baseMapper.pageList( query); } @Override public PageInfo pageList(XpStationQuery query) { PageInfo pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize()); if (null == query.getCreateSort() && null == query.getUpdateSort()){ query.setCreateSort(2); } List list = this.baseMapper.pageList(pageInfo, query); if (list.isEmpty()) { return new PageInfo<>(); } // List electronicIds = list.stream().map(XpElectronicFence::getId).collect(Collectors.toList()); // Map> electronicFenceAreaMap = xpElectronicFenceAreaMapper.selectList(new LambdaQueryWrapper() // .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 findList() { // 获取现有的运营区域(areaType=1) LambdaQueryWrapper 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 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() .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 xpPointListAdd = new ArrayList<>(); List 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 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; } }