已经成功集成百度鹰眼服务
This commit is contained in:
@@ -377,7 +377,47 @@
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="stayPoints.length > 0" class="staypoint-section">
|
||||
<div v-if="latestPoint" class="latest-point-panel">
|
||||
<h4>最新定位</h4>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="定位时间">
|
||||
{{ formatTimestamp(latestPoint.locTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="纬度">
|
||||
{{ latestPoint.lat }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="经度">
|
||||
{{ latestPoint.lng }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="速度(米/秒)">
|
||||
{{ latestPoint.speed ?? '--' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="航向角">
|
||||
{{ latestPoint.direction ?? '--' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="segmentStats && segmentStats.length > 0" class="segment-stats">
|
||||
<h4>轨迹查询分段({{ segmentStats.length }})</h4>
|
||||
<el-table :data="segmentStats" border size="small" max-height="220">
|
||||
<el-table-column label="段次" width="80" prop="index" />
|
||||
<el-table-column label="开始时间" min-width="160">
|
||||
<template #default="scope">
|
||||
{{ formatTimestamp(scope.row.startTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" min-width="160">
|
||||
<template #default="scope">
|
||||
{{ formatTimestamp(scope.row.endTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="轨迹点数" width="120" prop="trackCount" />
|
||||
<el-table-column label="停留点数" width="120" prop="stayCount" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-if="stayPoints && stayPoints.length > 0" class="staypoint-section">
|
||||
<h4>停留点分析(15分钟)</h4>
|
||||
<el-table :data="stayPoints" border style="width: 100%;" size="small">
|
||||
<el-table-column label="开始时间" min-width="160">
|
||||
@@ -468,6 +508,8 @@ const playTimer = ref(null); // 播放定时器
|
||||
const currentPlayIndex = ref(0); // 当前播放到的轨迹点索引
|
||||
const trackBMapGL = ref(null); // 保存 BMapGL 实例,避免重复加载
|
||||
const stayPoints = ref([]); // 停留点列表
|
||||
const latestPoint = ref(null); // 最新轨迹点
|
||||
const segmentStats = ref([]);
|
||||
const yingyanMeta = reactive({
|
||||
entityName: '',
|
||||
startTime: null,
|
||||
@@ -491,6 +533,11 @@ const isLocationWarning = computed(() => {
|
||||
return isLocWarning;
|
||||
});
|
||||
|
||||
const isStaticTrack = computed(() => {
|
||||
const status = Number(warningData.status || 0);
|
||||
return status >= 3;
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return `${warningData.warningTypeDesc || '预警'}详情`;
|
||||
});
|
||||
@@ -816,6 +863,25 @@ const formatDuration = (seconds) => {
|
||||
return parts.join('') || `${sec}秒`;
|
||||
};
|
||||
|
||||
function parseLatestPoint(point) {
|
||||
if (!point) {
|
||||
return null;
|
||||
}
|
||||
const lng = parseFloat(point.longitude ?? point.lng ?? 0);
|
||||
const lat = parseFloat(point.latitude ?? point.lat ?? 0);
|
||||
if (Number.isNaN(lng) || Number.isNaN(lat) || (lng === 0 && lat === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
lng,
|
||||
lat,
|
||||
locTime: point.locTime ?? point.loc_time ?? null,
|
||||
speed: point.speed ?? null,
|
||||
direction: point.direction ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
// 清理地图实例
|
||||
@@ -850,11 +916,17 @@ const handleTrackClick = async () => {
|
||||
ElMessage.warning('运单ID不存在,无法查看轨迹');
|
||||
return;
|
||||
}
|
||||
console.info('[TRACK] 开始轨迹定位流程', {
|
||||
deliveryId: warningData.deliveryId,
|
||||
status: warningData.status,
|
||||
estimatedDepartureTime: warningData.estimatedDepartureTime,
|
||||
});
|
||||
|
||||
trackDialogVisible.value = true;
|
||||
trackLoading.value = true;
|
||||
trackMapShow.value = false;
|
||||
trackPath.value = [];
|
||||
latestPoint.value = null;
|
||||
isPlaying.value = false;
|
||||
currentPlayIndex.value = 0;
|
||||
|
||||
@@ -913,6 +985,8 @@ const getDeliveryStatus = async () => {
|
||||
const loadYingyanTrack = async () => {
|
||||
stayPoints.value = [];
|
||||
trackPath.value = [];
|
||||
latestPoint.value = null;
|
||||
segmentStats.value = [];
|
||||
trackMapShow.value = false;
|
||||
yingyanMeta.entityName = '';
|
||||
yingyanMeta.startTime = null;
|
||||
@@ -925,7 +999,10 @@ const loadYingyanTrack = async () => {
|
||||
|
||||
try {
|
||||
const res = await getYingyanTrack({ deliveryId: warningData.deliveryId });
|
||||
console.info('[TRACK] 后端轨迹接口响应', res);
|
||||
if (res.code === 200 && res.data) {
|
||||
segmentStats.value = Array.isArray(res.data.segmentStats) ? res.data.segmentStats : [];
|
||||
console.info('[TRACK] 分段统计', segmentStats.value);
|
||||
const rawPoints = Array.isArray(res.data.trackPoints) ? res.data.trackPoints : [];
|
||||
trackPath.value = rawPoints
|
||||
.map(item => {
|
||||
@@ -946,9 +1023,18 @@ const loadYingyanTrack = async () => {
|
||||
yingyanMeta.entityName = res.data.entityName || '';
|
||||
yingyanMeta.startTime = res.data.startTime || null;
|
||||
yingyanMeta.endTime = res.data.endTime || null;
|
||||
latestPoint.value = parseLatestPoint(res.data.latestPoint);
|
||||
console.info('[TRACK] 轨迹点数量', trackPath.value.length);
|
||||
|
||||
if (trackPath.value.length > 0) {
|
||||
trackMapShow.value = true;
|
||||
} else if (latestPoint.value) {
|
||||
trackPath.value.push({
|
||||
lng: latestPoint.value.lng,
|
||||
lat: latestPoint.value.lat,
|
||||
locTime: latestPoint.value.locTime || null,
|
||||
});
|
||||
trackMapShow.value = true;
|
||||
} else {
|
||||
ElMessage.warning('暂无有效轨迹点');
|
||||
}
|
||||
@@ -1340,6 +1426,28 @@ defineExpose({
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.latest-point-panel {
|
||||
margin-top: 20px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.segment-stats {
|
||||
margin-top: 20px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.staypoint-section {
|
||||
margin-top: 20px;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
@@ -53,41 +52,46 @@ public class BaiduYingyanService {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ 去除空格,确保格式一致
|
||||
String cleanEntityName = entityName.trim();
|
||||
|
||||
// ✅ 验证 entityName 不是 "entity" 字符串
|
||||
if ("entity".equals(entityName) || "entity_name".equals(entityName)) {
|
||||
logger.error("ensureEntity 失败:entityName 参数错误,值为 '{}',这可能是参数传递错误", entityName);
|
||||
if ("entity".equals(cleanEntityName) || "entity_name".equals(cleanEntityName)) {
|
||||
logger.error("ensureEntity 失败:entityName 参数错误,值为 '{}',这可能是参数传递错误", cleanEntityName);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
MultiValueMap<String, String> form = baseForm();
|
||||
form.add("entity_name", entityName);
|
||||
form.add("entity_name", cleanEntityName);
|
||||
// entity_desc 为非必填项,且命名规则限制:只支持中文、英文字母、下划线、连字符、数字
|
||||
// 为避免参数错误,不传递 entity_desc 参数
|
||||
|
||||
logger.debug("确保终端存在 - entity={}", entityName);
|
||||
logger.debug("确保终端存在 - entity={}, service_id={}", cleanEntityName, BaiduYingyanConstants.SERVICE_ID);
|
||||
JsonNode result = postForm("/entity/add", form);
|
||||
int status = result.path("status").asInt(-1);
|
||||
String message = result.path("message").asText();
|
||||
|
||||
// ✅ 修复:status=0(创建成功)、3005(终端已存在)、3006(其他成功状态)都视为成功
|
||||
if (status == 0) {
|
||||
logger.info("✅ 鹰眼终端创建成功, entityName={}", entityName);
|
||||
logger.info("✅ 鹰眼终端创建成功, entityName={}, service_id={}", cleanEntityName, BaiduYingyanConstants.SERVICE_ID);
|
||||
return true;
|
||||
} else if (status == 3005) {
|
||||
// ✅ 终端已存在,这是正常情况,视为成功
|
||||
logger.info("✅ 鹰眼终端已存在, entityName={}, message={}", entityName, message);
|
||||
logger.info("✅ 鹰眼终端已存在, entityName={}, service_id={}, message={}",
|
||||
cleanEntityName, BaiduYingyanConstants.SERVICE_ID, message);
|
||||
return true;
|
||||
} else if (status == 3006) {
|
||||
logger.info("✅ 鹰眼终端操作成功, entityName={}, status={}", entityName, status);
|
||||
logger.info("✅ 鹰眼终端操作成功, entityName={}, service_id={}, status={}",
|
||||
cleanEntityName, BaiduYingyanConstants.SERVICE_ID, status);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 其他状态码视为失败
|
||||
logger.warn("❌ 鹰眼创建终端失败, entityName={}, status={}, message={}",
|
||||
entityName, status, message);
|
||||
logger.warn("❌ 鹰眼创建终端失败, entityName={}, service_id={}, status={}, message={}",
|
||||
cleanEntityName, BaiduYingyanConstants.SERVICE_ID, status, message);
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ 鹰眼创建终端异常, entityName={}", entityName, e);
|
||||
logger.error("❌ 鹰眼创建终端异常, entityName={}", cleanEntityName, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -155,6 +159,105 @@ public class BaiduYingyanService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量推送轨迹点(使用 /track/addpoints 接口)
|
||||
* @param entityName 终端名称
|
||||
* @param points 轨迹点列表
|
||||
* @return 成功上传的轨迹点数量
|
||||
*/
|
||||
public int pushTrackPoints(String entityName, List<YingyanTrackPoint> points) {
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
logger.warn("鹰眼批量上传轨迹失败:终端名称为空");
|
||||
return 0;
|
||||
}
|
||||
if (points == null || points.isEmpty()) {
|
||||
logger.warn("鹰眼批量上传轨迹失败:轨迹点列表为空, entity={}", entityName);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
// ✅ 确保终端存在
|
||||
if (!ensureEntity(entityName)) {
|
||||
logger.warn("鹰眼批量上传轨迹失败:终端不存在或创建失败, entity={}", entityName);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ✅ 构建 point_list JSON 数组字符串
|
||||
List<Map<String, Object>> pointList = new ArrayList<>();
|
||||
for (YingyanTrackPoint point : points) {
|
||||
// 验证经纬度有效性
|
||||
if (point.getLatitude() == 0 && point.getLongitude() == 0) {
|
||||
logger.warn("鹰眼批量上传:跳过无效轨迹点 (0,0), entity={}", entityName);
|
||||
continue;
|
||||
}
|
||||
if (point.getLatitude() < -90 || point.getLatitude() > 90 ||
|
||||
point.getLongitude() < -180 || point.getLongitude() > 180) {
|
||||
logger.warn("鹰眼批量上传:跳过超出范围的轨迹点, entity={}, lat={}, lon={}",
|
||||
entityName, point.getLatitude(), point.getLongitude());
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, Object> pointMap = new HashMap<>();
|
||||
pointMap.put("entity_name", entityName);
|
||||
pointMap.put("coord_type_input", "wgs84"); // 根据数据源调整坐标类型
|
||||
pointMap.put("loc_time", point.getLocTime());
|
||||
pointMap.put("longitude", point.getLongitude());
|
||||
pointMap.put("latitude", point.getLatitude());
|
||||
if (point.getSpeed() != null && point.getSpeed() > 0) {
|
||||
pointMap.put("speed", point.getSpeed());
|
||||
}
|
||||
if (point.getDirection() != null) {
|
||||
pointMap.put("direction", point.getDirection());
|
||||
}
|
||||
pointList.add(pointMap);
|
||||
}
|
||||
|
||||
if (pointList.isEmpty()) {
|
||||
logger.warn("鹰眼批量上传:过滤后无有效轨迹点, entity={}, 原始数量={}", entityName, points.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ✅ 将 pointList 转换为 JSON 字符串
|
||||
String pointListJson = objectMapper.writeValueAsString(pointList);
|
||||
|
||||
// ✅ 构建 POST 请求参数
|
||||
MultiValueMap<String, String> form = baseForm();
|
||||
form.add("point_list", pointListJson);
|
||||
|
||||
logger.info("鹰眼批量上传轨迹点 - entity={}, 轨迹点数量={}", entityName, pointList.size());
|
||||
|
||||
JsonNode result = postForm("/track/addpoints", form);
|
||||
int status = result.path("status").asInt(-1);
|
||||
String message = result.path("message").asText();
|
||||
|
||||
if (status == 0) {
|
||||
// ✅ 批量上传成功,返回成功数量
|
||||
int successCount = pointList.size();
|
||||
logger.info("鹰眼批量上传轨迹成功 - entity={}, 成功数量={}", entityName, successCount);
|
||||
return successCount;
|
||||
}
|
||||
|
||||
// ✅ 详细记录失败原因
|
||||
logger.warn("鹰眼批量上传轨迹失败 - entity={}, status={}, message={}, 轨迹点数量={}",
|
||||
entityName, status, message, pointList.size());
|
||||
|
||||
// 如果是常见错误,记录更详细的信息
|
||||
if (status == 3001) {
|
||||
logger.error("鹰眼批量上传轨迹失败:参数错误,请检查轨迹点格式");
|
||||
} else if (status == 3002) {
|
||||
logger.error("鹰眼批量上传轨迹失败:服务不存在或未启用");
|
||||
} else if (status == 3003) {
|
||||
logger.error("鹰眼批量上传轨迹失败:终端不存在");
|
||||
} else if (status == 3004) {
|
||||
logger.error("鹰眼批量上传轨迹失败:轨迹点时间格式错误");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("鹰眼批量上传轨迹异常 - entity={}, 轨迹点数量={}", entityName, points.size(), e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询轨迹(单次查询,限制24小时内)
|
||||
*/
|
||||
@@ -162,15 +265,31 @@ public class BaiduYingyanService {
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ✅ 查询前再次确保终端存在,避免查询时终端不存在
|
||||
if (!ensureEntity(entityName)) {
|
||||
logger.warn("查询轨迹前终端不存在或创建失败,无法查询 - entity={}", entityName);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
// ✅ 去除空格,确保格式一致
|
||||
String cleanEntityName = entityName.trim();
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("entity_name", entityName);
|
||||
params.put("start_time", startTime);
|
||||
params.put("end_time", endTime);
|
||||
params.put("is_processed", 1);
|
||||
params.put("need_denoise", 1);
|
||||
params.put("need_mapmatch", 0);
|
||||
params.put("entity_name", cleanEntityName);
|
||||
// ✅ 所有参数统一为 String 类型
|
||||
params.put("start_time", String.valueOf(startTime));
|
||||
params.put("end_time", String.valueOf(endTime));
|
||||
params.put("is_processed", "1");
|
||||
// ✅ 根据用户测试成功的 URL,添加 process_option 参数
|
||||
// 格式:need_denoise=1,need_vacuate=1,need_mapmatch=0
|
||||
params.put("process_option", "need_denoise=1,need_vacuate=1,need_mapmatch=0");
|
||||
// ✅ 可选参数,根据官方文档添加
|
||||
params.put("coord_type_output", "bd09ll");
|
||||
params.put("page_size", "5000");
|
||||
params.put("page_index", "1");
|
||||
params.put("sort_type", "asc");
|
||||
|
||||
logger.debug("查询轨迹 - entity={}, startTime={} ({}), endTime={} ({})",
|
||||
entityName, startTime, new Date(startTime * 1000), endTime, new Date(endTime * 1000));
|
||||
@@ -179,24 +298,42 @@ public class BaiduYingyanService {
|
||||
int status = result.path("status").asInt(-1);
|
||||
String message = result.path("message").asText();
|
||||
|
||||
logger.info("鹰眼查询轨迹响应 - entity={}, status={}, message={}, startTime={}, endTime={}",
|
||||
entityName, status, message, new Date(startTime * 1000), new Date(endTime * 1000));
|
||||
|
||||
if (!isSuccess(result)) {
|
||||
// ✅ status=3003 可能是"终端不存在"或"指定时间范围内无轨迹数据"
|
||||
// 如果终端确实存在(通过 ensureEntity 确认),则可能是时间范围内无数据
|
||||
if (status == 3003) {
|
||||
logger.debug("鹰眼查询轨迹:指定时间范围内可能无轨迹数据, entity={}, startTime={}, endTime={}, message={}",
|
||||
logger.info("鹰眼查询轨迹:指定时间范围内可能无轨迹数据, entity={}, startTime={}, endTime={}, message={}",
|
||||
entityName, new Date(startTime * 1000), new Date(endTime * 1000), message);
|
||||
} else {
|
||||
logger.warn("鹰眼查询轨迹失败, entity={}, status={}, message={}, startTime={}, endTime={}",
|
||||
entityName, status, message, new Date(startTime * 1000), new Date(endTime * 1000));
|
||||
logger.warn("鹰眼查询轨迹失败, entity={}, status={}, message={}, startTime={}, endTime={}, raw={}",
|
||||
entityName, status, message, new Date(startTime * 1000), new Date(endTime * 1000), result.toString());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
JsonNode pointsNode = result.path("track").path("points");
|
||||
if (pointsNode == null || !pointsNode.isArray() || pointsNode.size() == 0) {
|
||||
// 文档返回结构存在两种形式:直接 points 字段 或 track.points
|
||||
JsonNode pointsNode = result.path("points");
|
||||
if (pointsNode == null || pointsNode.isMissingNode()) {
|
||||
pointsNode = result.path("track").path("points");
|
||||
}
|
||||
if (pointsNode == null || pointsNode.isMissingNode()) {
|
||||
logger.warn("鹰眼响应缺少 points 字段,entity={}, raw={}", entityName, result.toString());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (!pointsNode.isArray()) {
|
||||
logger.warn("鹰眼响应 points 字段不是数组,entity={}, raw={}", entityName, result.toString());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (pointsNode.size() == 0) {
|
||||
logger.warn("鹰眼返回0个轨迹点,entity={}, startTime={}, endTime={}, raw={}",
|
||||
entityName, new Date(startTime * 1000), new Date(endTime * 1000), result.toString());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
logger.info("鹰眼返回轨迹点数量:entity={}, size={}", entityName, pointsNode.size());
|
||||
List<YingyanTrackPoint> points = new ArrayList<>(pointsNode.size());
|
||||
pointsNode.forEach(node -> {
|
||||
if (!node.hasNonNull("latitude") || !node.hasNonNull("longitude")) {
|
||||
@@ -221,6 +358,75 @@ public class BaiduYingyanService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最新轨迹点(实时位置)
|
||||
*/
|
||||
public YingyanTrackPoint queryLatestPoint(String entityName) {
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
logger.warn("查询最新轨迹点失败:终端名称为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// ✅ 查询前再次确保终端存在
|
||||
if (!ensureEntity(entityName)) {
|
||||
logger.warn("查询最新轨迹点前终端不存在或创建失败,entity={}", entityName);
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("entity_name", entityName);
|
||||
params.put("coord_type_output", "bd09ll");
|
||||
// ✅ 根据官方文档,process_option 为可选参数,暂时移除避免格式错误
|
||||
// params.put("process_option", "denoise_grade=1,radius_threshold=20,need_mapmatch=1,transport_mode=driving");
|
||||
|
||||
JsonNode result = get("/track/getlatestpoint", params);
|
||||
if (!isSuccess(result)) {
|
||||
int status = result.path("status").asInt(-1);
|
||||
String message = result.path("message").asText();
|
||||
logger.warn("鹰眼查询最新轨迹点失败, entity={}, status={}, message={}", entityName, status, message);
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonNode pointNode = result.path("latest_point");
|
||||
if (pointNode == null || pointNode.isMissingNode() || pointNode.isNull()) {
|
||||
logger.warn("鹰眼查询最新轨迹点成功但无数据, entity={}", entityName);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pointNode.hasNonNull("latitude") || !pointNode.hasNonNull("longitude")) {
|
||||
logger.warn("鹰眼最新轨迹点缺少经纬度, entity={}", entityName);
|
||||
return null;
|
||||
}
|
||||
|
||||
double latitude = pointNode.path("latitude").asDouble();
|
||||
double longitude = pointNode.path("longitude").asDouble();
|
||||
if (latitude == 0 && longitude == 0) {
|
||||
logger.warn("鹰眼最新轨迹点经纬度无效 (0,0), entity={}", entityName);
|
||||
return null;
|
||||
}
|
||||
|
||||
YingyanTrackPoint latestPoint = new YingyanTrackPoint();
|
||||
latestPoint.setLatitude(latitude);
|
||||
latestPoint.setLongitude(longitude);
|
||||
latestPoint.setLocTime(pointNode.path("loc_time").asLong(0));
|
||||
|
||||
if (pointNode.has("speed") && !pointNode.path("speed").isNull()) {
|
||||
latestPoint.setSpeed(pointNode.path("speed").asDouble());
|
||||
}
|
||||
if (pointNode.has("direction") && !pointNode.path("direction").isNull()) {
|
||||
latestPoint.setDirection(pointNode.path("direction").asDouble());
|
||||
}
|
||||
|
||||
logger.debug("查询最新轨迹点成功 - entity={}, lat={}, lon={}, locTime={}",
|
||||
entityName, latitude, longitude, latestPoint.getLocTime());
|
||||
return latestPoint;
|
||||
} catch (Exception e) {
|
||||
logger.error("鹰眼查询最新轨迹点异常, entity={}", entityName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分段查询轨迹(支持超过24小时的查询)
|
||||
* 按照24小时为间隔分段查询,然后拼接结果
|
||||
@@ -359,9 +565,10 @@ public class BaiduYingyanService {
|
||||
try {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("entity_name", entityName);
|
||||
params.put("start_time", startTime);
|
||||
params.put("end_time", endTime);
|
||||
params.put("stay_time", stayTimeSeconds);
|
||||
// ✅ 所有参数统一为 String 类型
|
||||
params.put("start_time", String.valueOf(startTime));
|
||||
params.put("end_time", String.valueOf(endTime));
|
||||
params.put("stay_time", String.valueOf(stayTimeSeconds));
|
||||
params.put("coord_type_output", "bd09ll");
|
||||
|
||||
logger.debug("查询停留点 - entity={}, startTime={} ({}), endTime={} ({}), stayTimeSeconds={}",
|
||||
@@ -541,25 +748,36 @@ public class BaiduYingyanService {
|
||||
}
|
||||
|
||||
private JsonNode get(String path, Map<String, Object> params) throws Exception {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(buildUrl(path))
|
||||
.queryParam("ak", BaiduYingyanConstants.AK)
|
||||
.queryParam("service_id", BaiduYingyanConstants.SERVICE_ID);
|
||||
// ✅ 手动构建 URL,所有参数都不进行 URL 编码(与用户测试成功的 URL 格式一致)
|
||||
StringBuilder urlBuilder = new StringBuilder(buildUrl(path));
|
||||
urlBuilder.append("?ak=").append(BaiduYingyanConstants.AK);
|
||||
urlBuilder.append("&service_id=").append(BaiduYingyanConstants.SERVICE_ID);
|
||||
|
||||
if (params != null) {
|
||||
params.forEach((key, value) -> {
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (value != null) {
|
||||
// ✅ 验证参数名和值,避免参数传递错误
|
||||
if ("entity_name".equals(key) && ("entity".equals(value) || "entity_name".equals(value))) {
|
||||
logger.error("参数传递错误:entity_name 的值不能是 '{}',这可能是参数名和值混淆了", value);
|
||||
throw new IllegalArgumentException("entity_name 参数值错误: " + value);
|
||||
}
|
||||
builder.queryParam(key, value);
|
||||
|
||||
// ✅ 所有参数都不进行 URL 编码,直接拼接(与用户测试成功的 URL 格式一致)
|
||||
urlBuilder.append("&").append(key).append("=").append(value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 记录请求URL(调试用,生产环境可关闭)
|
||||
String requestUrl = builder.toUriString();
|
||||
logger.debug("百度鹰眼API请求 - path={}, url={}", path, requestUrl.replaceAll("ak=[^&]+", "ak=***"));
|
||||
String requestUrl = urlBuilder.toString();
|
||||
String maskedUrl = requestUrl.replaceAll("ak=[^&]+", "ak=***").replaceAll("service_id=[^&]+", "service_id=***");
|
||||
Object entityNameParam = params != null ? params.get("entity_name") : "N/A";
|
||||
logger.info("百度鹰眼API请求 - path={}, entity_name={}, service_id={}, url={}",
|
||||
path, entityNameParam, BaiduYingyanConstants.SERVICE_ID, maskedUrl);
|
||||
|
||||
// ✅ 记录实际请求 URL(用于调试,生产环境可关闭)
|
||||
logger.debug("百度鹰眼API实际请求URL - {}", requestUrl.replaceAll("ak=[^&]+", "ak=***"));
|
||||
|
||||
String response = restTemplate.getForObject(requestUrl, String.class);
|
||||
return parseBody(response);
|
||||
|
||||
@@ -155,43 +155,35 @@ public class DeliveryYingyanSyncService {
|
||||
return 0;
|
||||
}
|
||||
|
||||
logger.info("运单 {} 找到 {} 个有效轨迹点,开始上传到百度鹰眼",
|
||||
logger.info("运单 {} 找到 {} 个有效轨迹点,开始批量上传到百度鹰眼",
|
||||
delivery.getDeliveryNumber(), points.size());
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
for (YingyanTrackPoint point : points) {
|
||||
boolean success = baiduYingyanService.pushTrackPoint(
|
||||
entityName, point.getLatitude(), point.getLongitude(), point.getLocTime());
|
||||
if (success) {
|
||||
successCount++;
|
||||
Date locDate = new Date(point.getLocTime() * 1000L);
|
||||
updateLastSync(delivery, locDate);
|
||||
handleArrivalIfNeeded(delivery, point);
|
||||
|
||||
if (successCount <= 3 || successCount % 20 == 0) {
|
||||
logger.debug("运单 {} 上传轨迹点成功 [{}/{}] - 纬度: {}, 经度: {}, 时间: {}",
|
||||
delivery.getDeliveryNumber(), successCount, points.size(),
|
||||
point.getLatitude(), point.getLongitude(), locDate);
|
||||
}
|
||||
} else {
|
||||
failCount++;
|
||||
if (failCount <= 3) {
|
||||
logger.warn("运单 {} 上传轨迹点失败 [{}/{}] - 纬度: {}, 经度: {}, 时间: {}",
|
||||
delivery.getDeliveryNumber(), failCount, points.size(),
|
||||
point.getLatitude(), point.getLongitude(),
|
||||
new Date(point.getLocTime() * 1000L));
|
||||
}
|
||||
}
|
||||
|
||||
if (delivery.getStatus() != null && delivery.getStatus() == 3) {
|
||||
logger.info("运单 {} 状态已变为已结束,停止同步", delivery.getDeliveryNumber());
|
||||
break;
|
||||
}
|
||||
// ✅ 检查运单状态,如果已结束则停止同步
|
||||
if (delivery.getStatus() != null && delivery.getStatus() == 3) {
|
||||
logger.info("运单 {} 状态已变为已结束,停止同步", delivery.getDeliveryNumber());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ✅ 使用批量上传接口
|
||||
int successCount = baiduYingyanService.pushTrackPoints(entityName, points);
|
||||
int failCount = points.size() - successCount;
|
||||
|
||||
if (successCount > 0) {
|
||||
// ✅ 批量上传成功后,使用最后一个轨迹点的时间更新同步时间
|
||||
YingyanTrackPoint lastPoint = points.get(points.size() - 1);
|
||||
Date locDate = new Date(lastPoint.getLocTime() * 1000L);
|
||||
updateLastSync(delivery, locDate);
|
||||
|
||||
// ✅ 检查是否到达目的地(使用最后一个轨迹点)
|
||||
handleArrivalIfNeeded(delivery, lastPoint);
|
||||
|
||||
logger.info("运单 {} 批量上传轨迹点成功 - 成功: {}, 失败: {}, 总计: {}, 最后时间: {}",
|
||||
delivery.getDeliveryNumber(), successCount, failCount, points.size(), locDate);
|
||||
} else {
|
||||
logger.warn("运单 {} 批量上传轨迹点全部失败 - 总计: {}",
|
||||
delivery.getDeliveryNumber(), points.size());
|
||||
}
|
||||
|
||||
logger.info("运单 {} 轨迹上传完成 - 成功: {}, 失败: {}, 总计: {}",
|
||||
delivery.getDeliveryNumber(), successCount, failCount, points.size());
|
||||
return successCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -2071,10 +2071,14 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
new Date(startTime * 1000));
|
||||
|
||||
long endTime = determineTrackEndTime(delivery);
|
||||
if (endTime <= startTime) {
|
||||
endTime = startTime + 3600;
|
||||
}
|
||||
|
||||
// ✅ 使用分段查询方法,支持超过24小时的轨迹查询
|
||||
long durationHours = Math.max(1, (endTime - startTime) / 3600);
|
||||
logger.info("查询运单 {} 的百度鹰眼轨迹 - entity={}, startTime={}, endTime={}, 时间跨度={}小时",
|
||||
delivery.getDeliveryNumber(), entityName, startTime, endTime, (endTime - startTime) / 3600);
|
||||
delivery.getDeliveryNumber(), entityName, startTime, endTime, durationHours);
|
||||
|
||||
// ✅ 确保终端存在后再查询(重要:如果终端不存在,查询会失败)
|
||||
boolean entityExists = baiduYingyanService.ensureEntity(entityName);
|
||||
@@ -2093,14 +2097,58 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
|
||||
logger.debug("运单 {} 终端已确保存在,开始查询轨迹 - entity={}",
|
||||
logger.info("运单 {} 终端已确保存在,开始查询轨迹 - entity={}",
|
||||
delivery.getDeliveryNumber(), entityName);
|
||||
|
||||
List<YingyanTrackPoint> trackPoints = baiduYingyanService.queryTrackSegmented(entityName, startTime, endTime);
|
||||
List<YingyanStayPoint> stayPoints = baiduYingyanService.queryStayPointsSegmented(entityName, startTime, endTime, 900);
|
||||
List<long[]> segmentRanges = buildTrackSegments(startTime, endTime);
|
||||
logger.info("运单 {} 将分 {} 段调用百度鹰眼接口(每段≤24小时)", delivery.getDeliveryNumber(), segmentRanges.size());
|
||||
|
||||
logger.info("运单 {} 轨迹查询完成 - 轨迹点数: {}, 停留点数: {}",
|
||||
delivery.getDeliveryNumber(), trackPoints.size(), stayPoints.size());
|
||||
List<YingyanTrackPoint> mergedTrackPoints = new ArrayList<>();
|
||||
List<YingyanStayPoint> mergedStayPoints = new ArrayList<>();
|
||||
List<Map<String, Object>> segmentStats = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < segmentRanges.size(); i++) {
|
||||
long[] range = segmentRanges.get(i);
|
||||
long segStart = range[0];
|
||||
long segEnd = range[1];
|
||||
logger.info("运单 {} 调用轨迹接口-第{}段: {} -> {}",
|
||||
delivery.getDeliveryNumber(), i + 1, new Date(segStart * 1000), new Date(segEnd * 1000));
|
||||
|
||||
List<YingyanTrackPoint> segmentTracks = baiduYingyanService.queryTrack(entityName, segStart, segEnd);
|
||||
logger.info("运单 {} 第{}段轨迹点数量:{}", delivery.getDeliveryNumber(), i + 1, segmentTracks.size());
|
||||
mergedTrackPoints.addAll(segmentTracks);
|
||||
|
||||
List<YingyanStayPoint> segmentStayPoints = baiduYingyanService.queryStayPoints(entityName, segStart, segEnd, 900);
|
||||
logger.info("运单 {} 第{}段停留点数量:{}", delivery.getDeliveryNumber(), i + 1, segmentStayPoints.size());
|
||||
mergedStayPoints.addAll(segmentStayPoints);
|
||||
|
||||
Map<String, Object> segmentInfo = new HashMap<>();
|
||||
segmentInfo.put("index", i + 1);
|
||||
segmentInfo.put("startTime", segStart * 1000);
|
||||
segmentInfo.put("endTime", segEnd * 1000);
|
||||
segmentInfo.put("trackCount", segmentTracks.size());
|
||||
segmentInfo.put("stayCount", segmentStayPoints.size());
|
||||
segmentStats.add(segmentInfo);
|
||||
}
|
||||
|
||||
List<YingyanTrackPoint> trackPoints = dedupTrackPoints(mergedTrackPoints);
|
||||
List<YingyanStayPoint> stayPoints = dedupStayPoints(mergedStayPoints);
|
||||
|
||||
logger.info("运单 {} 轨迹查询完成 - 分段:{}段, 合并后轨迹点数:{}, 停留点数:{}",
|
||||
delivery.getDeliveryNumber(), segmentRanges.size(), trackPoints.size(), stayPoints.size());
|
||||
|
||||
YingyanTrackPoint latestPoint = null;
|
||||
if (delivery.getStatus() != null && delivery.getStatus() == 2) {
|
||||
latestPoint = baiduYingyanService.queryLatestPoint(entityName);
|
||||
if (latestPoint != null) {
|
||||
logger.info("运单 {} 最新轨迹点:lat={}, lon={}, locTime={}",
|
||||
delivery.getDeliveryNumber(), latestPoint.getLatitude(), latestPoint.getLongitude(), latestPoint.getLocTime());
|
||||
} else {
|
||||
logger.warn("运单 {} 查询最新轨迹点返回为空", delivery.getDeliveryNumber());
|
||||
}
|
||||
} else {
|
||||
logger.info("运单 {} 状态为 {},跳过实时定位查询", delivery.getDeliveryNumber(), delivery.getStatus());
|
||||
}
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("trackPoints", trackPoints);
|
||||
@@ -2109,6 +2157,10 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
data.put("startTime", startTime * 1000);
|
||||
data.put("endTime", endTime * 1000);
|
||||
data.put("status", delivery.getStatus());
|
||||
data.put("segmentStats", segmentStats);
|
||||
if (latestPoint != null) {
|
||||
data.put("latestPoint", latestPoint);
|
||||
}
|
||||
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
@@ -2160,5 +2212,53 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
}
|
||||
}
|
||||
|
||||
private static final long MAX_SEGMENT_SECONDS = 24 * 60 * 60;
|
||||
|
||||
private List<long[]> buildTrackSegments(long startTime, long endTime) {
|
||||
List<long[]> segments = new ArrayList<>();
|
||||
long cursor = startTime;
|
||||
while (cursor < endTime) {
|
||||
long next = Math.min(cursor + MAX_SEGMENT_SECONDS, endTime);
|
||||
segments.add(new long[]{cursor, next});
|
||||
cursor = next;
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private List<YingyanTrackPoint> dedupTrackPoints(List<YingyanTrackPoint> points) {
|
||||
if (CollectionUtils.isEmpty(points)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
points.sort(Comparator.comparingLong(YingyanTrackPoint::getLocTime)
|
||||
.thenComparingDouble(YingyanTrackPoint::getLatitude)
|
||||
.thenComparingDouble(YingyanTrackPoint::getLongitude));
|
||||
Set<String> seen = new HashSet<>();
|
||||
List<YingyanTrackPoint> unique = new ArrayList<>();
|
||||
for (YingyanTrackPoint point : points) {
|
||||
String key = point.getLocTime() + "_" + point.getLatitude() + "_" + point.getLongitude();
|
||||
if (seen.add(key)) {
|
||||
unique.add(point);
|
||||
}
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
private List<YingyanStayPoint> dedupStayPoints(List<YingyanStayPoint> points) {
|
||||
if (CollectionUtils.isEmpty(points)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
points.sort(Comparator.comparingLong(YingyanStayPoint::getStartTime)
|
||||
.thenComparingLong(YingyanStayPoint::getEndTime));
|
||||
Set<String> seen = new HashSet<>();
|
||||
List<YingyanStayPoint> unique = new ArrayList<>();
|
||||
for (YingyanStayPoint point : points) {
|
||||
String key = point.getStartTime() + "_" + point.getEndTime() + "_" + point.getLatitude() + "_" + point.getLongitude();
|
||||
if (seen.add(key)) {
|
||||
unique.add(point);
|
||||
}
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
artifactId=aiotagro-redis
|
||||
groupId=com.aiotagro
|
||||
version=1.0.1
|
||||
Reference in New Issue
Block a user