diff --git a/admin/src/main/java/com/multictrl/common/constant/BusinessConstant.java b/admin/src/main/java/com/multictrl/common/constant/BusinessConstant.java index aac039f..b7cf553 100644 --- a/admin/src/main/java/com/multictrl/common/constant/BusinessConstant.java +++ b/admin/src/main/java/com/multictrl/common/constant/BusinessConstant.java @@ -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://"; diff --git a/admin/src/main/java/com/multictrl/modules/business/controller/NoflyZoneController.java b/admin/src/main/java/com/multictrl/modules/business/controller/NoflyZoneController.java new file mode 100644 index 0000000..e00f922 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/controller/NoflyZoneController.java @@ -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> page(@Parameter(hidden = true) @RequestParam Map params) { + PageData page = noflyZoneService.pageList(params); + + return new Result>().ok(page); + } + + @PostMapping + @Operation(summary = "保存") + @LogOperation("保存") + @RequiresPermissions("bus:noflyzone:save") + public Result 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 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 delete(@RequestBody Long[] ids) { + //效验数据 + AssertUtils.isArrayEmpty(ids, "id"); + noflyZoneService.delete(ids); + + return new Result<>(); + } + + @GetMapping("/getNoFlying") + @Operation(summary = "获取机库禁飞区", description = "返回机库所属组织以及上级组织设置的禁飞区") + public Result> getNoFlying(@RequestParam String dockSn) { + List list = noflyZoneService.getNoFlyZoneByDockSn(dockSn); + + return new Result>().ok(list); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/dao/NoflyZoneDao.java b/admin/src/main/java/com/multictrl/modules/business/dao/NoflyZoneDao.java new file mode 100644 index 0000000..fac31ec --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/dao/NoflyZoneDao.java @@ -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 { + + //分页列表 + IPage pageList(IPage page, @Param("ew") QueryWrapper ew); + + //新增禁飞区 + void saveNoFlyZone(NoflyZoneEntity noflyZoneEntity); + + //查询禁飞区 + List getNoFlyZoneList(@Param("ew") QueryWrapper ew); +} \ No newline at end of file diff --git a/admin/src/main/java/com/multictrl/modules/business/dto/NoflyZoneDTO.java b/admin/src/main/java/com/multictrl/modules/business/dto/NoflyZoneDTO.java new file mode 100644 index 0000000..d0dfabe --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/dto/NoflyZoneDTO.java @@ -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> 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; + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/entity/NoflyZoneEntity.java b/admin/src/main/java/com/multictrl/modules/business/entity/NoflyZoneEntity.java new file mode 100644 index 0000000..032a41c --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/entity/NoflyZoneEntity.java @@ -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; +} \ No newline at end of file diff --git a/admin/src/main/java/com/multictrl/modules/business/handler/OsdHandler.java b/admin/src/main/java/com/multictrl/modules/business/handler/OsdHandler.java index 9961b45..7771ea7 100644 --- a/admin/src/main/java/com/multictrl/modules/business/handler/OsdHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/handler/OsdHandler.java @@ -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 list = Optional.ofNullable(CacheUtils.get(BusinessConstant.DRC_HEART_BEAT_SN)) - .filter(obj -> obj instanceof List) - .map(obj -> (List) obj) - .orElse(Collections.emptyList()); + .filter(obj -> obj instanceof List) + .map(obj -> (List) 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 ALLOWED_MODE_CODES = Set.of(3, 4, 5, 16, 17); + // 按设备隔离的计数器(线程安全) + private final ConcurrentHashMap 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 noFlyZoneList = (List) 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()); + } } diff --git a/admin/src/main/java/com/multictrl/modules/business/service/NoflyZoneService.java b/admin/src/main/java/com/multictrl/modules/business/service/NoflyZoneService.java new file mode 100644 index 0000000..afbee94 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/service/NoflyZoneService.java @@ -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 { + + //分页列表 + PageData pageList(Map params); + + //新增禁飞区 + void saveNoFlying(NoflyZoneDTO noflyZone); + + //修改禁飞区 + void updateNoFlying(NoflyZoneDTO noflyZone); + + //获取机库禁飞区 + List getNoFlyZoneByDockSn(String dockSn); + + //判断是否在禁飞区内 + Boolean isInNoFlyZone(List> noFlyZone, NoflyZoneDTO.point point); +} \ No newline at end of file diff --git a/admin/src/main/java/com/multictrl/modules/business/service/impl/NoflyZoneServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/service/impl/NoflyZoneServiceImpl.java new file mode 100644 index 0000000..ed2f71d --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/service/impl/NoflyZoneServiceImpl.java @@ -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 implements NoflyZoneService { + private final DockService dockService; + private final SysDeptService deptService; + + @Override + public QueryWrapper getWrapper(Map params) { + String name = (String) params.get("f.name"); + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.like(StrUtil.isNotBlank(name), "f.name", name); + wrapper.orderByDesc("f.create_date"); + + return wrapper; + } + + @Override + public PageData pageList(Map params) { + IPage page = baseDao.pageList(getPage(params, null, false), getWrapper(params)); + for (NoflyZoneDTO record : page.getRecords()) { + String extentWkt = record.getExtentWkt(); + List> 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> lists = noflyZone.getExtent(); + for (List 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() + .eq("id", noflyZone.getId()) + .set(StringUtils.isNotBlank(noflyZone.getStatus()), "status", noflyZone.getStatus()) + .set(StringUtils.isNotBlank(noflyZone.getName()), "name", noflyZone.getName())); + } + + @Override + public List getNoFlyZoneByDockSn(String dockSn) { + DockEntity dockEntity = dockService.getDao().selectOne(new QueryWrapper().eq("dock_sn", dockSn)); + if (dockEntity == null) { + return List.of(); + } + + SysDeptEntity deptEntity = deptService.selectById(dockEntity.getDeptId()); + List 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 list = baseDao.getNoFlyZoneList(new QueryWrapper() + .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> noFlyZone, NoflyZoneDTO.point point) { + if (noFlyZone.size() != 2 || noFlyZone.get(0).size() < 3 || noFlyZone.get(1).size() < 3) { + throw new IllegalArgumentException("立方体必须由两个多边形面构成,每个面至少3个点"); + } + + List bottomFace = noFlyZone.get(0); + List 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> parseMultipolygonZ(String multipolygonZ) { + List> result = new ArrayList<>(); + + // 使用正则表达式匹配多边形数据 + Pattern pattern = Pattern.compile("\\(\\(([^)]+)\\)\\)"); + Matcher matcher = pattern.matcher(multipolygonZ); + + while (matcher.find()) { + String polygonData = matcher.group(1); + List polygon = parsePolygon(polygonData); + result.add(polygon); + } + + return result; + } + + /** + * 解析单个多边形数据 + * + * @param polygonData 多边形数据字符串 + * @return 多边形点列表 + */ + private List parsePolygon(String polygonData) { + List 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) + *

+ * 将3D多边形投影到2D平面(经纬度),忽略高度, + * 使用射线法判断点是否落在多边形内部。 + *

+ * + * @param polygon 多边形顶点列表 + * @param point 要检查的点 + * @return 如果点在多边形内返回true,否则返回false + */ + private boolean isPointInPolygon(List 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; + } +} \ No newline at end of file diff --git a/admin/src/main/resources/mapper/business/NoflyZoneDao.xml b/admin/src/main/resources/mapper/business/NoflyZoneDao.xml new file mode 100644 index 0000000..7b1acac --- /dev/null +++ b/admin/src/main/resources/mapper/business/NoflyZoneDao.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + 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}) + + + + \ No newline at end of file diff --git a/prj-deploy/file/pgsql/init.sql b/prj-deploy/file/pgsql/init.sql index 681360b..026ba85 100644 --- a/prj-deploy/file/pgsql/init.sql +++ b/prj-deploy/file/pgsql/init.sql @@ -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"); \ No newline at end of file