已经成功集成百度鹰眼服务

This commit is contained in:
xuqiuyun
2025-11-24 16:10:39 +08:00
parent 0f963bf535
commit f45a57fad6
6 changed files with 1017 additions and 551 deletions

View File

@@ -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;

View File

@@ -1,11 +1,7 @@
<template>
<!-- 参数缺失时的友好提示 -->
<div v-if="!route.query.id" class="error-container">
<el-result
icon="warning"
title="参数缺失"
sub-title="缺少必要的参数无法加载详情页面"
>
<el-result icon="warning" title="参数缺失" sub-title="缺少必要的参数无法加载详情页面">
<template #extra>
<el-button type="primary" @click="goBack">返回上一页</el-button>
<el-button @click="goToList">前往列表页面</el-button>
@@ -14,84 +10,89 @@
</div>
<!-- 正常内容 -->
<section v-else>
<div class="main-container">
<div class="info-box">
<div class="title">基础信息</div>
<el-descriptions :column="4">
<el-descriptions-item label="运单号:">{{ data.baseInfo.deliveryNumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="卖方:">{{ data.baseInfo.supplierName || '-' }}</el-descriptions-item>
<el-descriptions-item label="买方:">{{ data.baseInfo.buyerName || '-' }}</el-descriptions-item>
<el-descriptions-item label="车牌号:">{{ data.baseInfo.licensePlate || '-' }}</el-descriptions-item>
<el-descriptions-item label="司机姓名:">{{ data.baseInfo.driverName || '-' }}</el-descriptions-item>
<el-descriptions-item label="起始地:">{{ data.baseInfo.startLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="送达目的地:">{{ data.baseInfo.endLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="预计送达时间:">{{ data.baseInfo.estimatedDeliveryTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间:">{{ data.baseInfo.createTime || '' }}</el-descriptions-item>
<el-descriptions-item label="登记设备数量:">{{ totalRegisteredDevices }} </el-descriptions-item>
<el-descriptions-item label="状态:">
<el-tag :type="getStatusType(data.baseInfo.status)">{{ getStatusText(data.baseInfo.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="车身照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.carFrontPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.carFrontPhoto ? data.baseInfo.carFrontPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.carFrontPhoto] ? [data.baseInfo.carFrontPhoto] : []"
preview-teleported
/>
<el-image
v-if="data.baseInfo.carBehindPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.carBehindPhoto ? data.baseInfo.carBehindPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.carBehindPhoto] ? [data.baseInfo.carBehindPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
</el-descriptions>
<div v-else class="details-container">
<!-- 头部导航与操作 -->
<div class="page-header">
<div class="header-left">
<span class="page-title">运单详情</span>
<el-tag :type="getStatusType(data.baseInfo.status)" class="status-tag" size="large" effect="dark">
{{ getStatusText(data.baseInfo.status) }}
</el-tag>
</div>
<div class="info-box" v-if="data.length != 0">
<div class="title">预警信息</div>
<el-descriptions :column="4">
<template v-for="(item, index) in data.warnInfo" :key="index">
<div class="header-right">
<el-button @click="goBack">返回</el-button>
</div>
</div>
<!-- 基础信息卡片 -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="header-title">基础信息</span>
</div>
</template>
<el-descriptions :column="4" border size="large">
<el-descriptions-item label="运单号">{{ data.baseInfo.deliveryNumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="卖方">{{ data.baseInfo.supplierName || '-' }}</el-descriptions-item>
<el-descriptions-item label="买方">{{ data.baseInfo.buyerName || '-' }}</el-descriptions-item>
<el-descriptions-item label="车牌号">{{ data.baseInfo.licensePlate || '-' }}</el-descriptions-item>
<el-descriptions-item label="司机姓名">{{ data.baseInfo.driverName || '-' }}</el-descriptions-item>
<el-descriptions-item label="起始地">{{ data.baseInfo.startLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="送达目的地">{{ data.baseInfo.endLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="预计送达时间">{{ data.baseInfo.estimatedDeliveryTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ data.baseInfo.createTime || '' }}</el-descriptions-item>
<el-descriptions-item label="登记设备数量">{{ totalRegisteredDevices }} </el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 预警信息卡片 -->
<el-card v-if="data.warnInfo && data.warnInfo.length > 0" class="section-card warning-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="header-title warning-text">预警信息</span>
</div>
</template>
<div class="warning-list">
<el-descriptions :column="4" border v-for="(item, index) in data.warnInfo" :key="index" class="warning-item">
<template v-if="item.warningType == 2">
<el-descriptions-item label="预警原因:">
<el-descriptions-item label="预警原因">
<span class="red">{{ item.warningTypeDesc }}</span>
</el-descriptions-item>
<el-descriptions-item label="预警时间:">{{ item.warningTime || '--' }}</el-descriptions-item>
<el-descriptions-item label="智能耳标数:"> {{ totalRegisteredDevices }} </el-descriptions-item>
<el-descriptions-item label="车内盘点数量:"> {{ item.inventoryJbqCount || '--' }} </el-descriptions-item>
<el-descriptions-item label="预警时间">{{ item.warningTime || '--' }}</el-descriptions-item>
<el-descriptions-item label="智能耳标数"> {{ totalRegisteredDevices }} </el-descriptions-item>
<el-descriptions-item label="车内盘点数量"> {{ item.inventoryJbqCount || '--' }} </el-descriptions-item>
</template>
<template v-if="item.warningType == 3">
<el-descriptions-item label="预警原因:">
<el-descriptions-item label="预警原因">
<span class="red">{{ item.warningTypeDesc || '' }}</span>
</el-descriptions-item>
<el-descriptions-item label="预警时间:">{{ item.warningTime || '' }}</el-descriptions-item>
<el-descriptions-item label="应行驶距离:"> {{ item.expectedDistance || '' }} km </el-descriptions-item>
<el-descriptions-item label="实际行驶距离:"> {{ item.actualDistance || '' }} km </el-descriptions-item>
</template>
<el-descriptions-item label="预警时间">{{ item.warningTime || '' }}</el-descriptions-item>
<el-descriptions-item label="应行驶距离"> {{ item.expectedDistance || '' }} km </el-descriptions-item>
<el-descriptions-item label="实际行驶距离"> {{ item.actualDistance || '' }} km </el-descriptions-item>
</template>
</el-descriptions>
</div>
<div class="info-box">
<div class="title">装车信息</div>
</el-card>
<!-- 装车信息卡片 -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="header-title">装车信息</span>
</div>
</template>
<!-- 重量信息分组 -->
<div v-if="hasValue(data.baseInfo.emptyWeight) || hasValue(data.baseInfo.entruckWeight) || hasValue(data.baseInfo.landingEntruckWeight)" class="info-group">
<div class="group-title">重量信息</div>
<div class="sub-title">重量信息</div>
<el-descriptions :column="3" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyWeight)" label="空车过磅重量:">
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyWeight)" label="空车过磅重量">
{{ data.baseInfo.emptyWeight }}kg
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckWeight)" label="装车过磅重量:">
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckWeight)" label="装车过磅重量">
{{ data.baseInfo.entruckWeight }}kg
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.landingEntruckWeight)" label="落地过磅重量:">
<el-descriptions-item v-if="hasValue(data.baseInfo.landingEntruckWeight)" label="落地过磅重量">
{{ data.baseInfo.landingEntruckWeight }}kg
</el-descriptions-item>
</el-descriptions>
@@ -99,174 +100,161 @@
<!-- 照片信息分组 -->
<div v-if="hasValue(data.baseInfo.quarantineTickeyUrl) || hasValue(data.baseInfo.poundListImg) || hasValue(data.baseInfo.emptyVehicleFrontPhoto) || hasValue(data.baseInfo.loadedVehicleFrontPhoto) || hasValue(data.baseInfo.loadedVehicleWeightPhoto) || hasValue(data.baseInfo.driverIdCardPhoto) || hasValue(data.baseInfo.destinationPoundListImg) || hasValue(data.baseInfo.destinationVehicleFrontPhoto)" class="info-group">
<div class="group-title">照片信息</div>
<el-descriptions :column="3" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.quarantineTickeyUrl)" label="检疫票:">
<div class="photo-container">
<div class="sub-title">照片信息</div>
<div class="media-grid">
<div class="media-item" v-if="data.baseInfo.carFrontPhoto || data.baseInfo.carBehindPhoto">
<div class="media-label">车身照片</div>
<div class="media-content">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
v-if="data.baseInfo.carFrontPhoto"
class="photo-img"
:src="data.baseInfo.carFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.carFrontPhoto]"
preview-teleported
/>
<el-image
v-if="data.baseInfo.carBehindPhoto"
class="photo-img"
:src="data.baseInfo.carBehindPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.carBehindPhoto]"
preview-teleported
/>
</div>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.quarantineTickeyUrl)">
<div class="media-label">检疫票</div>
<el-image
class="photo-img"
:src="data.baseInfo.quarantineTickeyUrl"
fit="cover"
:preview-src-list="[data.baseInfo.quarantineTickeyUrl]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.poundListImg)" label="纸质磅单:">
<div class="photo-container">
<div class="media-item" v-if="hasValue(data.baseInfo.poundListImg)">
<div class="media-label">纸质磅单</div>
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
class="photo-img"
:src="data.baseInfo.poundListImg"
fit="cover"
:preview-src-list="[data.baseInfo.poundListImg]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyVehicleFrontPhoto)" label="车辆空磅上磅车头照片:">
<div class="photo-container">
<div class="media-item" v-if="hasValue(data.baseInfo.emptyVehicleFrontPhoto)">
<div class="media-label">空磅车头照片</div>
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
class="photo-img"
:src="data.baseInfo.emptyVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.emptyVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.loadedVehicleFrontPhoto)" label="车辆过重磅车头照片:">
<div class="photo-container">
<div class="media-item" v-if="hasValue(data.baseInfo.loadedVehicleFrontPhoto)">
<div class="media-label">过重磅车头照片</div>
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
class="photo-img"
:src="data.baseInfo.loadedVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.loadedVehicleWeightPhoto)" label="车辆重磅照片:">
<div class="photo-container">
<div class="media-item" v-if="hasValue(data.baseInfo.loadedVehicleWeightPhoto)">
<div class="media-label">车辆重磅照片</div>
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
class="photo-img"
:src="data.baseInfo.loadedVehicleWeightPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleWeightPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.driverIdCardPhoto)" label="驾驶员手持身份证站车头照片:">
<div class="photo-container">
<div class="media-item" v-if="hasValue(data.baseInfo.driverIdCardPhoto)">
<div class="media-label">驾驶员手持身份证</div>
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
class="photo-img"
:src="data.baseInfo.driverIdCardPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.driverIdCardPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationPoundListImg)" label="到地纸质磅单:">
<div class="photo-container">
<div class="media-item" v-if="hasValue(data.baseInfo.destinationPoundListImg)">
<div class="media-label">到地纸质磅单</div>
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
class="photo-img"
:src="data.baseInfo.destinationPoundListImg"
fit="cover"
:preview-src-list="[data.baseInfo.destinationPoundListImg]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationVehicleFrontPhoto)" label="到地车辆过重磅车头照片:">
<div class="photo-container">
<div class="media-item" v-if="hasValue(data.baseInfo.destinationVehicleFrontPhoto)">
<div class="media-label">到地过重磅车头</div>
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
class="photo-img"
:src="data.baseInfo.destinationVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.destinationVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<!-- 视频信息分组 -->
<div v-if="hasValue(data.baseInfo.emptyWeightVideo) || hasValue(data.baseInfo.entruckWeightVideo) || hasValue(data.baseInfo.entruckVideo) || hasValue(data.baseInfo.controlSlotVideo) || hasValue(data.baseInfo.cattleLoadingCircleVideo) || hasValue(data.baseInfo.unloadCattleVideo) || hasValue(data.baseInfo.destinationWeightVideo)" class="info-group">
<div class="group-title">视频信息</div>
<el-descriptions :column="1" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyWeightVideo)" label="空车过磅视频(含车牌、地磅数):">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.emptyWeightVideo"
/>
<div class="sub-title">视频信息</div>
<div class="media-grid">
<div class="media-item video-item" v-if="hasValue(data.baseInfo.emptyWeightVideo)">
<div class="media-label">空车过磅视频</div>
<video controls :src="data.baseInfo.emptyWeightVideo" />
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckWeightVideo)" label="装车过磅视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.entruckWeightVideo"
/>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.entruckWeightVideo)">
<div class="media-label">装车过磅视频</div>
<video controls :src="data.baseInfo.entruckWeightVideo" />
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckVideo)" label="装车视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.entruckVideo"
/>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.entruckVideo)">
<div class="media-label">装车视频</div>
<video controls :src="data.baseInfo.entruckVideo" />
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.controlSlotVideo)" label="控槽视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.controlSlotVideo"
/>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.controlSlotVideo)">
<div class="media-label">控槽视频</div>
<video controls :src="data.baseInfo.controlSlotVideo" />
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.cattleLoadingCircleVideo)" label="装完牛绕车一圈视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.cattleLoadingCircleVideo"
/>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.cattleLoadingCircleVideo)">
<div class="media-label">装完牛绕车一圈</div>
<video controls :src="data.baseInfo.cattleLoadingCircleVideo" />
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.unloadCattleVideo)" label="卸牛视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.unloadCattleVideo"
/>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.unloadCattleVideo)">
<div class="media-label">卸牛视频</div>
<video controls :src="data.baseInfo.unloadCattleVideo" />
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationWeightVideo)" label="到地过磅视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.destinationWeightVideo"
/>
</div>
</el-descriptions-item>
</el-descriptions>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.destinationWeightVideo)">
<div class="media-label">到地过磅视频</div>
<video controls :src="data.baseInfo.destinationWeightVideo" />
</div>
</div>
<div class="ear-box">
<div class="title">智能主机</div>
</div>
</el-card>
<!-- 设备信息卡片 -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="header-title">设备信息</span>
</div>
</template>
<el-tabs v-model="activeDeviceTab" type="border-card">
<el-tab-pane label="智能主机" name="host">
<el-table
:data="data.hostRows"
border
stripe
v-loading="data.hostDataListLoading"
element-loading-text="数据加载中..."
style="width: 100%"
@@ -284,7 +272,7 @@
<el-table-column label="数据最后更新时间" prop="time">
<template #default="scope"> {{ scope.row.time || scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" prop="">
<el-table-column label="操作" width="220" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="hostLogClick(scope.row)">日志</el-button>
<el-button link type="primary" @click="hostTrackClick(scope.row)">运动轨迹</el-button>
@@ -292,12 +280,12 @@
</template>
</el-table-column>
</el-table>
</div>
<div class="ear-box">
<div class="title">智能项圈</div>
</el-tab-pane>
<el-tab-pane label="智能项圈" name="collar">
<el-table
:data="data.collarRows"
border
stripe
v-loading="data.collarDataListLoading"
element-loading-text="数据加载中..."
style="width: 100%"
@@ -315,13 +303,14 @@
<el-table-column label="数据最后更新时间" prop="time">
<template #default="scope"> {{ scope.row.time || scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" prop="">
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="collarLogClick(scope.row)">日志</el-button>
<el-button link type="primary" @click="collarTrackClick(scope.row)">运动轨迹</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination
v-model:limit="collarForm.pageSize"
v-model:page="collarForm.pageNum"
@@ -329,9 +318,9 @@
@pagination="getCollarList"
/>
</div>
<div class="ear-box">
<div class="title">智能耳标</div>
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
</el-tab-pane>
<el-tab-pane label="智能耳标" name="ear">
<el-table :data="data.rows" border stripe v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能耳标编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="battery">
@@ -346,52 +335,22 @@
<el-table-column label="数据最后更新时间" prop="updateTime">
<template #default="scope"> {{ scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" prop="">
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="earLogClick(scope.row)">日志</el-button>
<el-button link type="primary" @click="earTrackClick(scope.row)">运动轨迹</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getEarList" />
</div>
<!-- <div class="info-box" v-if="data.status == 4 || data.status == 5">
<div class="title">核验信息</div>
<el-descriptions :column="4">
<el-descriptions-item label="到货核验人:">{{ data.baseInfo.checkByName || '' }}</el-descriptions-item>
<el-descriptions-item label="核验时间:">{{ data.baseInfo.checkTime || '' }}</el-descriptions-item>
<el-descriptions-item label="核验空车过磅重量:">{{
data.baseInfo.checkEmptyWeight ? data.baseInfo.checkEmptyWeight + 'kg' : ''
}}</el-descriptions-item>
<el-descriptions-item label="核验满车过磅重量:">{{
data.baseInfo.checkEntruckWeight ? data.baseInfo.checkEntruckWeight + 'kg' : ''
}}</el-descriptions-item>
<el-descriptions-item label="核验空车过磅视频:">
<span style="vertical-align: top" v-if="data.baseInfo.checkEmptyWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.checkEmptyWeightVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="核验装车过磅视频:">
<span style="vertical-align: top" v-if="data.baseInfo.checkEntruckWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.checkEntruckWeightVideo"
/>
</span>
</el-descriptions-item>
</el-descriptions>
</div> -->
<div class="btn-box">
<el-button @click="cancelClick">返回</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 查看主机定位 -->
<el-dialog v-model="data.dialogVisible" title="查看定位" style="width: 650px; padding-bottom: 20px">
<el-dialog v-model="data.dialogVisible" title="查看定位" style="width: 650px; padding-bottom: 20px" destroy-on-close>
<div>
<baidu-map style="height: 500px" class="map" :zoom="15" :center="data.center" :scroll-wheel-zoom="true" v-if="data.dialogVisible">
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
@@ -409,14 +368,10 @@
</bm-marker>
</baidu-map>
</div>
<template #footer>
<span class="dialog-footer">
<!-- <el-button @click="cancelClick">取消</el-button> -->
</span>
</template>
</el-dialog>
<!-- 查看轨迹 -->
<el-dialog v-model="data.trackDialogVisible" title="查看运动轨迹" style="width: 650px; padding-bottom: 20px">
<el-dialog v-model="data.trackDialogVisible" title="查看运动轨迹" style="width: 650px; padding-bottom: 20px" destroy-on-close>
<div
v-loading="data.trackLoading"
element-loading-text="正在加载中..."
@@ -437,25 +392,16 @@
:scroll-wheel-zoom="true"
style="height: 450px"
>
<!-- 运行轨迹的路线 stroke-weight边线的宽度stroke-opacity边线透明度-->
<bm-polyline stroke-color="blue" :path="data.path" :stroke-opacity="0.5" :stroke-weight="3" :editing="false"></bm-polyline>
<!-- marker 可以展示的图标 起点终点 -->
<bm-marker :icon="startMarkIcon" :position="{ lng: data.startMark.lng, lat: data.startMark.lat }"></bm-marker>
<bm-marker :icon="endMarkIcon" :position="{ lng: data.endMark.lng, lat: data.endMark.lat }"></bm-marker>
<!-- <bm-marker
v-for="(item, index) in data.path"
:key="index"
:icon="biaoMarkIcon"
:position="{ lng: item.lng, lat: item.lat }"
></bm-marker> -->
<!-- 暂停是根据你提供的坐标如果还没过第一个就会返回如果过了在第一个到第二个之间暂停他会跑掉第二个后面 -->
<!-- <bml-lushu @stop="reset" :path="data.path" :icon="iconss" :play="data.play" :rotation="true" :speed="1000"></bml-lushu> -->
</baidu-map>
</div>
</el-dialog>
<!-- 耳标日志 -->
<el-dialog v-model="data.earLogDialogVisible" title="设备日志" style="width: 900px; padding-bottom: 20px">
<el-table :data="data.earLogRows" border v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<!-- 日志弹窗耳标 -->
<el-dialog v-model="data.earLogDialogVisible" title="设备日志" width="900px" append-to-body destroy-on-close>
<el-table :data="data.earLogRows" border stripe v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能耳标编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="deviceVoltage">
<template #default="scope"> {{ scope.row.deviceVoltage || scope.row.battery || '-' }}% </template>
@@ -466,20 +412,23 @@
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp || scope.row.temperature || '-' }} </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="updateTime" width="200">
<el-table-column label="数据最后更新时间" prop="updateTime" width="180">
<template #default="scope"> {{ scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" prop="">
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button link type="primary" @click="locateClick(scope.row)">定位</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination v-model:limit="earLogForm.pageSize" v-model:page="earLogForm.pageNum" :total="data.earLogTotal" @pagination="getEarLogList" />
</div>
</el-dialog>
<!-- 项圈日志 -->
<el-dialog v-model="data.collarDialogVisible" title="设备日志" style="width: 900px; padding-bottom: 20px">
<el-table :data="data.collarLogRows" border v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<!-- 日志弹窗项圈 -->
<el-dialog v-model="data.collarDialogVisible" title="设备日志" width="900px" append-to-body destroy-on-close>
<el-table :data="data.collarLogRows" border stripe v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能项圈编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="battery">
<template #default="scope"> {{ scope.row.battery || scope.row.deviceVoltage || '-' }}% </template>
@@ -490,25 +439,28 @@
<el-table-column label="设备温度" prop="temperature">
<template #default="scope"> {{ scope.row.temperature || scope.row.deviceTemp || '-' }} </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="time" width="200">
<el-table-column label="数据最后更新时间" prop="time" width="180">
<template #default="scope"> {{ scope.row.time || scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" prop="">
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button link type="primary" @click="collarlocateClick(scope.row)">定位</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination
v-model:limit="collarLogForm.pageSize"
v-model:page="collarLogForm.pageNum"
:total="data.collarLogTotal"
@pagination="getCollarLogList"
/>
</div>
</el-dialog>
<!-- 智能主机日志 -->
<el-dialog v-model="data.hostLogDialogVisible" title="智能主机日志" style="width: 900px; padding-bottom: 20px">
<el-table :data="data.hostLogRows" border v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<!-- 日志弹窗主机 -->
<el-dialog v-model="data.hostLogDialogVisible" title="智能主机日志" width="900px" append-to-body destroy-on-close>
<el-table :data="data.hostLogRows" border stripe v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能主机编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="deviceVoltage">
<template #default="scope"> {{ scope.row.deviceVoltage || scope.row.battery || '-' }}% </template>
@@ -519,13 +471,13 @@
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp || scope.row.temperature || '-' }} </template>
</el-table-column>
<el-table-column label="小时时间" prop="hourTime" width="200">
<el-table-column label="小时时间" prop="hourTime" width="180">
<template #default="scope"> {{ scope.row.hourTime || '-' }}</template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="updateTime" width="200">
<el-table-column label="数据最后更新时间" prop="updateTime" width="180">
<template #default="scope"> {{ scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" prop="">
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button link type="primary" @click="hostLocationClick(scope.row)">定位</el-button>
</template>
@@ -534,12 +486,12 @@
</el-dialog>
<TrackDialog ref="TrackDialogRef" />
</section>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { earList, hostLocation, hostTrack, waybillDetail, collarList, collarLogList, earLogList, testDeliveryDevices, pageDeviceList, getEarTagLogs, getCollarLogs, getHostLogs, getEarTagTrajectory, getCollarTrajectory, getHostTrajectory } from '@/api/abroad.js';
import { vehicleList } from '@/api/userManage.js';
@@ -548,7 +500,10 @@ import endIcon from '../../assets/images/zhong.png';
import TrackDialog from '../hardware/trackDialog.vue';
const route = useRoute();
const router = useRouter(); // 添加 router
const TrackDialogRef = ref();
const activeDeviceTab = ref('host'); // 新增设备Tabs激活项
const startMarkIcon = reactive({
url: startIcon,
size: { width: 32, height: 32 },
@@ -1208,81 +1163,171 @@ onMounted(() => {
text-align: center;
}
.info-box {
margin-top: 10px;
.details-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.title {
font-weight: bold;
margin-bottom: 10px;
}
.info-group {
margin-top: 20px;
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
&:first-child {
margin-top: 0;
background-color: #fff;
padding: 16px 24px;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.header-left {
display: flex;
align-items: center;
.page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
margin-right: 16px;
}
.status-tag {
font-weight: bold;
}
}
.group-title {
}
.section-card {
margin-bottom: 20px;
:deep(.el-card__header) {
padding: 15px 20px;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
position: relative;
padding-left: 12px;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
&.warning-text::before {
background-color: #f56c6c;
}
}
}
}
.warning-card {
border-color: #fde2e2;
:deep(.el-card__header) {
background-color: #fef0f0;
}
}
.info-group {
margin-bottom: 30px;
&:last-child {
margin-bottom: 0;
}
.sub-title {
font-size: 15px;
font-weight: 600;
color: #606266;
margin-bottom: 15px;
padding-left: 8px;
border-left: 4px solid #409eff;
border-left: 3px solid #909399;
}
.quarantine-text {
margin-top: 20px;
}
:deep(.my-label) {
display: inline-block;
width: 120px;
text-align: right;
word-break: break-all;
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
.media-item {
background-color: #f5f7fa;
border-radius: 8px;
overflow: hidden;
border: 1px solid #ebeef5;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.label {
width: 140px;
font-size: 14px;
.media-label {
padding: 8px 12px;
font-size: 13px;
color: #606266;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
text-align: center;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn-box {
.photo-img {
width: 100%;
height: 160px;
display: block;
cursor: pointer;
}
.media-content {
display: flex;
align-items: center;
justify-content: center;
.photo-img {
width: 50%;
}
::v-deep .anchorBL {
display: none;
visibility: hidden;
}
.img-box {
margin-bottom: 10px;
video {
width: 100%;
height: 160px;
object-fit: contain;
background-color: #000;
}
}
}
.pagination-container {
margin-top: 15px;
display: flex;
.img-label {
width: 120px;
font-size: 14px;
margin-right: 16px;
text-align: right;
justify-content: flex-end;
}
.red {
color: #f56c6c;
font-weight: bold;
}
.empty-box {
display: flex;
justify-content: center;
align-items: center;
}
.red {
color: #ff6332;
}
.photo-container {
display: inline-block;
margin-right: 10px;
transition: transform 0.2s;
&:hover {
transform: scale(1.05);
}
}
.video-container {
margin-top: 8px;
margin-bottom: 8px;
video {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
height: 100%;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
artifactId=aiotagro-redis
groupId=com.aiotagro
version=1.0.1