liusuyi
2026-04-22 04a270ff2b5919127524913ffc45c59c6c6d9201
增加部门区域管理业务和无人机实时位置所属区域推送
15 files added
2 files modified
1318 ■■■■■ changed files
ard-admin/src/main/java/com/ard/web/controller/data/ArdDeptRegionController.java 145 ●●●●● patch | view | raw | blame | history
ard-common/src/main/java/com/ard/common/constant/RegionConstants.java 15 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/domain/ArdDeptRegion.java 62 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/domain/ConflictCheckResult.java 18 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/domain/ConflictRegion.java 17 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/domain/Coordinate.java 40 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/domain/PointQueryDTO.java 18 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/mapper/ArdDeptRegionMapper.java 89 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/service/IArdDeptRegionService.java 64 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/service/impl/ArdDeptRegionServiceImpl.java 298 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/region/utils/RegionCoordinateUtils.java 154 ●●●●● patch | view | raw | blame | history
ard-system/src/main/resources/mapper/data/ArdDeptRegionMapper.xml 210 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/component/websocket/handler/UavAllManager.java 10 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/cache/UavRegionCache.java 30 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java 16 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/UavPositionChangeService.java 89 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/UavRegionDetectService.java 43 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/data/ArdDeptRegionController.java
New file
@@ -0,0 +1,145 @@
package com.ard.web.controller.data;
import com.ard.common.annotation.Log;
import com.ard.common.core.controller.BaseController;
import com.ard.common.core.domain.AjaxResult;
import com.ard.common.core.page.TableDataInfo;
import com.ard.common.enums.BusinessType;
import com.ard.common.utils.poi.ExcelUtil;
import com.ard.data.region.domain.ArdDeptRegion;
import com.ard.data.region.domain.ConflictCheckResult;
import com.ard.data.region.domain.PointQueryDTO;
import com.ard.data.region.service.IArdDeptRegionService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
/**
 * 部门区域Controller
 *
 * @author ruoyi
 */
@Api(tags = "部门区域接口")
@RestController
@RequestMapping("/data/region")
public class ArdDeptRegionController extends BaseController {
    @Autowired
    private IArdDeptRegionService ardDeptRegionService;
    /**
     * 查询部门区域列表
     */
    @ApiOperation("查询部门区域列表")
    @PreAuthorize("@ss.hasPermi('data:region:list')")
    @GetMapping("/list")
    public TableDataInfo list(ArdDeptRegion ardDeptRegion) {
        startPage();
        List<ArdDeptRegion> list = ardDeptRegionService.selectArdDeptRegionList(ardDeptRegion);
        return getDataTable(list);
    }
    /**
     * 导出部门区域列表
     */
    @ApiOperation("导出部门区域列表")
    @PreAuthorize("@ss.hasPermi('data:region:export')")
    @Log(title = "部门区域", businessType = BusinessType.EXPORT)
    @GetMapping("/export")
    public AjaxResult export(ArdDeptRegion ardDeptRegion) {
        List<ArdDeptRegion> list = ardDeptRegionService.selectArdDeptRegionList(ardDeptRegion);
        ExcelUtil<ArdDeptRegion> util = new ExcelUtil<ArdDeptRegion>(ArdDeptRegion.class);
        return util.exportExcel(list, "部门区域数据");
    }
    /**
     * 获取部门区域详细信息
     */
    @ApiOperation("获取部门区域详细信息")
    @PreAuthorize("@ss.hasPermi('data:region:query')")
    @GetMapping(value = "/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id) {
        return success(ardDeptRegionService.selectArdDeptRegionById(id));
    }
    /**
     * 检查区域冲突(画区域时调用)
     */
    @ApiOperation("检查区域冲突")
    @PostMapping("/check")
    public AjaxResult checkConflict(@Valid @RequestBody ArdDeptRegion ardDeptRegion) {
        ConflictCheckResult result = ardDeptRegionService.checkConflict(ardDeptRegion);
        if (result.getHasConflict()) {
            return AjaxResult.error(result.getMessage(), result);
        }
        return AjaxResult.success(result.getMessage(), result);
    }
    /**
     * 新增部门区域
     */
    @ApiOperation("新增部门区域")
    @PreAuthorize("@ss.hasPermi('data:region:add')")
    @Log(title = "部门区域", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@Valid @RequestBody ArdDeptRegion ardDeptRegion) {
        if (!ardDeptRegionService.checkRegionNameUnique(ardDeptRegion)) {
            return error("新增区域'" + ardDeptRegion.getRegionName() + "'失败,区域名称已存在");
        }
        return toAjax(ardDeptRegionService.insertArdDeptRegion(ardDeptRegion));
    }
    /**
     * 修改部门区域
     */
    @ApiOperation("修改部门区域")
    @PreAuthorize("@ss.hasPermi('data:region:edit')")
    @Log(title = "部门区域", businessType = BusinessType.UPDATE)
    @PutMapping("/edit")
    public AjaxResult edit(@RequestBody ArdDeptRegion ardDeptRegion) {
        if (!ardDeptRegionService.checkRegionNameUnique(ardDeptRegion)) {
            return error("修改区域'" + ardDeptRegion.getRegionName() + "'失败,区域名称已存在");
        }
        return toAjax(ardDeptRegionService.updateArdDeptRegion(ardDeptRegion));
    }
    /**
     * 修改状态(启用/禁用)
     */
    @ApiOperation("修改状态")
    @PreAuthorize("@ss.hasPermi('data:region:edit')")
    @Log(title = "部门区域", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody ArdDeptRegion ardDeptRegion)
    {
        return toAjax(ardDeptRegionService.updateStatus(ardDeptRegion.getId(), ardDeptRegion.getStatus()));
    }
    /**
     * 删除部门区域
     */
    @ApiOperation("删除部门区域")
    @PreAuthorize("@ss.hasPermi('data:region:remove')")
    @Log(title = "部门区域", businessType = BusinessType.DELETE)
    @DeleteMapping("/remove/{ids}")
    public AjaxResult remove(@PathVariable String[] ids) {
        return toAjax(ardDeptRegionService.deleteArdDeptRegionByIds(ids));
    }
    /**
     * 根据坐标查询所属区域
     */
    @ApiOperation("根据坐标查询所属区域")
    @PostMapping("/getByPoint")
    public AjaxResult getRegionByPoint(@RequestBody PointQueryDTO query) {
        ArdDeptRegion region = ardDeptRegionService.selectRegionByPoint(query.getLat(), query.getLng());
        if (region == null) {
            return error("该坐标不在任何区域内");
        }
        return success(region);
    }
}
ard-common/src/main/java/com/ard/common/constant/RegionConstants.java
New file
@@ -0,0 +1,15 @@
package com.ard.common.constant;
/**
 * 区域常量
 *
 * @author ruoyi
 */
public class RegionConstants
{
    /** 区域名称唯一 */
    public static final boolean UNIQUE = true;
    /** 区域名称不唯一 */
    public static final boolean NOT_UNIQUE = false;
}
ard-system/src/main/java/com/ard/data/region/domain/ArdDeptRegion.java
New file
@@ -0,0 +1,62 @@
package com.ard.data.region.domain;
import com.ard.common.annotation.Excel;
import com.ard.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.List;
/**
 * 部门区域对象 ard_dept_region
 *
 * @author ruoyi
 */
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ArdDeptRegion extends BaseEntity
{
    private static final long serialVersionUID = 1L;
    /** 区域ID */
    private Long id;
    /** 部门ID */
    @Excel(name = "部门ID")
    private Long deptId;
    /** 区域名称 */
    @Excel(name = "区域名称")
    private String regionName;
    /** 区域颜色(十六进制颜色码) */
    @Excel(name = "区域颜色")
    private String regionColor;
    /** 区域类型(POINT/LINE/POLYGON,非数据库字段,自动判断) */
    private String regionType;
    /** 区域边界WKT(非数据库字段,用于传输) */
    private String boundaryWkt;
    /** 中心纬度 */
    @Excel(name = "中心纬度")
    private Double centerLat;
    /** 中心经度 */
    @Excel(name = "中心经度")
    private Double centerLng;
    /** 面积(平方米) */
    @Excel(name = "面积")
    private Double areaSize;
    /** 坐标集合(非数据库字段,用于接收前端坐标) */
    private List<Coordinate> coordinates;
    /** 状态(0正常 1停用) */
    @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
    private String status;
}
ard-system/src/main/java/com/ard/data/region/domain/ConflictCheckResult.java
New file
@@ -0,0 +1,18 @@
package com.ard.data.region.domain;
import lombok.Data;
import java.util.List;
/**
 * 冲突检查结果
 *
 * @author ruoyi
 */
@Data
public class ConflictCheckResult
{
    private Boolean hasConflict;
    private String message;
    private List<ConflictRegion> conflictRegions;
}
ard-system/src/main/java/com/ard/data/region/domain/ConflictRegion.java
New file
@@ -0,0 +1,17 @@
package com.ard.data.region.domain;
import lombok.Data;
/**
 * 冲突区域信息
 *
 * @author ruoyi
 */
@Data
public class ConflictRegion
{
    private Long id;
    private String regionName;
    private String regionColor;
    private String relationship;
}
ard-system/src/main/java/com/ard/data/region/domain/Coordinate.java
New file
@@ -0,0 +1,40 @@
package com.ard.data.region.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * 坐标实体类
 *
 * @author ruoyi
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Coordinate
{
    /** 经度 */
    private Double lng;
    /** 纬度 */
    private Double lat;
    /**
     * 转换为 MySQL 需要的格式 (纬度 经度)
     */
    public String toLatLngString()
    {
        return String.format("%.8f %.8f", lat, lng);
    }
    /**
     * 验证坐标是否有效
     */
    public boolean isValid()
    {
        return lng != null && lat != null
            && lng >= -180 && lng <= 180
            && lat >= -90 && lat <= 90;
    }
}
ard-system/src/main/java/com/ard/data/region/domain/PointQueryDTO.java
New file
@@ -0,0 +1,18 @@
package com.ard.data.region.domain;
import lombok.Data;
/**
 * 坐标查询参数
 *
 * @author ruoyi
 */
@Data
public class PointQueryDTO
{
    /** 纬度 */
    private Double lat;
    /** 经度 */
    private Double lng;
}
ard-system/src/main/java/com/ard/data/region/mapper/ArdDeptRegionMapper.java
New file
@@ -0,0 +1,89 @@
package com.ard.data.region.mapper;
import com.ard.data.region.domain.ArdDeptRegion;
import com.ard.data.region.domain.ConflictRegion;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * 部门区域Mapper接口
 *
 * @author ruoyi
 */
public interface ArdDeptRegionMapper
{
    /**
     * 查询部门区域
     */
    public ArdDeptRegion selectArdDeptRegionById(Long id);
    /**
     * 查询部门区域列表
     */
    public List<ArdDeptRegion> selectArdDeptRegionList(ArdDeptRegion ardDeptRegion);
    /**
     * 新增部门区域
     */
    public int insertArdDeptRegion(ArdDeptRegion ardDeptRegion);
    /**
     * 修改部门区域
     */
    public int updateArdDeptRegion(ArdDeptRegion ardDeptRegion);
    /**
     * 启用/禁用区域
     */
    public int updateStatus(@Param("id") Long id, @Param("status") String status);
    /**
     * 删除部门区域
     */
    public int deleteArdDeptRegionById(Long id);
    /**
     * 批量删除部门区域
     */
    public int deleteArdDeptRegionByIds(String[] ids);
    /**
     * 根据区域名称查询区域(用于唯一性校验)
     */
    public ArdDeptRegion checkRegionNameUnique(@Param("regionName") String regionName);
    /**
     * 检查点冲突(1个坐标)
     */
    public List<ConflictRegion> checkPointConflict(@Param("deptId") Long deptId,
                                                   @Param("wkt") String wkt,
                                                   @Param("excludeId") Long excludeId);
    /**
     * 检查线冲突(2个坐标)
     */
    public List<ConflictRegion> checkLineConflict(@Param("deptId") Long deptId,
                                                  @Param("wkt") String wkt,
                                                  @Param("excludeId") Long excludeId);
    /**
     * 检查面冲突(3个以上坐标)
     */
    public List<ConflictRegion> checkPolygonConflict(@Param("deptId") Long deptId,
                                                     @Param("wkt") String wkt,
                                                     @Param("excludeId") Long excludeId);
    /**
     * 获取边界WKT
     */
    public String selectBoundaryWkt(@Param("id") Long id);
    /**
     * 根据坐标查询所属区域
     */
    public ArdDeptRegion selectRegionByPoint(@Param("pointWkt") String pointWkt);
}
ard-system/src/main/java/com/ard/data/region/service/IArdDeptRegionService.java
New file
@@ -0,0 +1,64 @@
package com.ard.data.region.service;
import com.ard.data.region.domain.ArdDeptRegion;
import com.ard.data.region.domain.ConflictCheckResult;
import java.util.List;
/**
 * 部门区域Service接口
 *
 * @author ruoyi
 */
public interface IArdDeptRegionService
{
    /**
     * 查询部门区域
     */
    public ArdDeptRegion selectArdDeptRegionById(Long id);
    /**
     * 查询部门区域列表
     */
    public List<ArdDeptRegion> selectArdDeptRegionList(ArdDeptRegion ardDeptRegion);
    /**
     * 新增部门区域
     */
    public int insertArdDeptRegion(ArdDeptRegion ardDeptRegion);
    /**
     * 修改部门区域
     */
    public int updateArdDeptRegion(ArdDeptRegion ardDeptRegion);
    /**
     * 修改状态(启用/禁用)
     */
    public int updateStatus(Long id, String status);
    /**
     * 批量删除部门区域
     */
    public int deleteArdDeptRegionByIds(String[] ids);
    /**
     * 删除部门区域信息
     */
    public int deleteArdDeptRegionById(Long id);
    /**
     * 校验区域名称是否唯一
     */
    public boolean checkRegionNameUnique(ArdDeptRegion ardDeptRegion);
    /**
     * 检查区域冲突
     */
    public ConflictCheckResult checkConflict(ArdDeptRegion ardDeptRegion);
    /**
     * 根据坐标查询所属区域
     */
    public ArdDeptRegion selectRegionByPoint(Double lat, Double lng);
}
ard-system/src/main/java/com/ard/data/region/service/impl/ArdDeptRegionServiceImpl.java
New file
@@ -0,0 +1,298 @@
package com.ard.data.region.service.impl;
import com.ard.common.constant.RegionConstants;
import com.ard.common.utils.DateUtils;
import com.ard.common.utils.SecurityUtils;
import com.ard.common.utils.StringUtils;
import com.ard.data.region.domain.ArdDeptRegion;
import com.ard.data.region.domain.ConflictCheckResult;
import com.ard.data.region.domain.ConflictRegion;
import com.ard.data.region.domain.Coordinate;
import com.ard.data.region.mapper.ArdDeptRegionMapper;
import com.ard.data.region.service.IArdDeptRegionService;
import com.ard.data.region.utils.RegionCoordinateUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
/**
 * 部门区域Service业务层处理
 *
 * @author ruoyi
 */
@Slf4j
@Service
public class ArdDeptRegionServiceImpl implements IArdDeptRegionService
{
    @Autowired
    private ArdDeptRegionMapper ardDeptRegionMapper;
    @Override
    public ArdDeptRegion selectArdDeptRegionById(Long id)
    {
        ArdDeptRegion region = ardDeptRegionMapper.selectArdDeptRegionById(id);
        if (region != null)
        {
            region.setCoordinates(RegionCoordinateUtils.toCoordinates(region.getBoundaryWkt()));
        }
        return region;
    }
    @Override
    public List<ArdDeptRegion> selectArdDeptRegionList(ArdDeptRegion ardDeptRegion)
    {
        List<ArdDeptRegion> list = ardDeptRegionMapper.selectArdDeptRegionList(ardDeptRegion);
        list.forEach(region -> {
            region.setCoordinates(RegionCoordinateUtils.toCoordinates(region.getBoundaryWkt()));
        });
        return list;
    }
    /**
     * 校验区域名称是否唯一
     */
    @Override
    public boolean checkRegionNameUnique(ArdDeptRegion ardDeptRegion)
    {
        Long id = StringUtils.isNull(ardDeptRegion.getId()) ? -1L : ardDeptRegion.getId();
        ArdDeptRegion info = ardDeptRegionMapper.checkRegionNameUnique(ardDeptRegion.getRegionName());
        if (StringUtils.isNotNull(info) && info.getId().longValue() != id.longValue())
        {
            return RegionConstants.NOT_UNIQUE;
        }
        return RegionConstants.UNIQUE;
    }
    /**
     * 检查区域冲突
     */
    @Override
    public ConflictCheckResult checkConflict(ArdDeptRegion ardDeptRegion)
    {
        ConflictCheckResult result = new ConflictCheckResult();
        try
        {
            // 1. 验证坐标
            List<Coordinate> coordinates = ardDeptRegion.getCoordinates();
            RegionCoordinateUtils.validateCoordinates(coordinates);
            // 2. 根据坐标数量自动判断类型
            String regionType = RegionCoordinateUtils.getRegionTypeByCoordinates(coordinates);
            ardDeptRegion.setRegionType(regionType);
            // 3. 转换为WKT
            String wkt = RegionCoordinateUtils.toWkt(coordinates);
            ardDeptRegion.setBoundaryWkt(wkt);
            // 4. 根据类型检查冲突
            List<ConflictRegion> conflicts = null;
            Long excludeId = ardDeptRegion.getId();
            switch (regionType)
            {
                case "POINT":
                    conflicts = ardDeptRegionMapper.checkPointConflict(
                            ardDeptRegion.getDeptId(), wkt, excludeId);
                    break;
                case "LINE":
                    conflicts = ardDeptRegionMapper.checkLineConflict(
                            ardDeptRegion.getDeptId(), wkt, excludeId);
                    break;
                case "POLYGON":
                    conflicts = ardDeptRegionMapper.checkPolygonConflict(
                            ardDeptRegion.getDeptId(), wkt, excludeId);
                    break;
                default:
                    result.setHasConflict(true);
                    result.setMessage("不支持的几何类型");
                    return result;
            }
            if (!CollectionUtils.isEmpty(conflicts))
            {
                result.setHasConflict(true);
                result.setMessage(String.format("该%s与%d个已有区域存在冲突",
                        getRegionTypeName(regionType), conflicts.size()));
                result.setConflictRegions(conflicts);
            }
            else
            {
                result.setHasConflict(false);
                result.setMessage("无冲突,可以添加");
            }
        }
        catch (Exception e)
        {
            log.error("检查冲突失败", e);
            result.setHasConflict(true);
            result.setMessage("检查失败: " + e.getMessage());
        }
        return result;
    }
    @Override
    @Transactional
    public int insertArdDeptRegion(ArdDeptRegion ardDeptRegion)
    {
        // 1. 检查冲突
        ConflictCheckResult checkResult = checkConflict(ardDeptRegion);
        if (checkResult.getHasConflict())
        {
            throw new RuntimeException(checkResult.getMessage());
        }
        // 2. 计算中心点和面积
        List<Coordinate> coordinates = ardDeptRegion.getCoordinates();
        Coordinate center = RegionCoordinateUtils.calculateCenter(coordinates);
        ardDeptRegion.setCenterLat(center != null ? center.getLat() : null);
        ardDeptRegion.setCenterLng(center != null ? center.getLng() : null);
        // 只有面类型计算面积
        if ("POLYGON".equals(ardDeptRegion.getRegionType()))
        {
            ardDeptRegion.setAreaSize(RegionCoordinateUtils.calculateArea(coordinates));
        }
        else
        {
            ardDeptRegion.setAreaSize(0.0);
        }
        // 3. 设置默认颜色
        if (ardDeptRegion.getRegionColor() == null || ardDeptRegion.getRegionColor().isEmpty())
        {
            ardDeptRegion.setRegionColor("#3c8dbc");
        }
        // 4. 插入数据
        ardDeptRegion.setCreateBy(SecurityUtils.getUsername());
        ardDeptRegion.setCreateTime(DateUtils.getNowDate());
        ardDeptRegion.setStatus("0");
        return ardDeptRegionMapper.insertArdDeptRegion(ardDeptRegion);
    }
    @Override
    @Transactional
    public int updateArdDeptRegion(ArdDeptRegion ardDeptRegion)
    {
        // 1. 获取原区域
        ArdDeptRegion oldRegion = selectArdDeptRegionById(ardDeptRegion.getId());
        if (oldRegion == null)
        {
            throw new RuntimeException("区域不存在");
        }
        // 2. 如果坐标有变化,检查冲突
        if (ardDeptRegion.getCoordinates() != null && !ardDeptRegion.getCoordinates().isEmpty())
        {
            List<Coordinate> coordinates = ardDeptRegion.getCoordinates();
            RegionCoordinateUtils.validateCoordinates(coordinates);
            String regionType = RegionCoordinateUtils.getRegionTypeByCoordinates(coordinates);
            ardDeptRegion.setRegionType(regionType);
            String wkt = RegionCoordinateUtils.toWkt(coordinates);
            ardDeptRegion.setBoundaryWkt(wkt);
            // 根据类型检查冲突
            List<ConflictRegion> conflicts = null;
            switch (regionType)
            {
                case "POINT":
                    conflicts = ardDeptRegionMapper.checkPointConflict(
                            ardDeptRegion.getDeptId(), wkt, ardDeptRegion.getId());
                    break;
                case "LINE":
                    conflicts = ardDeptRegionMapper.checkLineConflict(
                            ardDeptRegion.getDeptId(), wkt, ardDeptRegion.getId());
                    break;
                case "POLYGON":
                    conflicts = ardDeptRegionMapper.checkPolygonConflict(
                            ardDeptRegion.getDeptId(), wkt, ardDeptRegion.getId());
                    break;
            }
            if (!CollectionUtils.isEmpty(conflicts))
            {
                throw new RuntimeException("更新后的区域与已有区域存在冲突");
            }
            // 重新计算中心点和面积
            Coordinate center = RegionCoordinateUtils.calculateCenter(coordinates);
            ardDeptRegion.setCenterLat(center != null ? center.getLat() : null);
            ardDeptRegion.setCenterLng(center != null ? center.getLng() : null);
            if ("POLYGON".equals(regionType))
            {
                ardDeptRegion.setAreaSize(RegionCoordinateUtils.calculateArea(coordinates));
            }
            else
            {
                ardDeptRegion.setAreaSize(0.0);
            }
        }
        ardDeptRegion.setUpdateBy(SecurityUtils.getUsername());
        ardDeptRegion.setUpdateTime(DateUtils.getNowDate());
        return ardDeptRegionMapper.updateArdDeptRegion(ardDeptRegion);
    }
    /**
     * 修改状态(启用/禁用)
     */
    @Override
    public int updateStatus(Long id, String status)
    {
        // 检查区域是否存在
        ArdDeptRegion region = selectArdDeptRegionById(id);
        if (region == null)
        {
            throw new RuntimeException("区域不存在");
        }
        // 更新状态
        return ardDeptRegionMapper.updateStatus(id, status);
    }
    @Override
    public ArdDeptRegion selectRegionByPoint(Double lat, Double lng)
    {
        String pointWkt = String.format("POINT(%f %f)", lat, lng);
        return ardDeptRegionMapper.selectRegionByPoint(pointWkt);
    }
    @Override
    public int deleteArdDeptRegionByIds(String[] ids)
    {
        return ardDeptRegionMapper.deleteArdDeptRegionByIds(ids);
    }
    @Override
    public int deleteArdDeptRegionById(Long id)
    {
        return ardDeptRegionMapper.deleteArdDeptRegionById(id);
    }
    /**
     * 获取区域类型中文名
     */
    private String getRegionTypeName(String type)
    {
        switch (type)
        {
            case "POINT": return "点";
            case "LINE": return "线";
            case "POLYGON": return "面";
            default: return type;
        }
    }
}
ard-system/src/main/java/com/ard/data/region/utils/RegionCoordinateUtils.java
New file
@@ -0,0 +1,154 @@
package com.ard.data.region.utils;
import com.ard.data.region.domain.Coordinate;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
 * 区域坐标工具类
 *
 * @author ruoyi
 */
public class RegionCoordinateUtils {
    /**
     * 根据坐标数量自动判断区域类型
     *
     * @param coordinates 坐标集合
     * @return POINT/LINE/POLYGON
     */
    public static String getRegionTypeByCoordinates(List<Coordinate> coordinates) {
        if (CollectionUtils.isEmpty(coordinates)) {
            throw new RuntimeException("坐标集合不能为空");
        }
        int size = coordinates.size();
        if (size == 1) {
            return "POINT";
        } else if (size == 2) {
            return "LINE";
        } else {
            return "POLYGON";
        }
    }
    /**
     * 将坐标集合转换为 WKT 格式(自动判断类型)
     */
    public static String toWkt(List<Coordinate> coordinates) {
        if (CollectionUtils.isEmpty(coordinates)) {
            throw new RuntimeException("坐标集合不能为空");
        }
        int size = coordinates.size();
        if (size == 1) {
            // 点
            Coordinate coord = coordinates.get(0);
            return String.format("POINT(%s)", coord.toLatLngString());
        } else if (size == 2) {
            // 线
            List<String> points = coordinates.stream()
                    .map(Coordinate::toLatLngString)
                    .collect(Collectors.toList());
            return String.format("LINESTRING(%s, %s)", points.get(0), points.get(1));
        } else {
            // 面(多边形)
            List<String> points = coordinates.stream()
                    .map(Coordinate::toLatLngString)
                    .collect(Collectors.toList());
            String allPoints = String.join(", ", points);
            String firstPoint = points.get(0);
            return String.format("POLYGON((%s, %s))", allPoints, firstPoint);
        }
    }
    /**
     * 将 WKT 字符串转换为坐标集合
     *
     * @param wkt 如:POLYGON((45.718839 126.670721, 45.708756 126.670151, ...))
     * @return 坐标集合
     */
    public static List<Coordinate> toCoordinates(String wkt) {
        List<Coordinate> coordinates = new ArrayList<>();
        if (wkt == null || wkt.isEmpty()) {
            return coordinates;
        }
        // 提取括号内的坐标字符串
        String coordStr = wkt;
        if (wkt.startsWith("POLYGON")) {
            coordStr = wkt.substring(wkt.indexOf("((") + 2, wkt.lastIndexOf("))"));
        } else if (wkt.startsWith("LINESTRING")) {
            coordStr = wkt.substring(wkt.indexOf("(") + 1, wkt.lastIndexOf(")"));
        } else if (wkt.startsWith("POINT")) {
            coordStr = wkt.substring(wkt.indexOf("(") + 1, wkt.lastIndexOf(")"));
        }
        // 分割坐标点
        String[] points = coordStr.split(",");
        for (String point : points) {
            String[] lngLat = point.trim().split(" ");
            if (lngLat.length >= 2) {
                Double lat = Double.parseDouble(lngLat[0]);
                Double lng = Double.parseDouble(lngLat[1]);
                coordinates.add(new Coordinate(lng, lat));
            }
        }
        return coordinates;
    }
    /**
     * 计算中心点
     */
    public static Coordinate calculateCenter(List<Coordinate> coordinates) {
        if (CollectionUtils.isEmpty(coordinates)) {
            return null;
        }
        double sumLng = 0;
        double sumLat = 0;
        for (Coordinate coord : coordinates) {
            sumLng += coord.getLng();
            sumLat += coord.getLat();
        }
        return new Coordinate(sumLng / coordinates.size(), sumLat / coordinates.size());
    }
    /**
     * 计算面积(仅面类型有效)
     */
    public static double calculateArea(List<Coordinate> coordinates) {
        if (coordinates == null || coordinates.size() < 3) {
            return 0;
        }
        double area = 0;
        int n = coordinates.size();
        for (int i = 0; i < n; i++) {
            Coordinate current = coordinates.get(i);
            Coordinate next = coordinates.get((i + 1) % n);
            area += current.getLng() * next.getLat() - next.getLng() * current.getLat();
        }
        area = Math.abs(area) / 2;
        double lat = coordinates.get(0).getLat();
        double meterPerDegree = 111000 * Math.cos(Math.toRadians(lat));
        return area * meterPerDegree * 111000;
    }
    /**
     * 验证坐标集合
     */
    public static void validateCoordinates(List<Coordinate> coordinates) {
        if (CollectionUtils.isEmpty(coordinates)) {
            throw new RuntimeException("坐标集合不能为空");
        }
        for (Coordinate coord : coordinates) {
            if (!coord.isValid()) {
                throw new RuntimeException("无效坐标");
            }
        }
    }
}
ard-system/src/main/resources/mapper/data/ArdDeptRegionMapper.xml
New file
@@ -0,0 +1,210 @@
<?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.ard.data.region.mapper.ArdDeptRegionMapper">
    <resultMap type="ArdDeptRegion" id="ArdDeptRegionResult">
        <id     property="id"          column="id"          />
        <result property="deptId"      column="dept_id"     />
        <result property="regionName"  column="region_name" />
        <result property="regionColor" column="region_color" />
        <result property="centerLat"   column="center_lat"  />
        <result property="centerLng"   column="center_lng"  />
        <result property="areaSize"    column="area_size"   />
        <result property="status"      column="status"      />
        <result property="createBy"    column="create_by"   />
        <result property="createTime"  column="create_time" />
        <result property="updateBy"    column="update_by"   />
        <result property="updateTime"  column="update_time" />
        <result property="remark"      column="remark"      />
    </resultMap>
    <sql id="selectArdDeptRegionVo">
        SELECT r.id, r.dept_id, r.region_name, r.region_color,
               ST_AsText(r.region_boundary) as boundary_wkt,
               r.center_lat, r.center_lng, r.area_size,
               r.status, r.create_by, r.create_time, r.update_by, r.update_time, r.remark,
               d.dept_name
        FROM ard_dept_region r
                 LEFT JOIN sys_dept d ON r.dept_id = d.dept_id
    </sql>
    <!-- 查询部门区域列表(查询所有,不过滤状态) -->
    <select id="selectArdDeptRegionList" parameterType="ArdDeptRegion" resultMap="ArdDeptRegionResult">
        <include refid="selectArdDeptRegionVo"/>
        <where>
            <if test="deptId != null"> AND r.dept_id = #{deptId}</if>
            <if test="regionName != null and regionName != ''"> AND r.region_name like concat('%', #{regionName}, '%')</if>
            <if test="status != null and status != ''"> AND r.status = #{status}</if>
        </where>
        ORDER BY r.create_time DESC
    </select>
    <!-- 查询部门区域(根据ID,查询所有) -->
    <select id="selectArdDeptRegionById" parameterType="Long" resultMap="ArdDeptRegionResult">
        <include refid="selectArdDeptRegionVo"/>
        WHERE r.id = #{id}
    </select>
    <!-- 获取边界WKT -->
    <select id="selectBoundaryWkt" parameterType="Long" resultType="String">
        SELECT ST_AsText(region_boundary)
        FROM ard_dept_region
        WHERE id = #{id}
    </select>
    <!-- 根据区域名称查询区域(用于唯一性校验,查询所有) -->
    <select id="checkRegionNameUnique" parameterType="String" resultMap="ArdDeptRegionResult">
        SELECT id, dept_id, region_name, region_color,
               center_lat, center_lng, area_size,
               status, create_by, create_time, update_by, update_time, remark
        FROM ard_dept_region
        WHERE region_name = #{regionName}
            LIMIT 1
    </select>
    <!-- 检查点冲突(只查启用的区域) -->
    <select id="checkPointConflict" resultType="ConflictRegion">
        SELECT
        id,
        region_name as regionName,
        region_color as regionColor,
        CASE
        WHEN ST_Contains(region_boundary, ST_GeomFromText(#{wkt}, 4326)) THEN '包含该点'
        WHEN ST_Intersects(region_boundary, ST_GeomFromText(#{wkt}, 4326)) THEN '相交'
        ELSE '无冲突'
        END as relationship
        FROM ard_dept_region
        WHERE status = '0'
        <if test="deptId != null">
            AND dept_id = #{deptId}
        </if>
        <if test="excludeId != null">
            AND id != #{excludeId}
        </if>
        AND ST_Intersects(region_boundary, ST_GeomFromText(#{wkt}, 4326))
    </select>
    <!-- 检查线冲突(只查启用的区域) -->
    <select id="checkLineConflict" resultType="ConflictRegion">
        SELECT
        id,
        region_name as regionName,
        region_color as regionColor,
        CASE
        WHEN ST_Intersects(region_boundary, ST_GeomFromText(#{wkt}, 4326)) THEN '相交'
        WHEN ST_Contains(region_boundary, ST_GeomFromText(#{wkt}, 4326)) THEN '包含该线'
        ELSE '无冲突'
        END as relationship
        FROM ard_dept_region
        WHERE status = '0'
        <if test="deptId != null">
            AND dept_id = #{deptId}
        </if>
        <if test="excludeId != null">
            AND id != #{excludeId}
        </if>
        AND ST_Intersects(region_boundary, ST_GeomFromText(#{wkt}, 4326))
    </select>
    <!-- 检查面冲突(只查启用的区域) -->
    <select id="checkPolygonConflict" resultType="ConflictRegion">
        SELECT
        id,
        region_name as regionName,
        region_color as regionColor,
        CASE
        WHEN ST_Contains(region_boundary, ST_GeomFromText(#{wkt}, 4326)) THEN '包含该面'
        WHEN ST_Within(ST_GeomFromText(#{wkt}, 4326), region_boundary) THEN '在该面内部'
        WHEN ST_Overlaps(region_boundary, ST_GeomFromText(#{wkt}, 4326)) THEN '重叠'
        WHEN ST_Intersects(region_boundary, ST_GeomFromText(#{wkt}, 4326)) THEN '相交'
        ELSE '无冲突'
        END as relationship
        FROM ard_dept_region
        WHERE status = '0'
        <if test="deptId != null">
            AND dept_id = #{deptId}
        </if>
        <if test="excludeId != null">
            AND id != #{excludeId}
        </if>
        AND ST_Intersects(region_boundary, ST_GeomFromText(#{wkt}, 4326))
    </select>
    <!-- 根据坐标查询所属区域(查询所有区域,包括禁用的) -->
    <select id="selectRegionByPoint" resultMap="ArdDeptRegionResult">
        <include refid="selectArdDeptRegionVo"/>
        WHERE ST_Contains(r.region_boundary, ST_GeomFromText(#{pointWkt}, 4326))
        LIMIT 1
    </select>
    <!-- 新增部门区域(默认状态为启用0) -->
    <insert id="insertArdDeptRegion" parameterType="ArdDeptRegion" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO ard_dept_region
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="deptId != null">dept_id,</if>
            <if test="regionName != null">region_name,</if>
            <if test="regionColor != null">region_color,</if>
            <if test="boundaryWkt != null">region_boundary,</if>
            <if test="centerLat != null">center_lat,</if>
            <if test="centerLng != null">center_lng,</if>
            <if test="areaSize != null">area_size,</if>
            <if test="createBy != null">create_by,</if>
            <if test="remark != null">remark,</if>
            status, create_time
        </trim>
        VALUES
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="deptId != null">#{deptId},</if>
            <if test="regionName != null">#{regionName},</if>
            <if test="regionColor != null">#{regionColor},</if>
            <if test="boundaryWkt != null">ST_GeomFromText(#{boundaryWkt}, 4326),</if>
            <if test="centerLat != null">#{centerLat},</if>
            <if test="centerLng != null">#{centerLng},</if>
            <if test="areaSize != null">#{areaSize},</if>
            <if test="createBy != null">#{createBy},</if>
            <if test="remark != null">#{remark},</if>
            '0', NOW()
        </trim>
    </insert>
    <!-- 修改部门区域 -->
    <update id="updateArdDeptRegion" parameterType="ArdDeptRegion">
        UPDATE ard_dept_region
        <set>
            <if test="regionName != null">region_name = #{regionName},</if>
            <if test="regionColor != null">region_color = #{regionColor},</if>
            <if test="boundaryWkt != null">region_boundary = ST_GeomFromText(#{boundaryWkt}, 4326),</if>
            <if test="centerLat != null">center_lat = #{centerLat},</if>
            <if test="centerLng != null">center_lng = #{centerLng},</if>
            <if test="areaSize != null">area_size = #{areaSize},</if>
            <if test="status != null">status = #{status},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
            <if test="remark != null">remark = #{remark},</if>
            update_time = NOW()
        </set>
        WHERE id = #{id}
    </update>
    <!-- 启用/禁用区域 -->
    <update id="updateStatus">
        UPDATE ard_dept_region
        SET status = #{status}, update_time = NOW()
        WHERE id = #{id}
    </update>
    <!-- 物理删除区域 -->
    <delete id="deleteArdDeptRegionById" parameterType="Long">
        DELETE FROM ard_dept_region WHERE id = #{id}
    </delete>
    <!-- 批量物理删除区域 -->
    <delete id="deleteArdDeptRegionByIds" parameterType="String">
        DELETE FROM ard_dept_region WHERE id IN
        <foreach item="id" collection="array" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
</mapper>
ard-work/src/main/java/com/ard/work/component/websocket/handler/UavAllManager.java
@@ -3,11 +3,10 @@
import com.ard.common.constant.CacheConstants;
import com.ard.common.core.domain.entity.SysUser;
import com.ard.common.core.redis.RedisCache;
import com.ard.data.region.domain.ArdDeptRegion;
import com.ard.data.region.service.IArdDeptRegionService;
import com.ard.work.config.RouteStateMachineService;
import com.ard.work.domain.cache.UavSignalInfoCache;
import com.ard.work.domain.cache.UavStateCache;
import com.ard.work.domain.cache.UavTelemetryDataCache;
import com.ard.work.domain.cache.UavWaylineCache;
import com.ard.work.domain.cache.*;
import com.ard.work.domain.dto.signal.SignalInfo;
import com.ard.work.domain.dto.state.UavState;
import com.ard.work.domain.dto.telemetry.TelemetryData;
@@ -69,6 +68,9 @@
            // 将载荷当前控制者用户ID添加到映射中
            map.put("payloadController", payloadUser);
        }
        // 获取当前无人机所属区域
        ArdDeptRegion ardDeptRegion = UavRegionCache.get(boxSn);
        map.put("region", ardDeptRegion);
        return map;
    }
ard-work/src/main/java/com/ard/work/domain/cache/UavRegionCache.java
New file
@@ -0,0 +1,30 @@
package com.ard.work.domain.cache;
import com.ard.data.region.domain.ArdDeptRegion;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 无人机区域缓存(简化版)
 *
 * @author 刘苏义
 */
@Slf4j
public class UavRegionCache {
    // 无人机当前区域名称缓存 Map<无人机ID, 区域>
    private static final ConcurrentHashMap<String, ArdDeptRegion> cache = new ConcurrentHashMap<>();
    // 存储数据到缓存
    public static void put(String boxSn, ArdDeptRegion region) {cache.put(boxSn, region);}
    // 从缓存中获取数据
    public static ArdDeptRegion get(String boxSn) {
        return cache.get(boxSn);
    }
    // 删除缓存中的数据
    public static void remove(String boxSn) {
        cache.remove(boxSn);
    }
}
ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java
@@ -86,10 +86,10 @@
    private TyClient tyClient;
    @Autowired
    private RouteStateMachineService stateMachineService;
    @Autowired
    private IArdFlightTaskPartService ardFlightTaskPartService;
    @Resource
    private MediamtxConfig mediamtxConfig;
    private UavPositionChangeService uavPositionChangeService;
    @Resource
    private UavRegionDetectService uavRegionDetectService;
    /**
     * 获取边缘服务器token信息
     *
@@ -981,6 +981,16 @@
                // 处理无人机遥测消息
                UavTelemetryDataCmd uavTelemetryDataCmd = (UavTelemetryDataCmd) tyCommand;
                TelemetryData telemetryData = uavTelemetryDataCmd.getTelemetryData();
                // 判断是否需要触发区域检测
                if (uavPositionChangeService.shouldTriggerRegionCheck(telemetryData)) {
                    log.debug("无人机 {} 位置变化,触发区域检测", telemetryData.getBoxSn());
                    // 异步触发区域检测(不阻塞)
                    uavRegionDetectService.detectRegionAsync(telemetryData);
                } else {
                    log.debug("无人机 {} 位置未变化,跳过区域检测", telemetryData.getBoxSn());
                }
                UavTelemetryDataCache.put(telemetryData);
                //logPrint("无人机遥测", uavTelemetryDataCmd.getTelemetryData().getBoxSn());
                //WebSocketUtil.sendMessageAll(uavTelemetryDataCmd.getTelemetryData());
ard-work/src/main/java/com/ard/work/service/impl/UavPositionChangeService.java
New file
@@ -0,0 +1,89 @@
package com.ard.work.service.impl;
import com.ard.work.domain.cache.UavTelemetryDataCache;
import com.ard.work.domain.dto.telemetry.TelemetryData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
 * 无人机位置变化判断服务
 *
 * @author 刘苏义
 */
@Slf4j
@Service
public class UavPositionChangeService {
    // 位置变化阈值(米)
    private static final double POSITION_CHANGE_THRESHOLD = 5.0;
    // 无效坐标的默认值
    private static final double INVALID_COORD = 0.0;
    /**
     * 判断是否需要触发区域检测
     * @param telemetryData 当前遥测数据
     * @return true-需要触发区域检测,false-不需要
     */
    public boolean shouldTriggerRegionCheck(TelemetryData telemetryData) {
        String boxSn = telemetryData.getBoxSn();
        // 获取上次遥测数据
        TelemetryData lastData = UavTelemetryDataCache.get(boxSn);
        // 首次上报,需要检测
        if (lastData == null) {
            log.debug("无人机 {} 首次上报,触发区域检测", boxSn);
            return true;
        }
        // 判断位置是否变化
        return isPositionChanged(lastData, telemetryData);
    }
    /**
     * 判断位置是否发生变化
     */
    private boolean isPositionChanged(TelemetryData lastData, TelemetryData newData) {
        double lastLng = lastData.getLng();
        double lastLat = lastData.getLat();
        double newLng = newData.getLng();
        double newLat = newData.getLat();
        // 坐标无效(为0),认为需要检测
        if (lastLng == INVALID_COORD || lastLat == INVALID_COORD ||
            newLng == INVALID_COORD || newLat == INVALID_COORD) {
            log.debug("坐标无效,触发区域检测");
            return true;
        }
        // 计算移动距离
        double distance = calculateDistance(lastLat, lastLng, newLat, newLng);
        boolean changed = distance > POSITION_CHANGE_THRESHOLD;
        if (changed) {
            log.debug("无人机 {} 移动了 {:.2f} 米,触发区域检测", newData.getBoxSn(), distance);
        }
        return changed;
    }
    /**
     * 计算两点间距离(米)- Haversine公式
     */
    private double calculateDistance(double lat1, double lng1, double lat2, double lng2) {
        double earthRadius = 6371000; // 地球半径(米)
        double dLat = Math.toRadians(lat2 - lat1);
        double dLng = Math.toRadians(lng2 - lng1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                   Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
                   Math.sin(dLng / 2) * Math.sin(dLng / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return earthRadius * c;
    }
}
ard-work/src/main/java/com/ard/work/service/impl/UavRegionDetectService.java
New file
@@ -0,0 +1,43 @@
package com.ard.work.service.impl;
import com.ard.data.region.domain.ArdDeptRegion;
import com.ard.data.region.service.IArdDeptRegionService;
import com.ard.work.domain.cache.UavRegionCache;
import com.ard.work.domain.cache.UavTelemetryDataCache;
import com.ard.work.domain.dto.telemetry.TelemetryData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
/**
 * 无人机区域检测服务(异步)
 *
 * @author 刘苏义
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UavRegionDetectService {
    private final IArdDeptRegionService ardDeptRegionService;
    /**
     * 异步检测无人机所在区域
     */
    @Async
    public void detectRegionAsync(TelemetryData telemetryData) {
        String boxSn = telemetryData.getBoxSn();
        try {
            // 1. 查询当前坐标所属区域
            ArdDeptRegion region = null;
            if (telemetryData.getLat() != 0.0 && telemetryData.getLng() != 0.0) {
                region = ardDeptRegionService.selectRegionByPoint(telemetryData.getLat(), telemetryData.getLng());
            }
            // 3. 更新区域缓存
            UavRegionCache.put(boxSn, region);
        } catch (Exception e) {
            log.error("无人机 {} 区域检测失败: {}", boxSn, e.getMessage(), e);
        }
    }
}