xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceAddDto.java
@@ -33,7 +33,6 @@ @ApiModelProperty(value = "生效类型 1=立即生效 2=定时生效") @NotNull(message = "生效类型不能为空") private Integer effectType; @ApiModelProperty(value = "生效时间") xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/map/XpGeoFencingPolygon.java
@@ -34,6 +34,10 @@ * 围栏区域id */ private String areaId; /** * 场站id */ private String stationId; /** * 城市code @@ -41,7 +45,7 @@ private String cityCode; /** * 围栏名称 * 围栏名称、场站名称 */ private String name; xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/map/XpGeoFencingProhibitedPolygon.java
@@ -35,6 +35,10 @@ */ private String areaId; /** * 场站id */ private String stationId; /** xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpStation.java
@@ -37,6 +37,7 @@ private Long id; @ApiModelProperty(value = "场站名称,C端展示主标题") @NotNull(message = "场站名称不能为空") private String stationName; @ApiModelProperty(value = "场站编号唯一 规则:城市首字母-HUB-4位流水号") @@ -93,6 +94,7 @@ private BigDecimal stationArea; @ApiModelProperty(value = "场站围栏点位 经度_纬度,经度_纬度") @NotNull(message = "场站围栏点位不能为空") private String fencePoints; @ApiModelProperty(value = "中心点经度") xp-robotaxi-api/src/main/java/com/xiaopeng/api/query/operation/XpStationQuery.java
New file @@ -0,0 +1,48 @@ package com.xiaopeng.api.query.operation; import com.xiaopeng.common.core.web.page.BasePage; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; @Data public class XpStationQuery extends BasePage { @ApiModelProperty(value = "ids") private List<Long> ids; @ApiModelProperty(value = "场站名称编码") private String stationNameOrNumber; @ApiModelProperty(value = "场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效") private Integer status; @ApiModelProperty(value = "场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效") private List<Integer> statusList; @ApiModelProperty(value = "创建人") private String createBy; @ApiModelProperty(value = "创建开始时间") private String createStartTime; @ApiModelProperty(value = "创建结束时间") private String createEndTime; @ApiModelProperty(value = "更新人") private String updateBy; @ApiModelProperty(value = "更新开始时间") private String updateStartTime; @ApiModelProperty(value = "更新结束时间") private String updateEndTime; @ApiModelProperty(value = "限停时长") private Integer parkingTimeLimitMin; @ApiModelProperty(value = "限停时长") private Integer parkingTimeLimitMax; @ApiModelProperty(value = "开始时间 1正序 2倒序") private Integer createSort; @ApiModelProperty(value = "更新时间 1正序 2倒序") private Integer updateSort; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/XpStationVo.java
New file @@ -0,0 +1,19 @@ package com.xiaopeng.api.vo.operation; import com.xiaopeng.api.model.operation.XpPoint; import com.xiaopeng.api.model.operation.XpStation; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; @Data public class XpStationVo extends XpStation { @ApiModelProperty(value = "点位设置") private List<XpPoint> xpPointList; } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpStationManagementController.java
New file @@ -0,0 +1,198 @@ package com.xiaopeng.map.controller; import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import com.xiaopeng.api.model.operation.XpStation; import com.xiaopeng.api.query.operation.XpStationQuery; import com.xiaopeng.api.vo.account.LoginUser; 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.utils.WebUtils; import com.xiaopeng.common.core.web.page.PageInfo; import com.xiaopeng.common.security.service.TokenService; import com.xiaopeng.map.exportExcel.XpStationExportExcel; import com.xiaopeng.map.service.XpStationService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Workbook; import org.springframework.beans.BeanUtils; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; @RestController @Slf4j @Api(tags = "场站管理") @RequestMapping("/api/v1/station") public class XpStationManagementController { @Resource private XpStationService xpStationService; @Resource private TokenService tokenService; @ApiOperation(value = "场站管理列表", tags = {"管理后台-运营-场站管理"}) @PostMapping("/pageList") public R<PageInfo<XpStation>> pageList(@RequestBody XpStationQuery query) { PageInfo<XpStation> pageList = xpStationService.pageList(query); return R.ok(pageList); } /** * 新增电子围栏 */ @PostMapping("/add") @ApiOperation(value = "新增场站管理", tags = {"管理后台-运营-场站管理"}) public R add(@Valid @RequestBody XpStationVo vo) { xpStationService.validate(vo); LoginUser loginUser = tokenService.getLoginUser(); vo.setCreateBy(loginUser.getUsername()); vo.setUpdateBy(loginUser.getUsername()); vo.setCreateTime(LocalDateTime.now()); vo.setUpdateTime(LocalDateTime.now()); return xpStationService.add(vo); } /** * 编辑电子围栏 */ @PostMapping("/edit") @ApiOperation(value = "编辑场站管理" , tags = {"管理后台-运营-场站管理"}) public R edit(@Valid @RequestBody XpStationVo vo) { if(vo.getId() == null){ return R.fail("参数错误"); } xpStationService.validate(vo); XpStation bean = xpStationService.getById(vo.getId()); if(bean == null){ return R.fail("参数错误"); } LoginUser loginUser = tokenService.getLoginUser(); vo.setUpdateBy(loginUser.getUsername()); vo.setUpdateTime(LocalDateTime.now()); return xpStationService.edit(vo); } /** * 启用中状态: 操作项展示为 [停用]。 停用 * - 操作项展示为 [停用]。点击后弹出二次确认框,“当前场站包含12个点位,确认一起停用? * - 是:确认后场站以及子点位状态流转至“已停用”。 * - 否:子点位维持当前状态,但自动退回为“独立点位”。 */ @ApiOperation(value = "停用" , tags = {"管理后台-运营-场站管理"}) @PostMapping("/updateStatus/{id}") public R updateStatusStop(@PathVariable Long id, @RequestParam Boolean pointStop) { XpStation bean = xpStationService.getById(id); if (bean == null) { throw new ServiceException("场站不存在,请刷新重试"); } // Integer status =6; // if(!statusEnable){ // status = 7; // } //场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效 xpStationService.updateStatusStop(bean,pointStop); return R.ok(); } /** * - 删除拦截逻辑: * - 测试阶段(待测试、测试中等):点击直接删除。 * - 历史运营阶段(已停用、失效):点击弹出二次确认框“当前场站包含12个点位,确认一起删除?” * - 是:点位统一一起删除 * - 否:不自动删除点位,点位自动退回为[待测试]的“独立点位”,并自动继承父级运营区的运营时段 * @param id * @param pointDelete * @return */ @ApiOperation(value = "删除场站管理", tags = {"管理后台-运营-场站管理"}) @PostMapping("/delete/{id}") public R delete(@PathVariable Long id,@RequestParam Boolean pointDelete) { XpStation bean = xpStationService.getById(id); if (bean == null) { throw new ServiceException("场站不存在,请刷新重试"); } // 场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效 // 只有状态为已停用(7)时才能删除 if (bean.getStatus() == 4 || bean.getStatus() == 5 || bean.getStatus() == 6) { String msg = ""; if(bean.getStatus() == 4){ msg = "可运营"; }else if (bean.getStatus() == 5){ msg = "待生效"; }else { msg = "启用中"; } throw new ServiceException("场站"+msg+",无法删除"); } boolean b = xpStationService.removeById(bean,pointDelete); if (b){ return R.ok(); } return R.fail("删除失败"); } @ApiOperation(value = "导出", tags = {"管理后台-运营-场站管理"}) @PostMapping("/exportExcel") public void exportExcel(@RequestBody XpStationQuery query) { if (query.getIds().isEmpty()) { return; } List<XpStation> list = xpStationService.findList(query); List<XpStationExportExcel> exportList = new ArrayList<>(list.size()); for (XpStation xpStation : list) { XpStationExportExcel excel = new XpStationExportExcel(); BeanUtils.copyProperties(xpStation, excel); excel.setStatusName(xpStation.getStatusName()); excel.setCreateTimeStr(xpStation.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); excel.setUpdateTimeStr(xpStation.getUpdateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); exportList.add(excel); } HttpServletResponse response = WebUtils.response(); if (response == null) { throw new ServiceException("导出失败:未获取到响应对象"); } response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); try { String fileName = URLEncoder.encode("场站列表.xls", StandardCharsets.UTF_8.name()); response.setHeader("Content-disposition", "attachment;filename=" + fileName); try (Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), XpStationExportExcel.class, exportList); ServletOutputStream outputStream = response.getOutputStream()) { workbook.write(outputStream); } } catch (IOException e) { log.error("点位导出失败 ids={}", query.getIds(), e); throw new ServiceException("点位导出失败"); } } } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/exportExcel/XpStationExportExcel.java
New file @@ -0,0 +1,41 @@ package com.xiaopeng.map.exportExcel; import cn.afterturn.easypoi.excel.annotation.Excel; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @Data @ApiModel(value="点位场站") public class XpStationExportExcel { @ApiModelProperty(value = "场站编号唯一 规则:城市首字母-HUB-4位流水号") @Excel(name = "场站ID",width = 20) private String stationNumber; @ApiModelProperty(value = "场站名称,C端展示主标题") @Excel(name = "场站名称",width = 20) private String stationName; @Excel(name = "场站构成",width = 20) private String pointsDesc; @Excel(name = "状态",width = 20) public String statusName; /** 更新者 */ @Excel(name = "更新人",width = 20) private String updateBy; @Excel(name = "更新时间",width = 20) private String updateTimeStr; @Excel(name = "创建人",width = 20) private String createBy; @Excel(name = "创建时间",width = 20) private String createTimeStr; } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpStationMapper.java
@@ -2,6 +2,11 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.xiaopeng.api.model.operation.XpStation; import com.xiaopeng.api.query.operation.XpStationQuery; import com.xiaopeng.common.core.web.page.PageInfo; import org.apache.ibatis.annotations.Param; import java.util.List; /** * <p> @@ -13,4 +18,11 @@ */ public interface XpStationMapper extends BaseMapper<XpStation> { List<XpStation> pageList(@Param("pageInfo") PageInfo<XpStation> pageInfo, @Param("query") XpStationQuery query); List<XpStation> pageList(@Param("query") XpStationQuery query); } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpElectronicFenceService.java
@@ -8,6 +8,7 @@ import com.xiaopeng.api.query.operation.ElectronicPageListQuery; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.web.page.PageInfo; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import java.util.List; @@ -26,6 +27,8 @@ R editElectronic(ElectronicFenceUpdateDto dto); GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr); /** * 校验围栏名称唯一 */ xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpPointService.java
@@ -19,6 +19,7 @@ import com.xiaopeng.map.vo.NearestVo; import com.xiaopeng.map.vo.OrderDistanceVo; import com.xiaopeng.map.vo.StationDetailVo; import org.springframework.transaction.annotation.Transactional; import javax.validation.Valid; import java.time.LocalTime; @@ -39,6 +40,8 @@ * 新增点位并根据规则生成点位编号 */ XpPoint addPointWithGeneratedNumber(@Valid PointAddDto pointAddDto); String getPointNumber(Long electronicAreaId); /** * 校验点位名称唯一 @@ -101,4 +104,14 @@ PageInfo<XpPoint> pageList(PointPageListListQuery query); List<XpPoint> listByStationId(Long stationId); @Transactional(rollbackFor = Exception.class) void clearStationInfoByStationId(Long stationId); @Transactional(rollbackFor = Exception.class) void statusStopByStationId(Long stationId, Boolean pointStop); @Transactional(rollbackFor = Exception.class) void pointDeleteByStationId(Long stationId, Boolean pointDelete); } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpStationService.java
@@ -2,6 +2,15 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.xiaopeng.api.model.operation.XpStation; 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.web.page.PageInfo; import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * <p> @@ -13,4 +22,26 @@ */ public interface XpStationService extends IService<XpStation> { List<XpStation> findList(XpStationQuery query); PageInfo<XpStation> pageList(XpStationQuery query); void validate(XpStationVo dto); @Transactional @ShardingSphereTransactionType(TransactionType.BASE) R add(XpStationVo dto); @Transactional @ShardingSphereTransactionType(TransactionType.BASE) R edit(XpStationVo dto); boolean setStatus(XpStation bean, Integer status); boolean updateStatusStop(XpStation bean, Boolean pointStop); @Transactional boolean removeById(XpStation bean, Boolean pointDelete); } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpElectronicFenceServiceImpl.java
@@ -53,7 +53,6 @@ import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.awt.geom.Point2D; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @@ -921,7 +920,8 @@ * 将围栏点字符串转换为GeoJsonPolygon * 格式:经度_维度,经度_维度 */ private GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) { @Override public GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) { if (fencePointsStr == null || fencePointsStr.trim().isEmpty()) { return null; } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
@@ -130,9 +130,19 @@ } validatePointNameUnique(pointAddDto.getPointName(), null); String cityPrefix = buildCityPrefix(pointAddDto.getElectronicAreaId()); String lockKey = buildPointNumberLockKey(cityPrefix, POINT_BIZ_TYPE); String pointNumber = getPointNumber(pointAddDto.getElectronicAreaId()); XpPoint xpPoint = new XpPoint(); BeanUtils.copyProperties(pointAddDto, xpPoint); xpPoint.setFpElectronicAreaId(pointAddDto.getElectronicAreaId()); xpPoint.setPointNumber(pointNumber); this.save(xpPoint); return xpPoint; } @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; @@ -159,13 +169,7 @@ log.warn("释放点位编号生成锁失败 lockKey={}", lockKey, e); } } XpPoint xpPoint = new XpPoint(); BeanUtils.copyProperties(pointAddDto, xpPoint); xpPoint.setFpElectronicAreaId(pointAddDto.getElectronicAreaId()); xpPoint.setPointNumber(pointNumber); this.save(xpPoint); return xpPoint; return pointNumber; } @Override @@ -674,6 +678,29 @@ 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, null) .update(); } /** * 查询用户收藏的点位ID。 @@ -695,4 +722,52 @@ } } /** * 场站停用 * - 操作项展示为 [停用]。点击后弹出二次确认框,“当前场站包含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); } } /** * 场站删除 * - 删除拦截逻辑: * - 测试阶段(待测试、测试中等):点击直接删除。 * - 历史运营阶段(已停用、失效):点击弹出二次确认框“当前场站包含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); } } } 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; } } xp-robotaxi-map-boot/src/main/resources/mapper/XpStationMapper.xml
@@ -40,4 +40,84 @@ sort_no, is_deleted, create_time, update_time, create_by, update_by </sql> <select id="pageList" resultType="com.xiaopeng.api.model.operation.XpStation"> select a.*, (select count(1) from xp_point b where b.station_id = a.id) point_count, (select count(1) from xp_point c where c.station_id = a.id and c.operation_status = 2) operatingPointCount from xp_station a inner join xp_electronic_fence b on a.electronic_id = b.id and b.is_deleted = 0 where a.is_deleted = 0 <if test="query.ids != null"> and a.id in <foreach item="id" collection="query.ids" open="(" separator="," close=")"> #{id} </foreach> </if> <if test="query.stationNameOrNumber != null and query.stationNameOrNumber != ''"> and( a.station_name like concat('%',#{query.stationNameOrNumber},'%') or a.station_number like concat('%',#{query.stationNameOrNumber},'%') ) </if> <if test="query.status != null"> and a.status = #{query.status} </if> <if test="query.statusList != null"> <foreach collection="query.statusList" item="status" open=" and a.status in (" separator="," close=")"> #{status} </foreach> </if> <if test="query.parkingTimeLimitMin != null"> and a.parking_time_limit >= #{query.parkingTimeLimitMin} </if> <if test="query.parkingTimeLimitMax != null"> and a.parking_time_limit <= #{query.parkingTimeLimitMax} </if> <if test="query.createBy != null and query.createBy!=''"> and a.create_by like concat('%',#{query.createBy},'%') </if> <if test="query.createStartTime != null and query.createStartTime!=''"> and a.create_time >= #{query.createStartTime} </if> <if test="query.createEndTime != null and query.createEndTime!=''"> and #{query.createEndTime} >= a.create_time </if> <if test="query.updateBy != null and query.updateBy!=''"> and a.update_by like concat('%',#{query.updateBy},'%') </if> <if test="query.updateStartTime != null and query.updateStartTime!=''"> and a.update_time >= #{query.updateStartTime} </if> <if test="query.updateEndTime != null and query.updateEndTime!=''"> and #{query.updateEndTime} >= a.update_time </if> <choose> <when test="query.createSort == 1 or query.createSort == 2 or query.updateSort == 1 or query.updateSort == 2"> <trim prefix="order by" suffixOverrides=","> <if test="query.createSort == 1"> a.create_time asc, </if> <if test="query.createSort == 2"> a.create_time desc, </if> <if test="query.updateSort == 1"> a.update_time asc, </if> <if test="query.updateSort == 2"> a.update_time desc, </if> </trim> </when> <otherwise> order by a.update_time desc </otherwise> </choose> </select> </mapper> xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpStationController.java
New file @@ -0,0 +1,19 @@ package com.xiaopeng.operation.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 场站 */ @RestController @RequestMapping public class XpStationController { } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/mapper/XpStationMapper.java
New file @@ -0,0 +1,24 @@ package com.xiaopeng.operation.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.xiaopeng.api.model.operation.XpStation; import com.xiaopeng.api.query.operation.XpStationQuery; import com.xiaopeng.common.core.web.page.PageInfo; import org.apache.ibatis.annotations.Param; import java.util.List; /** * <p> * 场站 Mapper 接口 * </p> * * @author mitao * @since 2026-04-12 */ public interface XpStationMapper extends BaseMapper<XpStation> { } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/IXpElectronicFenceService.java
@@ -7,6 +7,7 @@ import com.xiaopeng.api.dto.operation.ElectronicFenceAddDto; import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto; import com.xiaopeng.api.query.operation.ElectronicPageListQuery; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import java.util.List; @@ -22,10 +23,14 @@ PageInfo<XpElectronicFence> pageList(ElectronicPageListQuery query); String getPinyinInitials(String chinese); R addElectronic(ElectronicFenceAddDto dto); R editElectronic(ElectronicFenceUpdateDto dto); GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr); /** * 查询启用的电子围栏列表 * @return xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/XpStationService.java
New file @@ -0,0 +1,18 @@ package com.xiaopeng.operation.service; import com.baomidou.mybatisplus.extension.service.IService; import com.xiaopeng.api.model.operation.XpStation; /** * <p> * 场站 服务类 * </p> * * @author mitao * @since 2026-04-12 */ public interface XpStationService extends IService<XpStation> { } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpElectronicFenceServiceImpl.java
@@ -16,7 +16,6 @@ import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.exception.ServiceException; import com.xiaopeng.common.core.utils.DateUtils; import com.xiaopeng.common.core.utils.StringUtils; import com.xiaopeng.common.core.utils.bean.BeanUtils; import com.xiaopeng.common.core.web.page.PageInfo; import com.xiaopeng.operation.constant.I18nKeyConstant; @@ -105,7 +104,8 @@ pageInfo.setRecords(list); return pageInfo; } public static String getPinyinInitials(String chinese) { @Override public String getPinyinInitials(String chinese) { StringBuilder pinyinInitials = new StringBuilder(); for (char c : chinese.toCharArray()) { // 获取拼音数组(多音字会返回多个拼音) @@ -602,7 +602,8 @@ * 将围栏点字符串转换为GeoJsonPolygon * 格式:经度_维度,经度_维度 */ private GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) { @Override public GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) { if (fencePointsStr == null || fencePointsStr.trim().isEmpty()) { return null; } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpStationServiceImpl.java
New file @@ -0,0 +1,22 @@ package com.xiaopeng.operation.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.xiaopeng.api.model.operation.XpStation; import com.xiaopeng.operation.mapper.XpStationMapper; import com.xiaopeng.operation.service.XpStationService; import org.springframework.stereotype.Service; /** * <p> * 场站 服务实现类 * </p> * * @author mitao * @since 2026-04-12 */ @Service public class XpStationServiceImpl extends ServiceImpl<XpStationMapper, XpStation> implements XpStationService { } xp-robotaxi-operation-boot/src/main/resources/mapper/operation/XpStationMapper.xml
New file @@ -0,0 +1,37 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xiaopeng.operation.mapper.XpStationMapper"> <!-- 通用查询结果列 --> <sql id="Base_Column_List"> id, electronic_name, electronic_number, status, effect_type, effect_time, failure_type, failure_time, operation_strategy, is_deleted, create_time, update_time, create_by, update_by </sql> <select id="pageList" resultType="com.xiaopeng.api.model.operation.XpStation"> select a.* from xp_station a where a.is_deleted = 0 <choose> <when test="query.createSort == 1 or query.createSort == 2 or query.updateSort == 1 or query.updateSort == 2"> <trim prefix="order by" suffixOverrides=","> <if test="query.createSort == 1"> a.create_time asc, </if> <if test="query.createSort == 2"> a.create_time desc, </if> <if test="query.updateSort == 1"> a.update_time asc, </if> <if test="query.updateSort == 2"> a.update_time desc, </if> </trim> </when> <otherwise> order by a.update_time desc </otherwise> </choose> </select> </mapper>