初步完成轨迹

This commit is contained in:
xuqiuyun
2025-12-11 17:32:36 +08:00
parent 5b44cdb3eb
commit 3ce9757eff
30 changed files with 799 additions and 1703 deletions

View File

@@ -1,3 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic"
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
}

View File

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

View File

@@ -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;
};
// 组件卸载时清理

View File

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

View File

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

View File

@@ -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());
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
// 获取车牌号(供 try/catch 内外使用)
String licensePlate = delivery.getLicensePlate();
try {
if (StringUtils.isBlank(licensePlate)) {
return AjaxResult.error("运单缺少车牌信息,无法查询轨迹");
}
// ✅ 按照用户要求使用预计开始时间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;
}
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);
// ✅ 确保终端存在后再查询(重要:如果终端不存在,查询会失败)
boolean entityExists = baiduYingyanService.ensureEntity(entityName);
if (!entityExists) {
logger.error("运单 {} 终端不存在且创建失败,无法查询轨迹 - entity={}, deliveryNumber={}, deliveryId={}",
delivery.getDeliveryNumber(), entityName, delivery.getDeliveryNumber(), delivery.getId());
// 返回空结果,而不是继续查询(因为查询肯定会失败)
// 生成坐标
Double startLon = parseDoubleSafe(delivery.getStartLon());
Double startLat = parseDoubleSafe(delivery.getStartLat());
Double endLon = parseDoubleSafe(delivery.getEndLon());
Double endLat = parseDoubleSafe(delivery.getEndLat());
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);
} catch (Exception e) {
logger.error("查询运单 {} 轨迹异常 - 车牌号: {}",
delivery.getDeliveryNumber(), licensePlate, e);
return AjaxResult.error("查询轨迹失败:" + e.getMessage());
}
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;
}
/**
* 状态=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 分钟
}
return System.currentTimeMillis() / 1000;
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 查询)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
Manifest-Version: 1.0

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
2018年11月22日
去掉Apache第三方相关的包
2018年2月24日 sdk去des解密
2017年9月29日
去除白名单异常类。
2017年5月10日
开放平台SDK 包路径更换。
--------------------
数据共享平台-调用HTTPS请求客户端API

View File

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