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); } } }