This commit is contained in:
wxs 2026-01-30 14:57:10 +08:00
commit 418b81fa54
3 changed files with 145 additions and 103 deletions

View File

@ -13,9 +13,8 @@ import com.zhangy.skyeye.jm.dto.JmJobQueryDTO;
import com.zhangy.skyeye.jm.dto.JmJobUpdDTO;
import com.zhangy.skyeye.jm.entity.JmJob;
import com.zhangy.skyeye.jm.service.JmJobService;
import com.zhangy.skyeye.jm.service.JmJobStatusService;
import com.zhangy.skyeye.publics.consts.ExecStatusEnum;
import com.zhangy.skyeye.sar.service.ISarControlService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@ -28,17 +27,12 @@ import java.util.Collections;
@Validated
@RestController
@RequestMapping("/sar/job")
@Slf4j
public class JmJobController {
@Autowired
private JmJobService jobService;
@Autowired
private ISarControlService controlInfoService;
@Autowired
private JmJobStatusService sarJobStatusService;
/**
* 分页查询
*/
@ -93,7 +87,6 @@ public class JmJobController {
}
/**
* 修改
*/
@PostMapping("/update")
public Object update(@Valid @RequestBody JmJobUpdDTO param) {
@ -119,6 +112,7 @@ public class JmJobController {
/**
* 开始执行任务
*
* @return
*/
@GetMapping("/start")
@ -126,7 +120,7 @@ public class JmJobController {
// 查询执行任务的无人机
JmJobDTO job = jobService.selectDetail(id);
if (job == null) {
throw ServiceException.noLog("找不到任务id=" + job.getId());
throw ServiceException.noLog("找不到任务id=" + id);
} else if (EnumUtil.parseEx(ExecStatusEnum.class, job.getStatus()) != ExecStatusEnum.NOT) {
throw ServiceException.noLog("任务状态不是未执行,无法起飞");
}
@ -136,6 +130,7 @@ public class JmJobController {
/**
* 保存前清理ID重新生成
*
* @param e
*/
private void clearId(JmJobDTO e) {

View File

@ -14,7 +14,10 @@ import com.zhangy.skyeye.jm.entity.JmJobPayload;
import com.zhangy.skyeye.jm.entity.JmJobPoint;
import com.zhangy.skyeye.jm.entity.JmJobUav;
import com.zhangy.skyeye.jm.service.JmAirlinePlanService;
import com.zhangy.skyeye.py.dto.*;
import com.zhangy.skyeye.py.dto.PyAirlineParamDTO;
import com.zhangy.skyeye.py.dto.PyAirlinePayloadDTO;
import com.zhangy.skyeye.py.dto.PyAirlineTargetDTO;
import com.zhangy.skyeye.py.dto.PyAirlineUavDTO;
import com.zhangy.skyeye.py.service.IPyAirlineService;
import com.zhangy.skyeye.sar.consts.SarImageModeEnum;
import com.zhangy.skyeye.sar.dto.SarFlightPlanDTO;
@ -23,7 +26,10 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@ -46,7 +52,6 @@ public class JmAirlinePlanServiceImpl implements JmAirlinePlanService {
JSON.toJSONString(uavList), JSON.toJSONString(pointList));
checkParam(jobMode, imageMode, targetType);
// 非航线模式需要调算法生成航线需要从缓存取sar坐标
Map<Long, List<JmAirline>> airlineGroup = null;
// 1聚束
if (imageMode == SarImageModeEnum.JS) {
return planJs(uavList.get(0), pointList);
@ -111,8 +116,8 @@ public class JmAirlinePlanServiceImpl implements JmAirlinePlanService {
/**
* 聚束模式
*
* @param uav 无人机
* @param home 起飞点
* @param uav 无人机
* @param home 起飞点
* @return 航线
*/
public JmAirline planJs(JmJobUav uav, JmJobPoint home, JmJobPoint target) {
@ -176,7 +181,6 @@ public class JmAirlinePlanServiceImpl implements JmAirlinePlanService {
.map(p -> new double[]{p.getLongitude(), p.getLatitude()})
.toArray(double[][]::new);
log.info("提取到 {} 个目标点坐标", coords.length);
List<PyAirlineTargetDTO> pyTargets = new ArrayList<>();
List<PyAirlineUavDTO> pyUavList = new ArrayList<>();
for (int i = 0; i < uavList.size(); i++) {
@ -221,6 +225,7 @@ public class JmAirlinePlanServiceImpl implements JmAirlinePlanService {
/**
* 从无人机载荷列表中查找 SAR 类型载荷并设置 IP
*
* @param uav 无人机对象
* @return SAR 载荷
* @throws ServiceException 如果没有找到 SAR 载荷

View File

@ -1,6 +1,7 @@
package com.zhangy.skyeye.py.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson2.JSON;
import com.zhangy.skyeye.common.extend.exception.ServiceException;
import com.zhangy.skyeye.common.extend.util.HttpUtil;
import com.zhangy.skyeye.common.extend.util.JsonUtil;
@ -13,16 +14,16 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson2.*;
import java.io.IOException;
import java.net.ConnectException;
import java.net.http.HttpResponse;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 航线规划
@ -76,91 +77,132 @@ public class PyAirlineServiceImpl implements IPyAirlineService {
@Override
public Map<Long, List<JmAirline>> getAirline(PyAirlineParamDTO param) {
log.info("请求航线规划算法服务参数:{}", JSON.toJSONString(param));
// HttpResponse<String> post;
// try {
// post = HttpUtil.post(javaUrl, null, param);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// PyAirlineResponse pyAirlineResponse;
// pyAirlineResponse = JsonUtil.parse(post.body(), PyAirlineResponse.class);
// log.info("请求航线规划算法服务响应:{}",JSON.toJSONString(pyAirlineResponse));
HttpResponse<String> httpResponse;
try {
httpResponse = HttpUtil.post(url, null, param);
} catch (ConnectException ex) {
throw ServiceException.noLog("航线规划服务未启动!");
} catch (IOException ex) {
throw new ServiceException("Post请求错误url=[" + url + "] " + ex.getMessage(), ex);
}
PyAirlineResponse response;
response = JsonUtil.parse(httpResponse.body(), PyAirlineResponse.class);
log.info("请求航线规划算法服务响应:{}", JSON.toJSONString(response));
int responseCode = httpResponse.statusCode();
if (responseCode >= 400) {
log.warn("调用航线规划算法错误[{}]{},参数:{}", responseCode, response.getMessage(), JsonUtil.toString(param));
throw ServiceException.noLog("调用航线规划算法错误[" + responseCode + "]" + response.getMessage());
log.info("请求航线规划算法 | param: {}", JSON.toJSONString(param));
// 1. 调用Python算法服务
HttpResponse<String> httpResponse = callPythonAirlineService(param);
// 2. 解析响应
PyAirlineResponse response = parseResponse(httpResponse);
// 3. 检查HTTP状态码
int statusCode = httpResponse.statusCode();
if (statusCode >= 400) {
String errMsg = String.format("算法服务返回错误[%d]: %s", statusCode, response.getMessage());
log.warn("调用航线规划算法失败 | httpStatus={}, msg={}, param={}",
statusCode, response.getMessage(), JSON.toJSONString(param));
throw ServiceException.noLog(errMsg);
}
log.info("算法服务响应 | {}", JSON.toJSONString(response));
// 4. 获取核心数据
Map<Long, PyAirlineDTO[]> uavMap = response.getData();
log.info("航线规划算法结果:{}", JSON.toJSONString(uavMap));
if (checkEmpty(uavMap)) {
if ("success".endsWith(response.getMessage())) {
throw ServiceException.noLog("无法生成航线可能飞行区域超过25平方公里");
}
log.warn("调用航线规划算法未返回数据[{}]{},参数:{}", responseCode, response.getMessage(), JsonUtil.toString(param));
throw ServiceException.noLog("调用航线规划算法未返回数据[" + responseCode + "]" + response.getMessage());
// 5. 空结果的特殊处理
if (isEmpty(uavMap)) {
handleEmptyResult(statusCode, response.getMessage(), param);
}
log.info("算法返回航线数据 | uavCount={}, rawData={}",
uavMap.size(), JSON.toJSONString(uavMap));
// 6. 转换为业务对象
return convertToJmAirlineMap(uavMap);
}
Map<Long, List<JmAirline>> map = new HashMap<>();
/**
* 调用Python航线规划服务
*/
private HttpResponse<String> callPythonAirlineService(PyAirlineParamDTO param) {
try {
return HttpUtil.post(url, null, param);
} catch (ConnectException e) {
throw ServiceException.noLog("航线规划服务未启动");
} catch (IOException e) {
String err = String.format("Post请求失败 url=[%s] %s", url, e.getMessage());
throw new ServiceException(err, e);
}
}
/**
* 解析HTTP响应体为对象
*/
private PyAirlineResponse parseResponse(HttpResponse<String> httpResponse) {
try {
return JsonUtil.parse(httpResponse.body(), PyAirlineResponse.class);
} catch (Exception e) {
log.error("算法响应JSON解析失败 | body={}", httpResponse.body(), e);
throw new ServiceException("算法返回数据格式错误", e);
}
}
/**
* 处理算法返回空数据的情况兼容历史特殊逻辑
*/
private void handleEmptyResult(int statusCode, String message, PyAirlineParamDTO param) {
String paramJson = JSON.toJSONString(param);
if ("success".equals(message)) {
log.warn("算法返回空结果但message=success可能区域过大 | param={}", paramJson);
throw ServiceException.noLog("无法生成航线可能飞行区域超过25平方公里");
}
String errMsg = String.format("算法未返回有效数据[%d]: %s", statusCode, message);
log.warn("算法返回空数据 | httpStatus={}, msg={}, param={}", statusCode, message, paramJson);
throw ServiceException.noLog(errMsg);
}
/**
* 将算法返回的DTO数组转换为业务对象Map
* 注意算法可能出现多架无人机只对应一条航线的情况其他无人机返回空数组
*/
private Map<Long, List<JmAirline>> convertToJmAirlineMap(Map<Long, PyAirlineDTO[]> uavMap) {
Map<Long, List<JmAirline>> result = new HashMap<>();
for (Map.Entry<Long, PyAirlineDTO[]> entry : uavMap.entrySet()) {
long uavId = entry.getKey();
PyAirlineDTO[] arr = entry.getValue();
// 当多架无人机对应一条航线则只有一架无人机有航线数据其余为空数组
if (arr == null || arr.length == 0) {
Long uavId = entry.getKey();
PyAirlineDTO[] airlines = entry.getValue();
// 跳过空数组的无人机常见于多机共享一条航线的情况
if (airlines == null || airlines.length == 0) {
continue;
}
map.put(uavId, Stream.of(arr)
.map(airlineDTO -> {
// 算法返回高度为相对高度需要加起飞点高度为海拔高度
JmAirline airline = BeanUtil.copyProperties(airlineDTO, JmAirline.class);
double[] flightStart = airlineDTO.getFlightStart();
airline.setFlightStartLon(flightStart[0]);
airline.setFlightStartLat(flightStart[1]);
airline.setFlightStartHeight(flightStart[2]); // 相对起飞点高度
double[] flightEnd = airlineDTO.getFlightEnd();
airline.setFlightEndLon(flightEnd[0]);
airline.setFlightEndLat(flightEnd[1]);
airline.setFlightEndHeight(flightEnd[2]); // 相对起飞点高度
double[] groundStart = airlineDTO.getGroundStart();
airline.setGroundStartLon(groundStart[0]);
airline.setGroundStartLat(groundStart[1]);
airline.setGroundStartHeight(groundStart[2]); // 相对起飞点高度
double[] targetCentroid = airlineDTO.getTargetCentroid();
airline.setTargetCentroidLon(targetCentroid[0]);
airline.setTargetCentroidLat(targetCentroid[1]);
airline.setTargetCentroidHeight(targetCentroid[2]); // 相对起飞点高度
airline.setTargetWidth(airlineDTO.getTargetWidth());
airline.setTargetLength(airlineDTO.getTargetLength());
airline.setTargetHeading(airlineDTO.getTargetHeading());
double[] start = airlineDTO.getStart();
airline.setStartLon(start[0]);
airline.setStartLat(start[1]);
airline.setStartHeight(start[2]); // 相对起飞点高度
double[] end = airlineDTO.getEnd();
airline.setEndLon(end[0]);
airline.setEndLat(end[1]);
airline.setEndHeight(end[2]); // 相对起飞点高度
return airline;
})
.collect(Collectors.toList()));
List<JmAirline> jmAirlines = Arrays.stream(airlines)
.map(this::convertPyAirlineToJmAirline)
.collect(Collectors.toList());
result.put(uavId, jmAirlines);
}
return map;
return result;
}
/**
* 单条航线DTO 业务对象转换
* 核心点算法返回的是相对高度需要结合起飞点高度理解但本方法不做高度修正仅字段映射
*/
private JmAirline convertPyAirlineToJmAirline(PyAirlineDTO dto) {
JmAirline airline = BeanUtil.copyProperties(dto, JmAirline.class);
// 起飞点起点
setCoordinate(airline::setFlightStartLon, airline::setFlightStartLat, airline::setFlightStartHeight,
dto.getFlightStart());
// 结束点
setCoordinate(airline::setFlightEndLon, airline::setFlightEndLat, airline::setFlightEndHeight,
dto.getFlightEnd());
// 离地起点起飞段
setCoordinate(airline::setGroundStartLon, airline::setGroundStartLat, airline::setGroundStartHeight,
dto.getGroundStart());
// 目标区域中心
setCoordinate(airline::setTargetCentroidLon, airline::setTargetCentroidLat, airline::setTargetCentroidHeight,
dto.getTargetCentroid());
// 目标区域尺寸 & 朝向
airline.setTargetWidth(dto.getTargetWidth());
airline.setTargetLength(dto.getTargetLength());
airline.setTargetHeading(dto.getTargetHeading());
// 航线规划起点/终点通常是路径规划后的首末点
setCoordinate(airline::setStartLon, airline::setStartLat, airline::setStartHeight, dto.getStart());
setCoordinate(airline::setEndLon, airline::setEndLat, airline::setEndHeight, dto.getEnd());
return airline;
}
private void setCoordinate(Consumer<Double> lonSetter, Consumer<Double> latSetter, Consumer<Double> heightSetter,
double[] coord) {
if (coord != null && coord.length == 3) {
lonSetter.accept(coord[0]);
latSetter.accept(coord[1]);
heightSetter.accept(coord[2]); // 算法返回的是相对高度
}
}
private boolean isEmpty(Map<Long, PyAirlineDTO[]> map) {
return map == null || map.isEmpty() ||
map.values().stream().allMatch(arr -> arr == null || arr.length == 0);
}
}