puzhibing
2026-04-23 bc76a19bcee43e780fd1555bd9ea95b1156e638e
新增加测试demo围栏管理功能
13个文件已添加
4个文件已修改
2307 ■■■■■ 已修改文件
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceAddDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceUpdateDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpCheckLocation.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/map/XpTestGeoFencingPolygon.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpTestElectronicFence.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpTestElectronicFenceArea.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/TestElectronicFenceDetailVo.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleCollectionScheduleTask.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpTestElectronicFenceManagementController.java 367 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpTestElectronicFenceAreaMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpTestElectronicFenceMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpTestElectronicFenceAreaService.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpTestElectronicFenceService.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpTestElectronicFenceAreaServiceImpl.java 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpTestElectronicFenceServiceImpl.java 1069 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/mapper/XpTestElectronicFenceAreaMapper.xml 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/mapper/XpTestElectronicFenceMapper.xml 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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) &gt;= #{query.prohibitedAreaCountMin}
            </when>
            <when test="query.prohibitedAreaCountMax != null">
                having sum(case when efa.area_type = 2 then 1 else 0 end) &lt;= #{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>