增加禁飞区
This commit is contained in:
parent
139e2eff46
commit
61ca367330
|
|
@ -8,6 +8,9 @@ package com.multictrl.common.constant;
|
|||
*/
|
||||
public interface BusinessConstant {
|
||||
|
||||
String WEB_EVENT_TOPIC = "thing/product/%s/web_event";
|
||||
String NOFLY_ZONE_METHOD = "nofly_zone";
|
||||
|
||||
//********************************* minio *********************************//
|
||||
String ROUTE_IMG_BUCKET = "route-images";//航线图片桶
|
||||
String ROUTE_KMZ_BUCKET = "route-kmz";//航线桶
|
||||
|
|
@ -83,6 +86,10 @@ public interface BusinessConstant {
|
|||
String UAV_LIGHT_INDEX = "uav_light_index_";
|
||||
String UAV_MODE_CODE = "uav_mode_code_";
|
||||
|
||||
//********************************* other cache key *********************************//
|
||||
String DOCK_NOFLY_ZONE = "dock_nofly_zone_";
|
||||
String DOCK_NOFLY_ZONE_TRIGGER_SIGN = "dock_nofly_zone_trigger_sign_";
|
||||
|
||||
//********************************* other *********************************//
|
||||
String HTTP_PROTOCOL = "http://";
|
||||
String HTTPS_PROTOCOL = "https://";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
package com.multictrl.modules.business.controller;
|
||||
|
||||
import com.multictrl.common.annotation.ApiOrder;
|
||||
import com.multictrl.common.annotation.LogOperation;
|
||||
import com.multictrl.common.constant.Constant;
|
||||
import com.multictrl.common.page.PageData;
|
||||
import com.multictrl.common.utils.Result;
|
||||
import com.multictrl.common.validator.AssertUtils;
|
||||
import com.multictrl.common.validator.ValidatorUtils;
|
||||
import com.multictrl.common.validator.group.AddGroup;
|
||||
import com.multictrl.common.validator.group.UpdateGroup;
|
||||
import com.multictrl.modules.business.dto.NoflyZoneDTO;
|
||||
import com.multictrl.modules.business.service.NoflyZoneService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* 禁飞区
|
||||
*
|
||||
* @author Sdy
|
||||
* @since 1.0.0 2026-06-09
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("business/noflyzone")
|
||||
@Tag(name = "禁飞区")
|
||||
@ApiOrder(22)
|
||||
@RequiredArgsConstructor
|
||||
public class NoflyZoneController {
|
||||
private final NoflyZoneService noflyZoneService;
|
||||
|
||||
@GetMapping("page")
|
||||
@Operation(summary = "分页")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始"),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数"),
|
||||
@Parameter(name = "name", description = "名称")
|
||||
})
|
||||
@RequiresPermissions("bus:noflyzone:page")
|
||||
public Result<PageData<NoflyZoneDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
PageData<NoflyZoneDTO> page = noflyZoneService.pageList(params);
|
||||
|
||||
return new Result<PageData<NoflyZoneDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "保存")
|
||||
@LogOperation("保存")
|
||||
@RequiresPermissions("bus:noflyzone:save")
|
||||
public Result<Object> save(@RequestBody NoflyZoneDTO dto) {
|
||||
//效验数据
|
||||
ValidatorUtils.validateEntity(dto, AddGroup.class);
|
||||
noflyZoneService.saveNoFlying(dto);
|
||||
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "修改")
|
||||
@LogOperation("修改")
|
||||
@RequiresPermissions("bus:noflyzone:update")
|
||||
public Result<Object> update(@RequestBody NoflyZoneDTO dto) {
|
||||
//效验数据
|
||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class);
|
||||
noflyZoneService.update(dto);
|
||||
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
@RequiresPermissions("bus:noflyzone:delete")
|
||||
public Result<Object> delete(@RequestBody Long[] ids) {
|
||||
//效验数据
|
||||
AssertUtils.isArrayEmpty(ids, "id");
|
||||
noflyZoneService.delete(ids);
|
||||
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@GetMapping("/getNoFlying")
|
||||
@Operation(summary = "获取机库禁飞区", description = "返回机库所属组织以及上级组织设置的禁飞区")
|
||||
public Result<List<NoflyZoneDTO>> getNoFlying(@RequestParam String dockSn) {
|
||||
List<NoflyZoneDTO> list = noflyZoneService.getNoFlyZoneByDockSn(dockSn);
|
||||
|
||||
return new Result<List<NoflyZoneDTO>>().ok(list);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.multictrl.modules.business.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.multictrl.common.dao.BaseDao;
|
||||
import com.multictrl.modules.business.dto.NoflyZoneDTO;
|
||||
import com.multictrl.modules.business.entity.NoflyZoneEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 禁飞区
|
||||
*
|
||||
* @author Sdy
|
||||
* @since 1.0.0 2026-06-09
|
||||
*/
|
||||
@Mapper
|
||||
public interface NoflyZoneDao extends BaseDao<NoflyZoneEntity> {
|
||||
|
||||
//分页列表
|
||||
IPage<NoflyZoneDTO> pageList(IPage<NoflyZoneEntity> page, @Param("ew") QueryWrapper<NoflyZoneEntity> ew);
|
||||
|
||||
//新增禁飞区
|
||||
void saveNoFlyZone(NoflyZoneEntity noflyZoneEntity);
|
||||
|
||||
//查询禁飞区
|
||||
List<NoflyZoneDTO> getNoFlyZoneList(@Param("ew") QueryWrapper<NoflyZoneEntity> ew);
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.multictrl.modules.business.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.multictrl.common.validator.group.AddGroup;
|
||||
import com.multictrl.common.validator.group.UpdateGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.groups.Default;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* 禁飞区
|
||||
*
|
||||
* @author Sdy
|
||||
* @since 1.0.0 2026-06-09
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "禁飞区")
|
||||
public class NoflyZoneDTO implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Null(message = "标识必须为空", groups = {AddGroup.class})
|
||||
@NotNull(message = "标识不能为空", groups = {UpdateGroup.class})
|
||||
@Schema(description = "标识")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "名称")
|
||||
@NotBlank(message = "名称不能为空", groups = {AddGroup.class, UpdateGroup.class})
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "是否开启不能为空", groups = {AddGroup.class, UpdateGroup.class})
|
||||
@Schema(description = "是否开启 1开启 0未开启")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "范围")
|
||||
@NotEmpty(message = "范围不能为空", groups = {AddGroup.class})
|
||||
@Size(min = 2, max = 2, message = "范围数据错误", groups = {AddGroup.class})
|
||||
private List<List<@Valid point>> extent;
|
||||
|
||||
@Schema(description = "时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
private Date createDate;
|
||||
|
||||
@Schema(hidden = true)
|
||||
private String deptName;
|
||||
|
||||
@Schema(hidden = true)
|
||||
private String userName;
|
||||
|
||||
@Schema(hidden = true)
|
||||
@JsonIgnore
|
||||
private String extentWkt;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public static class point {
|
||||
|
||||
@NotNull(message = "经度不能为空", groups = {AddGroup.class})
|
||||
@Schema(description = "经度")
|
||||
private Double lng;
|
||||
|
||||
@NotNull(message = "纬度不能为空", groups = {AddGroup.class})
|
||||
@Schema(description = "纬度")
|
||||
private Double lat;
|
||||
|
||||
@NotNull(message = "海拔高度不能为空", groups = {AddGroup.class})
|
||||
@Schema(description = "海拔高度")
|
||||
private Double height;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.multictrl.modules.business.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.multictrl.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 禁飞区
|
||||
*
|
||||
* @author Sdy
|
||||
* @since 1.0.0 2026-06-09
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@TableName("bus_nofly_zone")
|
||||
public class NoflyZoneEntity extends BaseEntity {
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 是否开启 1开启 0未开启
|
||||
*/
|
||||
private String status;
|
||||
/**
|
||||
* 范围
|
||||
*/
|
||||
private String extent;
|
||||
/**
|
||||
* 部门标识
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long deptId;
|
||||
}
|
||||
|
|
@ -7,10 +7,10 @@ import com.multictrl.common.constant.BusinessConstant;
|
|||
import com.multictrl.common.constant.DockMode;
|
||||
import com.multictrl.common.utils.CacheUtils;
|
||||
import com.multictrl.common.utils.JsonUtils;
|
||||
import com.multictrl.modules.business.dto.NoflyZoneDTO;
|
||||
import com.multictrl.modules.business.influxdb.FlightLog;
|
||||
import com.multictrl.modules.business.influxdb.UavReport;
|
||||
import com.multictrl.modules.business.service.DockService;
|
||||
import com.multictrl.modules.business.service.FlightTaskService;
|
||||
import com.multictrl.modules.business.service.InfluxService;
|
||||
import com.multictrl.modules.business.service.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
|
@ -18,6 +18,9 @@ import org.springframework.stereotype.Component;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* osd设备属性定频上报0.5HZ
|
||||
|
|
@ -33,6 +36,9 @@ public class OsdHandler implements MessageHandler {
|
|||
private final InfluxService influxService;
|
||||
private final FlightTaskService flightTaskService;
|
||||
private final DockService dockService;
|
||||
private final DJIBaseService djiBaseService;
|
||||
private final MqttPushService mqttPushService;
|
||||
private final NoflyZoneService noflyZoneService;
|
||||
|
||||
@Override
|
||||
public void handleMessage(String topic, String payload, String gateway) {
|
||||
|
|
@ -59,13 +65,13 @@ public class OsdHandler implements MessageHandler {
|
|||
}
|
||||
//刚从“任务中”切到“空闲“,说明架次结束入库了
|
||||
if (modeCode == DockMode.IDLE.getCode()
|
||||
&& CacheUtils.get(BusinessConstant.DOCK_IN_WORK + dockSn) != null) {
|
||||
&& CacheUtils.get(BusinessConstant.DOCK_IN_WORK + dockSn) != null) {
|
||||
CacheUtils.delete(BusinessConstant.DOCK_IN_WORK + dockSn);
|
||||
log.debug("{}机库架次飞行完成,取消DRC心跳", dockSn);
|
||||
List<String> list = Optional.ofNullable(CacheUtils.get(BusinessConstant.DRC_HEART_BEAT_SN))
|
||||
.filter(obj -> obj instanceof List)
|
||||
.map(obj -> (List<String>) obj)
|
||||
.orElse(Collections.emptyList());
|
||||
.filter(obj -> obj instanceof List)
|
||||
.map(obj -> (List<String>) obj)
|
||||
.orElse(Collections.emptyList());
|
||||
if (CollUtil.isNotEmpty(list)) {
|
||||
list.remove(dockSn);
|
||||
}
|
||||
|
|
@ -87,9 +93,9 @@ public class OsdHandler implements MessageHandler {
|
|||
String latitude = data.getStr("latitude");
|
||||
String longitude = data.getStr("longitude");
|
||||
if (latitude != null && longitude != null && !GPS_ZERO.equals(latitude) && !GPS_ZERO.equals(
|
||||
longitude)) {
|
||||
longitude)) {
|
||||
String payload_index = data.getJSONArray("cameras") != null ?
|
||||
data.getJSONArray("cameras").getJSONObject(0).getStr("payload_index") : null;
|
||||
data.getJSONArray("cameras").getJSONObject(0).getStr("payload_index") : null;
|
||||
//获取电池电量,如果单电池则把 a赋值给b
|
||||
int capacity_percent_a;
|
||||
int capacity_percent_b;
|
||||
|
|
@ -102,53 +108,160 @@ public class OsdHandler implements MessageHandler {
|
|||
}
|
||||
|
||||
UavReport uavReport = UavReport.builder()
|
||||
.dockSn(dockSn)
|
||||
.obstacle_avoidance_horizon(data.getJSONObject("obstacle_avoidance").getInt("horizon"))
|
||||
.obstacle_avoidance_upside(data.getJSONObject("obstacle_avoidance").getInt("upside"))
|
||||
.obstacle_avoidance_downside(data.getJSONObject("obstacle_avoidance").getInt("downside"))
|
||||
.is_near_area_limit(data.getInt("is_near_area_limit"))
|
||||
.is_near_height_limit(data.getInt("is_near_height_limit"))
|
||||
.payload_index(payload_index)
|
||||
.gimbal_pitch(
|
||||
payload_index != null ? data.getJSONObject(payload_index).getDouble("gimbal_pitch") : null)
|
||||
.gimbal_roll(
|
||||
payload_index != null ? data.getJSONObject(payload_index).getDouble("gimbal_roll") : null)
|
||||
.gimbal_yaw(
|
||||
payload_index != null ? data.getJSONObject(payload_index).getDouble("gimbal_yaw") : null)
|
||||
.zoom_factor(
|
||||
payload_index != null ? data.getJSONObject(payload_index).getDouble("zoom_factor") : null)
|
||||
.track_id(data.getStr("track_id"))
|
||||
.position_state_is_fixed(data.getJSONObject("position_state").getInt("is_fixed"))
|
||||
.position_state_is_quality(data.getJSONObject("position_state").getInt("quality"))
|
||||
.position_state_is_gps_number(data.getJSONObject("position_state").getInt("gps_number"))
|
||||
.position_state_is_rtk_number(data.getJSONObject("position_state").getInt("rtk_number"))
|
||||
.battery_capacity_percent(data.getJSONObject("battery").getInt("capacity_percent"))
|
||||
.battery_remain_flight_time(data.getJSONObject("battery").getInt("remain_flight_time"))
|
||||
.battery_return_home_power(data.getJSONObject("battery").getInt("return_home_power"))
|
||||
.battery_landing_power(data.getJSONObject("battery").getInt("landing_power"))
|
||||
.battery_capacity_percent_a(capacity_percent_a)
|
||||
.battery_capacity_percent_b(capacity_percent_b)
|
||||
.wind_direction(data.getInt("wind_direction"))
|
||||
.wind_speed(data.getDouble("wind_speed"))
|
||||
.home_distance(data.getDouble("home_distance"))
|
||||
//state上报
|
||||
.dockSn(dockSn)
|
||||
.obstacle_avoidance_horizon(data.getJSONObject("obstacle_avoidance").getInt("horizon"))
|
||||
.obstacle_avoidance_upside(data.getJSONObject("obstacle_avoidance").getInt("upside"))
|
||||
.obstacle_avoidance_downside(data.getJSONObject("obstacle_avoidance").getInt("downside"))
|
||||
.is_near_area_limit(data.getInt("is_near_area_limit"))
|
||||
.is_near_height_limit(data.getInt("is_near_height_limit"))
|
||||
.payload_index(payload_index)
|
||||
.gimbal_pitch(
|
||||
payload_index != null ? data.getJSONObject(payload_index).getDouble("gimbal_pitch") : null)
|
||||
.gimbal_roll(
|
||||
payload_index != null ? data.getJSONObject(payload_index).getDouble("gimbal_roll") : null)
|
||||
.gimbal_yaw(
|
||||
payload_index != null ? data.getJSONObject(payload_index).getDouble("gimbal_yaw") : null)
|
||||
.zoom_factor(
|
||||
payload_index != null ? data.getJSONObject(payload_index).getDouble("zoom_factor") : null)
|
||||
.track_id(data.getStr("track_id"))
|
||||
.position_state_is_fixed(data.getJSONObject("position_state").getInt("is_fixed"))
|
||||
.position_state_is_quality(data.getJSONObject("position_state").getInt("quality"))
|
||||
.position_state_is_gps_number(data.getJSONObject("position_state").getInt("gps_number"))
|
||||
.position_state_is_rtk_number(data.getJSONObject("position_state").getInt("rtk_number"))
|
||||
.battery_capacity_percent(data.getJSONObject("battery").getInt("capacity_percent"))
|
||||
.battery_remain_flight_time(data.getJSONObject("battery").getInt("remain_flight_time"))
|
||||
.battery_return_home_power(data.getJSONObject("battery").getInt("return_home_power"))
|
||||
.battery_landing_power(data.getJSONObject("battery").getInt("landing_power"))
|
||||
.battery_capacity_percent_a(capacity_percent_a)
|
||||
.battery_capacity_percent_b(capacity_percent_b)
|
||||
.wind_direction(data.getInt("wind_direction"))
|
||||
.wind_speed(data.getDouble("wind_speed"))
|
||||
.home_distance(data.getDouble("home_distance"))
|
||||
//state上报
|
||||
// .home_latitude(data.getDouble("home_latitude"))
|
||||
// .home_longitude(data.getDouble("home_longitude"))
|
||||
.attitude_head(data.getInt("attitude_head"))
|
||||
.attitude_roll(data.getDouble("attitude_roll"))
|
||||
.attitude_pitch(data.getDouble("attitude_pitch"))
|
||||
.elevation(data.getDouble("elevation"))
|
||||
.height(data.getDouble("height"))
|
||||
.latitude(data.getDouble("latitude"))
|
||||
.longitude(data.getDouble("longitude"))
|
||||
.vertical_speed(data.getDouble("vertical_speed"))
|
||||
.horizontal_speed(data.getDouble("horizontal_speed"))
|
||||
.build();
|
||||
.attitude_head(data.getInt("attitude_head"))
|
||||
.attitude_roll(data.getDouble("attitude_roll"))
|
||||
.attitude_pitch(data.getDouble("attitude_pitch"))
|
||||
.elevation(data.getDouble("elevation"))
|
||||
.height(data.getDouble("height"))
|
||||
.latitude(data.getDouble("latitude"))
|
||||
.longitude(data.getDouble("longitude"))
|
||||
.vertical_speed(data.getDouble("vertical_speed"))
|
||||
.horizontal_speed(data.getDouble("horizontal_speed"))
|
||||
.build();
|
||||
influxService.addRecord(uavReport);
|
||||
|
||||
//禁飞区校验
|
||||
Double height = data.getDouble("height");
|
||||
NoflyZoneDTO.point point = new NoflyZoneDTO.point(uavReport.getLongitude(), uavReport.getLatitude(), height);
|
||||
judgingNoFlyZone(dockSn, point, mode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("osd --> payload解析失败,解析后为null");
|
||||
}
|
||||
}
|
||||
|
||||
//禁飞区限制
|
||||
// 允许触发返航的飞行模式集合
|
||||
//"3":"手动飞行","4":"自动起飞","5":"航线飞行","16":"虚拟摇杆状态","17":"指令飞行"
|
||||
private static final Set<Integer> ALLOWED_MODE_CODES = Set.of(3, 4, 5, 16, 17);
|
||||
// 按设备隔离的计数器(线程安全)
|
||||
private final ConcurrentHashMap<String, AtomicInteger> counterMap = new ConcurrentHashMap<>();
|
||||
// 单线程执行器(若需提高并发,可调整线程数或使用同步)
|
||||
private final ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L,
|
||||
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
|
||||
|
||||
private void judgingNoFlyZone(String dockSn, NoflyZoneDTO.point point, Integer modeCode) {
|
||||
// 1. 模式过滤(快速失败)
|
||||
if (!ALLOWED_MODE_CODES.contains(modeCode)) {
|
||||
return;
|
||||
}
|
||||
// 2. 按设备自增计数器,判断是否需要校验(每3次上报校验一次)
|
||||
AtomicInteger deviceCounter = counterMap.computeIfAbsent(dockSn, k -> new AtomicInteger(0));
|
||||
int count = deviceCounter.incrementAndGet();
|
||||
if (count % 3 != 1) {
|
||||
return;
|
||||
}
|
||||
// 3. 获取禁飞区列表
|
||||
Object obj = CacheUtils.get(BusinessConstant.DOCK_NOFLY_ZONE + dockSn);
|
||||
if (!(obj instanceof List)) {
|
||||
return;
|
||||
}
|
||||
List<NoflyZoneDTO> noFlyZoneList = (List<NoflyZoneDTO>) obj;
|
||||
// 4. 遍历禁飞区,检查是否已触发返航
|
||||
String triggerKey = BusinessConstant.DOCK_NOFLY_ZONE_TRIGGER_SIGN + dockSn;
|
||||
for (NoflyZoneDTO dto : noFlyZoneList) {
|
||||
// 先判断是否已在触发中(快速路径)
|
||||
if (CacheUtils.get(triggerKey) != null) {
|
||||
return;
|
||||
}
|
||||
// 执行点是否在禁飞区内
|
||||
if (noflyZoneService.isInNoFlyZone(dto.getExtent(), point)) {
|
||||
// 原子设置触发标记,防止并发重复触发
|
||||
if (trySetTrigger(triggerKey)) {
|
||||
// 异步执行返航任务
|
||||
CompletableFuture.runAsync(() -> executeReturnHome(dockSn, dto), executor);
|
||||
}
|
||||
// 只要命中一个禁飞区就停止遍历
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子设置触发标记(使用缓存框架的 putIfAbsent 或本地锁)
|
||||
* 返回 true 表示当前线程成功设置,false 表示已被其他线程设置
|
||||
*/
|
||||
private boolean trySetTrigger(String key) {
|
||||
synchronized (this) {
|
||||
if (CacheUtils.get(key) == null) {
|
||||
CacheUtils.set(key, true, 1000 * 60);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行返航并推送状态(抽离重复逻辑)
|
||||
*/
|
||||
private void executeReturnHome(String dockSn, NoflyZoneDTO dto) {
|
||||
String zoneName = dto.getName();
|
||||
// 1. 发送进入禁飞区通知(type=0)
|
||||
logAndPush(dockSn, zoneName, 0, "飞机进入禁飞区,强制返航");
|
||||
|
||||
try {
|
||||
// 2. 执行返航指令
|
||||
djiBaseService.executeAndReturnResult(dockSn, "return_home");
|
||||
// 3. 发送成功通知(type=1)
|
||||
logAndPush(dockSn, zoneName, 1, "飞机进入禁飞区,强制返航成功");
|
||||
} catch (Exception e) {
|
||||
// 记录异常堆栈,便于排查
|
||||
log.error("返航失败,dockSn={}, zone={}", dockSn, zoneName, e);
|
||||
// 4. 发送失败通知(type=2)
|
||||
logAndPush(dockSn, zoneName, 2, "飞机进入禁飞区,强制返航失败,请手动返航");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一记录飞行日志并推送 MQTT 消息
|
||||
*/
|
||||
private void logAndPush(String dockSn, String zoneName, int type, String msg) {
|
||||
// 记录 InfluxDB 日志
|
||||
FlightLog flightLog = new FlightLog();
|
||||
flightLog.setDockSn(dockSn);
|
||||
flightLog.setLevel(2);
|
||||
flightLog.setMessage(msg);
|
||||
influxService.addRecord(flightLog);
|
||||
|
||||
// 推送 MQTT 事件
|
||||
JSONObject data = new JSONObject();
|
||||
data.set("name", zoneName);
|
||||
data.set("type", type);
|
||||
data.set("msg", msg);
|
||||
JSONObject payload = djiBaseService.getPayload(BusinessConstant.NOFLY_ZONE_METHOD, data);
|
||||
mqttPushService.pushMessageByClient1(BusinessConstant.WEB_EVENT_TOPIC.formatted(dockSn), payload.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.multictrl.modules.business.service;
|
||||
|
||||
import com.multictrl.common.page.PageData;
|
||||
import com.multictrl.common.service.CrudService;
|
||||
import com.multictrl.modules.business.dto.NoflyZoneDTO;
|
||||
import com.multictrl.modules.business.entity.NoflyZoneEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 禁飞区
|
||||
*
|
||||
* @author Sdy
|
||||
* @since 1.0.0 2026-06-09
|
||||
*/
|
||||
public interface NoflyZoneService extends CrudService<NoflyZoneEntity, NoflyZoneDTO> {
|
||||
|
||||
//分页列表
|
||||
PageData<NoflyZoneDTO> pageList(Map<String, Object> params);
|
||||
|
||||
//新增禁飞区
|
||||
void saveNoFlying(NoflyZoneDTO noflyZone);
|
||||
|
||||
//修改禁飞区
|
||||
void updateNoFlying(NoflyZoneDTO noflyZone);
|
||||
|
||||
//获取机库禁飞区
|
||||
List<NoflyZoneDTO> getNoFlyZoneByDockSn(String dockSn);
|
||||
|
||||
//判断是否在禁飞区内
|
||||
Boolean isInNoFlyZone(List<List<NoflyZoneDTO.point>> noFlyZone, NoflyZoneDTO.point point);
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
package com.multictrl.modules.business.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.multictrl.common.constant.BusinessConstant;
|
||||
import com.multictrl.common.exception.RenException;
|
||||
import com.multictrl.common.page.PageData;
|
||||
import com.multictrl.common.service.impl.CrudServiceImpl;
|
||||
import com.multictrl.common.utils.CacheUtils;
|
||||
import com.multictrl.modules.business.dao.NoflyZoneDao;
|
||||
import com.multictrl.modules.business.dto.NoflyZoneDTO;
|
||||
import com.multictrl.modules.business.entity.DockEntity;
|
||||
import com.multictrl.modules.business.entity.NoflyZoneEntity;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.multictrl.modules.business.service.DockService;
|
||||
import com.multictrl.modules.business.service.NoflyZoneService;
|
||||
import com.multictrl.modules.sys.entity.SysDeptEntity;
|
||||
import com.multictrl.modules.sys.service.SysDeptService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 禁飞区
|
||||
*
|
||||
* @author Sdy
|
||||
* @since 1.0.0 2026-06-09
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class NoflyZoneServiceImpl extends CrudServiceImpl<NoflyZoneDao, NoflyZoneEntity, NoflyZoneDTO> implements NoflyZoneService {
|
||||
private final DockService dockService;
|
||||
private final SysDeptService deptService;
|
||||
|
||||
@Override
|
||||
public QueryWrapper<NoflyZoneEntity> getWrapper(Map<String, Object> params) {
|
||||
String name = (String) params.get("f.name");
|
||||
|
||||
QueryWrapper<NoflyZoneEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.like(StrUtil.isNotBlank(name), "f.name", name);
|
||||
wrapper.orderByDesc("f.create_date");
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<NoflyZoneDTO> pageList(Map<String, Object> params) {
|
||||
IPage<NoflyZoneDTO> page = baseDao.pageList(getPage(params, null, false), getWrapper(params));
|
||||
for (NoflyZoneDTO record : page.getRecords()) {
|
||||
String extentWkt = record.getExtentWkt();
|
||||
List<List<NoflyZoneDTO.point>> lists = parseMultipolygonZ(extentWkt);
|
||||
record.setExtent(lists);
|
||||
}
|
||||
return getPageData(page, currentDtoClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveNoFlying(NoflyZoneDTO noflyZone) {
|
||||
NoflyZoneEntity entity = new NoflyZoneEntity();
|
||||
entity.setName(noflyZone.getName());
|
||||
entity.setStatus(noflyZone.getStatus());
|
||||
StringBuilder extent = new StringBuilder("'MULTIPOLYGON Z (");
|
||||
List<List<NoflyZoneDTO.point>> lists = noflyZone.getExtent();
|
||||
for (List<NoflyZoneDTO.point> list : lists) {
|
||||
if (list.size() < 3) {
|
||||
throw new RenException("无效的面");
|
||||
}
|
||||
NoflyZoneDTO.point first = list.get(0);
|
||||
StringBuilder sb = new StringBuilder("((");
|
||||
for (NoflyZoneDTO.point point : list) {
|
||||
sb.append(point.getLng()).append(" ").append(point.getLat()).append(" ").append(point.getHeight()).append(",");
|
||||
}
|
||||
sb.append(first.getLng()).append(" ").append(first.getLat()).append(" ").append(first.getHeight()).append("))");
|
||||
extent.append(sb).append(",");
|
||||
}
|
||||
extent.deleteCharAt(extent.length() - 1);
|
||||
extent.append(")'");
|
||||
entity.setExtent(extent.toString());
|
||||
baseDao.saveNoFlyZone(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNoFlying(NoflyZoneDTO noflyZone) {
|
||||
update(null, new UpdateWrapper<NoflyZoneEntity>()
|
||||
.eq("id", noflyZone.getId())
|
||||
.set(StringUtils.isNotBlank(noflyZone.getStatus()), "status", noflyZone.getStatus())
|
||||
.set(StringUtils.isNotBlank(noflyZone.getName()), "name", noflyZone.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NoflyZoneDTO> getNoFlyZoneByDockSn(String dockSn) {
|
||||
DockEntity dockEntity = dockService.getDao().selectOne(new QueryWrapper<DockEntity>().eq("dock_sn", dockSn));
|
||||
if (dockEntity == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
SysDeptEntity deptEntity = deptService.selectById(dockEntity.getDeptId());
|
||||
List<Long> deptIds = new ArrayList<>();
|
||||
deptIds.add(dockEntity.getDeptId());
|
||||
if (StringUtils.isNotBlank(deptEntity.getPids())) {
|
||||
Arrays.stream(deptEntity.getPids().split(","))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(Long::parseLong)
|
||||
.forEach(deptIds::add);
|
||||
}
|
||||
|
||||
List<NoflyZoneDTO> list = baseDao.getNoFlyZoneList(new QueryWrapper<NoflyZoneEntity>()
|
||||
.eq("f.status", "1").and(v ->
|
||||
v.in("f.dept_id", deptIds).or().isNull("f.dept_id")));
|
||||
|
||||
list.forEach(dto -> dto.setExtent(parseMultipolygonZ(dto.getExtentWkt())));
|
||||
|
||||
CacheUtils.set(BusinessConstant.DOCK_NOFLY_ZONE + dockSn, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isInNoFlyZone(List<List<NoflyZoneDTO.point>> noFlyZone, NoflyZoneDTO.point point) {
|
||||
if (noFlyZone.size() != 2 || noFlyZone.get(0).size() < 3 || noFlyZone.get(1).size() < 3) {
|
||||
throw new IllegalArgumentException("立方体必须由两个多边形面构成,每个面至少3个点");
|
||||
}
|
||||
|
||||
List<NoflyZoneDTO.point> bottomFace = noFlyZone.get(0);
|
||||
List<NoflyZoneDTO.point> topFace = noFlyZone.get(1);
|
||||
|
||||
// 检查高度是否在立方体范围内
|
||||
double minHeight = Math.min(bottomFace.get(0).getHeight(), topFace.get(0).getHeight());
|
||||
double maxHeight = Math.max(bottomFace.get(0).getHeight(), topFace.get(0).getHeight());
|
||||
|
||||
if (point.getHeight() < minHeight || point.getHeight() > maxHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查点是否在底面多边形内
|
||||
boolean inBottomFace = isPointInPolygon(bottomFace, point);
|
||||
|
||||
// 检查点是否在顶面多边形内
|
||||
boolean inTopFace = isPointInPolygon(topFace, point);
|
||||
|
||||
// 如果点在底面或顶面多边形内,则它在立方体内
|
||||
return inBottomFace || inTopFace;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析MULTIPOLYGON Z字符串
|
||||
*
|
||||
* @param multipolygonZ MULTIPOLYGON Z格式的字符串
|
||||
* @return 解析后的范围数据
|
||||
*/
|
||||
private List<List<NoflyZoneDTO.point>> parseMultipolygonZ(String multipolygonZ) {
|
||||
List<List<NoflyZoneDTO.point>> result = new ArrayList<>();
|
||||
|
||||
// 使用正则表达式匹配多边形数据
|
||||
Pattern pattern = Pattern.compile("\\(\\(([^)]+)\\)\\)");
|
||||
Matcher matcher = pattern.matcher(multipolygonZ);
|
||||
|
||||
while (matcher.find()) {
|
||||
String polygonData = matcher.group(1);
|
||||
List<NoflyZoneDTO.point> polygon = parsePolygon(polygonData);
|
||||
result.add(polygon);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个多边形数据
|
||||
*
|
||||
* @param polygonData 多边形数据字符串
|
||||
* @return 多边形点列表
|
||||
*/
|
||||
private List<NoflyZoneDTO.point> parsePolygon(String polygonData) {
|
||||
List<NoflyZoneDTO.point> points = new ArrayList<>();
|
||||
String[] coordinates = polygonData.split(",");
|
||||
|
||||
for (String coord : coordinates) {
|
||||
NoflyZoneDTO.point point = parseCoordinate(coord.trim());
|
||||
if (point != null) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
// 去除最后一个点(如果它与第一个点相同,WKT格式多边形首尾闭合)
|
||||
if (points.size() > 1) {
|
||||
NoflyZoneDTO.point first = points.get(0);
|
||||
NoflyZoneDTO.point last = points.get(points.size() - 1);
|
||||
if (first.getLng().equals(last.getLng())
|
||||
&& first.getLat().equals(last.getLat())
|
||||
&& first.getHeight().equals(last.getHeight())) {
|
||||
points.remove(points.size() - 1);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个坐标点
|
||||
*
|
||||
* @param coordinate 坐标字符串
|
||||
* @return 点对象,解析失败返回null
|
||||
*/
|
||||
private NoflyZoneDTO.point parseCoordinate(String coordinate) {
|
||||
// 使用正则表达式匹配三个浮点数
|
||||
Pattern pattern = Pattern.compile("([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)");
|
||||
Matcher matcher = pattern.matcher(coordinate);
|
||||
if (matcher.find()) {
|
||||
return new NoflyZoneDTO.point(
|
||||
Double.parseDouble(matcher.group(1)),
|
||||
Double.parseDouble(matcher.group(2)),
|
||||
Double.parseDouble(matcher.group(3))
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断点是否在多边形内(射线法 Ray Casting)
|
||||
* <p>
|
||||
* 将3D多边形投影到2D平面(经纬度),忽略高度,
|
||||
* 使用射线法判断点是否落在多边形内部。
|
||||
* </p>
|
||||
*
|
||||
* @param polygon 多边形顶点列表
|
||||
* @param point 要检查的点
|
||||
* @return 如果点在多边形内返回true,否则返回false
|
||||
*/
|
||||
private boolean isPointInPolygon(List<NoflyZoneDTO.point> polygon, NoflyZoneDTO.point point) {
|
||||
int n = polygon.size();
|
||||
if (n < 3) return false;
|
||||
|
||||
double px = point.getLng();
|
||||
double py = point.getLat();
|
||||
boolean inside = false;
|
||||
|
||||
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||
double xi = polygon.get(i).getLng(), yi = polygon.get(i).getLat();
|
||||
double xj = polygon.get(j).getLng(), yj = polygon.get(j).getLat();
|
||||
|
||||
if (((yi > py) != (yj > py)) &&
|
||||
(px < (xj - xi) * (py - yi) / (yj - yi) + xi)) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?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.multictrl.modules.business.dao.NoflyZoneDao">
|
||||
<resultMap type="com.multictrl.modules.business.entity.NoflyZoneEntity" id="busNoflyZoneMap">
|
||||
<result property="id" column="id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="extent" column="extent"/>
|
||||
<result property="deptId" column="dept_id"/>
|
||||
<result property="creator" column="creator"/>
|
||||
<result property="createDate" column="create_date"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="pageList" resultType="com.multictrl.modules.business.dto.NoflyZoneDTO">
|
||||
SELECT
|
||||
f.id,
|
||||
f."name",
|
||||
ST_AsText(f.extent) AS extent_wkt,
|
||||
f.create_date,
|
||||
u.real_name user_name,
|
||||
d.name dept_name,
|
||||
f.status
|
||||
FROM bus_nofly_zone f
|
||||
left join sys_user u on f.creator = u.id
|
||||
left join sys_dept d on f.dept_id = d.id
|
||||
${ew.customSqlSegment}
|
||||
</select>
|
||||
|
||||
<insert id="saveNoFlyZone">
|
||||
INSERT INTO bus_nofly_zone (id, name, extent, dept_id, creator, create_date, status)
|
||||
VALUES (#{id}, #{name}, ST_GeomFromText(${extent}, 4326), #{deptId}, #{creator}, #{createDate}, #{status})
|
||||
</insert>
|
||||
|
||||
<select id="getNoFlyZoneList" resultType="com.multictrl.modules.business.dto.NoflyZoneDTO">
|
||||
SELECT f.id,
|
||||
f."name",
|
||||
ST_AsText(f.extent) AS extent_wkt,
|
||||
f.create_date,
|
||||
u.real_name user_name,
|
||||
d.name dept_name,
|
||||
f.status
|
||||
FROM bus_nofly_zone f
|
||||
left join sys_user u on f.creator = u.id
|
||||
left join sys_dept d on f.dept_id = d.id
|
||||
${ew.customSqlSegment}
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
@ -2264,3 +2264,34 @@ VALUES (2063906540177379330, 2063906301022359553, '妙算', 'MIAO_SUAN', '', 1,
|
|||
'2026-06-08 16:50:39.364', 1067246875800000001, '2026-06-08 16:52:08.349',
|
||||
'source-material/miao_suan.png');
|
||||
|
||||
CREATE
|
||||
EXTENSION IF NOT EXISTS postgis;
|
||||
|
||||
DROP TABLE IF EXISTS "public"."bus_nofly_zone";
|
||||
CREATE TABLE "public"."bus_nofly_zone"
|
||||
(
|
||||
"id" int8 NOT NULL,
|
||||
"name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||
"status" char(1) COLLATE "pg_catalog"."default",
|
||||
"extent" "public"."geography" NOT NULL,
|
||||
"dept_id" int8,
|
||||
"creator" int8,
|
||||
"create_date" timestamp(6)
|
||||
)
|
||||
;
|
||||
COMMENT
|
||||
ON COLUMN "public"."bus_nofly_zone"."extent" IS '范围';
|
||||
COMMENT
|
||||
ON COLUMN "public"."bus_nofly_zone"."dept_id" IS '部门标识';
|
||||
COMMENT
|
||||
ON COLUMN "public"."bus_nofly_zone"."name" IS '名称';
|
||||
COMMENT
|
||||
ON COLUMN "public"."bus_nofly_zone"."status" IS '是否开启 1开启 0未开启';
|
||||
COMMENT
|
||||
ON TABLE "public"."bus_nofly_zone" IS '禁飞区';
|
||||
|
||||
-- ----------------------------
|
||||
-- Primary Key structure for table bus_nofly_zone
|
||||
-- ----------------------------
|
||||
ALTER TABLE "public"."bus_nofly_zone"
|
||||
ADD CONSTRAINT "uav_no_flying_pkey" PRIMARY KEY ("id");
|
||||
Loading…
Reference in New Issue