初步完成轨迹
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
@@ -49,10 +49,10 @@ export function inspectionList(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// 查询百度鹰眼轨迹与停留点
|
||||
export function getYingyanTrack(data) {
|
||||
// 查询运单轨迹(基于车牌号,使用 OpenAPI)
|
||||
export function getDeliveryTrack(data) {
|
||||
return request({
|
||||
url: '/delivery/yingyan/track',
|
||||
url: '/delivery/track',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
|
||||
@@ -369,14 +369,6 @@
|
||||
<el-empty description="暂无轨迹数据" :image-size="100" />
|
||||
</div>
|
||||
|
||||
<div v-if="yingyanMeta.entityName" class="track-meta-panel">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="终端名称">{{ yingyanMeta.entityName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="查询开始">{{ formatTimestamp(yingyanMeta.startTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="查询结束">{{ formatTimestamp(yingyanMeta.endTime) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="latestPoint" class="latest-point-panel">
|
||||
<h4>最新定位</h4>
|
||||
<el-descriptions :column="3" border>
|
||||
@@ -461,7 +453,7 @@ import { ref, reactive, computed, nextTick, onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Location, VideoPlay, Refresh, InfoFilled, Connection, DataLine, Loading } from '@element-plus/icons-vue';
|
||||
import { BMPGL } from '@/utils/loadBmap.js';
|
||||
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs, getYingyanTrack, waybillDetail } from '@/api/abroad.js';
|
||||
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs, getDeliveryTrack, waybillDetail } from '@/api/abroad.js';
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const warningData = reactive({
|
||||
@@ -510,11 +502,6 @@ const trackBMapGL = ref(null); // 保存 BMapGL 实例,避免重复加载
|
||||
const stayPoints = ref([]); // 停留点列表
|
||||
const latestPoint = ref(null); // 最新轨迹点
|
||||
const segmentStats = ref([]);
|
||||
const yingyanMeta = reactive({
|
||||
entityName: '',
|
||||
startTime: null,
|
||||
endTime: null
|
||||
});
|
||||
|
||||
// 计算属性:判断预警类型
|
||||
const isTemperatureWarning = computed(() => {
|
||||
@@ -939,16 +926,8 @@ const handleTrackClick = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await loadYingyanTrack();
|
||||
|
||||
if (trackPath.value.length === 0) {
|
||||
// TODO: 接入新的轨迹服务
|
||||
trackLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
await nextTick();
|
||||
await initTrackMap();
|
||||
};
|
||||
|
||||
// 获取运送清单运输状态
|
||||
@@ -981,16 +960,13 @@ const getDeliveryStatus = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 加载百度鹰眼轨迹与停留点
|
||||
// 加载运单轨迹(基于车牌号,使用 OpenAPI)
|
||||
const loadYingyanTrack = async () => {
|
||||
stayPoints.value = [];
|
||||
trackPath.value = [];
|
||||
latestPoint.value = null;
|
||||
segmentStats.value = [];
|
||||
trackMapShow.value = false;
|
||||
yingyanMeta.entityName = '';
|
||||
yingyanMeta.startTime = null;
|
||||
yingyanMeta.endTime = null;
|
||||
|
||||
if (!warningData.deliveryId) {
|
||||
ElMessage.warning('运单ID缺失,无法查询轨迹');
|
||||
@@ -998,11 +974,10 @@ const loadYingyanTrack = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getYingyanTrack({ deliveryId: warningData.deliveryId });
|
||||
const res = await getDeliveryTrack({ 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 => {
|
||||
@@ -1019,30 +994,18 @@ const loadYingyanTrack = async () => {
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
stayPoints.value = Array.isArray(res.data.stayPoints) ? res.data.stayPoints : [];
|
||||
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('暂无有效轨迹点');
|
||||
ElMessage.warning(res.data.message || '暂无有效轨迹点');
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning(res.msg || '暂无轨迹数据');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[TRACK] 加载百度鹰眼轨迹失败:', error);
|
||||
console.error('[TRACK] 加载轨迹失败:', error);
|
||||
ElMessage.error('加载轨迹数据失败');
|
||||
}
|
||||
};
|
||||
@@ -1345,9 +1308,6 @@ const handleTrackDialogClose = () => {
|
||||
trackPath.value = [];
|
||||
trackMapShow.value = false;
|
||||
stayPoints.value = [];
|
||||
yingyanMeta.entityName = '';
|
||||
yingyanMeta.startTime = null;
|
||||
yingyanMeta.endTime = null;
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
|
||||
@@ -142,9 +142,17 @@
|
||||
<el-empty description="暂无轨迹数据" :image-size="100" />
|
||||
</div>
|
||||
<div v-else class="track-info">
|
||||
<el-tag type="info" style="margin-bottom: 15px;">
|
||||
轨迹点数:{{ trackPath.length }}
|
||||
</el-tag>
|
||||
<div class="track-summary">
|
||||
<el-tag type="info">轨迹点数:{{ trackPath.length }}</el-tag>
|
||||
<el-tag v-if="trackMileage" type="success">总里程:{{ trackMileage }} km</el-tag>
|
||||
<el-tag v-if="trackParkSize > 0" type="warning">停车次数:{{ trackParkSize }}</el-tag>
|
||||
</div>
|
||||
<div v-if="trackParks.length > 0" class="park-list">
|
||||
<span class="park-title">停车列表:</span>
|
||||
<span v-for="(p, idx) in trackParks" :key="idx" class="park-item">
|
||||
{{ p.name }} ({{ p.lng }}, {{ p.lat }})
|
||||
</span>
|
||||
</div>
|
||||
<div id="trackMap" style="width: 100%; height: 500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,7 +170,7 @@ import baseSearch from '@/components/common/searchCustom/index.vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
import { orderPageQuery, orderDelete, orderBatchImport } from '@/api/shipping.js';
|
||||
import { memberListByType, userAdd } from '@/api/userManage.js';
|
||||
import { getYingyanTrack, waybillDetail } from '@/api/abroad.js';
|
||||
import { getDeliveryTrack, waybillDetail } from '@/api/abroad.js';
|
||||
import { BMPGL } from '@/utils/loadBmap.js';
|
||||
import { nextTick } from 'vue';
|
||||
import OrderDialog from './orderDialog.vue';
|
||||
@@ -178,6 +186,9 @@ const fileInputRef = ref();
|
||||
const trackDialogVisible = ref(false);
|
||||
const trackLoading = ref(false);
|
||||
const trackPath = ref([]);
|
||||
const trackMileage = ref(''); // 总里程 km
|
||||
const trackParkSize = ref(0); // 停车次数
|
||||
const trackParks = ref([]); // 停车列表
|
||||
const deliveryStatus = ref(null);
|
||||
const trackMapInstance = ref(null);
|
||||
const formItemList = reactive([
|
||||
@@ -398,34 +409,86 @@ const viewTrack = async (deliveryId, status) => {
|
||||
trackDialogVisible.value = true;
|
||||
trackLoading.value = true;
|
||||
trackPath.value = [];
|
||||
trackMileage.value = '';
|
||||
trackParkSize.value = 0;
|
||||
trackParks.value = [];
|
||||
deliveryStatus.value = status;
|
||||
|
||||
try {
|
||||
const res = await getYingyanTrack({ deliveryId: deliveryId });
|
||||
const res = await getDeliveryTrack({ deliveryId: deliveryId });
|
||||
console.info('[track] response data:', res?.data);
|
||||
if (res.code === 200 && res.data) {
|
||||
const rawPoints = Array.isArray(res.data.trackPoints) ? res.data.trackPoints : [];
|
||||
trackPath.value = rawPoints
|
||||
const data = res.data;
|
||||
|
||||
// 尝试解析原始响应(兜底 trackArray/parkArray)
|
||||
let rawTrackFromResp = [];
|
||||
let rawParkFromResp = [];
|
||||
if (!Array.isArray(data.trackArray) && data.rawResponse) {
|
||||
try {
|
||||
const rawObj = JSON.parse(data.rawResponse);
|
||||
rawTrackFromResp = Array.isArray(rawObj?.result?.trackArray) ? rawObj.result.trackArray : [];
|
||||
rawParkFromResp = Array.isArray(rawObj?.result?.parkArray) ? rawObj.result.parkArray : [];
|
||||
console.info('[track] parsed rawResponse trackArray len:', rawTrackFromResp.length, 'parkArray len:', rawParkFromResp.length);
|
||||
} catch (e) {
|
||||
console.warn('解析 rawResponse 失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 里程/停车
|
||||
trackMileage.value = data.mileage || '';
|
||||
trackParkSize.value = Number(data.parkSize || 0);
|
||||
const parkArray = Array.isArray(data.parkArray) ? data.parkArray : rawParkFromResp;
|
||||
trackParks.value = parkArray
|
||||
.map((p, idx) => {
|
||||
const lng = parseFloat(p.lon ?? p.lng ?? p.longitude ?? 0);
|
||||
const lat = parseFloat(p.lat ?? p.latitude ?? 0);
|
||||
return {
|
||||
name: `停车点${idx + 1}`,
|
||||
lng,
|
||||
lat
|
||||
};
|
||||
})
|
||||
.filter(p => !Number.isNaN(p.lng) && !Number.isNaN(p.lat) && p.lng !== 0 && p.lat !== 0);
|
||||
|
||||
// 轨迹
|
||||
const rawTrack = Array.isArray(data.trackArray)
|
||||
? data.trackArray
|
||||
: (Array.isArray(data.trackPoints) ? data.trackPoints : rawTrackFromResp);
|
||||
console.info('[track] source arrays len -> trackArray:', Array.isArray(data.trackArray) ? data.trackArray.length : 0,
|
||||
'trackPoints:', Array.isArray(data.trackPoints) ? data.trackPoints.length : 0,
|
||||
'rawTrackFromResp:', rawTrackFromResp.length);
|
||||
trackPath.value = rawTrack
|
||||
.map(item => {
|
||||
const lng = parseFloat(item.longitude ?? item.lng ?? 0);
|
||||
const lat = parseFloat(item.latitude ?? item.lat ?? 0);
|
||||
let lng = parseFloat(item.lon ?? item.lng ?? item.longitude ?? 0);
|
||||
let lat = parseFloat(item.lat ?? item.latitude ?? 0);
|
||||
// 如果疑似放大(>180/90),尝试除以600000
|
||||
if (Math.abs(lng) > 180 || Math.abs(lat) > 90) {
|
||||
lng = lng / 600000;
|
||||
lat = lat / 600000;
|
||||
}
|
||||
if (Number.isNaN(lng) || Number.isNaN(lat) || lng === 0 || lat === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
lng,
|
||||
lat,
|
||||
locTime: item.locTime
|
||||
locTime: item.locTime || item.timestamp || item.utc || item.gtm || '',
|
||||
speed: item.spd || item.speed || '',
|
||||
direction: item.agl || item.direction || '',
|
||||
altitude: item.hgt || item.altitude || '',
|
||||
mileage: item.mlg || ''
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
console.info('[track] normalized trackPath len:', trackPath.value.length,
|
||||
'sample:', trackPath.value.slice(0, 3));
|
||||
|
||||
if (trackPath.value.length === 0) {
|
||||
ElMessage.warning('暂无有效轨迹点');
|
||||
ElMessage.warning(data.message || '暂无有效轨迹点');
|
||||
trackLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
await nextTick();
|
||||
await initTrackMap();
|
||||
} else {
|
||||
|
||||
@@ -192,6 +192,13 @@
|
||||
<artifactId>xxl-job-core</artifactId>
|
||||
<version>2.4.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenAPI SDK 用于车载 GPS 轨迹查询 -->
|
||||
<dependency>
|
||||
<groupId>com.openapi</groupId>
|
||||
<artifactId>openapi-sdk</artifactId>
|
||||
<version>6.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.aiotagro.cattletrade.business.entity.Delivery;
|
||||
import com.aiotagro.cattletrade.business.entity.DeliveryDevice;
|
||||
import com.aiotagro.cattletrade.business.entity.JbqClient;
|
||||
import com.aiotagro.cattletrade.business.entity.XqClient;
|
||||
import com.aiotagro.cattletrade.business.service.BaiduYingyanService;
|
||||
import com.aiotagro.cattletrade.business.service.IDeliveryService;
|
||||
import com.aiotagro.cattletrade.business.service.IDeliveryDeviceService;
|
||||
import com.aiotagro.cattletrade.business.service.IJbqClientService;
|
||||
@@ -87,11 +86,6 @@ public class DeliveryController {
|
||||
@Autowired
|
||||
private OrderMapper orderMapper;
|
||||
|
||||
@Autowired
|
||||
private BaiduYingyanService baiduYingyanService;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 小程序运送清单-分页查询
|
||||
*
|
||||
@@ -604,33 +598,6 @@ public class DeliveryController {
|
||||
if (StringUtils.isBlank(existDelivery.getLicensePlate())) {
|
||||
return AjaxResult.error("车牌号为空,无法开启运输状态");
|
||||
}
|
||||
// 去除车牌号中的空格,确保格式一致
|
||||
String licensePlate = existDelivery.getLicensePlate().trim();
|
||||
String entityName = StringUtils.defaultIfBlank(existDelivery.getYingyanEntityName(), licensePlate);
|
||||
// 如果终端名称与车牌号不一致,使用车牌号(去除空格)
|
||||
if (!licensePlate.equals(entityName)) {
|
||||
entityName = licensePlate;
|
||||
}
|
||||
boolean ensureResult = baiduYingyanService.ensureEntity(entityName);
|
||||
|
||||
Delivery syncInfo = new Delivery();
|
||||
syncInfo.setId(id);
|
||||
syncInfo.setYingyanEntityName(entityName);
|
||||
if (existDelivery.getYingyanLastSyncTime() == null) {
|
||||
Date syncStart = existDelivery.getEstimatedDeliveryTime();
|
||||
if (syncStart == null) {
|
||||
syncStart = existDelivery.getEstimatedDepartureTime();
|
||||
}
|
||||
if (syncStart == null) {
|
||||
syncStart = existDelivery.getCreateTime();
|
||||
}
|
||||
syncInfo.setYingyanLastSyncTime(syncStart);
|
||||
}
|
||||
deliveryService.updateById(syncInfo);
|
||||
|
||||
if (!ensureResult) {
|
||||
logger.warn("运单 {} 创建百度鹰眼终端失败,将在后台重试", existDelivery.getDeliveryNumber());
|
||||
}
|
||||
} else if (status == 3) {
|
||||
Delivery arrivalUpdate = new Delivery();
|
||||
arrivalUpdate.setId(id);
|
||||
@@ -657,12 +624,12 @@ public class DeliveryController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询运送清单百度鹰眼轨迹/停留点
|
||||
* 查询运单轨迹(基于车牌号,使用 OpenAPI)
|
||||
*/
|
||||
@SaCheckPermission("delivery:view")
|
||||
@PostMapping("/yingyan/track")
|
||||
public AjaxResult getYingyanTrack(@Validated @RequestBody DeliveryTrackQueryDto dto) {
|
||||
return deliveryService.queryYingyanTrack(dto.getDeliveryId());
|
||||
@PostMapping("/track")
|
||||
public AjaxResult getTrack(@Validated @RequestBody DeliveryTrackQueryDto dto) {
|
||||
return deliveryService.queryTrack(dto.getDeliveryId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.aiotagro.cattletrade.business.dto.openapi;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* OpenAPI 轨迹点 DTO
|
||||
* 表示从 OpenAPI 返回的单个轨迹点数据
|
||||
*
|
||||
* @author System
|
||||
* @date 2025-01-20
|
||||
*/
|
||||
@Data
|
||||
public class OpenApiTrackPoint {
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private Double longitude;
|
||||
|
||||
/**
|
||||
* 定位时间(Unix 时间戳,秒)
|
||||
*/
|
||||
private Long locTime;
|
||||
|
||||
/**
|
||||
* 速度(米/秒)
|
||||
*/
|
||||
private Double speed;
|
||||
|
||||
/**
|
||||
* 方向角(度)
|
||||
*/
|
||||
private Double direction;
|
||||
|
||||
/**
|
||||
* 海拔(米)
|
||||
*/
|
||||
private Double altitude;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.aiotagro.cattletrade.business.dto.openapi;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OpenAPI 轨迹查询响应 DTO
|
||||
* 根据实际 API 返回格式定义字段
|
||||
*
|
||||
* @author System
|
||||
* @date 2025-01-20
|
||||
*/
|
||||
@Data
|
||||
public class OpenApiTrackResponse {
|
||||
|
||||
/**
|
||||
* 响应状态码(0 表示成功)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 轨迹点列表
|
||||
*/
|
||||
private List<OpenApiTrackPoint> data;
|
||||
|
||||
/**
|
||||
* 其他可能的字段(根据实际 API 返回格式添加)
|
||||
*/
|
||||
private Object other;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.aiotagro.cattletrade.business.dto.yingyan;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 百度鹰眼停留点
|
||||
*/
|
||||
@Data
|
||||
public class YingyanStayPoint {
|
||||
|
||||
private double latitude;
|
||||
|
||||
private double longitude;
|
||||
|
||||
/**
|
||||
* 停留开始时间(Unix 秒)
|
||||
*/
|
||||
private long startTime;
|
||||
|
||||
/**
|
||||
* 停留结束时间(Unix 秒)
|
||||
*/
|
||||
private long endTime;
|
||||
|
||||
/**
|
||||
* 停留时长(秒)
|
||||
*/
|
||||
private long duration;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.aiotagro.cattletrade.business.dto.yingyan;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 百度鹰眼轨迹点
|
||||
*/
|
||||
@Data
|
||||
public class YingyanTrackPoint {
|
||||
|
||||
private double latitude;
|
||||
|
||||
private double longitude;
|
||||
|
||||
/**
|
||||
* 轨迹点定位时间(Unix 秒)
|
||||
*/
|
||||
private long locTime;
|
||||
|
||||
/**
|
||||
* 米/秒
|
||||
*/
|
||||
private Double speed;
|
||||
|
||||
/**
|
||||
* 航向角
|
||||
*/
|
||||
private Double direction;
|
||||
}
|
||||
|
||||
@@ -180,20 +180,7 @@ public class Delivery implements Serializable {
|
||||
private Date estimatedDeliveryTime;
|
||||
|
||||
/**
|
||||
* 百度鹰眼终端名称(默认为车牌)
|
||||
*/
|
||||
@TableField("yingyan_entity_name")
|
||||
private String yingyanEntityName;
|
||||
|
||||
/**
|
||||
* 百度鹰眼最后同步时间
|
||||
*/
|
||||
@TableField("yingyan_last_sync_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date yingyanLastSyncTime;
|
||||
|
||||
/**
|
||||
* 实际到达时间(鹰眼自动回写)
|
||||
* 实际到达时间
|
||||
*/
|
||||
@TableField("arrival_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
|
||||
@@ -27,7 +27,7 @@ public interface JbqClientLogMapper extends BaseMapper<JbqClientLog> {
|
||||
int batchInsert(@Param("list") List<JbqClientLog> logList);
|
||||
|
||||
/**
|
||||
* 供百度鹰眼同步使用的增量查询
|
||||
* 查询耳标轨迹日志
|
||||
*/
|
||||
List<JbqClientLog> listLogsForYingyan(@Param("deviceIds") List<String> deviceIds,
|
||||
@Param("startTime") Date startTime,
|
||||
|
||||
@@ -28,7 +28,7 @@ public interface JbqServerLogMapper extends BaseMapper<JbqServerLog> {
|
||||
int batchInsert(@Param("list") List<JbqServerLog> logList);
|
||||
|
||||
/**
|
||||
* 查询用于百度鹰眼推送的主机轨迹
|
||||
* 查询主机轨迹日志
|
||||
*/
|
||||
List<JbqServerLog> listLogsForYingyan(@Param("deviceIds") List<String> deviceIds,
|
||||
@Param("startTime") Date startTime,
|
||||
|
||||
@@ -27,7 +27,7 @@ public interface XqClientLogMapper extends BaseMapper<XqClientLog> {
|
||||
int batchInsert(@Param("list") List<XqClientLog> logList);
|
||||
|
||||
/**
|
||||
* 查询用于百度鹰眼推送的项圈轨迹
|
||||
* 查询项圈轨迹日志
|
||||
*/
|
||||
List<XqClientLog> listLogsForYingyan(@Param("deviceIds") List<String> deviceIds,
|
||||
@Param("startTime") Date startTime,
|
||||
|
||||
@@ -1,829 +0,0 @@
|
||||
package com.aiotagro.cattletrade.business.service;
|
||||
|
||||
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanStayPoint;
|
||||
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanTrackPoint;
|
||||
import com.aiotagro.common.core.constant.BaiduYingyanConstants;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 百度鹰眼 API 封装
|
||||
*/
|
||||
@Service
|
||||
public class BaiduYingyanService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BaiduYingyanService.class);
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public BaiduYingyanService(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.restTemplate = buildRestTemplate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保终端存在(幂等)
|
||||
* 返回值:true-终端存在或创建成功,false-创建失败(终端不存在且创建失败)
|
||||
*/
|
||||
public boolean ensureEntity(String entityName) {
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
logger.warn("ensureEntity 失败:终端名称为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ 去除空格,确保格式一致
|
||||
String cleanEntityName = entityName.trim();
|
||||
|
||||
// ✅ 验证 entityName 不是 "entity" 字符串
|
||||
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", cleanEntityName);
|
||||
// entity_desc 为非必填项,且命名规则限制:只支持中文、英文字母、下划线、连字符、数字
|
||||
// 为避免参数错误,不传递 entity_desc 参数
|
||||
|
||||
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={}, service_id={}", cleanEntityName, BaiduYingyanConstants.SERVICE_ID);
|
||||
return true;
|
||||
} else if (status == 3005) {
|
||||
// ✅ 终端已存在,这是正常情况,视为成功
|
||||
logger.info("✅ 鹰眼终端已存在, entityName={}, service_id={}, message={}",
|
||||
cleanEntityName, BaiduYingyanConstants.SERVICE_ID, message);
|
||||
return true;
|
||||
} else if (status == 3006) {
|
||||
logger.info("✅ 鹰眼终端操作成功, entityName={}, service_id={}, status={}",
|
||||
cleanEntityName, BaiduYingyanConstants.SERVICE_ID, status);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 其他状态码视为失败
|
||||
logger.warn("❌ 鹰眼创建终端失败, entityName={}, service_id={}, status={}, message={}",
|
||||
cleanEntityName, BaiduYingyanConstants.SERVICE_ID, status, message);
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ 鹰眼创建终端异常, entityName={}", cleanEntityName, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送单条轨迹点
|
||||
*/
|
||||
public boolean pushTrackPoint(String entityName, double latitude, double longitude, long locTime) {
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
logger.warn("鹰眼上传轨迹失败:终端名称为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ 验证经纬度有效性
|
||||
if (latitude == 0 && longitude == 0) {
|
||||
logger.warn("鹰眼上传轨迹失败:经纬度无效 (0,0), entity={}", entityName);
|
||||
return false;
|
||||
}
|
||||
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
|
||||
logger.warn("鹰眼上传轨迹失败:经纬度超出有效范围, entity={}, lat={}, lon={}",
|
||||
entityName, latitude, longitude);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
MultiValueMap<String, String> form = baseForm();
|
||||
form.add("entity_name", entityName);
|
||||
form.add("latitude", String.valueOf(latitude));
|
||||
form.add("longitude", String.valueOf(longitude));
|
||||
form.add("loc_time", String.valueOf(locTime));
|
||||
form.add("coord_type_input", "wgs84");
|
||||
|
||||
logger.debug("鹰眼上传轨迹点 - entity={}, lat={}, lon={}, locTime={}",
|
||||
entityName, latitude, longitude, locTime);
|
||||
|
||||
JsonNode result = postForm("/track/addpoint", form);
|
||||
int status = result.path("status").asInt(-1);
|
||||
String message = result.path("message").asText();
|
||||
|
||||
if (status == 0) {
|
||||
logger.debug("鹰眼上传轨迹成功 - entity={}, lat={}, lon={}",
|
||||
entityName, latitude, longitude);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ✅ 详细记录失败原因
|
||||
logger.warn("鹰眼上传轨迹失败 - entity={}, status={}, message={}, lat={}, lon={}, locTime={}",
|
||||
entityName, status, message, latitude, longitude, locTime);
|
||||
|
||||
// 如果是常见错误,记录更详细的信息
|
||||
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={}, lat={}, lon={}, locTime={}",
|
||||
entityName, latitude, longitude, locTime, e);
|
||||
}
|
||||
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小时内)
|
||||
*/
|
||||
public List<YingyanTrackPoint> queryTrack(String entityName, long startTime, long endTime) {
|
||||
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", 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));
|
||||
|
||||
JsonNode result = get("/track/gettrack", params);
|
||||
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.info("鹰眼查询轨迹:指定时间范围内可能无轨迹数据, entity={}, startTime={}, endTime={}, message={}",
|
||||
entityName, new Date(startTime * 1000), new Date(endTime * 1000), message);
|
||||
} else {
|
||||
logger.warn("鹰眼查询轨迹失败, entity={}, status={}, message={}, startTime={}, endTime={}, raw={}",
|
||||
entityName, status, message, new Date(startTime * 1000), new Date(endTime * 1000), result.toString());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 文档返回结构存在两种形式:直接 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")) {
|
||||
return;
|
||||
}
|
||||
YingyanTrackPoint point = new YingyanTrackPoint();
|
||||
point.setLatitude(node.path("latitude").asDouble());
|
||||
point.setLongitude(node.path("longitude").asDouble());
|
||||
point.setLocTime(node.path("loc_time").asLong());
|
||||
if (node.has("speed")) {
|
||||
point.setSpeed(node.path("speed").asDouble());
|
||||
}
|
||||
if (node.has("direction")) {
|
||||
point.setDirection(node.path("direction").asDouble());
|
||||
}
|
||||
points.add(point);
|
||||
});
|
||||
return points;
|
||||
} catch (Exception e) {
|
||||
logger.error("鹰眼查询轨迹异常, entity={}", entityName, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最新轨迹点(实时位置)
|
||||
*/
|
||||
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小时为间隔分段查询,然后拼接结果
|
||||
*
|
||||
* @param entityName 终端名称
|
||||
* @param startTime 开始时间(秒级时间戳)
|
||||
* @param endTime 结束时间(秒级时间戳,不能超过当前时间)
|
||||
* @return 拼接后的轨迹点列表,按时间排序
|
||||
*/
|
||||
public List<YingyanTrackPoint> queryTrackSegmented(String entityName, long startTime, long endTime) {
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ✅ 首先确保终端存在
|
||||
if (!ensureEntity(entityName)) {
|
||||
logger.warn("查询轨迹前终端不存在或创建失败,无法查询 - entity={}", entityName);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 验证时间范围
|
||||
long currentTime = System.currentTimeMillis() / 1000;
|
||||
if (endTime > currentTime) {
|
||||
logger.warn("结束时间不能超过当前时间,自动调整为当前时间。entity={}, endTime={}, currentTime={}",
|
||||
entityName, endTime, currentTime);
|
||||
endTime = currentTime;
|
||||
}
|
||||
|
||||
if (startTime >= endTime) {
|
||||
logger.warn("开始时间不能大于等于结束时间。entity={}, startTime={}, endTime={}",
|
||||
entityName, startTime, endTime);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 计算时间差(秒)
|
||||
long timeDiff = endTime - startTime;
|
||||
// 24小时 = 86400秒
|
||||
final long SEGMENT_INTERVAL = 86400L;
|
||||
|
||||
// 如果时间差小于等于24小时,直接查询
|
||||
if (timeDiff <= SEGMENT_INTERVAL) {
|
||||
logger.debug("时间范围在24小时内,直接查询。entity={}, startTime={}, endTime={}, diff={}秒",
|
||||
entityName, startTime, endTime, timeDiff);
|
||||
return queryTrack(entityName, startTime, endTime);
|
||||
}
|
||||
|
||||
// 需要分段查询
|
||||
logger.info("开始分段查询轨迹 - entity={}, startTime={}, endTime={}, 总时长={}小时",
|
||||
entityName, startTime, endTime, timeDiff / 3600);
|
||||
|
||||
List<YingyanTrackPoint> allPoints = new ArrayList<>();
|
||||
long segmentStart = startTime;
|
||||
int segmentIndex = 1;
|
||||
|
||||
while (segmentStart < endTime) {
|
||||
// 计算当前段的结束时间(不超过endTime,且不超过24小时)
|
||||
long segmentEnd = Math.min(segmentStart + SEGMENT_INTERVAL, endTime);
|
||||
|
||||
logger.debug("查询第{}段轨迹 - entity={}, segmentStart={}, segmentEnd={}, 时长={}小时",
|
||||
segmentIndex, entityName, segmentStart, segmentEnd, (segmentEnd - segmentStart) / 3600);
|
||||
|
||||
try {
|
||||
// ✅ 在查询前确保终端存在(每段都检查,因为可能在某些时间段终端还未创建)
|
||||
if (!ensureEntity(entityName)) {
|
||||
logger.warn("第{}段查询前终端不存在或创建失败,跳过该段 - entity={}", segmentIndex, entityName);
|
||||
segmentStart = segmentEnd;
|
||||
segmentIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
List<YingyanTrackPoint> segmentPoints = queryTrack(entityName, segmentStart, segmentEnd);
|
||||
if (!segmentPoints.isEmpty()) {
|
||||
allPoints.addAll(segmentPoints);
|
||||
logger.debug("第{}段查询成功,获得{}个轨迹点", segmentIndex, segmentPoints.size());
|
||||
} else {
|
||||
logger.debug("第{}段无轨迹点", segmentIndex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("第{}段轨迹查询异常 - entity={}, segmentStart={}, segmentEnd={}",
|
||||
segmentIndex, entityName, segmentStart, segmentEnd, e);
|
||||
// 继续查询下一段,不中断
|
||||
}
|
||||
|
||||
// 移动到下一段(下一段的开始时间 = 当前段的结束时间)
|
||||
segmentStart = segmentEnd;
|
||||
segmentIndex++;
|
||||
|
||||
// 避免无限循环
|
||||
if (segmentIndex > 100) {
|
||||
logger.error("分段查询超过100段,可能存在逻辑错误,停止查询。entity={}", entityName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间排序并去重(基于locTime)
|
||||
if (!allPoints.isEmpty()) {
|
||||
// 按时间排序
|
||||
allPoints.sort(Comparator.comparingLong(YingyanTrackPoint::getLocTime));
|
||||
|
||||
// 去重:相同时间戳的轨迹点只保留一个
|
||||
List<YingyanTrackPoint> uniquePoints = new ArrayList<>();
|
||||
long lastLocTime = -1;
|
||||
for (YingyanTrackPoint point : allPoints) {
|
||||
if (point.getLocTime() != lastLocTime) {
|
||||
uniquePoints.add(point);
|
||||
lastLocTime = point.getLocTime();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("分段查询完成 - entity={}, 总段数={}, 原始轨迹点数={}, 去重后轨迹点数={}",
|
||||
entityName, segmentIndex - 1, allPoints.size(), uniquePoints.size());
|
||||
|
||||
return uniquePoints;
|
||||
}
|
||||
|
||||
logger.warn("分段查询未获得任何轨迹点 - entity={}, startTime={}, endTime={}",
|
||||
entityName, startTime, endTime);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询停留点(单次查询,限制24小时内)
|
||||
*/
|
||||
public List<YingyanStayPoint> queryStayPoints(String entityName, long startTime, long endTime, int stayTimeSeconds) {
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
logger.warn("查询停留点失败:终端名称为空");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ✅ 验证 entityName 不是 "entity" 字符串
|
||||
if ("entity".equals(entityName) || "entity_name".equals(entityName)) {
|
||||
logger.error("查询停留点失败:entityName 参数错误,值为 '{}',这可能是参数传递错误", entityName);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("entity_name", entityName);
|
||||
// ✅ 所有参数统一为 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={}",
|
||||
entityName, startTime, new Date(startTime * 1000), endTime, new Date(endTime * 1000), stayTimeSeconds);
|
||||
|
||||
JsonNode result = get("/analysis/staypoint", params);
|
||||
int status = result.path("status").asInt(-1);
|
||||
String message = result.path("message").asText();
|
||||
|
||||
if (!isSuccess(result)) {
|
||||
// ✅ status=3003 可能是"终端不存在"或"指定时间范围内无停留点数据"
|
||||
// 如果终端确实存在(通过 ensureEntity 确认),则可能是时间范围内无数据
|
||||
if (status == 3003) {
|
||||
logger.debug("鹰眼查询停留点:指定时间范围内可能无停留点数据, 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));
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
JsonNode stayNode = result.path("stay_points");
|
||||
if (stayNode == null || !stayNode.isArray() || stayNode.size() == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<YingyanStayPoint> stayPoints = new ArrayList<>(stayNode.size());
|
||||
stayNode.forEach(node -> {
|
||||
if (!node.hasNonNull("latitude") || !node.hasNonNull("longitude")) {
|
||||
return;
|
||||
}
|
||||
YingyanStayPoint stayPoint = new YingyanStayPoint();
|
||||
stayPoint.setLatitude(node.path("latitude").asDouble());
|
||||
stayPoint.setLongitude(node.path("longitude").asDouble());
|
||||
stayPoint.setStartTime(node.path("start_time").asLong());
|
||||
stayPoint.setEndTime(node.path("end_time").asLong());
|
||||
stayPoint.setDuration(node.path("duration").asLong());
|
||||
stayPoints.add(stayPoint);
|
||||
});
|
||||
return stayPoints;
|
||||
} catch (Exception e) {
|
||||
logger.error("鹰眼查询停留点异常, entity={}", entityName, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分段查询停留点(支持超过24小时的查询)
|
||||
* 按照24小时为间隔分段查询,然后拼接结果
|
||||
*
|
||||
* @param entityName 终端名称
|
||||
* @param startTime 开始时间(秒级时间戳)
|
||||
* @param endTime 结束时间(秒级时间戳,不能超过当前时间)
|
||||
* @param stayTimeSeconds 停留时间阈值(秒)
|
||||
* @return 拼接后的停留点列表,按开始时间排序
|
||||
*/
|
||||
public List<YingyanStayPoint> queryStayPointsSegmented(String entityName, long startTime, long endTime, int stayTimeSeconds) {
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ✅ 首先确保终端存在
|
||||
if (!ensureEntity(entityName)) {
|
||||
logger.warn("查询停留点前终端不存在或创建失败,无法查询 - entity={}", entityName);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 验证时间范围
|
||||
long currentTime = System.currentTimeMillis() / 1000;
|
||||
if (endTime > currentTime) {
|
||||
logger.warn("结束时间不能超过当前时间,自动调整为当前时间。entity={}, endTime={}, currentTime={}",
|
||||
entityName, endTime, currentTime);
|
||||
endTime = currentTime;
|
||||
}
|
||||
|
||||
if (startTime >= endTime) {
|
||||
logger.warn("开始时间不能大于等于结束时间。entity={}, startTime={}, endTime={}",
|
||||
entityName, startTime, endTime);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 计算时间差(秒)
|
||||
long timeDiff = endTime - startTime;
|
||||
// 24小时 = 86400秒
|
||||
final long SEGMENT_INTERVAL = 86400L;
|
||||
|
||||
// 如果时间差小于等于24小时,直接查询
|
||||
if (timeDiff <= SEGMENT_INTERVAL) {
|
||||
logger.debug("时间范围在24小时内,直接查询停留点。entity={}, startTime={}, endTime={}, diff={}秒",
|
||||
entityName, startTime, endTime, timeDiff);
|
||||
return queryStayPoints(entityName, startTime, endTime, stayTimeSeconds);
|
||||
}
|
||||
|
||||
// 需要分段查询
|
||||
logger.info("开始分段查询停留点 - entity={}, startTime={}, endTime={}, 总时长={}小时",
|
||||
entityName, startTime, endTime, timeDiff / 3600);
|
||||
|
||||
List<YingyanStayPoint> allStayPoints = new ArrayList<>();
|
||||
long segmentStart = startTime;
|
||||
int segmentIndex = 1;
|
||||
|
||||
while (segmentStart < endTime) {
|
||||
// 计算当前段的结束时间(不超过endTime,且不超过24小时)
|
||||
long segmentEnd = Math.min(segmentStart + SEGMENT_INTERVAL, endTime);
|
||||
|
||||
logger.debug("查询第{}段停留点 - entity={}, segmentStart={}, segmentEnd={}, 时长={}小时",
|
||||
segmentIndex, entityName, segmentStart, segmentEnd, (segmentEnd - segmentStart) / 3600);
|
||||
|
||||
try {
|
||||
// ✅ 在查询前确保终端存在(每段都检查,因为可能在某些时间段终端还未创建)
|
||||
if (!ensureEntity(entityName)) {
|
||||
logger.warn("第{}段查询前终端不存在或创建失败,跳过该段 - entity={}", segmentIndex, entityName);
|
||||
segmentStart = segmentEnd;
|
||||
segmentIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
List<YingyanStayPoint> segmentStayPoints = queryStayPoints(entityName, segmentStart, segmentEnd, stayTimeSeconds);
|
||||
if (!segmentStayPoints.isEmpty()) {
|
||||
allStayPoints.addAll(segmentStayPoints);
|
||||
logger.debug("第{}段查询成功,获得{}个停留点", segmentIndex, segmentStayPoints.size());
|
||||
} else {
|
||||
logger.debug("第{}段无停留点", segmentIndex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("第{}段停留点查询异常 - entity={}, segmentStart={}, segmentEnd={}",
|
||||
segmentIndex, entityName, segmentStart, segmentEnd, e);
|
||||
// 继续查询下一段,不中断
|
||||
}
|
||||
|
||||
// 移动到下一段(下一段的开始时间 = 当前段的结束时间)
|
||||
segmentStart = segmentEnd;
|
||||
segmentIndex++;
|
||||
|
||||
// 避免无限循环
|
||||
if (segmentIndex > 100) {
|
||||
logger.error("分段查询超过100段,可能存在逻辑错误,停止查询。entity={}", entityName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 按开始时间排序并去重(基于startTime)
|
||||
if (!allStayPoints.isEmpty()) {
|
||||
// 按开始时间排序
|
||||
allStayPoints.sort(Comparator.comparingLong(YingyanStayPoint::getStartTime));
|
||||
|
||||
// 去重:相同开始时间的停留点只保留一个
|
||||
List<YingyanStayPoint> uniqueStayPoints = new ArrayList<>();
|
||||
long lastStartTime = -1;
|
||||
for (YingyanStayPoint stayPoint : allStayPoints) {
|
||||
if (stayPoint.getStartTime() != lastStartTime) {
|
||||
uniqueStayPoints.add(stayPoint);
|
||||
lastStartTime = stayPoint.getStartTime();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("分段查询停留点完成 - entity={}, 总段数={}, 原始停留点数={}, 去重后停留点数={}",
|
||||
entityName, segmentIndex - 1, allStayPoints.size(), uniqueStayPoints.size());
|
||||
|
||||
return uniqueStayPoints;
|
||||
}
|
||||
|
||||
logger.warn("分段查询未获得任何停留点 - entity={}, startTime={}, endTime={}",
|
||||
entityName, startTime, endTime);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private JsonNode postForm(String path, MultiValueMap<String, String> form) throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(form, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(buildUrl(path), httpEntity, String.class);
|
||||
return parseBody(response.getBody());
|
||||
}
|
||||
|
||||
private JsonNode get(String path, Map<String, Object> params) throws Exception {
|
||||
// ✅ 手动构建 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
// ✅ 所有参数都不进行 URL 编码,直接拼接(与用户测试成功的 URL 格式一致)
|
||||
urlBuilder.append("&").append(key).append("=").append(value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private JsonNode parseBody(String body) throws Exception {
|
||||
if (StringUtils.isBlank(body)) {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
return objectMapper.readTree(body);
|
||||
}
|
||||
|
||||
private boolean isSuccess(JsonNode node) {
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
int status = node.path("status").asInt(-1);
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
private MultiValueMap<String, String> baseForm() {
|
||||
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
||||
form.add("ak", BaiduYingyanConstants.AK);
|
||||
form.add("service_id", String.valueOf(BaiduYingyanConstants.SERVICE_ID));
|
||||
return form;
|
||||
}
|
||||
|
||||
private RestTemplate buildRestTemplate() {
|
||||
org.springframework.http.client.SimpleClientHttpRequestFactory factory =
|
||||
new org.springframework.http.client.SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout((int) Duration.ofSeconds(10).toMillis());
|
||||
factory.setReadTimeout((int) Duration.ofSeconds(30).toMillis());
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
|
||||
private String buildUrl(String path) {
|
||||
if (StringUtils.isBlank(path)) {
|
||||
return BaiduYingyanConstants.BASE_URL;
|
||||
}
|
||||
if (path.startsWith("http")) {
|
||||
return path;
|
||||
}
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
return BaiduYingyanConstants.BASE_URL + path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
package com.aiotagro.cattletrade.business.service;
|
||||
|
||||
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanTrackPoint;
|
||||
import com.aiotagro.cattletrade.business.entity.Delivery;
|
||||
import com.aiotagro.cattletrade.business.entity.IotDeviceData;
|
||||
import com.aiotagro.cattletrade.business.entity.JbqClientLog;
|
||||
import com.aiotagro.cattletrade.business.entity.JbqServerLog;
|
||||
import com.aiotagro.cattletrade.business.entity.XqClientLog;
|
||||
import com.aiotagro.cattletrade.business.mapper.IotDeviceDataMapper;
|
||||
import com.aiotagro.cattletrade.business.mapper.JbqClientLogMapper;
|
||||
import com.aiotagro.cattletrade.business.mapper.JbqServerLogMapper;
|
||||
import com.aiotagro.cattletrade.business.mapper.XqClientLogMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 运送清单鹰眼同步服务
|
||||
*/
|
||||
@Service
|
||||
public class DeliveryYingyanSyncService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DeliveryYingyanSyncService.class);
|
||||
|
||||
/**
|
||||
* 每次最多同步 120 个轨迹点,避免请求过多
|
||||
*/
|
||||
private static final int MAX_SYNC_POINTS = 120;
|
||||
|
||||
private static final double ARRIVAL_RADIUS_METERS = 500D;
|
||||
|
||||
@Autowired
|
||||
private IDeliveryService deliveryService;
|
||||
|
||||
@Autowired
|
||||
private IotDeviceDataMapper iotDeviceDataMapper;
|
||||
|
||||
@Autowired
|
||||
private JbqClientLogMapper jbqClientLogMapper;
|
||||
|
||||
@Autowired
|
||||
private JbqServerLogMapper jbqServerLogMapper;
|
||||
|
||||
@Autowired
|
||||
private XqClientLogMapper xqClientLogMapper;
|
||||
|
||||
@Autowired
|
||||
private BaiduYingyanService baiduYingyanService;
|
||||
|
||||
/**
|
||||
* 同步所有运输中的运送清单
|
||||
*/
|
||||
public void syncActiveDeliveries() {
|
||||
logger.info("========== 开始执行百度鹰眼轨迹同步任务 ==========");
|
||||
LambdaQueryWrapper<Delivery> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Delivery::getStatus, 2);
|
||||
wrapper.isNotNull(Delivery::getLicensePlate);
|
||||
List<Delivery> deliveries = deliveryService.list(wrapper);
|
||||
if (CollectionUtils.isEmpty(deliveries)) {
|
||||
logger.info("未找到运输中的运单(status=2),跳过同步");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("找到 {} 个运输中的运单,开始同步轨迹", deliveries.size());
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
int noPointsCount = 0;
|
||||
|
||||
for (Delivery delivery : deliveries) {
|
||||
try {
|
||||
int result = syncDelivery(delivery);
|
||||
if (result > 0) {
|
||||
successCount++;
|
||||
logger.info("运单 {} 同步成功,上传了 {} 个轨迹点", delivery.getDeliveryNumber(), result);
|
||||
} else if (result == 0) {
|
||||
noPointsCount++;
|
||||
logger.debug("运单 {} 无有效轨迹点可同步", delivery.getDeliveryNumber());
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failCount++;
|
||||
logger.error("运单 {} 同步百度鹰眼失败", delivery.getDeliveryNumber(), e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("百度鹰眼轨迹同步完成 - 成功: {}, 无轨迹点: {}, 失败: {}", successCount, noPointsCount, failCount);
|
||||
logger.info("========== 百度鹰眼轨迹同步任务执行完成 ==========");
|
||||
}
|
||||
|
||||
private int syncDelivery(Delivery delivery) {
|
||||
String entityName = resolveEntityName(delivery);
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
logger.warn("运单 {} 无法同步,车牌为空", delivery.getDeliveryNumber());
|
||||
return -1;
|
||||
}
|
||||
|
||||
logger.info("开始同步运单 {} 的轨迹,终端名称: {}", delivery.getDeliveryNumber(), entityName);
|
||||
|
||||
// ✅ 确保终端存在(如果已存在则继续,如果不存在则创建)
|
||||
boolean entityExists = baiduYingyanService.ensureEntity(entityName);
|
||||
if (!entityExists) {
|
||||
logger.warn("运单 {} 创建/确保终端失败,终端名称: {},但继续尝试上传轨迹点(终端可能已存在)",
|
||||
delivery.getDeliveryNumber(), entityName);
|
||||
// 注意:即使 ensureEntity 返回 false,也继续尝试上传轨迹点
|
||||
// 因为终端可能已经存在,只是创建时返回了错误状态码
|
||||
} else {
|
||||
logger.debug("运单 {} 终端已存在或创建成功,终端名称: {}", delivery.getDeliveryNumber(), entityName);
|
||||
}
|
||||
|
||||
Date startTime = determineSyncStart(delivery);
|
||||
Date endTime = new Date();
|
||||
logger.info("运单 {} 查询轨迹点时间范围: {} 至 {}",
|
||||
delivery.getDeliveryNumber(), startTime, endTime);
|
||||
|
||||
List<YingyanTrackPoint> points = collectTrackPoints(delivery, startTime, endTime);
|
||||
if (CollectionUtils.isEmpty(points)) {
|
||||
logger.warn("运单 {} 未找到有效轨迹点,终端名称: {}, 时间范围: {} 至 {}",
|
||||
delivery.getDeliveryNumber(), entityName, startTime, endTime);
|
||||
|
||||
// ✅ 详细排查原因:从 iot_device_data 表查询设备
|
||||
QueryWrapper<IotDeviceData> deviceQueryWrapper = new QueryWrapper<>();
|
||||
deviceQueryWrapper.eq("delivery_id", delivery.getId());
|
||||
deviceQueryWrapper.and(wrapper -> wrapper.eq("is_delet", 0).or().isNull("is_delet"));
|
||||
List<IotDeviceData> devices = iotDeviceDataMapper.selectList(deviceQueryWrapper);
|
||||
|
||||
logger.warn("运单 {} 在 iot_device_data 表中的设备数量: {}",
|
||||
delivery.getDeliveryNumber(), devices != null ? devices.size() : 0);
|
||||
|
||||
if (devices != null && !devices.isEmpty()) {
|
||||
devices.forEach(device -> {
|
||||
logger.warn("运单 {} 设备信息 - deviceId: {}, deviceType: {}, latitude: {}, longitude: {}",
|
||||
delivery.getDeliveryNumber(), device.getDeviceId(), device.getDeviceType(),
|
||||
device.getLatitude(), device.getLongitude());
|
||||
});
|
||||
} else {
|
||||
logger.warn("运单 {} 在 iot_device_data 表中没有找到设备,deliveryId: {}",
|
||||
delivery.getDeliveryNumber(), delivery.getId());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
logger.info("运单 {} 找到 {} 个有效轨迹点,开始批量上传到百度鹰眼",
|
||||
delivery.getDeliveryNumber(), points.size());
|
||||
|
||||
// ✅ 检查运单状态,如果已结束则停止同步
|
||||
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());
|
||||
}
|
||||
|
||||
return successCount;
|
||||
}
|
||||
|
||||
private List<YingyanTrackPoint> collectTrackPoints(Delivery delivery, Date startTime, Date endTime) {
|
||||
// ✅ 修改:从 iot_device_data 表查询设备,而不是 delivery_device 表
|
||||
QueryWrapper<IotDeviceData> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("delivery_id", delivery.getId());
|
||||
queryWrapper.and(wrapper -> wrapper.eq("is_delet", 0).or().isNull("is_delet")); // 只查询未删除的设备
|
||||
List<IotDeviceData> devices = iotDeviceDataMapper.selectList(queryWrapper);
|
||||
|
||||
if (CollectionUtils.isEmpty(devices)) {
|
||||
logger.warn("运单 {} 在 iot_device_data 表中未找到设备,deliveryId: {}",
|
||||
delivery.getDeliveryNumber(), delivery.getId());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
logger.info("运单 {} 从 iot_device_data 表查询到 {} 个设备",
|
||||
delivery.getDeliveryNumber(), devices.size());
|
||||
|
||||
// ✅ 按设备类型分组:1=主机,2=耳标,4=项圈
|
||||
Map<Integer, List<String>> deviceMap = devices.stream()
|
||||
.filter(item -> StringUtils.isNotBlank(item.getDeviceId()))
|
||||
.collect(Collectors.groupingBy(device -> {
|
||||
Integer deviceType = device.getDeviceType();
|
||||
// 设备类型:1=主机,2=耳标,4=项圈
|
||||
return deviceType == null ? 0 : deviceType;
|
||||
}, Collectors.mapping(IotDeviceData::getDeviceId, Collectors.toList())));
|
||||
|
||||
// ✅ 记录设备分组情况
|
||||
logger.debug("运单 {} 设备分组 - 主机(1): {}, 耳标(2): {}, 项圈(4): {}",
|
||||
delivery.getDeliveryNumber(),
|
||||
deviceMap.getOrDefault(1, Collections.emptyList()).size(),
|
||||
deviceMap.getOrDefault(2, Collections.emptyList()).size(),
|
||||
deviceMap.getOrDefault(4, Collections.emptyList()).size());
|
||||
|
||||
List<YingyanTrackPoint> result = new ArrayList<>();
|
||||
int remaining = MAX_SYNC_POINTS;
|
||||
|
||||
// 1=主机设备,查询 jbq_server_log 表
|
||||
remaining = appendServerLogs(deviceMap.get(1), startTime, endTime, remaining, result);
|
||||
if (remaining <= 0) {
|
||||
return sortPoints(result);
|
||||
}
|
||||
|
||||
// 2=耳标设备,查询 jbq_client_log 表
|
||||
remaining = appendEarTagLogs(deviceMap.get(2), startTime, endTime, remaining, result);
|
||||
if (remaining <= 0) {
|
||||
return sortPoints(result);
|
||||
}
|
||||
|
||||
// 4=项圈设备,查询 xq_client_log 表
|
||||
appendCollarLogs(deviceMap.get(4), startTime, endTime, remaining, result);
|
||||
return sortPoints(result);
|
||||
}
|
||||
|
||||
private int appendServerLogs(List<String> deviceIds, Date startTime, Date endTime, int remaining, List<YingyanTrackPoint> result) {
|
||||
if (CollectionUtils.isEmpty(deviceIds) || remaining <= 0) {
|
||||
return remaining;
|
||||
}
|
||||
List<JbqServerLog> logs = jbqServerLogMapper.listLogsForYingyan(deviceIds, startTime, endTime, remaining);
|
||||
if (CollectionUtils.isEmpty(logs)) {
|
||||
return remaining;
|
||||
}
|
||||
logs.forEach(log -> {
|
||||
YingyanTrackPoint point = convertToPoint(log.getLatitude(), log.getLongitude(), log.getUpdateTime());
|
||||
if (point != null) {
|
||||
result.add(point);
|
||||
}
|
||||
});
|
||||
return Math.max(0, remaining - logs.size());
|
||||
}
|
||||
|
||||
private int appendEarTagLogs(List<String> deviceIds, Date startTime, Date endTime, int remaining, List<YingyanTrackPoint> result) {
|
||||
if (CollectionUtils.isEmpty(deviceIds) || remaining <= 0) {
|
||||
return remaining;
|
||||
}
|
||||
List<JbqClientLog> logs = jbqClientLogMapper.listLogsForYingyan(deviceIds, startTime, endTime, remaining);
|
||||
if (CollectionUtils.isEmpty(logs)) {
|
||||
return remaining;
|
||||
}
|
||||
logs.forEach(log -> {
|
||||
YingyanTrackPoint point = convertToPoint(log.getLatitude(), log.getLongitude(), log.getUpdateTime());
|
||||
if (point != null) {
|
||||
result.add(point);
|
||||
}
|
||||
});
|
||||
return Math.max(0, remaining - logs.size());
|
||||
}
|
||||
|
||||
private void appendCollarLogs(List<String> deviceIds, Date startTime, Date endTime, int remaining, List<YingyanTrackPoint> result) {
|
||||
if (CollectionUtils.isEmpty(deviceIds) || remaining <= 0) {
|
||||
return;
|
||||
}
|
||||
List<XqClientLog> logs = xqClientLogMapper.listLogsForYingyan(deviceIds, startTime, endTime, remaining);
|
||||
if (CollectionUtils.isEmpty(logs)) {
|
||||
return;
|
||||
}
|
||||
logs.forEach(log -> {
|
||||
YingyanTrackPoint point = convertToPoint(log.getLatitude(), log.getLongitude(), log.getUpdateTime());
|
||||
if (point != null) {
|
||||
result.add(point);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private List<YingyanTrackPoint> sortPoints(List<YingyanTrackPoint> points) {
|
||||
if (CollectionUtils.isEmpty(points)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return points.stream()
|
||||
.sorted(Comparator.comparingLong(YingyanTrackPoint::getLocTime))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private YingyanTrackPoint convertToPoint(String latStr, String lonStr, Date time) {
|
||||
if (StringUtils.isAnyBlank(latStr, lonStr) || time == null) {
|
||||
logger.debug("转换轨迹点失败:经纬度或时间为空 - lat={}, lon={}, time={}", latStr, lonStr, time);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
double lat = Double.parseDouble(latStr);
|
||||
double lon = Double.parseDouble(lonStr);
|
||||
|
||||
// ✅ 详细验证坐标有效性
|
||||
if (lat == 0 && lon == 0) {
|
||||
logger.debug("转换轨迹点失败:经纬度为 (0,0) - lat={}, lon={}", latStr, lonStr);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isValidCoordinate(lat, lon)) {
|
||||
logger.debug("转换轨迹点失败:经纬度超出有效范围 - lat={}, lon={}", lat, lon);
|
||||
return null;
|
||||
}
|
||||
|
||||
YingyanTrackPoint point = new YingyanTrackPoint();
|
||||
point.setLatitude(lat);
|
||||
point.setLongitude(lon);
|
||||
point.setLocTime(time.getTime() / 1000);
|
||||
|
||||
logger.debug("转换轨迹点成功 - lat={}, lon={}, time={}", lat, lon, time);
|
||||
return point;
|
||||
} catch (NumberFormatException ex) {
|
||||
logger.warn("解析经纬度失败: lat={}, lon={}, error={}", latStr, lonStr, ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidCoordinate(double lat, double lon) {
|
||||
return lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180;
|
||||
}
|
||||
|
||||
private String resolveEntityName(Delivery delivery) {
|
||||
if (StringUtils.isNotBlank(delivery.getYingyanEntityName())) {
|
||||
return delivery.getYingyanEntityName().trim();
|
||||
}
|
||||
if (StringUtils.isNotBlank(delivery.getLicensePlate())) {
|
||||
return delivery.getLicensePlate().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Date determineSyncStart(Delivery delivery) {
|
||||
if (delivery.getYingyanLastSyncTime() != null) {
|
||||
return delivery.getYingyanLastSyncTime();
|
||||
}
|
||||
if (delivery.getEstimatedDeliveryTime() != null) {
|
||||
return delivery.getEstimatedDeliveryTime();
|
||||
}
|
||||
if (delivery.getEstimatedDepartureTime() != null) {
|
||||
return delivery.getEstimatedDepartureTime();
|
||||
}
|
||||
if (delivery.getCreateTime() != null) {
|
||||
return delivery.getCreateTime();
|
||||
}
|
||||
return new Date(System.currentTimeMillis() - Duration.ofHours(12).toMillis());
|
||||
}
|
||||
|
||||
private void updateLastSync(Delivery delivery, Date newTime) {
|
||||
if (newTime == null) {
|
||||
return;
|
||||
}
|
||||
Delivery update = new Delivery();
|
||||
update.setId(delivery.getId());
|
||||
update.setYingyanLastSyncTime(newTime);
|
||||
deliveryService.updateById(update);
|
||||
delivery.setYingyanLastSyncTime(newTime);
|
||||
}
|
||||
|
||||
private void handleArrivalIfNeeded(Delivery delivery, YingyanTrackPoint point) {
|
||||
if (delivery.getStatus() == null || delivery.getStatus() != 2) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isAnyBlank(delivery.getEndLat(), delivery.getEndLon())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
double targetLat = Double.parseDouble(delivery.getEndLat());
|
||||
double targetLon = Double.parseDouble(delivery.getEndLon());
|
||||
double distance = calculateDistance(targetLat, targetLon, point.getLatitude(), point.getLongitude());
|
||||
if (distance <= ARRIVAL_RADIUS_METERS) {
|
||||
Delivery update = new Delivery();
|
||||
update.setId(delivery.getId());
|
||||
update.setStatus(3);
|
||||
Date arrivalTime = new Date(point.getLocTime() * 1000L);
|
||||
update.setArrivalTime(arrivalTime);
|
||||
update.setYingyanLastSyncTime(arrivalTime);
|
||||
deliveryService.updateById(update);
|
||||
|
||||
delivery.setStatus(3);
|
||||
delivery.setArrivalTime(arrivalTime);
|
||||
delivery.setYingyanLastSyncTime(arrivalTime);
|
||||
|
||||
logger.info("运单 {} 已到达终点,自动更新为已结束,距离 {} 米", delivery.getDeliveryNumber(), Math.round(distance));
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
logger.warn("运单 {} 终点经纬度格式错误: lat={}, lon={}",
|
||||
delivery.getDeliveryNumber(), delivery.getEndLat(), delivery.getEndLon());
|
||||
}
|
||||
}
|
||||
|
||||
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||
final double R = 6378137D;
|
||||
double dLat = Math.toRadians(lat2 - lat1);
|
||||
double dLon = Math.toRadians(lon2 - lon1);
|
||||
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ public interface IDeliveryService extends IService<Delivery> {
|
||||
PageResultResponse<Delivery> pageQueryListLog(DeliverListDto dto);
|
||||
|
||||
/**
|
||||
* 查询百度鹰眼轨迹与停留点
|
||||
* 查询运单轨迹(基于车牌号,使用 OpenAPI)
|
||||
*
|
||||
* @param deliveryId 运单ID
|
||||
* @return AjaxResult
|
||||
*/
|
||||
AjaxResult queryYingyanTrack(Integer deliveryId);
|
||||
AjaxResult queryTrack(Integer deliveryId);
|
||||
}
|
||||
|
||||
@@ -74,8 +74,7 @@ public class IotDeviceLogSyncService {
|
||||
})
|
||||
.count();
|
||||
logger.info("其中 {} 个设备包含有效的经纬度坐标数据", devicesWithCoordinates);
|
||||
logger.info("注意:设备日志同步任务仅将数据同步到日志表,不直接上传到百度鹰眼服务");
|
||||
logger.info("百度鹰眼轨迹上传由 DeliveryYingyanSyncService 定时任务负责");
|
||||
logger.info("注意:设备日志同步任务仅将数据同步到日志表");
|
||||
|
||||
int hostCount = 0;
|
||||
int earTagCount = 0;
|
||||
@@ -202,7 +201,7 @@ public class IotDeviceLogSyncService {
|
||||
logger.info("设备日志同步完成 - 主机: {}, 耳标: {}, 项圈: {}", hostCount, earTagCount, collarCount);
|
||||
logger.info("包含有效经纬度坐标的设备数量: {}", devicesWithCoordinatesCount);
|
||||
logger.info("========== 设备日志同步任务执行完成 ==========");
|
||||
logger.info("提示:同步到日志表的经纬度数据将由 DeliveryYingyanSyncService 定时任务上传到百度鹰眼服务");
|
||||
logger.info("提示:同步到日志表的经纬度数据可用于轨迹服务");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("设备日志同步任务执行失败", e);
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
package com.aiotagro.cattletrade.business.service;
|
||||
|
||||
import com.aiotagro.cattletrade.business.dto.openapi.OpenApiTrackPoint;
|
||||
import com.aiotagro.cattletrade.properties.OpenApiProperties;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.openapi.sdk.exception.RequestLimitExceededException;
|
||||
import com.openapi.sdk.exception.UnauthorizedException;
|
||||
import com.openapi.sdk.service.DataExchangeService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OpenAPI 轨迹服务
|
||||
* 封装 OpenAPI SDK 调用,提供轨迹查询功能
|
||||
*
|
||||
* @author System
|
||||
* @date 2025-01-20
|
||||
*/
|
||||
@Service
|
||||
public class OpenApiTrackService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OpenApiTrackService.class);
|
||||
|
||||
@Autowired
|
||||
private OpenApiProperties openApiProperties;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private DataExchangeService dataExchangeService;
|
||||
|
||||
/**
|
||||
* 初始化 DataExchangeService
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
int connectTimeout = openApiProperties.getConnectTimeout() != null
|
||||
? openApiProperties.getConnectTimeout() : 5000;
|
||||
int readTimeout = openApiProperties.getReadTimeout() != null
|
||||
? openApiProperties.getReadTimeout() : 8000;
|
||||
|
||||
dataExchangeService = new DataExchangeService(connectTimeout, readTimeout);
|
||||
logger.info("OpenAPI DataExchangeService 初始化成功 - connectTimeout: {}, readTimeout: {}",
|
||||
connectTimeout, readTimeout);
|
||||
} catch (Exception e) {
|
||||
logger.error("OpenAPI DataExchangeService 初始化失败", e);
|
||||
throw new RuntimeException("OpenAPI SDK 初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径回放(状态=3 时使用 routerPath)
|
||||
*/
|
||||
public String queryRoutePath(String licensePlate,
|
||||
Date startTime,
|
||||
Date endTime,
|
||||
Double startLon,
|
||||
Double startLat,
|
||||
Double endLon,
|
||||
Double endLat) {
|
||||
// vclN 去空格,不追加后缀;vco 固定 2(黄色)
|
||||
String vclN = licensePlate.trim().replaceAll("\\s+", "");
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("cid", openApiProperties.getClientId());
|
||||
params.put("srt", openApiProperties.getPrivateKey());
|
||||
params.put("vclN", vclN);
|
||||
params.put("vco", "2");
|
||||
params.put("qryBtm", formatDateTime(startTime));
|
||||
params.put("qryEtm", formatDateTime(endTime));
|
||||
params.put("parkMins", "15");
|
||||
params.put("startLonlat", formatLonLat(startLon, startLat));
|
||||
params.put("endLonlat", formatLonLat(endLon, endLat));
|
||||
|
||||
String apiUrl = buildApiUrl("/save/apis/routerPath");
|
||||
logger.info("调用 OpenAPI 路径回放 - vclN: {}, vco: {}, qryBtm: {}, qryEtm: {}, startLonlat: {}, endLonlat: {}, URL: {}",
|
||||
vclN, "2", params.get("qryBtm"), params.get("qryEtm"), params.get("startLonlat"), params.get("endLonlat"), apiUrl);
|
||||
// 记录请求参数(不含私钥)
|
||||
Map<String, String> logParams = new HashMap<>();
|
||||
logParams.put("cid", openApiProperties.getClientId());
|
||||
logParams.put("vclN", vclN);
|
||||
logParams.put("vco", "2");
|
||||
logParams.put("qryBtm", params.get("qryBtm"));
|
||||
logParams.put("qryEtm", params.get("qryEtm"));
|
||||
logParams.put("startLonlat", params.get("startLonlat"));
|
||||
logParams.put("endLonlat", params.get("endLonlat"));
|
||||
logParams.put("parkMins", "15");
|
||||
logger.info("OpenAPI 路径回放参数(不含srt): {}", logParams);
|
||||
try {
|
||||
String resp = dataExchangeService.postHttps(apiUrl, params);
|
||||
logger.info("OpenAPI 路径回放响应 raw: {}", resp);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
logger.error("调用路径回放接口失败 - 车牌号: {}", vclN, e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的 API URL
|
||||
*/
|
||||
private String buildApiUrl(String path) {
|
||||
String baseUrl = openApiProperties.getUrl();
|
||||
if (StringUtils.isBlank(baseUrl)) {
|
||||
throw new IllegalArgumentException("OpenAPI URL 配置为空");
|
||||
}
|
||||
|
||||
// 确保 baseUrl 不以 / 结尾
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||
}
|
||||
|
||||
// 确保 path 以 / 开头
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
return baseUrl + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 API 响应
|
||||
*/
|
||||
private List<OpenApiTrackPoint> parseResponse(String response) {
|
||||
List<OpenApiTrackPoint> trackPoints = new ArrayList<>();
|
||||
|
||||
if (StringUtils.isBlank(response)) {
|
||||
logger.warn("OpenAPI 响应为空");
|
||||
return trackPoints;
|
||||
}
|
||||
|
||||
try {
|
||||
JsonNode rootNode = objectMapper.readTree(response);
|
||||
|
||||
// 检查响应状态
|
||||
if (rootNode.has("status")) {
|
||||
int status = rootNode.path("status").asInt(-1);
|
||||
// 平台文档:1001 代表服务执行成功
|
||||
if (status != 0 && status != 1001) {
|
||||
String message = rootNode.path("message").asText("未知错误");
|
||||
logger.warn("OpenAPI 返回错误状态 - status: {}, message: {}", status, message);
|
||||
return trackPoints;
|
||||
}
|
||||
}
|
||||
|
||||
// 解析轨迹点数据
|
||||
// 注意:这里需要根据实际 API 返回格式调整解析逻辑
|
||||
JsonNode dataNode = rootNode.path("data");
|
||||
if (dataNode.isArray()) {
|
||||
for (JsonNode pointNode : dataNode) {
|
||||
OpenApiTrackPoint point = parseTrackPoint(pointNode);
|
||||
if (point != null) {
|
||||
trackPoints.add(point);
|
||||
}
|
||||
}
|
||||
} else if (dataNode.isObject()) {
|
||||
// 如果 data 是对象,可能包含 points 或其他字段
|
||||
JsonNode pointsNode = dataNode.path("points");
|
||||
if (pointsNode.isArray()) {
|
||||
for (JsonNode pointNode : pointsNode) {
|
||||
OpenApiTrackPoint point = parseTrackPoint(pointNode);
|
||||
if (point != null) {
|
||||
trackPoints.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 若未解析到轨迹点,尝试从 result.firstVcl 取最新定位(状态=1001 也视为成功)
|
||||
if (trackPoints.isEmpty()) {
|
||||
JsonNode resultNode = rootNode.path("result");
|
||||
JsonNode firstVcl = resultNode.path("firstVcl");
|
||||
OpenApiTrackPoint latest = parseFirstVcl(firstVcl);
|
||||
if (latest != null) {
|
||||
trackPoints.add(latest);
|
||||
logger.info("解析 OpenAPI firstVcl 成功,返回最新定位 1 条");
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("解析 OpenAPI 响应成功,获得 {} 个轨迹点", trackPoints.size());
|
||||
return trackPoints;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("解析 OpenAPI 响应失败 - response: {}", response, e);
|
||||
return trackPoints;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 firstVcl 为最新点(lat/lon 为 1/600000 WGS84)
|
||||
*/
|
||||
private OpenApiTrackPoint parseFirstVcl(JsonNode firstVcl) {
|
||||
if (firstVcl == null || firstVcl.isMissingNode() || firstVcl.isNull()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String vno = firstVcl.path("vno").asText("");
|
||||
String latStr = firstVcl.path("lat").asText("");
|
||||
String lonStr = firstVcl.path("lon").asText("");
|
||||
if (StringUtils.isBlank(latStr) || StringUtils.isBlank(lonStr)) {
|
||||
return null;
|
||||
}
|
||||
double lat = Double.parseDouble(latStr) / 600000d;
|
||||
double lon = Double.parseDouble(lonStr) / 600000d;
|
||||
|
||||
OpenApiTrackPoint point = new OpenApiTrackPoint();
|
||||
point.setLatitude(lat);
|
||||
point.setLongitude(lon);
|
||||
|
||||
// 时间戳 utc
|
||||
long utc = firstVcl.path("utc").asLong(0);
|
||||
if (utc > 0) {
|
||||
point.setLocTime(utc);
|
||||
}
|
||||
// 速度 km/h
|
||||
if (firstVcl.has("spd")) {
|
||||
point.setSpeed(firstVcl.path("spd").asDouble());
|
||||
}
|
||||
// 方向
|
||||
if (firstVcl.has("drc")) {
|
||||
point.setDirection(firstVcl.path("drc").asDouble());
|
||||
}
|
||||
logger.info("firstVcl 最新定位 - vno:{}, lat:{}, lon:{}, utc:{}, spd:{}, drc:{}",
|
||||
vno, lat, lon, utc, point.getSpeed(), point.getDirection());
|
||||
return point;
|
||||
} catch (Exception ex) {
|
||||
logger.warn("解析 firstVcl 失败", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个轨迹点
|
||||
*/
|
||||
private OpenApiTrackPoint parseTrackPoint(JsonNode pointNode) {
|
||||
try {
|
||||
OpenApiTrackPoint point = new OpenApiTrackPoint();
|
||||
|
||||
// 解析经纬度(根据实际 API 返回字段名调整)
|
||||
if (pointNode.has("latitude") || pointNode.has("lat")) {
|
||||
double lat = pointNode.path("latitude").asDouble(
|
||||
pointNode.path("lat").asDouble(0));
|
||||
point.setLatitude(lat);
|
||||
}
|
||||
|
||||
if (pointNode.has("longitude") || pointNode.has("lng") || pointNode.has("lon")) {
|
||||
double lng = pointNode.path("longitude").asDouble(
|
||||
pointNode.path("lng").asDouble(
|
||||
pointNode.path("lon").asDouble(0)));
|
||||
point.setLongitude(lng);
|
||||
}
|
||||
|
||||
// 验证经纬度有效性
|
||||
if (point.getLatitude() == null || point.getLongitude() == null ||
|
||||
point.getLatitude() == 0 && point.getLongitude() == 0 ||
|
||||
point.getLatitude() < -90 || point.getLatitude() > 90 ||
|
||||
point.getLongitude() < -180 || point.getLongitude() > 180) {
|
||||
logger.debug("跳过无效轨迹点 - lat: {}, lng: {}",
|
||||
point.getLatitude(), point.getLongitude());
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析时间(根据实际 API 返回格式调整)
|
||||
if (pointNode.has("locTime") || pointNode.has("time") || pointNode.has("timestamp")) {
|
||||
long locTime = pointNode.path("locTime").asLong(
|
||||
pointNode.path("time").asLong(
|
||||
pointNode.path("timestamp").asLong(0)));
|
||||
point.setLocTime(locTime);
|
||||
}
|
||||
|
||||
// 解析速度
|
||||
if (pointNode.has("speed")) {
|
||||
point.setSpeed(pointNode.path("speed").asDouble());
|
||||
}
|
||||
|
||||
// 解析方向
|
||||
if (pointNode.has("direction") || pointNode.has("bearing")) {
|
||||
point.setDirection(pointNode.path("direction").asDouble(
|
||||
pointNode.path("bearing").asDouble(0)));
|
||||
}
|
||||
|
||||
// 解析海拔
|
||||
if (pointNode.has("altitude") || pointNode.has("alt")) {
|
||||
point.setAltitude(pointNode.path("altitude").asDouble(
|
||||
pointNode.path("alt").asDouble(0)));
|
||||
}
|
||||
|
||||
return point;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.warn("解析轨迹点失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 车牌号清洗并追加颜色后缀
|
||||
*/
|
||||
private String buildVnos(String licensePlate, String colorCode) {
|
||||
String clean = licensePlate.trim().replaceAll("\\s+", "");
|
||||
if (StringUtils.isNotBlank(colorCode) && !clean.endsWith("_" + colorCode)) {
|
||||
clean = clean + "_" + colorCode;
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
private String formatDateTime(Date date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
// 平台未给出格式,这里使用常见格式 yyyy-MM-dd HH:mm:ss
|
||||
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
|
||||
}
|
||||
|
||||
private String formatLonLat(Double lon, Double lat) {
|
||||
if (lon == null || lat == null) {
|
||||
return "";
|
||||
}
|
||||
return String.format(java.util.Locale.US, "%.7f,%.7f", lon, lat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,8 @@ import com.aiotagro.cattletrade.business.dto.DeliveryAddDto;
|
||||
import com.aiotagro.cattletrade.business.dto.DeliveryCreateDto;
|
||||
import com.aiotagro.cattletrade.business.dto.DeliveryEditDto;
|
||||
import com.aiotagro.cattletrade.business.dto.DeliveryQueryDto;
|
||||
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanStayPoint;
|
||||
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanTrackPoint;
|
||||
import com.aiotagro.cattletrade.business.entity.*;
|
||||
import com.aiotagro.cattletrade.business.mapper.*;
|
||||
import com.aiotagro.cattletrade.business.service.BaiduYingyanService;
|
||||
import com.aiotagro.cattletrade.business.service.IDeliveryService;
|
||||
import com.aiotagro.cattletrade.business.service.IDeliveryDeviceService;
|
||||
import com.aiotagro.cattletrade.business.service.IXqClientService;
|
||||
@@ -69,14 +66,14 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
@Autowired
|
||||
private IDeliveryDeviceService deliveryDeviceService;
|
||||
|
||||
@Autowired
|
||||
private BaiduYingyanService baiduYingyanService;
|
||||
@Autowired
|
||||
private IXqClientService xqClientService;
|
||||
@Autowired
|
||||
private com.aiotagro.cattletrade.business.mapper.OrderMapper orderMapper;
|
||||
@Autowired
|
||||
private com.aiotagro.cattletrade.business.mapper.VehicleMapper vehicleMapper;
|
||||
@Autowired
|
||||
private com.aiotagro.cattletrade.business.service.OpenApiTrackService openApiTrackService;
|
||||
|
||||
/**
|
||||
* 列表查询
|
||||
@@ -571,38 +568,6 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
String newLicensePlate = StringUtils.isNotEmpty(dto.getLicensePlate()) ? dto.getLicensePlate().trim() : null;
|
||||
delivery.setLicensePlate(newLicensePlate);
|
||||
logger.info("更新车牌号: oldLicensePlate={}, newLicensePlate={}", oldLicensePlate, newLicensePlate);
|
||||
|
||||
// 如果车牌号发生变化,且运单状态为运输中(2),需要同步更新鹰眼终端名称
|
||||
if (StringUtils.isNotEmpty(newLicensePlate) &&
|
||||
delivery.getStatus() != null &&
|
||||
delivery.getStatus() == 2) {
|
||||
|
||||
// 检查车牌号是否真的发生了变化
|
||||
boolean licensePlateChanged = oldLicensePlate == null || !newLicensePlate.equals(oldLicensePlate);
|
||||
|
||||
if (licensePlateChanged) {
|
||||
String newEntityName = newLicensePlate;
|
||||
String oldEntityName = delivery.getYingyanEntityName();
|
||||
|
||||
// 更新鹰眼终端名称
|
||||
delivery.setYingyanEntityName(newEntityName);
|
||||
|
||||
// 如果旧的终端名称存在且与新名称不同,需要重新创建终端
|
||||
if (StringUtils.isNotEmpty(oldEntityName) && !oldEntityName.equals(newEntityName)) {
|
||||
logger.info("车牌号变更,重新创建鹰眼终端: oldEntityName={}, newEntityName={}", oldEntityName, newEntityName);
|
||||
boolean ensureResult = baiduYingyanService.ensureEntity(newEntityName);
|
||||
if (!ensureResult) {
|
||||
logger.warn("运单 {} 重新创建百度鹰眼终端失败,将在后台重试", delivery.getDeliveryNumber());
|
||||
}
|
||||
} else {
|
||||
// 如果之前没有终端名称,直接创建
|
||||
boolean ensureResult = baiduYingyanService.ensureEntity(newEntityName);
|
||||
if (!ensureResult) {
|
||||
logger.warn("运单 {} 创建百度鹰眼终端失败,将在后台重试", delivery.getDeliveryNumber());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dto.getBuyerId() != null) {
|
||||
delivery.setBuyerId(dto.getBuyerId());
|
||||
@@ -1663,156 +1628,116 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult queryYingyanTrack(Integer deliveryId) {
|
||||
public AjaxResult queryTrack(Integer deliveryId) {
|
||||
if (deliveryId == null) {
|
||||
return AjaxResult.error("运单ID不能为空");
|
||||
}
|
||||
|
||||
Delivery delivery = this.getById(deliveryId);
|
||||
if (delivery == null) {
|
||||
return AjaxResult.error("运单不存在");
|
||||
}
|
||||
|
||||
if (delivery.getStatus() == null || delivery.getStatus() == 1) {
|
||||
return AjaxResult.error("该运单尚未开始运输");
|
||||
}
|
||||
String entityName = StringUtils.defaultIfBlank(delivery.getYingyanEntityName(), delivery.getLicensePlate());
|
||||
if (StringUtils.isBlank(entityName)) {
|
||||
return AjaxResult.error("缺少车牌信息,无法查询轨迹");
|
||||
}
|
||||
// 去除空格,确保格式一致
|
||||
entityName = entityName.trim();
|
||||
|
||||
baiduYingyanService.ensureEntity(entityName);
|
||||
|
||||
// ✅ 按照用户要求:使用预计开始时间(estimatedDeliveryTime)作为轨迹查询的起始时间
|
||||
// 如果没有 estimatedDeliveryTime,则依次尝试 estimatedDepartureTime、createTime
|
||||
long startTime = dateToSeconds(delivery.getEstimatedDeliveryTime());
|
||||
if (startTime <= 0) {
|
||||
startTime = dateToSeconds(delivery.getEstimatedDepartureTime());
|
||||
}
|
||||
if (startTime <= 0) {
|
||||
startTime = dateToSeconds(delivery.getCreateTime());
|
||||
}
|
||||
if (startTime <= 0) {
|
||||
startTime = System.currentTimeMillis() / 1000 - 12 * 3600;
|
||||
// 获取车牌号(供 try/catch 内外使用)
|
||||
String licensePlate = delivery.getLicensePlate();
|
||||
try {
|
||||
if (StringUtils.isBlank(licensePlate)) {
|
||||
return AjaxResult.error("运单缺少车牌信息,无法查询轨迹");
|
||||
}
|
||||
|
||||
logger.info("运单 {} 轨迹查询开始时间确定 - estimatedDeliveryTime: {}, estimatedDepartureTime: {}, createTime: {}, 最终使用: {}",
|
||||
delivery.getDeliveryNumber(),
|
||||
delivery.getEstimatedDeliveryTime(),
|
||||
delivery.getEstimatedDepartureTime(),
|
||||
delivery.getCreateTime(),
|
||||
new Date(startTime * 1000));
|
||||
Integer status = delivery.getStatus();
|
||||
|
||||
long endTime = determineTrackEndTime(delivery);
|
||||
if (endTime <= startTime) {
|
||||
endTime = startTime + 3600;
|
||||
}
|
||||
// 状态 2 或 3:统一调用 routerPath
|
||||
Date start = safeStartTime(delivery);
|
||||
Date end = safeEndTime(delivery);
|
||||
|
||||
// ✅ 使用分段查询方法,支持超过24小时的轨迹查询
|
||||
long durationHours = Math.max(1, (endTime - startTime) / 3600);
|
||||
logger.info("查询运单 {} 的百度鹰眼轨迹 - entity={}, startTime={}, endTime={}, 时间跨度={}小时",
|
||||
delivery.getDeliveryNumber(), entityName, startTime, endTime, durationHours);
|
||||
// 生成坐标
|
||||
Double startLon = parseDoubleSafe(delivery.getStartLon());
|
||||
Double startLat = parseDoubleSafe(delivery.getStartLat());
|
||||
Double endLon = parseDoubleSafe(delivery.getEndLon());
|
||||
Double endLat = parseDoubleSafe(delivery.getEndLat());
|
||||
|
||||
// ✅ 确保终端存在后再查询(重要:如果终端不存在,查询会失败)
|
||||
boolean entityExists = baiduYingyanService.ensureEntity(entityName);
|
||||
if (!entityExists) {
|
||||
logger.error("运单 {} 终端不存在且创建失败,无法查询轨迹 - entity={}, deliveryNumber={}, deliveryId={}",
|
||||
delivery.getDeliveryNumber(), entityName, delivery.getDeliveryNumber(), delivery.getId());
|
||||
// 返回空结果,而不是继续查询(因为查询肯定会失败)
|
||||
logger.info("查询运单 {} 的路径回放 - 车牌号: {}, qryBtm: {}, qryEtm: {}",
|
||||
delivery.getDeliveryNumber(), licensePlate, start, end);
|
||||
logger.debug("路径回放参数 - deliveryId:{}, status:{}, start:{}, end:{}, startLonlat:{}, endLonlat:{}",
|
||||
deliveryId, status, start, end, formatLonLat(startLon, startLat), formatLonLat(endLon, endLat));
|
||||
|
||||
String response = openApiTrackService.queryRoutePath(licensePlate, start, end, startLon, startLat, endLon, endLat);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("trackPoints", Collections.emptyList());
|
||||
data.put("stayPoints", Collections.emptyList());
|
||||
data.put("entityName", entityName);
|
||||
data.put("startTime", startTime * 1000);
|
||||
data.put("endTime", endTime * 1000);
|
||||
data.put("status", delivery.getStatus());
|
||||
data.put("error", "终端不存在且创建失败,请检查百度鹰眼服务配置或终端名称是否正确");
|
||||
data.put("rawResponse", response);
|
||||
data.put("licensePlate", licensePlate);
|
||||
data.put("status", status);
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
|
||||
logger.info("运单 {} 终端已确保存在,开始查询轨迹 - entity={}",
|
||||
delivery.getDeliveryNumber(), entityName);
|
||||
|
||||
List<long[]> segmentRanges = buildTrackSegments(startTime, endTime);
|
||||
logger.info("运单 {} 将分 {} 段调用百度鹰眼接口(每段≤24小时)", delivery.getDeliveryNumber(), segmentRanges.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);
|
||||
data.put("stayPoints", stayPoints);
|
||||
data.put("entityName", entityName);
|
||||
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);
|
||||
}
|
||||
|
||||
private long determineTrackEndTime(Delivery delivery) {
|
||||
if (delivery.getStatus() != null && delivery.getStatus() == 3) {
|
||||
long arrivalTime = dateToSeconds(delivery.getArrivalTime());
|
||||
if (arrivalTime > 0) {
|
||||
return arrivalTime;
|
||||
} catch (Exception e) {
|
||||
logger.error("查询运单 {} 轨迹异常 - 车牌号: {}",
|
||||
delivery.getDeliveryNumber(), licensePlate, e);
|
||||
return AjaxResult.error("查询轨迹失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
return System.currentTimeMillis() / 1000;
|
||||
|
||||
/**
|
||||
* 状态=2:计算 timeNearby(分钟)
|
||||
* 按 estimatedDeliveryTime - estimatedDepartureTime,若结束时间在未来则用当前时间
|
||||
*/
|
||||
private String calculateNearbyByEstimate(Delivery delivery) {
|
||||
Date start = safeStartTime(delivery);
|
||||
Date end = safeEndTime(delivery);
|
||||
if (start == null || end == null) {
|
||||
return "30"; // 无时间信息时给默认 30 分钟
|
||||
}
|
||||
long diffMillis = end.getTime() - start.getTime();
|
||||
long diffMinutes = diffMillis / (1000 * 60);
|
||||
if (diffMinutes < 1) {
|
||||
diffMinutes = 1;
|
||||
}
|
||||
// 平台常见上限 1440/720,防止过大:这里上限 1440
|
||||
if (diffMinutes > 1440) {
|
||||
diffMinutes = 1440;
|
||||
}
|
||||
return String.valueOf(diffMinutes);
|
||||
}
|
||||
|
||||
private long dateToSeconds(Date date) {
|
||||
if (date == null) {
|
||||
return 0;
|
||||
private Date safeStartTime(Delivery delivery) {
|
||||
if (delivery.getEstimatedDepartureTime() != null) {
|
||||
return delivery.getEstimatedDepartureTime();
|
||||
}
|
||||
if (delivery.getCreateTime() != null) {
|
||||
return delivery.getCreateTime();
|
||||
}
|
||||
return new Date();
|
||||
}
|
||||
|
||||
private Date safeEndTime(Delivery delivery) {
|
||||
Date end = delivery.getEstimatedDeliveryTime() != null
|
||||
? delivery.getEstimatedDeliveryTime() : new Date();
|
||||
Date now = new Date();
|
||||
if (end.after(now)) {
|
||||
end = now;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
private String formatLonLat(Double lon, Double lat) {
|
||||
if (lon == null || lat == null) {
|
||||
return "";
|
||||
}
|
||||
return String.format(java.util.Locale.US, "%.7f,%.7f", lon, lat);
|
||||
}
|
||||
|
||||
private Double parseDoubleSafe(String val) {
|
||||
if (StringUtils.isBlank(val)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(val);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
return date.getTime() / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1845,53 +1770,6 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充 Delivery 列表的关联信息(优化性能,避免 N+1 查询)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.aiotagro.cattletrade.job;
|
||||
|
||||
import com.aiotagro.cattletrade.business.service.DeliveryYingyanSyncService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 百度鹰眼轨迹同步任务
|
||||
*/
|
||||
@Component
|
||||
public class BaiduYingyanSyncJob {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BaiduYingyanSyncJob.class);
|
||||
|
||||
@Autowired
|
||||
private DeliveryYingyanSyncService deliveryYingyanSyncService;
|
||||
|
||||
/**
|
||||
* 每两分钟同步一次轨迹
|
||||
*/
|
||||
@Scheduled(initialDelay = 30_000, fixedDelay = 120_000)
|
||||
public void syncDeliveryTrack() {
|
||||
try {
|
||||
logger.debug("开始执行运单鹰眼轨迹同步任务");
|
||||
deliveryYingyanSyncService.syncActiveDeliveries();
|
||||
} catch (Exception e) {
|
||||
logger.error("运单鹰眼轨迹同步任务执行失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.aiotagro.cattletrade.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* OpenAPI 配置属性类
|
||||
* 用于读取 OpenAPI SDK 的配置信息
|
||||
*
|
||||
* @author System
|
||||
* @date 2025-01-20
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "openapi")
|
||||
public class OpenApiProperties {
|
||||
|
||||
/**
|
||||
* API 地址(测试环境:https://openapi-test.sinoiov.cn)
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 客户端ID
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 私钥(用于 SDK 内部生成签名)
|
||||
*/
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 连接超时时间(毫秒),默认 5000
|
||||
*/
|
||||
private Integer connectTimeout = 5000;
|
||||
|
||||
/**
|
||||
* 读取超时时间(毫秒),默认 8000
|
||||
*/
|
||||
private Integer readTimeout = 8000;
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.aiotagro.common.core.constant;
|
||||
|
||||
/**
|
||||
* 百度鹰眼常量配置
|
||||
*
|
||||
* <p>注意:AK 与 ServiceId 根据业务要求写死在后端,禁止透出给前端。</p>
|
||||
*/
|
||||
public final class BaiduYingyanConstants {
|
||||
|
||||
private BaiduYingyanConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度鹰眼控制台申请的 AK
|
||||
*/
|
||||
public static final String AK = "3AN3VahoqaXUs32U8luXD2Dwn86KK5B7";
|
||||
|
||||
/**
|
||||
* 百度鹰眼服务 ID
|
||||
*/
|
||||
public static final long SERVICE_ID = 242517L;
|
||||
|
||||
/**
|
||||
* 百度鹰眼 API 基础路径
|
||||
*/
|
||||
public static final String BASE_URL = "https://yingyan.baidu.com/api/v3";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk1.8.0_111"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>openapi-sdk</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>edu.umd.cs.findbugs.plugin.eclipse.findbugsBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>edu.umd.cs.findbugs.plugin.eclipse.findbugsNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="openapi-sdk" default="jar" basedir=".">
|
||||
<property name="project.name" value="openapi-sdk" />
|
||||
<property name="build.dir" value="${basedir}/build" />
|
||||
<property name="build.classes.dir" value="${build.dir}/classes" />
|
||||
<property name="src.dir" value="${basedir}/src" />
|
||||
<property name="target.dir" value="${basedir}/target" />
|
||||
|
||||
|
||||
<target name="clean">
|
||||
<delete dir="${build.classes.dir}" />
|
||||
<delete dir="${target.dir}" />
|
||||
</target>
|
||||
<target name="init" depends="clean">
|
||||
<mkdir dir="${build.dir}" />
|
||||
<mkdir dir="${build.classes.dir}" />
|
||||
<mkdir dir="${target.dir}" />
|
||||
</target>
|
||||
<target name="compile" depends="init">
|
||||
<echo>=== COMPILE ===</echo>
|
||||
<echo>Compiling ${src.dir} files ...</echo>
|
||||
<javac debug="on" srcdir="${src.dir}" destdir="${build.classes.dir}" includes="**/*">
|
||||
<compilerarg line="-encoding UTF-8 " />
|
||||
</javac>
|
||||
</target>
|
||||
<target name="jar" depends="compile">
|
||||
<echo>=== PACKAGE ===</echo>
|
||||
<jar destfile="${target.dir}/${project.name}-4.0.jar">
|
||||
<fileset dir="${build.dir}/classes" includes="**/*.class" />
|
||||
</jar>
|
||||
</target>
|
||||
</project>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="EclipseModuleManager">
|
||||
<libelement value="jar://$MODULE_DIR$/lib/httpclient-4.1.2.jar!/" />
|
||||
<libelement value="jar://$MODULE_DIR$/lib/httpcore-4.1.2.jar!/" />
|
||||
<libelement value="jar://$MODULE_DIR$/lib/commons-logging-1.1.1.jar!/" />
|
||||
<src_description expected_position="0">
|
||||
<src_folder value="file://$MODULE_DIR$/src" expected_position="0" />
|
||||
<src_folder value="file://$MODULE_DIR$/resouce" expected_position="1" />
|
||||
<src_folder value="file://$MODULE_DIR$/lib" expected_position="2" />
|
||||
</src_description>
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<output url="file://$MODULE_DIR$/bin" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/resouce" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="jdk" jdkName="1.6" jdkType="JavaSDK" />
|
||||
<orderEntry type="module-library">
|
||||
<library name="httpclient-4.1.2.jar">
|
||||
<CLASSES>
|
||||
<root url="jar://$MODULE_DIR$/lib/httpclient-4.1.2.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</orderEntry>
|
||||
<orderEntry type="module-library">
|
||||
<library name="httpcore-4.1.2.jar">
|
||||
<CLASSES>
|
||||
<root url="jar://$MODULE_DIR$/lib/httpcore-4.1.2.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</orderEntry>
|
||||
<orderEntry type="module-library">
|
||||
<library name="commons-logging-1.1.1.jar">
|
||||
<CLASSES>
|
||||
<root url="jar://$MODULE_DIR$/lib/commons-logging-1.1.1.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</orderEntry>
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,11 @@
|
||||
2018年11月22日
|
||||
去掉Apache第三方相关的包
|
||||
|
||||
2018年2月24日 sdk去des解密
|
||||
2017年9月29日
|
||||
去除白名单异常类。
|
||||
|
||||
2017年5月10日
|
||||
开放平台SDK 包路径更换。
|
||||
--------------------
|
||||
数据共享平台-调用HTTPS请求客户端API
|
||||
@@ -91,7 +91,13 @@ sms:
|
||||
template-id: 2175348
|
||||
|
||||
openapi:
|
||||
url: http://api.aiotagro.com/api/business/xq/locationLog
|
||||
url: https://openapi-test.sinoiov.cn # 测试环境
|
||||
client-id: 262e6525-a1c8-43a1-aa35-b7d66f587a19
|
||||
private-key: 6b4d3006-19ce-4910-92e0-3ad4849f7af3
|
||||
username: 32d4175c-1c30-4350-83cc-dffdbde3b004
|
||||
password: HI57V6dZg26e92298539m971oLBHo7
|
||||
connect-timeout: 5000
|
||||
read-timeout: 8000
|
||||
|
||||
iot:
|
||||
url: http://aiot.aiotagro.com/iotPlateform/iotBusiness/sendCmd
|
||||
|
||||
Reference in New Issue
Block a user