xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceAddDto.java
@@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.model.operation.XpTestElectronicFenceArea; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -52,5 +53,5 @@ private String operationStrategy; @ApiModelProperty(value = "电子围栏区域") @NotEmpty(message = "电子围栏区域不能为空") private List<XpElectronicFenceArea> electronicFenceAreas; private List<XpTestElectronicFenceArea> electronicFenceAreas; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceUpdateDto.java
@@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.model.operation.XpTestElectronicFenceArea; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -53,7 +54,7 @@ private String operationStrategy; @ApiModelProperty(value = "电子围栏区域") @NotEmpty(message = "电子围栏区域不能为空") private List<XpElectronicFenceArea> electronicFenceAreas; private List<XpTestElectronicFenceArea> electronicFenceAreas; @ApiModelProperty(value = "范围扩大 需要新增的点位ID列表") private List<Long> addPointIdList; xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpCheckLocation.java
@@ -47,7 +47,7 @@ @ApiModelProperty(value = "状态 1=启用 0=禁用") private Integer status; @ApiModelProperty(value = "状态 1=上班打卡 0=巡游打卡") @ApiModelProperty(value = "类型 1=上班打卡 0=巡游打卡") private Integer type = 1; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/map/XpTestGeoFencingPolygon.java
New file @@ -0,0 +1,63 @@ package com.xiaopeng.api.model.map; import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; import org.springframework.data.mongodb.core.mapping.Document; import java.io.Serializable; /** * 地理围栏-多边形 * @author zhibing.pu * @date 2023/4/29 23:53 */ @Data @Document(collection = "test_operating_area") public class XpTestGeoFencingPolygon implements Serializable { private static final long serialVersionUID = 4508868382007529971L; @Id private String id; /** * 围栏id */ private String fenceId; /** * 围栏区域id */ private String areaId; /** * 场站id */ private String stationId; /** * 城市code */ private String cityCode; /** * 围栏名称、场站名称 */ private String name; /** * 运营策略 */ private String operationStrategy; /** * 多边形经纬度 */ @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) private GeoJsonPolygon points; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpTestElectronicFence.java
New file @@ -0,0 +1,73 @@ package com.xiaopeng.api.model.operation; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import com.xiaopeng.common.core.web.domain.BaseModel; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.time.LocalDateTime; /** * <p> * 电子围栏 * </p> * * @author mitao * @since 2026-02-03 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("xp_test_electronic_fence") @ApiModel(value="XpTestElectronicFence对象", description="电子围栏") public class XpTestElectronicFence extends BaseModel { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; @ApiModelProperty(value = "围栏名称") private String electronicName; @ApiModelProperty(value = "围栏编号唯一 规则:城市首字母-类型简写-4位自增流水号(如 SZ-OP-001)") private String electronicNumber; @ApiModelProperty(value = "围栏状态 0=草稿 1=待启用 2=启用中 3=已停用") private Integer status; @ApiModelProperty(value = "生效类型 1=立即生效 2=定时生效") private Integer effectType; @ApiModelProperty(value = "生效时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime effectTime; @ApiModelProperty(value = "失效类型 1=永不失效 2=定时失效") private Integer failureType; @ApiModelProperty(value = "失效时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime failureTime; @ApiModelProperty(value = "运营策略") private String operationStrategy; @ApiModelProperty(value = "城市code") private String cityCode; @ApiModelProperty(value = "XPU围栏版本号,每次推送触发时递增") private Long xpuVersion; @ApiModelProperty(value = "运营区域数量") @TableField(exist = false) private Long operatingAreaCount; @ApiModelProperty(value = "禁行区域数量") @TableField(exist = false) private Long prohibitedAreaCount; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpTestElectronicFenceArea.java
New file @@ -0,0 +1,58 @@ package com.xiaopeng.api.model.operation; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.xiaopeng.common.core.web.domain.BaseModel; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.math.BigDecimal; /** * <p> * 电子围栏区域构成 * </p> * * @author mitao * @since 2026-02-03 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("xp_test_electronic_fence_area") @ApiModel(value="XpTestElectronicFenceArea对象", description="电子围栏区域构成") public class XpTestElectronicFenceArea extends BaseModel { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; @ApiModelProperty(value = "围栏id") private Long testElectronicId; @ApiModelProperty(value = "面积") private BigDecimal area; @ApiModelProperty(value = "区域名称") private String areaName; @ApiModelProperty(value = "区域类型 1=运营区 2=禁行区") private Integer areaType; @ApiModelProperty(value = "所属区县") private String cityCode; @ApiModelProperty(value = "区域描述") private String areaDescription; @ApiModelProperty(value = "区域状态 1=启用 2=禁用") private Integer areaStatus; @ApiModelProperty(value = "围栏点位 经度_维度,经度_维度") private String fencePoints; @ApiModelProperty(value = "运营策略") private String operationStrategy; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/TestElectronicFenceDetailVo.java
New file @@ -0,0 +1,19 @@ package com.xiaopeng.api.vo.operation; import com.xiaopeng.api.model.operation.XpElectronicFence; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.model.operation.XpTestElectronicFence; import com.xiaopeng.api.model.operation.XpTestElectronicFenceArea; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; @Data @ApiModel("电子围栏详情VO") public class TestElectronicFenceDetailVo extends XpTestElectronicFence { @ApiModelProperty("电子围栏区域列表") private List<XpTestElectronicFenceArea> electronicFenceAreas; } xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleCollectionScheduleTask.java
@@ -221,7 +221,7 @@ xpCruiseOrder.setOrderType(2); xpCruiseOrder.setStatus(1); xpCruiseOrder.setOrderReverse(1); xpCruiseOrder.setLastOrderId(r.getData()); xpCruiseOrder.setLastOrderId(oldCruiseOrder.getId()); xpCruiseOrder.setOrderReverse(1); xpCruiseOrder.setXpuTaskId(snowflake.nextId() + ""); xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpTestElectronicFenceManagementController.java
New file @@ -0,0 +1,367 @@ package com.xiaopeng.map.controller; import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.xiaopeng.api.dto.operation.ElectronicFenceAddDto; import com.xiaopeng.api.dto.operation.ElectronicFenceAreaUpdateStatusDto; import com.xiaopeng.api.dto.operation.ElectronicFenceOperationStrategyValidateDto; import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto; import com.xiaopeng.api.model.operation.*; import com.xiaopeng.api.query.operation.ElectronicPageListQuery; import com.xiaopeng.api.vo.operation.ElectronicFenceDetailVo; import com.xiaopeng.api.vo.operation.TestElectronicFenceDetailVo; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.exception.ServiceException; import com.xiaopeng.common.core.utils.bean.BeanUtils; import com.xiaopeng.common.core.web.page.PageInfo; import com.xiaopeng.map.constant.I18nKeyConstant; import com.xiaopeng.map.service.*; import com.xiaopeng.map.utils.OperationStrategyIndex; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static com.xiaopeng.map.service.serviceimpl.XpElectronicFenceServiceImpl.getPinyinInitials; /** * <p> * 电子围栏 前端控制器 * </p> * * @author mitao * @since 2026-02-03 */ @RestController @Slf4j @Api(tags = "测试demo电子围栏管理") @RequestMapping("/api/v1/testElectronicFence") public class XpTestElectronicFenceManagementController { @Resource private XpTestElectronicFenceService xpElectronicFenceService; @Resource private XpTestElectronicFenceAreaService xpElectronicFenceAreaService; @Resource private XpRegionService regionService; @Autowired private XpRegionService xpRegionService; @Autowired private XpPointService xpPointService; /** * 查询省市管理 */ @ApiOperation(value = "查询省市区",response = XpRegion.class) @GetMapping(value = "/getRegion") public R<List<List<XpRegion>>> geXpRegion() { return R.ok(regionService.getXpRegion()); } @ApiOperation(value = "获取不分页的电子围栏列表",tags = {"管理后台-电子围栏"}) @PostMapping("/list") R<List<XpTestElectronicFence>> getList(@RequestBody ElectronicPageListQuery query){ return R.ok(xpElectronicFenceService.getList(query)); } @ApiOperation(value = "电子围栏管理列表", response = XpElectronicFence.class) @PostMapping("/pageList") public R<PageInfo<XpTestElectronicFence>> pageList(@RequestBody ElectronicPageListQuery query) { PageInfo<XpTestElectronicFence> pageList = xpElectronicFenceService.pageList(query); return R.ok(pageList); } /** * 电子围栏列表接口 * * @return 电子围栏列表 */ @ApiOperation(value = "电子围栏列表接口", tags = {"运维APP-电子围栏"}) @GetMapping("/maintenance-app/api/v1/electronicFence/list") public R<List<XpTestElectronicFence>> vehicleMap() { List<XpTestElectronicFence> list = xpElectronicFenceService.list(); return R.ok(list); } /** * 新增电子围栏 */ @PostMapping("/add") @ApiOperation(value = "新增电子围栏") public R add(@Validated @RequestBody ElectronicFenceAddDto dto) { validateFenceTime(dto.getStatus(), dto.getEffectType(), dto.getEffectTime(), dto.getFailureType(), dto.getFailureTime()); return xpElectronicFenceService.addElectronic(dto); } /** * 修改状态 */ @PostMapping("/updateStatus") @ResponseBody @ApiOperation(value = "修改状态") public R updateStatus(@RequestBody ElectronicFenceAreaUpdateStatusDto dto) { XpTestElectronicFenceArea electronicFenceArea = xpElectronicFenceAreaService.getById(dto.getId()); if (electronicFenceArea == null) { throw new ServiceException( "围栏构成不存在,请刷新重试", I18nKeyConstant.FENCE_STRUCTURE_NOT_EXIST ); } xpElectronicFenceAreaService.updateById(electronicFenceArea); return R.ok(); } /** * 电子围栏详情 */ @PostMapping("/queryById") @ApiOperation(value = "电子围栏详情") public R<TestElectronicFenceDetailVo> queryById(@RequestParam("id")Long id) { TestElectronicFenceDetailVo res = new TestElectronicFenceDetailVo(); XpTestElectronicFence electronicFence = xpElectronicFenceService.getById(id); if (electronicFence == null) { throw new ServiceException( "电子围栏不存在,请刷新重试", I18nKeyConstant.FENCE_NOT_EXIST ); } BeanUtils.copyProperties(electronicFence, res); List<XpTestElectronicFenceArea> electronicFenceAreaList = xpElectronicFenceAreaService.lambdaQuery().eq(XpTestElectronicFenceArea::getTestElectronicId, id) .list(); res.setElectronicFenceAreas(electronicFenceAreaList); return R.ok(res); } /** * 编辑电子围栏 */ @PostMapping("/edit") @ApiOperation(value = "编辑电子围栏") @Transactional @ShardingSphereTransactionType(TransactionType.BASE) public R edit(@Validated @RequestBody ElectronicFenceUpdateDto dto) { validateFenceTime(dto.getStatus(), dto.getEffectType(), dto.getEffectTime(), dto.getFailureType(), dto.getFailureTime()); return xpElectronicFenceService.editElectronic(dto); } /** * 校验编辑围栏时运营时间段缩短影响的点位数量 * <p> * 统计:围栏下 operation_type=2(自定义运营时间段) 的点位中, * 当围栏运营时间范围发生缩短时,不在新运营时间范围(Union)内的点位数量(需要停用)。 * </p> */ @PostMapping("/validateOperationStrategyShorten") @ApiOperation(value = "校验围栏运营时间段缩短影响点位数量") public R<Integer> validateOperationStrategyShorten(@Valid @RequestBody ElectronicFenceOperationStrategyValidateDto dto) { XpTestElectronicFence fence = xpElectronicFenceService.getById(dto.getId()); if (fence == null) { throw new ServiceException("围栏不存在", I18nKeyConstant.FENCE_NOT_EXIST); } OperationStrategyIndex oldIndex = OperationStrategyIndex.parse(fence.getOperationStrategy()); OperationStrategyIndex newIndex = OperationStrategyIndex.parse(dto.getOperationStrategy()); if (oldIndex.equalsNormalized(newIndex)) { return R.ok(0); } if (!oldIndex.hasAnyShrinkComparedTo(newIndex)) { return R.ok(0); } List<Long> fenceAreaIds = xpElectronicFenceAreaService.lambdaQuery() .eq(XpTestElectronicFenceArea::getTestElectronicId, dto.getId()) .list() .stream() .map(XpTestElectronicFenceArea::getId) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); if (CollUtil.isEmpty(fenceAreaIds)) { return R.ok(0); } List<XpPoint> customPoints = xpPointService.lambdaQuery() .in(XpPoint::getElectronicAreaId, fenceAreaIds) .eq(XpPoint::getPointType, 1) .eq(XpPoint::getOperationType, 2) .list(); if (CollUtil.isEmpty(customPoints)) { return R.ok(0); } int disableCount = 0; for (XpPoint point : customPoints) { if (point == null || point.getEffectiveTimePeriod() == null || point.getEffectiveTimePeriod().isEmpty()) { continue; } List<OperationStrategyIndex.TimeInterval> pointIntervals = OperationStrategyIndex.parsePointEffectiveTimePeriod(point.getEffectiveTimePeriod()); if (pointIntervals.isEmpty()) { continue; } boolean shouldDisable = !newIndex.isPointScheduleWithin(pointIntervals); if (shouldDisable) { disableCount++; } } return R.ok(disableCount); } /** * 复制电子围栏 */ @PostMapping("/copy") @ApiOperation(value = "复制电子围栏") @Transactional @ShardingSphereTransactionType(TransactionType.BASE) public R<Long> copy(@RequestParam("id") Integer id) { XpTestElectronicFence electronicFence = xpElectronicFenceService.getById(id); if (electronicFence.getStatus()==2){ throw new ServiceException( "当前电子围栏状态为启用中,不可复制", I18nKeyConstant.FENCE_STATUS_CANNOT_COPIED ); } electronicFence.setElectronicName(buildCopyFenceName(electronicFence.getElectronicName())); electronicFence.setId(null); electronicFence.setStatus(0); List<XpTestElectronicFenceArea> electronicFenceAreaList = xpElectronicFenceAreaService.lambdaQuery().eq(XpTestElectronicFenceArea::getTestElectronicId, id) .list(); //根据经纬度获取当前城市code // 生成围栏id 规则:城市首字母-类型简写-4位自增流水号(如 SZ-OP-001),支持复制;数据停用或删除后,流水号不回收 XpRegion xpRegion = xpRegionService.getOne(new LambdaQueryWrapper<XpRegion>() .eq(XpRegion::getCode, electronicFence.getCityCode())); String name = xpRegion.getName(); // 根据城市中文名,截取前两位的首字母并且大写 // 截取前两个字符 String prefix = name.substring(0, 2); // 获取拼音首字母并大写 String result = getPinyinInitials(prefix).toUpperCase(); xpElectronicFenceService.save(electronicFence); electronicFence.setElectronicNumber(result + "-OP-" + String.format("%04d", electronicFence.getId())); xpElectronicFenceService.updateById(electronicFence); for (XpTestElectronicFenceArea area : electronicFenceAreaList) { area.setId(null); area.setTestElectronicId(electronicFence.getId()); area.setAreaStatus(2); area.setAreaName("副本_"+area.getAreaName()); } xpElectronicFenceAreaService.saveBatch(electronicFenceAreaList); return R.ok(electronicFence.getId()); } private String buildCopyFenceName(String sourceName) { Set<String> existingNames = new HashSet<>(xpElectronicFenceService.lambdaQuery() .likeRight(XpTestElectronicFence::getElectronicName, sourceName) .list() .stream() .map(XpTestElectronicFence::getElectronicName) .collect(Collectors.toSet())); String candidateName = sourceName + "_副本"; while (existingNames.contains(candidateName)) { candidateName = candidateName + "_副本"; } return candidateName; } /** * 根据ID列表批量查询电子围栏 * * @param electronicIdSet * @return */ @PostMapping("/listByIds") R<List<XpTestElectronicFence>> listByIds(@RequestBody Collection<Long> electronicIdSet) { if (CollUtil.isEmpty(electronicIdSet)) { return R.ok(Collections.emptyList()); } return R.ok(xpElectronicFenceService.listByIds(electronicIdSet)); } @ApiOperation(value = "获取已启用的电子围栏列表",tags = {"管理后台-电子围栏"}) @GetMapping("/available/list") R<List<XpTestElectronicFence>> getAvailableList(){ return R.ok(xpElectronicFenceService.getAvailableList()); } /** * 批量查询电子围栏 * @param fenceIds * @return */ @PostMapping("/inner/xpElectronicFence/selectBatchIds") public R<List<XpTestElectronicFence>> selectBatchIds(@RequestBody List<String> fenceIds){ List<XpTestElectronicFence> xpElectronicFences = xpElectronicFenceService.listByIds(fenceIds); return R.ok(xpElectronicFences); } /** * 根据ID查询电子围栏 * @param fenceId * @return */ @GetMapping("/inner/xpElectronicFence/selectById") public R<XpTestElectronicFence> selectById(@RequestParam String fenceId){ XpTestElectronicFence xpElectronicFence = xpElectronicFenceService.getById(fenceId); return R.ok(xpElectronicFence); } /** * 校验围栏生效时间和失效时间的合法性: * <ul> * <li>生效类型不是立即生效(effectType != 1)时,生效时间不能早于当前时间</li> * <li>失效类型不是永不失效(failureType != 1)时,失效时间不能早于当前时间</li> * </ul> * * @param status * @param effectType 生效类型(1=立即生效 2=定时生效) * @param effectTime 生效时间 * @param failureType 失效类型(1=永不失效 2=定时失效) * @param failureTime 失效时间 */ private void validateFenceTime(@NotNull Integer status, Integer effectType, LocalDateTime effectTime, Integer failureType, LocalDateTime failureTime) { LocalDateTime now = LocalDateTime.now(); // 发布/启用时,定时生效的生效时间不能早于当前时间 if (requiresScheduleValidation(status) && effectType != null && effectType != 1) { if (effectTime == null || effectTime.isBefore(now)) { throw new ServiceException("围栏生效时间不合法,定时生效时生效时间不能早于当前时间", I18nKeyConstant.FENCE_EFFECT_TIME_INVALID); } } // 发布/启用时,定时失效的失效时间不能早于当前时间 if (requiresScheduleValidation(status) && failureType != null && failureType != 1) { if (failureTime == null || failureTime.isBefore(now)) { throw new ServiceException("围栏失效时间不合法,定时失效时失效时间不能早于当前时间", I18nKeyConstant.FENCE_FAILURE_TIME_INVALID); } } } private boolean requiresScheduleValidation(Integer status) { return status != null && (status == 1 || status == 2); } } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpTestElectronicFenceAreaMapper.java
New file @@ -0,0 +1,18 @@ package com.xiaopeng.map.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.model.operation.XpTestElectronicFenceArea; /** * <p> * 电子围栏区域构成 Mapper 接口 * </p> * * @author liu * @since 2026-02-03 */ public interface XpTestElectronicFenceAreaMapper extends BaseMapper<XpTestElectronicFenceArea> { } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpTestElectronicFenceMapper.java
New file @@ -0,0 +1,14 @@ package com.xiaopeng.map.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.xiaopeng.api.model.operation.XpElectronicFence; import com.xiaopeng.api.model.operation.XpTestElectronicFence; import com.xiaopeng.api.query.operation.ElectronicPageListQuery; import com.xiaopeng.common.core.web.page.PageInfo; import org.apache.ibatis.annotations.Param; import java.util.List; public interface XpTestElectronicFenceMapper extends BaseMapper<XpTestElectronicFence> { List<XpTestElectronicFence> pageList(@Param("pageInfo") PageInfo<XpTestElectronicFence> pageInfo, @Param("query") ElectronicPageListQuery query); } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpTestElectronicFenceAreaService.java
New file @@ -0,0 +1,54 @@ package com.xiaopeng.map.service; import com.baomidou.mybatisplus.extension.service.IService; import com.xiaopeng.api.dto.map.RouteAndFareDto; import com.xiaopeng.api.dto.operation.LonLatCityCodeDto; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.model.operation.XpTestElectronicFenceArea; import com.xiaopeng.api.vo.operation.XpElectronicFenceVo; import com.xiaopeng.map.dto.WalkTimeDto; import com.xiaopeng.map.vo.IsInOperationAreaVo; import com.xiaopeng.map.vo.WalkTimeVo; import java.util.List; /** * <p> * 电子围栏区域构成 服务类 * </p> * * @author liu * @since 2026-02-03 */ public interface XpTestElectronicFenceAreaService extends IService<XpTestElectronicFenceArea> { /** * 获取所有的开放区域 * @return */ List<XpElectronicFenceVo> citiesList(String cityCode); /** * 判断当前位置是否在运营区域内 * @param dto * @return */ IsInOperationAreaVo isInOperationArea(LonLatCityCodeDto dto); /** * 根据点位获取步行时间 * @param dto * @return */ WalkTimeVo getWalkTime(WalkTimeDto dto); /** * 检测区域 * @param dto * @return */ Boolean diagnosisArea(RouteAndFareDto dto); } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpTestElectronicFenceService.java
New file @@ -0,0 +1,54 @@ package com.xiaopeng.map.service; import com.baomidou.mybatisplus.extension.service.IService; import com.xiaopeng.api.dto.operation.ElectronicFenceAddDto; import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto; import com.xiaopeng.api.model.operation.XpTestElectronicFence; 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; /** * <p> * 电子围栏 服务类 * </p> * * @author liu * @since 2026-02-03 */ public interface XpTestElectronicFenceService extends IService<XpTestElectronicFence> { PageInfo<XpTestElectronicFence> pageList(ElectronicPageListQuery query); R addElectronic(ElectronicFenceAddDto dto); R editElectronic(ElectronicFenceUpdateDto dto); GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr); /** * 校验围栏名称唯一 */ void validateElectronicNameUnique(String electronicName, Long excludeId); /** * 查询启用的电子围栏列表 * @return */ List<XpTestElectronicFence> getAvailableList(); /** * 获取不分页的围栏列表 * @return */ List<XpTestElectronicFence> getList(ElectronicPageListQuery query); /** * 将围栏置为已停用,并同步禁用围栏区域 * @param electronicId 围栏ID */ void expireFence(Long electronicId); } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpTestElectronicFenceAreaServiceImpl.java
New file @@ -0,0 +1,379 @@ package com.xiaopeng.map.service.serviceimpl; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.xiaopeng.api.dto.map.RouteAndFareDto; import com.xiaopeng.api.dto.operation.LonLatCityCodeDto; import com.xiaopeng.api.feignClient.operation.XpElectronicFenceClient; import com.xiaopeng.api.feignClient.operation.XpSystemSettingClient; import com.xiaopeng.api.model.map.XpTestGeoFencingPolygon; import com.xiaopeng.api.model.operation.XpElectronicFence; import com.xiaopeng.api.model.operation.XpTestElectronicFenceArea; import com.xiaopeng.api.vo.operation.XpElectronicFenceVo; import com.xiaopeng.api.vo.operation.XpPointsVo; import com.xiaopeng.common.core.constant.CacheConstants; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.utils.DateUtils; import com.xiaopeng.common.redis.service.RedisService; import com.xiaopeng.map.dto.WalkTimeDto; import com.xiaopeng.map.mangodb.model.GeoPoint; import com.xiaopeng.map.mangodb.service.XpGeoFencingPolygonService; import com.xiaopeng.map.mapper.XpTestElectronicFenceAreaMapper; import com.xiaopeng.map.service.XpTestElectronicFenceAreaService; import com.xiaopeng.map.utils.AmapWalkingRouteUtil; import com.xiaopeng.map.utils.ElectricFenceUtil; import com.xiaopeng.map.utils.GDMapGeocodingUtil; import com.xiaopeng.map.utils.GaodeMapService; import com.xiaopeng.map.utils.holiday.HolidayUtil; import com.xiaopeng.map.vo.IsInOperationAreaVo; import com.xiaopeng.map.vo.WalkTimeVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * <p> * 电子围栏区域构成 服务实现类 * </p> * * @author liu * @since 2026-02-03 */ @Service public class XpTestElectronicFenceAreaServiceImpl extends ServiceImpl<XpTestElectronicFenceAreaMapper, XpTestElectronicFenceArea> implements XpTestElectronicFenceAreaService { @Resource private XpElectronicFenceClient xpElectronicFenceClient; @Resource private XpSystemSettingClient xpSystemSettingClient; @Resource private XpGeoFencingPolygonService xpGeoFencingPolygonService; @Resource private RedisService redisService; @Resource private HolidayUtil holidayUtil; @Autowired private MongoTemplate mongoTemplate; @Resource private GDMapGeocodingUtil gdMapGeocodingUtil; @Override public List<XpElectronicFenceVo> citiesList(String cityCode) { List<XpTestGeoFencingPolygon> xpGeoFencingPolygons = mongoTemplate.find(new Query(Criteria.where("cityCode").is(cityCode)), XpTestGeoFencingPolygon.class); if(xpGeoFencingPolygons.isEmpty()){ return new ArrayList<>(); } List<XpElectronicFenceVo> result = new ArrayList<>(); List<String> fenceIds = xpGeoFencingPolygons.stream().map(XpTestGeoFencingPolygon::getFenceId).collect(Collectors.toList()); if(fenceIds.isEmpty()){ return new ArrayList<>(); } R<List<XpElectronicFence>> xpElectronicFences = xpElectronicFenceClient.selectBatchIds(fenceIds); if(xpElectronicFences==null){ return new ArrayList<>(); } for (XpTestGeoFencingPolygon polygon : xpGeoFencingPolygons) { XpElectronicFence xpElectronicFence = xpElectronicFences.getData().stream().filter(e -> e.getId().toString().equals(polygon.getFenceId())).findFirst().orElse(null); if(xpElectronicFence==null){ continue; } if(xpElectronicFence.getStatus()!=2){ continue; } String operationStrategy = xpElectronicFence.getOperationStrategy(); if(operationStrategy==null){ continue; } // 是否运营时间内 if(xpElectronicFence.getFailureType() == 2){ JSONArray jsonArray = JSONArray.parseArray(operationStrategy); if(!jsonArray.isEmpty()){ Boolean inOperationTime = isInOperationTime(operationStrategy); if(!inOperationTime){ continue; } } if(LocalDateTime.now().isAfter(xpElectronicFence.getFailureTime())){ continue; } }else{ //永不失效,需要判断开始时间是否到达 if(LocalDateTime.now().isBefore(xpElectronicFence.getEffectTime())){ continue; } } XpElectronicFenceVo vo = new XpElectronicFenceVo(); // 设置id(使用siteId作为id) vo.setFenceId(polygon.getFenceId()); vo.setAreaId(polygon.getAreaId()); // 设置围栏名称 vo.setName(polygon.getName()); // 转换GeoJsonPolygon为List<XpPointsVo> List<XpPointsVo> pointsVoList = new ArrayList<>(); if (polygon.getPoints() != null && polygon.getPoints().getCoordinates() != null) { // GeoJsonPolygon的coordinates是一个二维数组,第一个元素是外部环 List<Point> points = polygon.getPoints().getPoints(); for (Point coord : points) { XpPointsVo pointsVo = new XpPointsVo(); // GeoJson的坐标顺序是[经度, 纬度] // x经度 y维度 pointsVo.setLng(coord.getX()); pointsVo.setLat(coord.getY()); pointsVoList.add(pointsVo); } } vo.setPoints(pointsVoList); vo.setRule(operationStrategy); result.add(vo); } return result; } /** * 是否在运营内 * @param operationStrategy 规则json字符串 * @return */ private Boolean isInOperationTime(String operationStrategy) { JSONArray jsonArray = JSONArray.parseArray(operationStrategy); Integer holidayToday = holidayUtil.isHolidayToday(); for (Object o : jsonArray) { JSONObject jsonObject =JSONObject.parseObject(o.toString()); String type = jsonObject.getString("type"); if(type.equals(holidayToday.toString())){ JSONArray time = jsonObject.getJSONArray("time"); // 判断当前时间 是否在time时间段中 for (Object object : time) { String[] timeArray = object.toString().split("-"); String startTime = timeArray[0]; String endTime = timeArray[1]; boolean currentTimeInRange = DateUtils.isCurrentTimeInRange(startTime, endTime); if(currentTimeInRange){ return true; } } }else if(type.equals("3")){ // 判断是什么类型 0每天 1-7 想起 String typeStr = jsonObject.getString("typeStr"); // 查看今天周几 Integer week = DateUtils.getWeek(); // 包含每天 或者 包含当天 if(typeStr.contains("0") || typeStr.contains(week.toString())){ JSONArray time = jsonObject.getJSONArray("time"); // 判断当前时间 是否在time时间段中 for (Object object : time) { String[] timeArray = object.toString().split("-"); String startTime = timeArray[0]; String endTime = timeArray[1]; boolean currentTimeInRange = DateUtils.isCurrentTimeInRange(startTime, endTime); if(currentTimeInRange){ return true; } } } } } return false; } /** * 判断经纬度是否在开放区域中 * @param dto * @return */ @Override public IsInOperationAreaVo isInOperationArea(LonLatCityCodeDto dto) { IsInOperationAreaVo vo = new IsInOperationAreaVo(); String cityCode = dto.getCityCode(); List<XpTestGeoFencingPolygon> xpGeoFencingPolygons = mongoTemplate.find(new Query(Criteria.where("cityCode").is(cityCode)), XpTestGeoFencingPolygon.class); for (XpTestGeoFencingPolygon polygon : xpGeoFencingPolygons) { if (polygon.getPoints() != null && polygon.getPoints().getCoordinates() != null) { // GeoJsonPolygon的coordinates是一个二维数组,第一个元素是外部环 ArrayList<String> coordinates = new ArrayList<>(); List<Point> points = polygon.getPoints().getPoints(); for (Point point : points) { coordinates.add( point.getX()+","+point.getY() ); } String collect = coordinates.stream().collect(Collectors.joining(";")); if (ElectricFenceUtil.areaMonitoring(collect, dto.getLongitude()+","+dto.getLatitude())) { String fenceId = polygon.getFenceId(); R<XpElectronicFence> xpElectronicFence = xpElectronicFenceClient.selectById(fenceId); if(xpElectronicFence!=null && null != xpElectronicFence.getData()){ XpElectronicFence electronicFence = xpElectronicFence.getData(); String operationStrategy = electronicFence.getOperationStrategy(); Boolean inOperationTime = true; if(electronicFence.getFailureType() == 2){ inOperationTime = isInOperationTime(operationStrategy); } XpTestElectronicFenceArea electronicFenceArea = this.getById(Long.valueOf(polygon.getAreaId())); if(inOperationTime && null != electronicFenceArea && !electronicFenceArea.getDeleted()){ vo.setStatus("inside"); vo.setElectronicAreaId(Long.valueOf(polygon.getAreaId())); return vo; } } } } } return vo; // try { // executor.execute(() -> { // for (XpElectronicFenceVo xpElectronicFenceVo : xpElectronicFenceVos) { // ArrayList<AmapGeoFenceUtil.GeoPoint> geoPoints = new ArrayList<>(); // // 获取多边形顶点坐标 // List<XpPointsVo> points = xpElectronicFenceVo.getPoints(); // for (XpPointsVo point : points) { // geoPoints.add(new AmapGeoFenceUtil.GeoPoint(point.getLng(), point.getLat())); // } // AmapGeoFenceUtil.GeoFence geoFence = new AmapGeoFenceUtil.GeoFence(); // geoFence.setPoints(geoPoints); // // AmapGeoFenceUtil.GeoFenceResult geoFenceResult = null; // try { // // // ElectricFenceUtil.areaMonitoring() // geoFenceResult = amapGeoFenceUtil.checkInPolygonFence(geoPoint, geoFence); // if (geoFenceResult.isInFence()) { // vo.setStatus("inside"); // } // } catch (IOException e) { // throw new RuntimeException(e); // } // } // }); // return vo; // }catch (Exception e){ // e.printStackTrace(); // vo.setStatus("outside"); // }finally { // gracefulShutdown(executor); // } // // return vo; } /** * 优雅关闭线程池通用方法 * * @param pool 需要关闭的线程池 */ private static void gracefulShutdown(ExecutorService pool) { pool.shutdown(); // 拒绝新任务提交 try { // 等待现有任务完成 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { pool.shutdownNow(); // 取消等待中的任务 只等待运行中的任务 // 再次等待任务响应中断 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { } } } catch (InterruptedException e) { // 重新尝试关闭 pool.shutdownNow(); Thread.currentThread().interrupt(); } finally { } } @Override public WalkTimeVo getWalkTime(WalkTimeDto dto) { WalkTimeVo walkTimeVo = new WalkTimeVo(); // 创建服务实例 GaodeMapService gaodeService = new GaodeMapService(); AmapWalkingRouteUtil.WalkingRouteResult result = gaodeService.getWalkingRoute(dto.getStartLongitude()+","+dto.getStartLatitude(),dto.getEndLongitude()+","+dto.getEndLatitude()); if (result.isSuccess()) { System.out.println("预计时间: " + result.getFormattedDuration()); System.out.println("步行距离: " + result.getFormattedDistance()); walkTimeVo.setWalkTime(result.getFormattedDuration()); walkTimeVo.setWalkDistance(result.getFormattedDistance()); Double orderDistanceDouble = 0.0; // 走缓存 Object orderDistance = redisService.getCacheObject(CacheConstants.ORDER_DISTANCE); if (orderDistance != null) { orderDistanceDouble = Double.parseDouble(orderDistance.toString()); }else { R r = xpSystemSettingClient.getOrderDistance(); if (r.getCode()==200) { Object orderDistanceObj = r.getData(); if (orderDistanceObj != null) { orderDistanceDouble = Double.parseDouble(orderDistanceObj.toString()); } } } if(orderDistanceDouble>result.getDistance()){ walkTimeVo.setStatus(1); } return walkTimeVo; }else { System.out.println("错误信息: " + result.getMessage()); // 抛出异常 throw new RuntimeException(result.getMessage()); } } @Override public Boolean diagnosisArea(RouteAndFareDto dto) { // 起点数据 GeoPoint startPoint = new GeoPoint(dto.getOrigin().getLongitude(),dto.getOrigin().getLatitude()); // 终点数据 GeoPoint endPoint = new GeoPoint(dto.getDestination().getLongitude(),dto.getDestination().getLatitude()); List<String> operatingArea = xpGeoFencingPolygonService.findFencesContainingPoint(startPoint, "operating_area", "points"); if (!operatingArea.isEmpty()) { List<String> operatingArea2 = xpGeoFencingPolygonService.findFencesContainingPoint(endPoint, "operating_area", "points"); if (!operatingArea2.isEmpty()) { // "开始判断是否在相同区域"; if(operatingArea.get(0).equals(operatingArea2.get(0))){ return true; } } } return null; } } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpTestElectronicFenceServiceImpl.java
New file @@ -0,0 +1,1069 @@ package com.xiaopeng.map.service.serviceimpl; import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.common.collect.ImmutableMap; import com.xiaopeng.api.dto.operation.ElectronicFenceAddDto; import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto; import com.xiaopeng.api.model.map.XpGeoFencingPolygon; import com.xiaopeng.api.model.map.XpGeoFencingProhibitedPolygon; import com.xiaopeng.api.model.operation.*; import com.xiaopeng.api.query.operation.ElectronicPageListQuery; 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.map.constant.I18nKeyConstant; import com.xiaopeng.map.event.FenceChangedEvent; import com.xiaopeng.map.job.base.QuartzManager; import com.xiaopeng.map.job.base.TimeJobType; import com.xiaopeng.map.job.jobs.ElectronicEffectJob; import com.xiaopeng.map.mangodb.service.XpGeoFencingPolygonService; import com.xiaopeng.map.mapper.*; import com.xiaopeng.map.service.*; import com.xiaopeng.map.utils.GDMapGeocodingUtil; import com.xiaopeng.map.utils.OperationStrategyIndex; import com.xiaopeng.map.utils.PolygonOverlapService; import lombok.extern.slf4j.Slf4j; import net.sourceforge.pinyin4j.PinyinHelper; import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.Polygon; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.geo.Point; 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.*; import java.util.stream.Collectors; /** * <p> * 电子围栏 服务实现类 * </p> * * @author liu * @since 2026-02-03 */ @Service @Slf4j public class XpTestElectronicFenceServiceImpl extends ServiceImpl<XpTestElectronicFenceMapper, XpTestElectronicFence> implements XpTestElectronicFenceService { @Resource private PolygonOverlapService polygonOverlapService; @Resource private XpTestElectronicFenceAreaMapper xpElectronicFenceAreaMapper; @Autowired private MongoTemplate mongoTemplate; @Autowired private XpPointMapper xpPointMapper; @Autowired private XpPointService xpPointService; @Resource private XpGeoFencingPolygonService xpGeoFencingPolygonService; @Autowired private XpRegionMapper xpRegionMapper; @Autowired private GDMapGeocodingUtil gdMapGeocodingUtil; @Resource private XpTestElectronicFenceAreaService xpElectronicFenceAreaService; @Autowired private ApplicationEventPublisher applicationEventPublisher; @Override public PageInfo<XpTestElectronicFence> pageList(ElectronicPageListQuery query) { if (query == null) { return new PageInfo<>(); } normalizeProhibitedAreaCountFilter(query); PageInfo<XpTestElectronicFence> pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize()); List<XpTestElectronicFence> list = this.baseMapper.pageList(pageInfo, query); if (list.isEmpty()) { return new PageInfo<>(); } List<Long> electronicIds = list.stream().map(XpTestElectronicFence::getId).collect(Collectors.toList()); Map<Long, List<XpTestElectronicFenceArea>> electronicFenceAreaMap = xpElectronicFenceAreaMapper.selectList(new LambdaQueryWrapper<XpTestElectronicFenceArea>() .in(XpTestElectronicFenceArea::getTestElectronicId, electronicIds)).stream() .collect(Collectors.groupingBy(XpTestElectronicFenceArea::getTestElectronicId)); for (XpTestElectronicFence xpElectronicFence : list) { List<XpTestElectronicFenceArea> xpElectronicFenceAreas = electronicFenceAreaMap.get(xpElectronicFence.getId()); if (xpElectronicFenceAreas != null) { xpElectronicFence.setOperatingAreaCount(xpElectronicFenceAreas.stream().filter(xpElectronicFenceArea -> xpElectronicFenceArea.getAreaType() == 1).count()); xpElectronicFence.setProhibitedAreaCount(xpElectronicFenceAreas.stream().filter(xpElectronicFenceArea -> xpElectronicFenceArea.getAreaType() == 2).count()); } else { xpElectronicFence.setOperatingAreaCount(0L); xpElectronicFence.setProhibitedAreaCount(0L); } } pageInfo.setRecords(list); return pageInfo; } private void normalizeProhibitedAreaCountFilter(ElectronicPageListQuery query) { Integer prohibitedAreaCountMin = query.getProhibitedAreaCountMin(); Integer prohibitedAreaCountMax = query.getProhibitedAreaCountMax(); if (prohibitedAreaCountMin != null || prohibitedAreaCountMax != null) { if (prohibitedAreaCountMin != null && prohibitedAreaCountMax != null) { int min = Math.min(prohibitedAreaCountMin, prohibitedAreaCountMax); int max = Math.max(prohibitedAreaCountMin, prohibitedAreaCountMax); if (min < 0) { clearAreaFilter(query); return; } query.setProhibitedAreaCountMin(min); query.setProhibitedAreaCountMax(max); query.setElectronicAreaId(null); return; } if (prohibitedAreaCountMin != null) { if (prohibitedAreaCountMin < 0) { clearAreaFilter(query); return; } query.setElectronicAreaId(null); return; } if (prohibitedAreaCountMax < 0) { clearAreaFilter(query); return; } query.setElectronicAreaId(null); return; } String electronicAreaId = query.getElectronicAreaId(); if (StringUtils.isEmpty(electronicAreaId)) { return; } try { if (electronicAreaId.contains(",")) { String[] parts = electronicAreaId.split(","); if (parts.length != 2) { clearAreaFilter(query); return; } int left = Integer.parseInt(parts[0].trim()); int right = Integer.parseInt(parts[1].trim()); int min = Math.min(left, right); int max = Math.max(left, right); if (min < 0) { clearAreaFilter(query); return; } query.setProhibitedAreaCountMin(min); query.setProhibitedAreaCountMax(max); query.setElectronicAreaId(null); return; } int threshold = Integer.parseInt(electronicAreaId.trim()); if (threshold < 0 || threshold == Integer.MAX_VALUE) { clearAreaFilter(query); return; } query.setProhibitedAreaCountMin(threshold + 1); query.setProhibitedAreaCountMax(null); query.setElectronicAreaId(null); } catch (Exception e) { clearAreaFilter(query); } } private void clearAreaFilter(ElectronicPageListQuery query) { query.setElectronicAreaId(null); query.setProhibitedAreaCountMin(null); query.setProhibitedAreaCountMax(null); } 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(); } @Override @Transactional @ShardingSphereTransactionType(TransactionType.BASE) public R addElectronic(ElectronicFenceAddDto dto) { validateElectronicNameUnique(dto.getElectronicName(), null); // 获取现有的运营区域(areaType=1) LambdaQueryWrapper<XpTestElectronicFenceArea> xpElectronicFenceAreaLambdaQueryWrapper = new LambdaQueryWrapper<>(); xpElectronicFenceAreaLambdaQueryWrapper.eq(XpTestElectronicFenceArea::getAreaType, 1); xpElectronicFenceAreaLambdaQueryWrapper.eq(XpTestElectronicFenceArea::getAreaStatus, 1); List<XpTestElectronicFenceArea> existingOperationAreas = xpElectronicFenceAreaMapper.selectList(xpElectronicFenceAreaLambdaQueryWrapper); // 获取待添加的新区域 List<XpTestElectronicFenceArea> newElectronicFenceAreas = dto.getElectronicFenceAreas(); // 检查新区域与现有运营区域是否有重叠 for (XpTestElectronicFenceArea newArea : newElectronicFenceAreas) { //处理围栏闭合 String[] split = newArea.getFencePoints().split(","); if (!split[0].equals(split[split.length - 1])) { newArea.setFencePoints(newArea.getFencePoints() + "," + split[0]); } if (newArea.getAreaType() == 1) { // 只检查运营区域 for (XpTestElectronicFenceArea existingArea : existingOperationAreas) { if (polygonOverlapService.checkPolygonOverlap( newArea.getFencePoints(), existingArea.getFencePoints())) { throw new ServiceException( "运营区域{}与现有运营区域{}发生重叠", I18nKeyConstant.AREA_OVERLAP_ERROR, new Object[]{existingArea.getAreaName(), existingArea.getAreaName()} ); } } } } // 保存主表数据 XpTestElectronicFence xpElectronicFence = new XpTestElectronicFence(); BeanUtils.copyProperties(dto, xpElectronicFence); LocalDateTime now = LocalDateTime.now(); int targetStatus = resolveFenceStatus(dto.getStatus(), dto.getEffectType(), dto.getEffectTime(), now); xpElectronicFence.setStatus(targetStatus); if (targetStatus == 2 && dto.getEffectType() != null && dto.getEffectType() == 1) { xpElectronicFence.setEffectTime(now); } //根据经纬度获取当前城市code XpTestElectronicFenceArea xpElectronicFenceArea = dto.getElectronicFenceAreas().get(0); String[] split = xpElectronicFenceArea.getFencePoints().split(","); Coordinate[] coordinates = new Coordinate[split.length + 1]; for (int i = 0; i < split.length; i++) { String[] split1 = split[i].split("_"); coordinates[i] = new Coordinate(Double.parseDouble(split1[0]), Double.parseDouble(split1[1])); } org.locationtech.jts.geom.Point point = calculateCentroid(coordinates); Map<String, String> geocode = gdMapGeocodingUtil.geocode(point.getX() + "", point.getY() + ""); if (null == geocode) { return R.fail("经纬度转换城市code失败"); } String cityCode = geocode.get("cityCode"); dto.setCityCode(cityCode); xpElectronicFence.setCityCode(dto.getCityCode()); // 生成围栏id 规则:城市首字母-类型简写-4位自增流水号(如 SZ-OP-001),支持复制;数据停用或删除后,流水号不回收 XpRegion xpRegion = xpRegionMapper.selectOne(new LambdaQueryWrapper<XpRegion>() .eq(XpRegion::getCode, dto.getCityCode())); String name = xpRegion.getName(); this.save(xpElectronicFence); // 根据城市中文名,截取前两位的首字母并且大写 // 截取前两个字符 String prefix = name.substring(0, 2); // 获取拼音首字母并大写 String result = getPinyinInitials(prefix).toUpperCase(); xpElectronicFence.setElectronicNumber(result + "-OP-" + String.format("%04d", xpElectronicFence.getId())); if (dto.getStatus() == 1) { registerEffectJobIfNeeded(xpElectronicFence.getId(), targetStatus, dto.getEffectType(), dto.getEffectTime(), now); registerFailureJobIfNeeded(xpElectronicFence.getId(), dto.getFailureType(), dto.getFailureTime(), now); } this.updateById(xpElectronicFence); if (xpElectronicFence.getStatus() == 2) { // 设置电子围栏ID并保存区域数据 for (XpTestElectronicFenceArea area : newElectronicFenceAreas) { area.setTestElectronicId(xpElectronicFence.getId()); area.setAreaStatus(1); area.setArea(area.getArea()); area.setCityCode(dto.getCityCode()); xpElectronicFenceAreaMapper.insert(area); } } else { // 设置电子围栏ID并保存区域数据 for (XpTestElectronicFenceArea area : newElectronicFenceAreas) { area.setTestElectronicId(xpElectronicFence.getId()); area.setAreaStatus(2); area.setArea(area.getArea()); area.setCityCode(dto.getCityCode()); xpElectronicFenceAreaMapper.insert(area); } } if (xpElectronicFence.getStatus() == 1 || xpElectronicFence.getStatus() == 2) { for (XpTestElectronicFenceArea newElectronicFenceArea : newElectronicFenceAreas) { if (newElectronicFenceArea.getAreaType() == 1 && newElectronicFenceArea.getAreaStatus() == 1) { XpGeoFencingPolygon xpGeoFencingPolygon = new XpGeoFencingPolygon(); xpGeoFencingPolygon.setFenceId(String.valueOf(xpElectronicFence.getId())); xpGeoFencingPolygon.setName(xpElectronicFence.getElectronicName()); xpGeoFencingPolygon.setAreaId(String.valueOf(newElectronicFenceArea.getId())); xpGeoFencingPolygon.setCityCode(xpElectronicFence.getCityCode()); xpGeoFencingPolygon.setOperationStrategy(xpElectronicFence.getOperationStrategy()); // 将围栏点转换为GeoJSON多边形 GeoJsonPolygon geoJsonPolygon = convertToGeoJsonPolygon(newElectronicFenceArea.getFencePoints()); xpGeoFencingPolygon.setPoints(geoJsonPolygon); mongoTemplate.insert(xpGeoFencingPolygon); } if (newElectronicFenceArea.getAreaType() == 2 && newElectronicFenceArea.getAreaStatus() == 1) { XpGeoFencingProhibitedPolygon xpGeoFencingProhibitedPolygon = new XpGeoFencingProhibitedPolygon(); xpGeoFencingProhibitedPolygon.setFenceId(String.valueOf(xpElectronicFence.getId())); xpGeoFencingProhibitedPolygon.setName(xpElectronicFence.getElectronicName()); xpGeoFencingProhibitedPolygon.setAreaId(String.valueOf(newElectronicFenceArea.getId())); // 将围栏点转换为GeoJSON多边形 GeoJsonPolygon geoJsonPolygon = convertToGeoJsonPolygon(newElectronicFenceArea.getFencePoints()); xpGeoFencingProhibitedPolygon.setPoints(geoJsonPolygon); mongoTemplate.save(xpGeoFencingProhibitedPolygon); } } } return R.ok(); } @Override @Transactional @ShardingSphereTransactionType(TransactionType.BASE) public R editElectronic(ElectronicFenceUpdateDto dto) { validateElectronicNameUnique(dto.getElectronicName(), dto.getId()); // 获取现有的运营区域(areaType=1),排除当前正在编辑的电子围栏 LambdaQueryWrapper<XpTestElectronicFenceArea> xpElectronicFenceAreaLambdaQueryWrapper = new LambdaQueryWrapper<>(); xpElectronicFenceAreaLambdaQueryWrapper.eq(XpTestElectronicFenceArea::getAreaType, 1); xpElectronicFenceAreaLambdaQueryWrapper.eq(XpTestElectronicFenceArea::getAreaStatus, 1); xpElectronicFenceAreaLambdaQueryWrapper.ne(XpTestElectronicFenceArea::getTestElectronicId, dto.getId()); List<XpTestElectronicFenceArea> existingOperationAreas = xpElectronicFenceAreaMapper.selectList(xpElectronicFenceAreaLambdaQueryWrapper); // 获取待更新的区域 List<XpTestElectronicFenceArea> updatedElectronicFenceAreas = dto.getElectronicFenceAreas(); // 检查更新后的区域与现有运营区域是否有重叠 for (XpTestElectronicFenceArea updatedArea : updatedElectronicFenceAreas) { //处理围栏闭合 String[] split = updatedArea.getFencePoints().split(","); if (!split[0].equals(split[split.length - 1])) { updatedArea.setFencePoints(updatedArea.getFencePoints() + "," + split[0]); } if (updatedArea.getAreaType() == 1) { // 只检查运营区域 for (XpTestElectronicFenceArea existingArea : existingOperationAreas) { if (polygonOverlapService.checkPolygonOverlap( updatedArea.getFencePoints(), existingArea.getFencePoints())) { List<String> res = new ArrayList<>(); res.add(updatedArea.getAreaName()); res.add(existingArea.getAreaName()); throw new ServiceException( "运营区域{}与现有运营区域{}发生重叠,请编辑已有的运营区域", I18nKeyConstant.AREA_OVERLAP_ERROR, res.toArray() ); } } } } XpTestElectronicFence electronicFence = this.getById(dto.getId()); Integer status = electronicFence.getStatus(); String oldOperationStrategy = electronicFence.getOperationStrategy(); String newOperationStrategy = dto.getOperationStrategy(); // 更新主表数据 XpTestElectronicFence xpElectronicFence = new XpTestElectronicFence(); BeanUtils.copyProperties(dto, xpElectronicFence); LocalDateTime now = LocalDateTime.now(); int targetStatus = resolveFenceStatus(dto.getStatus(), dto.getEffectType(), dto.getEffectTime(), now); xpElectronicFence.setStatus(targetStatus); if (targetStatus == 2 && dto.getEffectType() != null && dto.getEffectType() == 1) { xpElectronicFence.setEffectTime(now); } if (dto.getStatus() == 1 || dto.getStatus() == 2) { registerEffectJobIfNeeded(xpElectronicFence.getId(), targetStatus, dto.getEffectType(), dto.getEffectTime(), now); registerFailureJobIfNeeded(xpElectronicFence.getId(), dto.getFailureType(), dto.getFailureTime(), now); } else { QuartzManager.removeJob((ElectronicEffectJob.effect + xpElectronicFence.getId()).toUpperCase(), TimeJobType.ELECTRONIC_EFFECT); QuartzManager.removeJob((ElectronicEffectJob.failure + xpElectronicFence.getId()).toUpperCase(), TimeJobType.ELECTRONIC_FAILURE); } //根据经纬度获取当前城市code XpTestElectronicFenceArea electronicFenceArea = dto.getElectronicFenceAreas().get(0); String[] split = electronicFenceArea.getFencePoints().split(","); Coordinate[] coordinates = new Coordinate[split.length]; for (int i = 0; i < split.length; i++) { String[] split1 = split[i].split("_"); coordinates[i] = new Coordinate(Double.parseDouble(split1[0]), Double.parseDouble(split1[1])); } org.locationtech.jts.geom.Point point = calculateCentroid(coordinates); Map<String, String> geocode = gdMapGeocodingUtil.geocode(point.getX() + "", point.getY() + ""); if (null == geocode) { return R.fail("经纬度转换城市code失败"); } String cityCode = geocode.get("cityCode"); dto.setCityCode(cityCode); xpElectronicFence.setCityCode(dto.getCityCode()); this.updateById(xpElectronicFence); // 禁行区(areaType=2)差异删除:对比原有禁行区与本次编辑入参禁行区(按id) List<XpTestElectronicFenceArea> existingProhibitedAreas = xpElectronicFenceAreaService.lambdaQuery() .eq(XpTestElectronicFenceArea::getTestElectronicId, dto.getId()) .eq(XpTestElectronicFenceArea::getAreaType, 2) .list(); Set<Long> incomingProhibitedIds = updatedElectronicFenceAreas.stream() .filter(a -> a != null && a.getAreaType() != null && a.getAreaType() == 2 && a.getId() != null) .map(XpTestElectronicFenceArea::getId) .collect(Collectors.toSet()); List<Long> toDeleteProhibitedIds = existingProhibitedAreas.stream() .map(XpTestElectronicFenceArea::getId) .filter(id -> id != null && !incomingProhibitedIds.contains(id)) .collect(Collectors.toList()); if (!toDeleteProhibitedIds.isEmpty()) { // 逻辑删除MySQL禁行区 xpElectronicFenceAreaService.removeByIds(toDeleteProhibitedIds); // 同步删除Mongo禁行区多边形 List<String> toDeleteAreaIdStrings = toDeleteProhibitedIds.stream().map(String::valueOf).collect(Collectors.toList()); Query delMongoQuery = new Query(); delMongoQuery.addCriteria(Criteria.where("fenceId").is(String.valueOf(dto.getId()))); delMongoQuery.addCriteria(Criteria.where("areaId").in(toDeleteAreaIdStrings)); mongoTemplate.remove(delMongoQuery, XpGeoFencingProhibitedPolygon.class); } // 删除旧的区域数据,然后保存新的区域数据 mongoTemplate.remove(new Query(Criteria.where("fenceId").is(String.valueOf(dto.getId()))), XpGeoFencingPolygon.class); if (xpElectronicFence.getStatus() == 2) { // 设置电子围栏ID并保存区域数据 for (XpTestElectronicFenceArea area : updatedElectronicFenceAreas) { area.setTestElectronicId(xpElectronicFence.getId()); if (area.getAreaType() == null || area.getAreaType() == 1) { area.setAreaStatus(1); } area.setArea(area.getArea()); area.setCityCode(dto.getCityCode()); if (null != area.getId()) { xpElectronicFenceAreaService.updateById(area); } else { xpElectronicFenceAreaService.save(area); } } } else { // 设置电子围栏ID并保存区域数据 for (XpTestElectronicFenceArea area : updatedElectronicFenceAreas) { area.setTestElectronicId(xpElectronicFence.getId()); if (area.getAreaType() == null || area.getAreaType() == 1) { area.setAreaStatus(2); } area.setArea(area.getArea()); area.setCityCode(dto.getCityCode()); if (null != area.getId()) { xpElectronicFenceAreaService.updateById(area); } else { xpElectronicFenceAreaService.save(area); } } } if (dto.getStatus() == 1 || dto.getStatus() == 2) { for (XpTestElectronicFenceArea newElectronicFenceArea : updatedElectronicFenceAreas) { if (newElectronicFenceArea.getAreaType() == 1 && newElectronicFenceArea.getAreaStatus() == 1) { // 先查询出mongo里围栏相同的数据 如果有 那么更新数据 Query query = new Query(); query.addCriteria(Criteria.where("fenceId").is(String.valueOf(xpElectronicFence.getId()))); query.addCriteria(Criteria.where("areaId").is(String.valueOf(newElectronicFenceArea.getId()))); XpGeoFencingPolygon old = mongoTemplate.findOne(query, XpGeoFencingPolygon.class); if (old == null) { XpGeoFencingPolygon xpGeoFencingPolygon = new XpGeoFencingPolygon(); xpGeoFencingPolygon.setFenceId(String.valueOf(xpElectronicFence.getId())); xpGeoFencingPolygon.setName(xpElectronicFence.getElectronicName()); xpGeoFencingPolygon.setCityCode(cityCode); xpGeoFencingPolygon.setAreaId(String.valueOf(newElectronicFenceArea.getId())); xpGeoFencingPolygon.setOperationStrategy(xpElectronicFence.getOperationStrategy()); // 将围栏点转换为GeoJSON多边形 GeoJsonPolygon geoJsonPolygon = convertToGeoJsonPolygon(newElectronicFenceArea.getFencePoints()); xpGeoFencingPolygon.setPoints(geoJsonPolygon); mongoTemplate.save(xpGeoFencingPolygon); } else { // 如果存在,则更新已有文档 old.setFenceId(String.valueOf(xpElectronicFence.getId())); old.setName(xpElectronicFence.getElectronicName()); old.setCityCode(cityCode); old.setAreaId(String.valueOf(newElectronicFenceArea.getId())); old.setOperationStrategy(xpElectronicFence.getOperationStrategy()); // 将围栏点转换为GeoJSON多边形 GeoJsonPolygon geoJsonPolygon = convertToGeoJsonPolygon(newElectronicFenceArea.getFencePoints()); old.setPoints(geoJsonPolygon); // 执行更新操作 mongoTemplate.save(old); // save() 会根据 _id 自动判断是插入还是更新 } } if (newElectronicFenceArea.getAreaType() == 2 && newElectronicFenceArea.getAreaStatus() == 1) { // 先查询出mongo里围栏相同的数据 如果有 那么更新数据 Query query = new Query(); query.addCriteria(Criteria.where("fenceId").is(String.valueOf(xpElectronicFence.getId()))); query.addCriteria(Criteria.where("areaId").is(String.valueOf(newElectronicFenceArea.getId()))); XpGeoFencingProhibitedPolygon old = mongoTemplate.findOne(query, XpGeoFencingProhibitedPolygon.class); if (old == null) { XpGeoFencingProhibitedPolygon xpGeoFencingProhibitedPolygon = new XpGeoFencingProhibitedPolygon(); xpGeoFencingProhibitedPolygon.setFenceId(String.valueOf(xpElectronicFence.getId())); xpGeoFencingProhibitedPolygon.setName(xpElectronicFence.getElectronicName()); xpGeoFencingProhibitedPolygon.setAreaId(String.valueOf(newElectronicFenceArea.getId())); // 将围栏点转换为GeoJSON多边形 GeoJsonPolygon geoJsonPolygon = convertToGeoJsonPolygon(newElectronicFenceArea.getFencePoints()); xpGeoFencingProhibitedPolygon.setPoints(geoJsonPolygon); mongoTemplate.save(xpGeoFencingProhibitedPolygon); } else { old.setFenceId(String.valueOf(xpElectronicFence.getId())); old.setName(xpElectronicFence.getElectronicName()); old.setAreaId(String.valueOf(newElectronicFenceArea.getId())); // 将围栏点转换为GeoJSON多边形 GeoJsonPolygon geoJsonPolygon = convertToGeoJsonPolygon(newElectronicFenceArea.getFencePoints()); old.setPoints(geoJsonPolygon); mongoTemplate.save(old); } } else if (newElectronicFenceArea.getAreaType() == 2 && newElectronicFenceArea.getId() != null) { Query query = new Query(); query.addCriteria(Criteria.where("fenceId").is(String.valueOf(xpElectronicFence.getId()))); query.addCriteria(Criteria.where("areaId").is(String.valueOf(newElectronicFenceArea.getId()))); mongoTemplate.remove(query, XpGeoFencingProhibitedPolygon.class); } } } // 判断区域下的所有运营点位是否处于运营区域内 如果不是则更新为测试点位 List<Long> areaIds = updatedElectronicFenceAreas.stream().map(XpTestElectronicFenceArea::getId).collect(Collectors.toList()); LambdaQueryWrapper<XpPoint> pointLambdaQueryWrapper = new LambdaQueryWrapper<>(); pointLambdaQueryWrapper.in(XpPoint::getElectronicAreaId, areaIds).eq(XpPoint::getPointType, 1); List<XpPoint> xpPoints = xpPointMapper.selectList(pointLambdaQueryWrapper); if (!xpPoints.isEmpty()) { List<Long> electronicAreaId = xpPoints.stream().map(XpPoint::getElectronicAreaId).distinct().collect(Collectors.toList()); if (!electronicAreaId.isEmpty()) { Map<Long, XpTestElectronicFenceArea> longXpElectronicFenceAreaMap = updatedElectronicFenceAreas .stream().collect(Collectors.toMap(XpTestElectronicFenceArea::getId, e -> e)); for (XpPoint xpPoint : xpPoints) { XpTestElectronicFenceArea xpElectronicFenceArea = longXpElectronicFenceAreaMap.get(xpPoint.getElectronicAreaId()); if (xpElectronicFenceArea != null) { String fencePoints = xpElectronicFenceArea.getFencePoints(); Double pointLat = xpPoint.getPointLat(); Double pointLon = xpPoint.getPointLon(); // 判断这个点位有没有处于当前区域 由多个经纬度点构成的面里 if (fencePoints != null && !fencePoints.trim().isEmpty() && pointLat != null && pointLon != null) { boolean isInside = isPointInPolygon(fencePoints, pointLon, pointLat); // 如果点不在区域内,更新为测试点位 if (!isInside) { xpPoint.setPointType(2); // log.info("点位ID: {} 经纬度: ({}, {}) 不在电子围栏区域内,已更新为测试点位", xpPoint.getId(), pointLon, pointLat); UpdateWrapper<XpPoint> xpPointUpdateWrapper = new UpdateWrapper<>(); xpPointUpdateWrapper.eq("id", xpPoint.getId()); xpPointUpdateWrapper.set("point_type", 2); xpPointMapper.update(xpPoint, xpPointUpdateWrapper); } } } } } } // 围栏从已停用(3)改为启用中(2)时:同步启用点位需校验点位在运营区内且不在禁行区内 Integer newFenceStatus = xpElectronicFence.getStatus(); if (status != null && status == 3 && newFenceStatus != null && newFenceStatus == 2) { XpTestElectronicFenceArea operationAreaToEnable = xpElectronicFenceAreaService.lambdaQuery() .eq(XpTestElectronicFenceArea::getTestElectronicId, dto.getId()) .eq(XpTestElectronicFenceArea::getAreaType, 1) .orderByDesc(XpTestElectronicFenceArea::getId) .last("limit 1") .one(); if (operationAreaToEnable == null || operationAreaToEnable.getId() == null) { log.warn("围栏启用同步启用点位跳过:未查询到运营区 fenceId={}", dto.getId()); } else { Long operationAreaIdToEnable = operationAreaToEnable.getId(); LambdaQueryWrapper<XpPoint> pendingEnableQuery = new LambdaQueryWrapper<>(); pendingEnableQuery.eq(XpPoint::getElectronicAreaId, operationAreaIdToEnable) .eq(XpPoint::getPointType, 1) .eq(XpPoint::getOperationStatus, 3); List<XpPoint> pendingEnablePoints = xpPointMapper.selectList(pendingEnableQuery); int pendingCount = pendingEnablePoints == null ? 0 : pendingEnablePoints.size(); if (pendingCount == 0) { log.info("围栏启用同步启用点位:无待启用点位 fenceId={}, operationAreaId={}", dto.getId(), operationAreaIdToEnable); } else { String fenceId = String.valueOf(dto.getId()); List<Long> enableIds = new ArrayList<>(); for (XpPoint pointItem : pendingEnablePoints) { if (pointItem == null || pointItem.getId() == null) { continue; } Double lon = pointItem.getPointLon(); Double lat = pointItem.getPointLat(); if (lon == null || lat == null) { continue; } try { boolean inOperatingArea = xpGeoFencingPolygonService.isInFence(lon, lat, fenceId, "operating_area"); if (!inOperatingArea) { continue; } boolean inProhibitedArea = xpGeoFencingPolygonService.isInFence(lon, lat, fenceId, "prohibited_area"); if (inProhibitedArea) { continue; } } catch (Exception e) { log.error("围栏启用同步启用点位校验失败 fenceId={}, pointId={}", dto.getId(), pointItem.getId(), e); throw new ServiceException("系统异常", I18nKeyConstant.SYSTEM_EXCEPTION); } enableIds.add(pointItem.getId()); } if (CollUtil.isNotEmpty(enableIds)) { xpPointService.lambdaUpdate() .in(XpPoint::getId, enableIds) .eq(XpPoint::getOperationStatus, 3) .set(XpPoint::getOperationStatus, 2) .update(); } log.info("围栏启用同步启用点位完成 fenceId={}, pendingCount={}, enableCount={}, skipCount={}", dto.getId(), pendingCount, enableIds.size(), Math.max(0, pendingCount - enableIds.size())); } } } // 围栏改为已停用(3)时:将围栏下启用中点位(operation_status=2)更新为已停用(operation_status=3) if (newFenceStatus != null && newFenceStatus == 3) { XpTestElectronicFenceArea operationAreaToDisable = xpElectronicFenceAreaService.lambdaQuery() .eq(XpTestElectronicFenceArea::getTestElectronicId, dto.getId()) .eq(XpTestElectronicFenceArea::getAreaType, 1) .orderByDesc(XpTestElectronicFenceArea::getId) .last("limit 1") .one(); if (operationAreaToDisable == null || operationAreaToDisable.getId() == null) { log.warn("围栏停用同步停用点位跳过:未查询到运营区 fenceId={}", dto.getId()); } else { Long operationAreaIdToDisable = operationAreaToDisable.getId(); boolean updated = xpPointService.lambdaUpdate() .eq(XpPoint::getElectronicAreaId, operationAreaIdToDisable) .eq(XpPoint::getPointType, 1) .eq(XpPoint::getOperationStatus, 2) .set(XpPoint::getOperationStatus, 3) .update(); log.info("围栏停用同步停用点位完成 fenceId={}, operationAreaId={}, updated={}", dto.getId(), operationAreaIdToDisable, updated); } } OperationStrategyIndex oldIndex = OperationStrategyIndex.parse(oldOperationStrategy); OperationStrategyIndex newIndex = OperationStrategyIndex.parse(newOperationStrategy); boolean needAutoDisableByUnion = !oldIndex.equalsNormalized(newIndex); List<Long> addPointIds = normalizePointIds(dto.getAddPointIdList()); List<Long> delPointIds = normalizePointIds(dto.getDelPointIdList()); List<Long> disablePointIds = normalizePointIds(dto.getDisablePointIdList()); boolean needPointHandling = CollUtil.isNotEmpty(addPointIds) || CollUtil.isNotEmpty(delPointIds) || CollUtil.isNotEmpty(disablePointIds); boolean needFenceAreaIds = needPointHandling || needAutoDisableByUnion; XpTestElectronicFenceArea operationArea = xpElectronicFenceAreaService.lambdaQuery() .eq(XpTestElectronicFenceArea::getTestElectronicId, dto.getId()) .eq(XpTestElectronicFenceArea::getAreaType, 1) .orderByDesc(XpTestElectronicFenceArea::getId) .last("limit 1") .one(); Long operationAreaId = operationArea == null ? null : operationArea.getId(); if (needPointHandling && operationAreaId == null) { throw new ServiceException("运营区域不存在", I18nKeyConstant.POINT_ELECTRONIC_AREA_NOT_EXIST); } if (CollUtil.isNotEmpty(addPointIds)) { if (operationAreaId == null) { throw new ServiceException("运营区域不存在", I18nKeyConstant.POINT_ELECTRONIC_AREA_NOT_EXIST); } xpPointService.lambdaUpdate() .in(XpPoint::getId, addPointIds) .set(XpPoint::getElectronicAreaId, operationAreaId) .set(XpPoint::getVerificationStatus, 1) .update(); } if (CollUtil.isNotEmpty(disablePointIds)) { // operation_status: 3=已停用 xpPointService.lambdaUpdate() .in(XpPoint::getId, disablePointIds) .eq(XpPoint::getElectronicAreaId, operationAreaId) .set(XpPoint::getOperationStatus, 3) .update(); } if (CollUtil.isNotEmpty(delPointIds)) { // operation_status: 4=已失效 xpPointService.lambdaUpdate() .in(XpPoint::getId, delPointIds) .eq(XpPoint::getElectronicAreaId, operationAreaId) .set(XpPoint::getOperationStatus, 4) .update(); } // 围栏运营时间缩短校验:若缩短后点位时间段不在新范围内,则更新为已停用(operation_status=3) if (true) { LambdaQueryWrapper<XpPoint> pointQuery = new LambdaQueryWrapper<>(); pointQuery.eq(XpPoint::getElectronicAreaId, operationAreaId) .eq(XpPoint::getPointType, 1) .eq(XpPoint::getOperationType, 2); List<XpPoint> points = xpPointMapper.selectList(pointQuery); if (points != null && !points.isEmpty()) { List<Long> disablePointIdList = new ArrayList<>(); for (XpPoint pointItem : points) { if (pointItem == null) { continue; } if (pointItem.getOperationStatus() != null && pointItem.getOperationStatus() == 4) { continue; } String effectiveTimePeriod = pointItem.getEffectiveTimePeriod(); if (effectiveTimePeriod == null || effectiveTimePeriod.isEmpty()) { continue; } List<OperationStrategyIndex.TimeInterval> pointIntervals = OperationStrategyIndex.parsePointEffectiveTimePeriod(effectiveTimePeriod); if (pointIntervals.isEmpty()) { continue; } boolean shouldDisable = !newIndex.isPointScheduleWithin(pointIntervals); if (shouldDisable && pointItem.getId() != null) { disablePointIdList.add(pointItem.getId()); } } if (!disablePointIdList.isEmpty()) { for (Long l : disablePointIdList) { XpPoint xpPoint = xpPointMapper.selectById(l); if(xpPoint.getOperationStatus()!=4){ xpPoint.setOperationStatus(3); xpPoint.setVerificationStatus(5); xpPointMapper.updateById(xpPoint); } } } } } // 发布围栏变更事件,事务提交后异步触发 ADC 推送 applicationEventPublisher.publishEvent( new FenceChangedEvent(this, xpElectronicFence.getId(), xpElectronicFence.getStatus())); return R.ok(); } @Override public void validateElectronicNameUnique(String electronicName, Long excludeId) { if (!StringUtils.hasText(electronicName)) { return; } long count = this.lambdaQuery() .eq(XpTestElectronicFence::getElectronicName, electronicName) .ne(excludeId != null, XpTestElectronicFence::getId, excludeId) .count(); if (count > 0) { throw new ServiceException("围栏名称已存在", I18nKeyConstant.FENCE_NAME_EXISTS); } } private static List<Long> normalizePointIds(List<Long> ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); } private int resolveFenceStatus(Integer requestedStatus, Integer effectType, LocalDateTime effectTime, LocalDateTime now) { if (requestedStatus == null) { return 0; } if (requestedStatus == 0) { return 0; } if (requestedStatus == 3) { return 3; } if (effectType != null && effectType == 1) { return 2; } if (effectTime != null && !effectTime.isAfter(now)) { return 2; } return 1; } private void registerEffectJobIfNeeded(Long electronicId, Integer targetStatus, Integer effectType, LocalDateTime effectTime, LocalDateTime now) { QuartzManager.removeJob((ElectronicEffectJob.effect + electronicId).toUpperCase(), TimeJobType.ELECTRONIC_EFFECT); if (targetStatus != 1 || effectType == null || effectType == 1 || effectTime == null || !effectTime.isAfter(now)) { return; } Map<String, ? extends Object> effectMaps = new ImmutableMap.Builder<String, Long>() .put("type", 1L) .put("id", electronicId) .build(); QuartzManager.addJob( ElectronicEffectJob.class, (ElectronicEffectJob.effect + electronicId).toUpperCase(), TimeJobType.ELECTRONIC_EFFECT, DateUtils.localDateTimeToDate(effectTime), effectMaps ); } /** * 失效 * @param electronicId * @param failureType * @param failureTime * @param now */ private void registerFailureJobIfNeeded(Long electronicId, Integer failureType, LocalDateTime failureTime, LocalDateTime now) { QuartzManager.removeJob((ElectronicEffectJob.failure + electronicId).toUpperCase(), TimeJobType.ELECTRONIC_FAILURE); if (failureType == null || failureType != 2 || failureTime == null || failureTime.isBefore(now)) { return; } Map<String, ? extends Object> failureMaps = new ImmutableMap.Builder<String, Long>() .put("type", 2L) .put("id", electronicId) .build(); QuartzManager.addJob( ElectronicEffectJob.class, (ElectronicEffectJob.failure + electronicId).toUpperCase(), TimeJobType.ELECTRONIC_FAILURE, DateUtils.localDateTimeToDate(failureTime), failureMaps ); } /** * 判断点是否在多边形内 * * @param fencePointsStr 多边形顶点字符串,格式:经度_维度,经度_维度 * @param pointLon 待检测点的经度 * @param pointLat 待检测点的纬度 * @return true表示在多边形内,false表示在多边形外 */ private boolean isPointInPolygon(String fencePointsStr, double pointLon, double pointLat) { if (fencePointsStr == null || fencePointsStr.trim().isEmpty()) { return false; } // 解析多边形顶点 String[] pointPairs = fencePointsStr.split(","); List<double[]> polygonVertices = new ArrayList<>(); for (String pointPair : pointPairs) { String[] lngLat = pointPair.trim().split("_"); if (lngLat.length >= 2) { try { double longitude = Double.parseDouble(lngLat[0]); double latitude = Double.parseDouble(lngLat[1]); polygonVertices.add(new double[]{longitude, latitude}); } catch (NumberFormatException e) { log.error("解析多边形顶点坐标失败: {}", pointPair, e); return false; } } } if (polygonVertices.size() < 3) { return false; // 至少需要3个点才能构成多边形 } return isPointInPolygon(polygonVertices, pointLon, pointLat); } /** * 使用射线法判断点是否在多边形内 * * @param polygonVertices 多边形顶点列表 * @param pointLon 待检测点的经度 * @param pointLat 待检测点的纬度 * @return true表示在多边形内,false表示在多边形外 */ private boolean isPointInPolygon(List<double[]> polygonVertices, double pointLon, double pointLat) { int n = polygonVertices.size(); boolean inside = false; // 射线法判断点是否在多边形内 double x = pointLon, y = pointLat; for (int i = 0, j = n - 1; i < n; j = i++) { double xi = polygonVertices.get(i)[0], yi = polygonVertices.get(i)[1]; double xj = polygonVertices.get(j)[0], yj = polygonVertices.get(j)[1]; // 判断当前边是否与水平射线相交 boolean intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) { inside = !inside; } } return inside; } /** * 将围栏点字符串转换为GeoJsonPolygon * 格式:经度_维度,经度_维度 */ @Override public GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) { if (fencePointsStr == null || fencePointsStr.trim().isEmpty()) { return null; } // 分割多个点对 String[] pointPairs = fencePointsStr.split(","); List<Point> points = new ArrayList<>(); for (String pointPair : pointPairs) { String[] lngLat = pointPair.trim().split("_"); if (lngLat.length >= 2) { try { double longitude = Double.parseDouble(lngLat[0]); double latitude = Double.parseDouble(lngLat[1]); points.add(new Point(longitude, latitude)); // Point 构造函数是 (x, y) -> (longitude, latitude) } catch (NumberFormatException e) { log.error("解析经纬度坐标失败: {}", pointPair, e); throw new IllegalArgumentException("经纬度格式错误: " + pointPair); } } } // 如果没有足够的点构成多边形,至少需要3个点 if (points.size() < 3) { throw new IllegalArgumentException("围栏点数量不足,至少需要3个点构成多边形"); } // 确保多边形是闭合的(如果最后一个点不等于第一个点,则添加第一个点) if (!points.get(0).equals(points.get(points.size() - 1))) { points.add(points.get(0)); // 添加第一个点作为最后一个点,闭合多边形 } return new GeoJsonPolygon(points); } /** * 检查新添加的区域之间是否有重叠 */ private boolean hasOverlapWithinNewAreas(List<XpTestElectronicFenceArea> newAreas) { for (int i = 0; i < newAreas.size(); i++) { XpTestElectronicFenceArea area1 = newAreas.get(i); if (area1.getAreaType() != 1) continue; // 只检查运营区域 for (int j = i + 1; j < newAreas.size(); j++) { XpTestElectronicFenceArea area2 = newAreas.get(j); if (area2.getAreaType() == 1) { if (polygonOverlapService.checkPolygonOverlap( area1.getFencePoints(), area2.getFencePoints())) { return true; } } } } return false; } /** * @return */ @Override public List<XpTestElectronicFence> getAvailableList() { return lambdaQuery().eq(XpTestElectronicFence::getStatus, 2).list(); //查询已启用的围栏 } @Override public List<XpTestElectronicFence> getList(ElectronicPageListQuery query) { LambdaQueryWrapper<XpTestElectronicFence> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(query.getCityCode() != null, XpTestElectronicFence::getCityCode, query.getCityCode()); return list(queryWrapper); } @Override @Transactional @ShardingSphereTransactionType(TransactionType.BASE) public void expireFence(Long electronicId) { if (electronicId == null) { return; } XpTestElectronicFence electronicFence = this.getById(electronicId); if (electronicFence == null || electronicFence.getStatus() == null) { return; } Integer status = electronicFence.getStatus(); if (status != 1 && status != 2) { return; } electronicFence.setStatus(3); this.updateById(electronicFence); List<XpTestElectronicFenceArea> electronicFenceAreaList = xpElectronicFenceAreaService.lambdaQuery() .eq(XpTestElectronicFenceArea::getTestElectronicId, electronicId) .list(); if (CollUtil.isEmpty(electronicFenceAreaList)) { return; } for (XpTestElectronicFenceArea electronicFenceArea : electronicFenceAreaList) { electronicFenceArea.setAreaStatus(2); } xpElectronicFenceAreaService.updateBatchById(electronicFenceAreaList); } /** * 计算围栏中心点 * * @param coordinates * @return */ private org.locationtech.jts.geom.Point calculateCentroid(Coordinate[] coordinates) { // 1. 判空校验 if (coordinates == null || coordinates.length == 0) { throw new ServiceException("坐标数组不能为空"); } // 2. 过滤掉 null 坐标点 Coordinate[] validCoords = Arrays.stream(coordinates) .filter(Objects::nonNull) .toArray(Coordinate[]::new); // 3. LinearRing 至少需要 4 个点(首尾相同),且必须闭合 if (validCoords.length < 4) { throw new ServiceException("有效坐标点不足,至少需要4个点"); } // 4. 确保首尾坐标相同(闭合) Coordinate first = validCoords[0]; Coordinate last = validCoords[validCoords.length - 1]; if (!first.equals2D(last)) { // 自动补上闭合点 validCoords = Arrays.copyOf(validCoords, validCoords.length + 1); validCoords[validCoords.length - 1] = new Coordinate(first.x, first.y); } GeometryFactory factory = new GeometryFactory(); LinearRing ring = factory.createLinearRing(validCoords); Polygon polygon = factory.createPolygon(ring); org.locationtech.jts.geom.Point centroid = polygon.getCentroid(); System.out.println("中心点经度: " + centroid.getX()); System.out.println("中心点纬度: " + centroid.getY()); return centroid; } } xp-robotaxi-map-boot/src/main/resources/mapper/XpTestElectronicFenceAreaMapper.xml
New file @@ -0,0 +1,26 @@ <?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.map.mapper.XpTestElectronicFenceAreaMapper"> <!-- 通用查询映射结果 --> <resultMap id="BaseResultMap" type="com.xiaopeng.api.model.operation.XpTestElectronicFenceArea"> <id column="id" property="id" /> <result column="test_electronic_id" property="testElectronicId" /> <result column="area_name" property="areaName" /> <result column="area_type" property="areaType" /> <result column="area_description" property="areaDescription" /> <result column="area_status" property="areaStatus" /> <result column="fence_points" property="fencePoints" /> <result column="is_deleted" property="isDeleted" /> <result column="create_time" property="createTime" /> <result column="update_time" property="updateTime" /> <result column="create_by" property="createBy" /> <result column="update_by" property="updateBy" /> </resultMap> <!-- 通用查询结果列 --> <sql id="Base_Column_List"> id, electronic_id, area_name, area_type, area_description, area_status, fence_points, is_deleted, create_time, update_time, create_by, update_by </sql> </mapper> xp-robotaxi-map-boot/src/main/resources/mapper/XpTestElectronicFenceMapper.xml
New file @@ -0,0 +1,103 @@ <?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.map.mapper.XpTestElectronicFenceMapper"> <!-- 通用查询映射结果 --> <resultMap id="BaseResultMap" type="com.xiaopeng.api.model.operation.XpTestElectronicFence"> <id column="id" property="id" /> <result column="electronic_name" property="electronicName" /> <result column="electronic_number" property="electronicNumber" /> <result column="status" property="status" /> <result column="effect_type" property="effectType" /> <result column="effect_time" property="effectTime" /> <result column="failure_type" property="failureType" /> <result column="failure_time" property="failureTime" /> <result column="operation_strategy" property="operationStrategy" /> <result column="create_time" property="createTime" /> <result column="update_time" property="updateTime" /> <result column="create_by" property="createBy" /> <result column="update_by" property="updateBy" /> </resultMap> <!-- 通用查询结果列 --> <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.XpTestElectronicFence"> select ef.id, ef.electronic_name as electronicName, ef.electronic_number as electronicNumber, ef.status, ef.effect_type as effectType, ef.city_code, ef.effect_time as effectTime, ef.failure_type as failureType, ef.failure_time as failureTime, ef.operation_strategy as operationStrategy , ef.create_time as createTime, ef.update_time as updateTime, ef.create_by as createBy, ef.update_by as updateBy from xp_test_electronic_fence ef left join xp_test_electronic_fence_area efa on efa.electronic_id = ef.id and efa.is_deleted = 0 where ef.is_deleted = 0 <if test="query.electronicNumberOrName != null and query.electronicNumberOrName != ''"> and( ef.electronic_name like concat('%',#{query.electronicNumberOrName},'%') or ef.electronic_number like concat('%',#{query.electronicNumberOrName},'%') ) </if> <if test="query.cityCode != null and query.cityCode != ''"> and ef.city_code = #{query.cityCode} </if> <if test="query.statusList != null"> <foreach collection="query.statusList" item="status" open="and ef.status in (" separator="," close=")"> #{status} </foreach> </if> <if test="query.createBy != null and query.createBy!=''"> and ef.create_by like concat('%',#{query.createBy},'%') </if> <if test="query.createStartTime != null and query.createStartTime!=''"> and ef.create_time >= #{query.createStartTime} </if> <if test="query.createEndTime != null and query.createEndTime!=''"> and #{query.createEndTime} >= ef.create_time </if> <if test="query.updateBy != null and query.updateBy!=''"> and ef.update_by like concat('%',#{query.updateBy},'%') </if> <if test="query.updateStartTime != null and query.updateStartTime!=''"> and ef.update_time >= #{query.updateStartTime} </if> <if test="query.updateEndTime != null and query.updateEndTime!=''"> and #{query.updateEndTime} >= ef.update_time </if> group by ef.id <choose> <when test="query.prohibitedAreaCountMin != null and query.prohibitedAreaCountMax != null"> having sum(case when efa.area_type = 2 then 1 else 0 end) between #{query.prohibitedAreaCountMin} and #{query.prohibitedAreaCountMax} </when> <when test="query.prohibitedAreaCountMin != null"> having sum(case when efa.area_type = 2 then 1 else 0 end) >= #{query.prohibitedAreaCountMin} </when> <when test="query.prohibitedAreaCountMax != null"> having sum(case when efa.area_type = 2 then 1 else 0 end) <= #{query.prohibitedAreaCountMax} </when> </choose> <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"> ef.create_time asc, </if> <if test="query.createSort == 2"> ef.create_time desc, </if> <if test="query.updateSort == 1"> ef.update_time asc, </if> <if test="query.updateSort == 2"> ef.update_time desc, </if> </trim> </when> <otherwise> order by ef.update_time desc </otherwise> </choose> </select> </mapper>