集成百度鹰眼服务AK

This commit is contained in:
xuqiuyun
2025-11-21 17:22:20 +08:00
parent 73051df002
commit 0f963bf535
57 changed files with 6036 additions and 685 deletions

1
BaiduMapApi Submodule

Submodule BaiduMapApi added at 2e29dc1fd8

View File

@@ -47,7 +47,8 @@
"vue-use": "^0.2.0",
"vue3-baidu-map": "^1.0.0",
"vue3-json-viewer": "^2.2.2",
"vue3-print-nb": "^0.1.4"
"vue3-print-nb": "^0.1.4",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^17.1.2",

View File

@@ -48,6 +48,15 @@ export function inspectionList(data) {
data,
});
}
// 查询百度鹰眼轨迹与停留点
export function getYingyanTrack(data) {
return request({
url: '/delivery/yingyan/track',
method: 'POST',
data,
});
}
// 详情
export function waybillDetail(id) {
return request({

View File

@@ -265,6 +265,15 @@ export function orderGetDetail(id) {
});
}
// 批量导入订单
export function orderBatchImport(data) {
return request({
url: '/order/batchImport',
method: 'POST',
data,
});
}
// ==================== 运送清单管理新增 API ====================
// 逻辑删除运送清单

View File

@@ -109,7 +109,7 @@ export default defineComponent({
maxFileSize: {
// 新增最大文件大小属性
type: Number,
default: 2 * 1024 * 1024, // 默认值为2MB
default: 10 * 1024 * 1024, // 默认值为10MB
required: false,
},
},
@@ -188,7 +188,7 @@ export default defineComponent({
if (acceptType.value == 'video/*') {
dialogVisible.value = true;
}
// // },
},
imgPreviewClose: () => {
showImageViewer.value = false;
},

View File

@@ -308,4 +308,31 @@ el-form-item--large asterisk-left .top-title {
::v-deep(.el-form-item) {
width: 100%;
}
/* 修复标签与输入框的垂直居中与水平对齐问题 */
.top-screen :deep(.el-form-item) {
display: flex;
align-items: center;
}
.top-screen :deep(.el-form-item__label) {
display: flex;
align-items: center;
height: 40px; /* 与 Element Plus large 尺寸一致 */
line-height: 40px;
padding-right: 8px;
box-sizing: border-box;
white-space: nowrap;
}
.top-screen :deep(.el-input),
.top-screen :deep(.el-select),
.top-screen :deep(.el-date-editor),
.top-screen :deep(.el-cascader) {
height: 40px;
}
.top-screen :deep(.el-input__wrapper) {
min-height: 40px;
}
</style>

View File

@@ -0,0 +1,262 @@
import { defineStore } from 'pinia';
/**
* 运送清单表单数据缓存 Store
* 支持新增模式和编辑模式的表单数据缓存
*/
export const useDeliveryFormStore = defineStore('deliveryForm', {
state: () => {
return {
// 新增模式的缓存数据
newFormData: {
orderId: null as number | null,
shipper: null as number | null,
buyer: null as number | null,
plateNumber: null as string | null,
driverId: null as number | null,
driverPhone: '' as string,
serverId: null as number | null,
serverDeviceId: '' as string,
eartagIds: [] as number[],
collarIds: [] as number[],
eartagDeviceIds: [] as string[],
collarDeviceIds: [] as string[],
estimatedDepartureTime: '' as string,
estimatedArrivalTime: '' as string,
startLocation: '' as string,
endLocation: '' as string,
startLat: '' as string,
startLon: '' as string,
endLat: '' as string,
endLon: '' as string,
cattleCount: 1 as number,
quarantineCertNo: '' as string,
remark: '' as string,
emptyWeight: null as number | null,
entruckWeight: null as number | null,
landingEntruckWeight: null as number | null,
quarantineTickeyUrl: '' as string,
poundListImg: '' as string,
emptyVehicleFrontPhoto: '' as string,
loadedVehicleFrontPhoto: '' as string,
loadedVehicleWeightPhoto: '' as string,
driverIdCardPhoto: '' as string,
destinationPoundListImg: '' as string,
destinationVehicleFrontPhoto: '' as string,
entruckWeightVideo: '' as string,
emptyWeightVideo: '' as string,
cattleLoadingVideo: '' as string,
controlSlotVideo: '' as string,
cattleLoadingCircleVideo: '' as string,
unloadCattleVideo: '' as string,
destinationWeightVideo: '' as string,
},
// 编辑模式的缓存数据key为editId
editFormData: {} as Record<number, {
editId: number;
orderId: number | null;
shipper: number | null;
buyer: number | null;
plateNumber: string | null;
driverId: number | null;
driverPhone: string;
serverId: number | null;
serverDeviceId: string;
eartagIds: number[];
collarIds: number[];
eartagDeviceIds: string[];
collarDeviceIds: string[];
estimatedDepartureTime: string;
estimatedArrivalTime: string;
startLocation: string;
endLocation: string;
startLat: string;
startLon: string;
endLat: string;
endLon: string;
cattleCount: number;
quarantineCertNo: string;
remark: string;
emptyWeight: number | null;
entruckWeight: number | null;
landingEntruckWeight: number | null;
quarantineTickeyUrl: string;
poundListImg: string;
emptyVehicleFrontPhoto: string;
loadedVehicleFrontPhoto: string;
loadedVehicleWeightPhoto: string;
driverIdCardPhoto: string;
destinationPoundListImg: string;
destinationVehicleFrontPhoto: string;
entruckWeightVideo: string;
emptyWeightVideo: string;
cattleLoadingVideo: string;
controlSlotVideo: string;
cattleLoadingCircleVideo: string;
unloadCattleVideo: string;
destinationWeightVideo: string;
}>,
};
},
persist: {
enabled: true,
strategies: [
{
key: 'deliveryFormCache',
storage: localStorage,
},
],
},
actions: {
/**
* 保存新增模式表单数据
*/
saveNewFormData(data: Partial<typeof this.newFormData>) {
// 深度合并,确保数组和对象正确更新
Object.keys(data).forEach(key => {
const value = data[key];
if (Array.isArray(value)) {
// 数组类型,创建新数组
this.newFormData[key] = [...value] as any;
} else {
// 其他类型,直接赋值
this.newFormData[key] = value as any;
}
});
},
/**
* 获取新增模式表单数据
*/
getNewFormData() {
return { ...this.newFormData };
},
/**
* 清除新增模式表单数据
*/
clearNewFormData() {
this.newFormData = {
orderId: null,
shipper: null,
buyer: null,
plateNumber: null,
driverId: null,
driverPhone: '',
serverId: null,
serverDeviceId: '',
eartagIds: [],
collarIds: [],
eartagDeviceIds: [],
collarDeviceIds: [],
estimatedDepartureTime: '',
estimatedArrivalTime: '',
startLocation: '',
endLocation: '',
startLat: '',
startLon: '',
endLat: '',
endLon: '',
cattleCount: 1,
quarantineCertNo: '',
remark: '',
emptyWeight: null,
entruckWeight: null,
landingEntruckWeight: null,
quarantineTickeyUrl: '',
poundListImg: '',
emptyVehicleFrontPhoto: '',
loadedVehicleFrontPhoto: '',
loadedVehicleWeightPhoto: '',
driverIdCardPhoto: '',
destinationPoundListImg: '',
destinationVehicleFrontPhoto: '',
entruckWeightVideo: '',
emptyWeightVideo: '',
cattleLoadingVideo: '',
controlSlotVideo: '',
cattleLoadingCircleVideo: '',
unloadCattleVideo: '',
destinationWeightVideo: '',
};
},
/**
* 保存编辑模式表单数据
*/
saveEditFormData(editId: number, data: Partial<typeof this.editFormData[number]>) {
if (!this.editFormData[editId]) {
this.editFormData[editId] = {
editId,
orderId: null,
shipper: null,
buyer: null,
plateNumber: null,
driverId: null,
driverPhone: '',
serverId: null,
serverDeviceId: '',
eartagIds: [],
collarIds: [],
eartagDeviceIds: [],
collarDeviceIds: [],
estimatedDepartureTime: '',
estimatedArrivalTime: '',
startLocation: '',
endLocation: '',
startLat: '',
startLon: '',
endLat: '',
endLon: '',
cattleCount: 1,
quarantineCertNo: '',
remark: '',
emptyWeight: null,
entruckWeight: null,
landingEntruckWeight: null,
quarantineTickeyUrl: '',
poundListImg: '',
emptyVehicleFrontPhoto: '',
loadedVehicleFrontPhoto: '',
loadedVehicleWeightPhoto: '',
driverIdCardPhoto: '',
destinationPoundListImg: '',
destinationVehicleFrontPhoto: '',
entruckWeightVideo: '',
emptyWeightVideo: '',
cattleLoadingVideo: '',
controlSlotVideo: '',
cattleLoadingCircleVideo: '',
unloadCattleVideo: '',
destinationWeightVideo: '',
};
}
this.editFormData[editId] = { ...this.editFormData[editId], ...data, editId };
},
/**
* 获取编辑模式表单数据
*/
getEditFormData(editId: number) {
return this.editFormData[editId] ? { ...this.editFormData[editId] } : null;
},
/**
* 清除指定编辑模式表单数据
*/
clearEditFormData(editId: number) {
if (this.editFormData[editId]) {
delete this.editFormData[editId];
}
},
/**
* 清除所有缓存
*/
clearAllCache() {
this.clearNewFormData();
this.editFormData = {};
},
},
});

View File

@@ -58,12 +58,36 @@
</div>
</template>
</el-table-column>
<el-table-column prop="startLocation" label="起始地" />
<el-table-column prop="endLocation" label="送达目的地" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column prop="estimatedDeliveryTime" label="预计送达时间" />
<el-table-column prop="driverName" label="司机姓名" />
<el-table-column prop="createByName" label="创建人" />
<el-table-column prop="startLocation" label="起始地">
<template #default="scope">
{{ scope.row.startLocation || '--' }}
</template>
</el-table-column>
<el-table-column prop="endLocation" label="送达目的地">
<template #default="scope">
{{ scope.row.endLocation || '--' }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">
{{ scope.row.createTime || '--' }}
</template>
</el-table-column>
<el-table-column prop="estimatedDeliveryTime" label="预计送达时间">
<template #default="scope">
{{ scope.row.estimatedDeliveryTime || '--' }}
</template>
</el-table-column>
<el-table-column prop="driverName" label="司机姓名">
<template #default="scope">
{{ scope.row.driverName || '--' }}
</template>
</el-table-column>
<el-table-column prop="createByName" label="创建人">
<template #default="scope">
{{ scope.row.createByName || '--' }}
</template>
</el-table-column>
<el-table-column label="预警类型" prop="warningType">
<template #default="scope">
<el-tag type="warning" v-if="scope.row.warningType == 3">运输距离预警</el-tag>

View File

@@ -6,6 +6,20 @@
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 轨迹定位按钮 -->
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ dialogTitle }}</span>
<el-button
type="primary"
:icon="Location"
@click="handleTrackClick"
:disabled="!warningData.deliveryId"
>
轨迹定位
</el-button>
</div>
</template>
<!-- 温度预警 - 只显示设备信息不显示地图 -->
<div v-if="isTemperatureWarning" class="warning-content temperature-warning">
<!-- 预警基本信息 -->
@@ -54,6 +68,10 @@
<!-- 绑定设备列表 -->
<div v-if="deviceList.length > 0" class="device-list-section">
<!-- 调试信息开发环境可显示 -->
<div v-if="false" style="font-size: 12px; color: #909399; margin-bottom: 10px;">
调试设备数量={{ deviceList.length }}, deliveryId={{ warningData.deliveryId }}
</div>
<div class="section-header">
<h4>
<el-icon style="vertical-align: middle;"><Connection /></el-icon>
@@ -90,8 +108,16 @@
</el-table-column>
</el-table>
</div>
<div v-else class="no-data-tip">
<div v-else-if="!loadingDevices" class="no-data-tip">
<el-empty description="暂无绑定设备信息" :image-size="80" />
<div style="margin-top: 10px; font-size: 12px; color: #909399;">
<p v-if="!warningData.deliveryId">提示运单ID为空无法查询设备</p>
<p v-else>提示该运单可能没有绑定设备或设备已被删除</p>
</div>
</div>
<div v-else class="no-data-tip">
<el-icon class="is-loading"><Loading /></el-icon>
<span style="margin-left: 10px;">正在加载设备列表...</span>
</div>
<!-- 设备温度日志重点显示温度数据 -->
@@ -284,13 +310,118 @@
</span>
</template>
</el-dialog>
<!-- 轨迹定位对话框 -->
<el-dialog
v-model="trackDialogVisible"
title="轨迹定位"
width="1000px"
:close-on-click-modal="false"
@close="handleTrackDialogClose"
>
<div v-loading="trackLoading" style="min-height: 500px;">
<!-- 状态提示 -->
<div v-if="deliveryStatus === 1" class="status-tip">
<el-alert
title="运单尚未开始运输"
description="当前运单状态为准备中,暂无轨迹数据"
type="info"
:closable="false"
show-icon
/>
</div>
<!-- 轨迹地图容器 -->
<div v-else>
<!-- 控制按钮 -->
<div class="track-controls" style="margin-bottom: 15px;">
<el-button
type="primary"
:icon="VideoPlay"
@click="handlePlayTrack"
:disabled="!trackMapShow || trackPath.length === 0"
>
{{ isPlaying ? '暂停' : '播放' }}
</el-button>
<el-button
:icon="Refresh"
@click="handleResetTrack"
:disabled="!trackMapShow || trackPath.length === 0"
>
重置
</el-button>
<el-tag type="info" style="margin-left: 10px;">
轨迹点数{{ trackPath.length }}
</el-tag>
<el-tag type="info" style="margin-left: 10px;">
状态{{ getDeliveryStatusText(deliveryStatus) }}
</el-tag>
</div>
<!-- 地图容器 -->
<div
id="trackMap"
style="width: 100%; height: 500px; border: 1px solid #dcdfe6; border-radius: 4px;"
></div>
<!-- 无轨迹数据提示 -->
<div v-if="!trackMapShow && !trackLoading" class="no-track-tip">
<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="stayPoints.length > 0" class="staypoint-section">
<h4>停留点分析15分钟</h4>
<el-table :data="stayPoints" border style="width: 100%;" size="small">
<el-table-column label="开始时间" min-width="160">
<template #default="scope">
{{ formatTimestamp(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column label="结束时间" min-width="160">
<template #default="scope">
{{ formatTimestamp(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column label="停留时长" width="120">
<template #default="scope">
{{ formatDuration(scope.row.duration) }}
</template>
</el-table-column>
<el-table-column label="位置" min-width="160">
<template #default="scope">
{{ `${scope.row.latitude || '--'}, ${scope.row.longitude || '--'}` }}
</template>
</el-table-column>
</el-table>
</div>
<div v-else-if="trackMapShow && !trackLoading" class="no-data-tip">
暂无满足 15 分钟的停留点数据
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="trackDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, nextTick } from 'vue';
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 } from '@/api/abroad.js';
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs, getYingyanTrack, waybillDetail } from '@/api/abroad.js';
const dialogVisible = ref(false);
const warningData = reactive({
@@ -321,6 +452,28 @@ const loadingLogs = ref(false); // 新增:加载日志状态
let mapInstance = null;
let markerInstance = null;
// 轨迹定位相关
const trackDialogVisible = ref(false);
const trackLoading = ref(false);
const trackMapShow = ref(false);
const trackPath = ref([]); // 轨迹点数组
const trackMapInstance = ref(null); // 轨迹地图实例
const trackPolyline = ref(null); // 轨迹线条
const trackStartMarker = ref(null); // 起点标记
const trackEndMarker = ref(null); // 终点标记
const trackPlayMarker = ref(null); // 播放位置标记
const deliveryStatus = ref(null); // 运输状态1-准备中2-运输中3-已结束
const isPlaying = ref(false); // 是否正在播放
const playTimer = ref(null); // 播放定时器
const currentPlayIndex = ref(0); // 当前播放到的轨迹点索引
const trackBMapGL = ref(null); // 保存 BMapGL 实例,避免重复加载
const stayPoints = ref([]); // 停留点列表
const yingyanMeta = reactive({
entityName: '',
startTime: null,
endTime: null
});
// 计算属性:判断预警类型
const isTemperatureWarning = computed(() => {
// 5-高温预警6-低温预警
@@ -344,6 +497,7 @@ const dialogTitle = computed(() => {
// 打开对话框
const open = async (row) => {
console.log('[WARNING-DETAIL] 打开预警详情对话框,原始数据:', row);
// 填充数据
Object.keys(warningData).forEach(key => {
@@ -352,12 +506,18 @@ const open = async (row) => {
}
});
console.log('[WARNING-DETAIL] 填充后的 warningData:', warningData);
console.log('[WARNING-DETAIL] deliveryId:', warningData.deliveryId);
dialogVisible.value = true;
// ✅ 查询运单绑定的设备列表
if (warningData.deliveryId) {
console.log('[WARNING-DETAIL] 开始加载设备列表deliveryId:', warningData.deliveryId);
await loadDeviceList(warningData.deliveryId);
} else {
console.warn('[WARNING-DETAIL] 警告deliveryId 为空,无法加载设备列表');
console.warn('[WARNING-DETAIL] 请检查预警详情 API 是否返回了 deliveryId 字段');
}
// 如果是位置相关预警,加载地图
@@ -379,6 +539,11 @@ const loadDeviceList = async (deliveryId) => {
loadingDevices.value = true;
try {
console.log('[WARNING-DETAIL] 调用 pageDeviceList API参数:', {
deliveryId: deliveryId,
pageNum: 1,
pageSize: 100
});
const res = await pageDeviceList({
deliveryId: deliveryId,
@@ -386,22 +551,41 @@ const loadDeviceList = async (deliveryId) => {
pageSize: 100, // 一次性加载所有设备
});
console.log('[WARNING-DETAIL] pageDeviceList API 返回结果:', res);
if (res.code === 200 && res.data) {
// ✅ 修复:后端直接返回数组,不是嵌套在 list 或 rows 中
let devices = [];
if (Array.isArray(res.data)) {
deviceList.value = res.data;
devices = res.data;
} else {
deviceList.value = res.data.list || res.data.rows || [];
devices = res.data.list || res.data.rows || [];
}
// 自动加载所有设备的日志
await loadAllDeviceLogs();
console.log('[WARNING-DETAIL] 解析后的设备列表:', devices);
console.log('[WARNING-DETAIL] 设备数量:', devices.length);
deviceList.value = devices;
if (devices.length === 0) {
console.warn('[WARNING-DETAIL] 警告:设备列表为空');
console.warn('[WARNING-DETAIL] 可能原因:');
console.warn('[WARNING-DETAIL] 1. 该运单确实没有绑定设备');
console.warn('[WARNING-DETAIL] 2. 设备被标记为已删除is_delet=1');
console.warn('[WARNING-DETAIL] 3. 设备表中的 delivery_id 字段为空或不匹配');
ElMessage.warning('该运单暂无绑定设备');
} else {
console.log('[WARNING-DETAIL] 成功加载设备列表,设备数量:', devices.length);
// 自动加载所有设备的日志
await loadAllDeviceLogs();
}
} else {
console.error('[WARNING-DETAIL] API 返回错误:', res);
ElMessage.warning('加载设备列表失败:' + (res.msg || '未知错误'));
}
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备列表失败:', error);
ElMessage.error('加载设备列表失败');
console.error('[WARNING-DETAIL] 加载设备列表异常:', error);
ElMessage.error('加载设备列表失败' + (error.message || '网络错误'));
} finally {
loadingDevices.value = false;
}
@@ -586,6 +770,52 @@ const getWarningTagType = (type) => {
}
};
const normalizeTimestamp = (value) => {
if (value === null || value === undefined || value === '') {
return null;
}
const num = Number(value);
if (Number.isNaN(num)) {
return null;
}
return num < 1e12 ? num * 1000 : num;
};
const formatTimestamp = (value) => {
const ms = normalizeTimestamp(value);
if (!ms) return '--';
const date = new Date(ms);
if (Number.isNaN(date.getTime())) {
return '--';
}
const pad = (num) => `${num}`.padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const formatDuration = (seconds) => {
if (seconds === null || seconds === undefined) {
return '--';
}
const total = Number(seconds);
if (Number.isNaN(total) || total <= 0) {
return '--';
}
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
const sec = Math.floor(total % 60);
const parts = [];
if (hours > 0) {
parts.push(`${hours}小时`);
}
if (minutes > 0) {
parts.push(`${minutes}分钟`);
}
if (sec > 0 && hours === 0) {
parts.push(`${sec}`);
}
return parts.join('') || `${sec}`;
};
// 关闭对话框
const handleClose = () => {
// 清理地图实例
@@ -614,6 +844,436 @@ const handleClose = () => {
});
};
// 轨迹定位按钮点击
const handleTrackClick = async () => {
if (!warningData.deliveryId) {
ElMessage.warning('运单ID不存在无法查看轨迹');
return;
}
trackDialogVisible.value = true;
trackLoading.value = true;
trackMapShow.value = false;
trackPath.value = [];
isPlaying.value = false;
currentPlayIndex.value = 0;
// 获取运单运输状态
await getDeliveryStatus();
// 如果状态为准备中,直接返回
if (deliveryStatus.value === 1) {
trackLoading.value = false;
return;
}
await loadYingyanTrack();
if (trackPath.value.length === 0) {
trackLoading.value = false;
return;
}
// 初始化地图
await nextTick();
await initTrackMap();
};
// 获取运送清单运输状态
const getDeliveryStatus = async () => {
// 先检查 warningData 中是否包含 status
if (warningData.status !== undefined && warningData.status !== null) {
// 注意:这里的 status 可能是业务状态1-7需要判断是否是运输状态1-3
// 如果 status 在 1-3 范围内,可能是运输状态
const status = parseInt(warningData.status);
if (status >= 1 && status <= 3) {
deliveryStatus.value = status;
return;
}
}
// 如果没有,通过 API 获取运单详情
try {
const res = await waybillDetail(warningData.deliveryId);
if (res.code === 200 && res.data) {
const delivery = res.data.delivery || res.data;
// 注意Delivery 实体中的 status 是业务状态1-7不是运输状态
// 需要查看是否有专门的运输状态字段,或者根据业务状态推断
// 这里先假设 status 字段就是运输状态,如果不对需要调整
deliveryStatus.value = delivery.status || null;
}
} catch (error) {
console.error('[TRACK] 获取运单状态失败:', error);
// 默认设置为运输中,允许查看轨迹
deliveryStatus.value = 2;
}
};
// 加载百度鹰眼轨迹与停留点
const loadYingyanTrack = async () => {
stayPoints.value = [];
trackPath.value = [];
trackMapShow.value = false;
yingyanMeta.entityName = '';
yingyanMeta.startTime = null;
yingyanMeta.endTime = null;
if (!warningData.deliveryId) {
ElMessage.warning('运单ID缺失无法查询轨迹');
return;
}
try {
const res = await getYingyanTrack({ deliveryId: warningData.deliveryId });
if (res.code === 200 && res.data) {
const rawPoints = Array.isArray(res.data.trackPoints) ? res.data.trackPoints : [];
trackPath.value = rawPoints
.map(item => {
const lng = parseFloat(item.longitude ?? item.lng ?? 0);
const lat = parseFloat(item.latitude ?? item.lat ?? 0);
if (Number.isNaN(lng) || Number.isNaN(lat) || lng === 0 || lat === 0) {
return null;
}
return {
lng,
lat,
locTime: item.locTime
};
})
.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;
if (trackPath.value.length > 0) {
trackMapShow.value = true;
} else {
ElMessage.warning('暂无有效轨迹点');
}
} else {
ElMessage.warning(res.msg || '暂无轨迹数据');
}
} catch (error) {
console.error('[TRACK] 加载百度鹰眼轨迹失败:', error);
ElMessage.error('加载轨迹数据失败');
}
};
// 初始化轨迹地图
const initTrackMap = async () => {
if (trackPath.value.length === 0) {
return;
}
try {
const BMapGL = await BMPGL('fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC');
trackBMapGL.value = BMapGL; // 保存 BMapGL 实例
// 创建地图实例
trackMapInstance.value = new BMapGL.Map('trackMap');
// 计算地图中心点和缩放级别
const bounds = calculateBounds(trackPath.value);
const centerPoint = new BMapGL.Point(bounds.center.lng, bounds.center.lat);
trackMapInstance.value.centerAndZoom(centerPoint, bounds.zoom);
trackMapInstance.value.enableScrollWheelZoom(true);
// 根据状态绘制轨迹
if (deliveryStatus.value === 3) {
// 已结束:静态轨迹
drawStaticTrack(BMapGL);
} else if (deliveryStatus.value === 2) {
// 运输中:动态轨迹(先绘制已有部分)
drawDynamicTrack(BMapGL);
}
trackLoading.value = false;
} catch (error) {
console.error('[TRACK] 地图初始化失败:', error);
ElMessage.error('地图加载失败');
trackLoading.value = false;
}
};
// 计算轨迹边界
const calculateBounds = (points) => {
if (points.length === 0) {
return { center: { lng: 116.404, lat: 39.915 }, zoom: 15 };
}
let minLng = points[0].lng;
let maxLng = points[0].lng;
let minLat = points[0].lat;
let maxLat = points[0].lat;
points.forEach(point => {
minLng = Math.min(minLng, point.lng);
maxLng = Math.max(maxLng, point.lng);
minLat = Math.min(minLat, point.lat);
maxLat = Math.max(maxLat, point.lat);
});
const center = {
lng: (minLng + maxLng) / 2,
lat: (minLat + maxLat) / 2
};
// 计算缩放级别(简单估算)
const lngDiff = maxLng - minLng;
const latDiff = maxLat - minLat;
const maxDiff = Math.max(lngDiff, latDiff);
let zoom = 15;
if (maxDiff > 0.1) zoom = 10;
else if (maxDiff > 0.05) zoom = 11;
else if (maxDiff > 0.02) zoom = 12;
else if (maxDiff > 0.01) zoom = 13;
else if (maxDiff > 0.005) zoom = 14;
return { center, zoom };
};
// 绘制静态轨迹(已结束)
const drawStaticTrack = (BMapGL) => {
if (!trackMapInstance.value || trackPath.value.length === 0) {
return;
}
// 清除之前的覆盖物
trackMapInstance.value.clearOverlays();
// 绘制轨迹线
const polyline = new BMapGL.Polyline(
trackPath.value.map(p => new BMapGL.Point(p.lng, p.lat)),
{
strokeColor: '#3388ff',
strokeWeight: 4,
strokeOpacity: 0.8
}
);
trackMapInstance.value.addOverlay(polyline);
trackPolyline.value = polyline;
// 添加起点标记
if (trackPath.value.length > 0) {
const startPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
// 使用简单的圆形标记作为起点(绿色)
const startMarker = new BMapGL.Marker(startPoint, {
icon: new BMapGL.Icon(
'http://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(startMarker);
trackStartMarker.value = startMarker;
// 起点信息窗口
const startInfoWindow = new BMapGL.InfoWindow(
'<div style="padding: 5px;"><strong style="color: #67c23a;">起点</strong></div>',
{ width: 80, height: 30 }
);
startMarker.addEventListener('click', () => {
trackMapInstance.value.openInfoWindow(startInfoWindow, startPoint);
});
}
// 添加终点标记
if (trackPath.value.length > 1) {
const endPoint = new BMapGL.Point(
trackPath.value[trackPath.value.length - 1].lng,
trackPath.value[trackPath.value.length - 1].lat
);
// 使用红色标记作为终点
const endMarker = new BMapGL.Marker(endPoint, {
icon: new BMapGL.Icon(
'http://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(endMarker);
trackEndMarker.value = endMarker;
// 终点信息窗口
const endInfoWindow = new BMapGL.InfoWindow(
'<div style="padding: 5px;"><strong style="color: #f56c6c;">终点</strong></div>',
{ width: 80, height: 30 }
);
endMarker.addEventListener('click', () => {
trackMapInstance.value.openInfoWindow(endInfoWindow, endPoint);
});
}
};
// 绘制动态轨迹(运输中)
const drawDynamicTrack = (BMapGL) => {
if (!trackMapInstance.value || trackPath.value.length === 0) {
return;
}
// 清除之前的覆盖物
trackMapInstance.value.clearOverlays();
// 绘制已有轨迹线
const polyline = new BMapGL.Polyline(
trackPath.value.map(p => new BMapGL.Point(p.lng, p.lat)),
{
strokeColor: '#3388ff',
strokeWeight: 4,
strokeOpacity: 0.6
}
);
trackMapInstance.value.addOverlay(polyline);
trackPolyline.value = polyline;
// 添加起点标记
if (trackPath.value.length > 0) {
const startPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
const startMarker = new BMapGL.Marker(startPoint, {
icon: new BMapGL.Icon(
'http://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(startMarker);
trackStartMarker.value = startMarker;
}
// 添加当前位置标记(用于动画)
const currentPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
const playMarker = new BMapGL.Marker(currentPoint, {
icon: new BMapGL.Icon(
'http://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(20, 20),
{ anchor: new BMapGL.Size(10, 20) }
)
});
trackMapInstance.value.addOverlay(playMarker);
trackPlayMarker.value = playMarker;
currentPlayIndex.value = 0;
};
// 播放轨迹动画
const handlePlayTrack = () => {
if (trackPath.value.length === 0 || !trackMapInstance.value) {
return;
}
if (isPlaying.value) {
// 暂停
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
} else {
// 播放
if (currentPlayIndex.value >= trackPath.value.length - 1) {
// 如果已经播放完,重新开始
currentPlayIndex.value = 0;
}
isPlaying.value = true;
playTimer.value = setInterval(() => {
if (currentPlayIndex.value < trackPath.value.length - 1) {
currentPlayIndex.value++;
updatePlayMarker();
} else {
// 播放完成
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
}
}, 200); // 每200ms移动一次
}
};
// 更新播放位置标记
const updatePlayMarker = () => {
if (!trackMapInstance.value || !trackPlayMarker.value || !trackBMapGL.value || currentPlayIndex.value >= trackPath.value.length) {
return;
}
const point = trackPath.value[currentPlayIndex.value];
const bdPoint = new trackBMapGL.value.Point(point.lng, point.lat);
trackPlayMarker.value.setPosition(bdPoint);
// 地图跟随
trackMapInstance.value.panTo(bdPoint);
};
// 重置轨迹动画
const handleResetTrack = () => {
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
currentPlayIndex.value = 0;
if (trackPlayMarker.value && trackPath.value.length > 0 && trackMapInstance.value && trackBMapGL.value) {
const point = trackPath.value[0];
const bdPoint = new trackBMapGL.value.Point(point.lng, point.lat);
trackPlayMarker.value.setPosition(bdPoint);
trackMapInstance.value.panTo(bdPoint);
}
};
// 获取运输状态文本
const getDeliveryStatusText = (status) => {
const statusMap = {
1: '准备中',
2: '运输中',
3: '已结束'
};
return statusMap[status] || '未知';
};
// 关闭轨迹对话框
const handleTrackDialogClose = () => {
// 清理定时器
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
// 清理地图实例
if (trackMapInstance.value) {
trackMapInstance.value.clearOverlays();
trackMapInstance.value = null;
}
trackPolyline.value = null;
trackStartMarker.value = null;
trackEndMarker.value = null;
trackPlayMarker.value = null;
trackBMapGL.value = null;
isPlaying.value = false;
currentPlayIndex.value = 0;
trackPath.value = [];
trackMapShow.value = false;
stayPoints.value = [];
yingyanMeta.entityName = '';
yingyanMeta.startTime = null;
yingyanMeta.endTime = null;
};
// 组件卸载时清理
onUnmounted(() => {
if (playTimer.value) {
clearInterval(playTimer.value);
}
if (trackMapInstance.value) {
trackMapInstance.value.clearOverlays();
}
});
// 导出方法
defineExpose({
open
@@ -676,6 +1336,21 @@ defineExpose({
}
}
.track-meta-panel {
margin-top: 20px;
}
.staypoint-section {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
font-size: 15px;
color: #303133;
font-weight: 600;
}
}
.map-container {
margin-top: 20px;
@@ -721,5 +1396,24 @@ defineExpose({
font-size: 12px;
}
}
// 轨迹对话框样式
.status-tip {
margin-bottom: 20px;
}
.track-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.no-track-tip {
display: flex;
align-items: center;
justify-content: center;
height: 500px;
}
</style>

View File

@@ -111,7 +111,13 @@
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<Pagination
v-if="data.total > 0"
v-model:limit="form.pageSize"
v-model:page="form.pageNum"
:total="data.total"
@pagination="handlePagination"
/>
</div>
<!-- 编辑对话框 -->
@@ -124,6 +130,7 @@ import { ref, reactive, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import baseSearch from '@/components/common/searchCustom/index.vue';
import Pagination from '@/components/Pagination/index.vue';
import createDeliveryDialog from '@/views/shipping/createDeliveryDialog.vue';
import { inspectionList, downloadZip, pageDeviceList } from '@/api/abroad.js';
import { updateDeliveryStatus, deleteDeliveryLogic, downloadDeliveryPackage, getDeliveryDetail, downloadAcceptanceForm } from '@/api/shipping.js';
@@ -136,11 +143,11 @@ const dataListLoading = ref(false);
const downLoading = reactive({});
const form = reactive({
pageNum: 1,
pageSize: 20,
pageSize: 10,
});
const data = reactive({
rows: [],
total: 20,
total: 10,
deviceCounts: {}, // 存储每个订单的设备数量 {orderId: {host: 0, ear: 0, collar: 0, total: 0}}
});
const formItemList = reactive([
@@ -235,6 +242,18 @@ const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
// 处理分页事件
const handlePagination = (paginationData) => {
console.log('[PAGINATION] 分页变化:', paginationData);
if (paginationData) {
form.pageNum = paginationData.page || form.pageNum;
form.pageSize = paginationData.limit || form.pageSize;
console.log('[PAGINATION] 更新后的分页参数 - pageNum:', form.pageNum, 'pageSize:', form.pageSize);
}
getDataList();
};
const getDataList = () => {
dataListLoading.value = true;
@@ -263,34 +282,46 @@ const getDataList = () => {
}
console.log('[INSPECTION-LIST] 请求参数:', params);
inspectionList(params)
.then(async (ret) => {
console.log('[INSPECTION-LIST] 响应数据:', ret);
data.rows = ret.data.rows;
data.total = ret.data.total;
// 处理响应数据结构
// PageResultResponse 返回的结构是 { code, msg, data: { total, rows } }
let responseData = ret.data || ret;
// 如果 data 是对象且包含 total 和 rows直接使用
if (responseData && typeof responseData === 'object' && 'total' in responseData && 'rows' in responseData) {
data.rows = responseData.rows || [];
data.total = responseData.total || 0;
} else if (responseData && responseData.data) {
// 如果 data 是嵌套的,使用 data.data
data.rows = responseData.data.rows || [];
data.total = responseData.data.total || 0;
} else {
// 兜底处理
data.rows = [];
data.total = 0;
}
console.log('[INSPECTION-LIST] 解析后的数据 - 总数:', data.total, '当前页数据:', data.rows.length);
dataListLoading.value = false;
// 为每个订单获取设备数量
if (ret.data.rows && ret.data.rows.length > 0) {
for (const row of ret.data.rows) {
if (data.rows && data.rows.length > 0) {
for (const row of data.rows) {
if (row.id) {
await getDeviceCounts(row.id);
}
}
}
// 调试:检查第一行数据的字段
if (ret.data.rows && ret.data.rows.length > 0) {
const firstRow = ret.data.rows[0];
// 检查Word导出所需字段
}
})
.catch(() => {
.catch((error) => {
console.error('[INSPECTION-LIST] 请求失败:', error);
dataListLoading.value = false;
data.rows = [];
data.total = 0;
});
};
// 详情

View File

@@ -80,183 +80,187 @@
</div>
<div class="info-box">
<div class="title">装车信息</div>
<el-descriptions :column="4">
<!-- 重量信息 -->
<el-descriptions-item label="空车过磅重量:">{{
data.baseInfo.emptyWeight ? data.baseInfo.emptyWeight + 'kg' : ''
}}</el-descriptions-item>
<el-descriptions-item label="装车过磅重量:">{{
data.baseInfo.entruckWeight ? data.baseInfo.entruckWeight + 'kg' : ''
}}</el-descriptions-item>
<el-descriptions-item label="落地过磅重量:">{{
data.baseInfo.landingEntruckWeight ? data.baseInfo.landingEntruckWeight + 'kg' : ''
}}</el-descriptions-item>
<!-- 照片上传区域 -->
<el-descriptions-item label="检疫票:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.quarantineTickeyUrl"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.quarantineTickeyUrl ? data.baseInfo.quarantineTickeyUrl : ''"
fit="cover"
:preview-src-list="[data.baseInfo.quarantineTickeyUrl] ? [data.baseInfo.quarantineTickeyUrl] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="纸质磅单:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.poundListImg"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.poundListImg ? data.baseInfo.poundListImg : ''"
fit="cover"
:preview-src-list="[data.baseInfo.poundListImg] ? [data.baseInfo.poundListImg] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="车辆空磅上磅车头照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.emptyVehicleFrontPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.emptyVehicleFrontPhoto ? data.baseInfo.emptyVehicleFrontPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.emptyVehicleFrontPhoto] ? [data.baseInfo.emptyVehicleFrontPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="车辆过重磅车头照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.loadedVehicleFrontPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.loadedVehicleFrontPhoto ? data.baseInfo.loadedVehicleFrontPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleFrontPhoto] ? [data.baseInfo.loadedVehicleFrontPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="车辆重磅照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.loadedVehicleWeightPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.loadedVehicleWeightPhoto ? data.baseInfo.loadedVehicleWeightPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleWeightPhoto] ? [data.baseInfo.loadedVehicleWeightPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="驾驶员手持身份证站车头照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.driverIdCardPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.driverIdCardPhoto ? data.baseInfo.driverIdCardPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.driverIdCardPhoto] ? [data.baseInfo.driverIdCardPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<!-- 重量信息分组 -->
<div v-if="hasValue(data.baseInfo.emptyWeight) || hasValue(data.baseInfo.entruckWeight) || hasValue(data.baseInfo.landingEntruckWeight)" class="info-group">
<div class="group-title">重量信息</div>
<el-descriptions :column="3" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyWeight)" label="空车过磅重量:">
{{ data.baseInfo.emptyWeight }}kg
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckWeight)" label="装车过磅重量:">
{{ data.baseInfo.entruckWeight }}kg
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.landingEntruckWeight)" label="落地过磅重量:">
{{ data.baseInfo.landingEntruckWeight }}kg
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 到地相关照片 -->
<el-descriptions-item label="到地纸质磅单:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.destinationPoundListImg"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.destinationPoundListImg ? data.baseInfo.destinationPoundListImg : ''"
fit="cover"
:preview-src-list="[data.baseInfo.destinationPoundListImg] ? [data.baseInfo.destinationPoundListImg] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="到地车辆过重磅车头照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.destinationVehicleFrontPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.destinationVehicleFrontPhoto ? data.baseInfo.destinationVehicleFrontPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.destinationVehicleFrontPhoto] ? [data.baseInfo.destinationVehicleFrontPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<!-- 照片信息分组 -->
<div v-if="hasValue(data.baseInfo.quarantineTickeyUrl) || hasValue(data.baseInfo.poundListImg) || hasValue(data.baseInfo.emptyVehicleFrontPhoto) || hasValue(data.baseInfo.loadedVehicleFrontPhoto) || hasValue(data.baseInfo.loadedVehicleWeightPhoto) || hasValue(data.baseInfo.driverIdCardPhoto) || hasValue(data.baseInfo.destinationPoundListImg) || hasValue(data.baseInfo.destinationVehicleFrontPhoto)" class="info-group">
<div class="group-title">照片信息</div>
<el-descriptions :column="3" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.quarantineTickeyUrl)" label="检疫票:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.quarantineTickeyUrl"
fit="cover"
:preview-src-list="[data.baseInfo.quarantineTickeyUrl]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.poundListImg)" label="纸质磅单:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.poundListImg"
fit="cover"
:preview-src-list="[data.baseInfo.poundListImg]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyVehicleFrontPhoto)" label="车辆空磅上磅车头照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.emptyVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.emptyVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.loadedVehicleFrontPhoto)" label="车辆过重磅车头照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.loadedVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.loadedVehicleWeightPhoto)" label="车辆重磅照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.loadedVehicleWeightPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleWeightPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.driverIdCardPhoto)" label="驾驶员手持身份证站车头照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.driverIdCardPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.driverIdCardPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationPoundListImg)" label="到地纸质磅单:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.destinationPoundListImg"
fit="cover"
:preview-src-list="[data.baseInfo.destinationPoundListImg]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationVehicleFrontPhoto)" label="到地车辆过重磅车头照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.destinationVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.destinationVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 视频上传区域 -->
<el-descriptions-item label="空车过磅视频(含车牌、地磅数):">
<span style="vertical-align: top" v-if="data.baseInfo.emptyWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.emptyWeightVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="装车过磅视频:">
<span style="vertical-align: top" v-if="data.baseInfo.entruckWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.entruckWeightVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="装车视频:">
<span style="vertical-align: top" v-if="data.baseInfo.entruckVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.entruckVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="控槽视频:">
<span style="vertical-align: top" v-if="data.baseInfo.controlSlotVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.controlSlotVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="装完牛绕车一圈视频:">
<span style="vertical-align: top" v-if="data.baseInfo.cattleLoadingCircleVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.cattleLoadingCircleVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="卸牛视频:">
<span style="vertical-align: top" v-if="data.baseInfo.unloadCattleVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.unloadCattleVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="到地过磅视频:">
<span style="vertical-align: top" v-if="data.baseInfo.destinationWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.destinationWeightVideo"
/>
</span>
</el-descriptions-item>
</el-descriptions>
<!-- 视频信息分组 -->
<div v-if="hasValue(data.baseInfo.emptyWeightVideo) || hasValue(data.baseInfo.entruckWeightVideo) || hasValue(data.baseInfo.entruckVideo) || hasValue(data.baseInfo.controlSlotVideo) || hasValue(data.baseInfo.cattleLoadingCircleVideo) || hasValue(data.baseInfo.unloadCattleVideo) || hasValue(data.baseInfo.destinationWeightVideo)" class="info-group">
<div class="group-title">视频信息</div>
<el-descriptions :column="1" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyWeightVideo)" label="空车过磅视频(含车牌、地磅数):">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.emptyWeightVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckWeightVideo)" label="装车过磅视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.entruckWeightVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckVideo)" label="装车视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.entruckVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.controlSlotVideo)" label="控槽视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.controlSlotVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.cattleLoadingCircleVideo)" label="装完牛绕车一圈视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.cattleLoadingCircleVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.unloadCattleVideo)" label="卸牛视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.unloadCattleVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationWeightVideo)" label="到地过磅视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.destinationWeightVideo"
/>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<div class="ear-box">
<div class="title">智能主机</div>
@@ -1158,6 +1162,17 @@ const getStatusType = (status) => {
return typeMap[status] || 'info';
};
// 判断字段是否有有效值(用于隐藏空值字段)
const hasValue = (value) => {
if (value === null || value === undefined) {
return false;
}
if (typeof value === 'string') {
return value.trim() !== '';
}
return true;
};
onMounted(() => {
data.id = route.query.id;
data.status = route.query.status;
@@ -1200,6 +1215,21 @@ onMounted(() => {
font-weight: bold;
margin-bottom: 10px;
}
.info-group {
margin-top: 20px;
margin-bottom: 20px;
&:first-child {
margin-top: 0;
}
}
.group-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-left: 8px;
border-left: 4px solid #409eff;
}
.quarantine-text {
margin-top: 20px;
}
@@ -1240,4 +1270,19 @@ onMounted(() => {
.red {
color: #ff6332;
}
.photo-container {
display: inline-block;
margin-right: 10px;
transition: transform 0.2s;
&:hover {
transform: scale(1.05);
}
}
.video-container {
margin-top: 8px;
margin-bottom: 8px;
video {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
</style>

View File

@@ -90,13 +90,13 @@ const calculateBatteryPercentage = (voltage) => {
const getList = async () => {
const { pageSize, pageNum, sn, startNo, endNo } = form;
// 为了获取所有项圈设备使用更大的pageSize
const params = {
pageSize: 100, // 使用更大的页面大小确保能获取所有设备
pageNum: 1,
pageSize,
pageNum,
sn,
startNo,
endNo,
type: 4, // 项圈设备类型
};
try {
@@ -104,16 +104,9 @@ const getList = async () => {
const { data = {}, code } = res;
if (code === 200) {
// 后端已经过滤了organName为'牛只运输跟踪系统'的数据
// 前端根据设备类型过滤项圈数据type=4
const allData = data.rows || [];
const filteredData = allData.filter(item => item.type === 4);
form.tableData = filteredData;
form.total = filteredData.length;
// 重新计算分页,因为我们现在显示的是过滤后的数据
form.pageNum = 1; // 重置到第一页
// 使用后端返回的分页数据
form.tableData = data.rows || [];
form.total = data.total || 0;
} else {
console.error('API调用失败:', res.msg);
form.tableData = [];

View File

@@ -84,6 +84,7 @@ const getList = async () => {
deviceId,
startNo,
endNo,
type: 2, // 耳标设备类型
};
try {
@@ -91,13 +92,9 @@ const getList = async () => {
const { data = {}, code } = res;
if (code === 200) {
// 后端已经过滤了organName为'牛只运输跟踪系统'的数据
// 前端根据设备类型过滤耳标数据type=2
const allData = data.rows || [];
const filteredData = allData.filter(item => item.type === 2);
form.tableData = filteredData;
form.total = filteredData.length;
// 使用后端返回的分页数据
form.tableData = data.rows || [];
form.total = data.total || 0;
} else {
console.error('API调用失败:', res.msg);
form.tableData = [];

View File

@@ -75,6 +75,7 @@ const getList = async () => {
pageSize,
pageNum,
deviceId,
type: 1, // 主机设备类型
};
try {
@@ -82,13 +83,9 @@ const getList = async () => {
const { data = {}, code } = res;
if (code === 200) {
// 后端已经过滤了organName为'牛只运输跟踪系统'的数据
// 前端根据设备类型过滤主机数据type=1
const allData = data.rows || [];
const filteredData = allData.filter(item => item.type === 1);
form.tableData = filteredData;
form.total = filteredData.length;
// 使用后端返回的分页数据
form.tableData = data.rows || [];
form.total = data.total || 0;
} else {
console.error('API调用失败:', res.msg);
form.tableData = [];

View File

@@ -13,6 +13,23 @@
>
新增运送清单
</el-button>
<el-dropdown
v-hasPermi="['order:import']"
@command="handleImportCommand"
style="margin-left: 10px"
>
<el-button type="success">
导入数据
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="import">导入数据</el-dropdown-item>
<el-dropdown-item command="download">下载模板</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display: none" @change="handleFileChange" />
</div>
</div>
@@ -58,7 +75,13 @@
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<Pagination
v-if="data.total > 0"
v-model:limit="form.pageSize"
v-model:page="form.pageNum"
:total="data.total"
@pagination="handlePagination"
/>
<OrderDialog ref="OrderDialogRef" @success="getDataList" />
<CreateDeliveryDialog ref="CreateDeliveryDialogRef" @success="getDataList" />
</div>
@@ -68,14 +91,19 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ArrowDown } from '@element-plus/icons-vue';
import * as XLSX from 'xlsx';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { orderPageQuery, orderDelete } from '@/api/shipping.js';
import Pagination from '@/components/Pagination/index.vue';
import { orderPageQuery, orderDelete, orderBatchImport } from '@/api/shipping.js';
import { memberListByType, userAdd } from '@/api/userManage.js';
import OrderDialog from './orderDialog.vue';
import CreateDeliveryDialog from './createDeliveryDialog.vue';
const baseSearchRef = ref();
const OrderDialogRef = ref();
const CreateDeliveryDialogRef = ref();
const fileInputRef = ref();
const formItemList = reactive([
{
label: '买方',
@@ -129,10 +157,19 @@ const form = reactive({
});
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
// 处理分页事件
const handlePagination = (paginationData) => {
if (paginationData) {
form.pageNum = paginationData.page || form.pageNum;
form.pageSize = paginationData.limit || form.pageSize;
}
getDataList();
};
// 列表
const getDataList = () => {
data.dataListLoading = true;
@@ -163,23 +200,38 @@ const getDataList = () => {
// 调用订单列表接口,而不是装车订单接口
console.log('[ORDER-LIST] 请求参数:', params);
orderPageQuery(params)
.then((res) => {
data.dataListLoading = false;
// 直接赋值订单数据
console.log('[ORDER-LIST] 响应数据:', res);
rows.value = res.data?.rows || [];
data.total = res.data?.total || 0;
if (rows.value.length > 0) {
// 处理响应数据结构
// PageResultResponse 返回的结构是 { code, msg, data: { total, rows } }
let responseData = res.data || res;
// 如果 data 是对象且包含 total 和 rows直接使用
if (responseData && typeof responseData === 'object' && 'total' in responseData && 'rows' in responseData) {
rows.value = responseData.rows || [];
data.total = responseData.total || 0;
} else if (responseData && responseData.data) {
// 如果 data 是嵌套的,使用 data.data
rows.value = responseData.data.rows || [];
data.total = responseData.data.total || 0;
} else {
// 兜底处理
rows.value = [];
data.total = 0;
}
console.log('[ORDER-LIST] 解析后的数据 - 总数:', data.total, '当前页数据:', rows.value.length);
})
.catch(() => {
.catch((error) => {
console.error('[ORDER-LIST] 请求失败:', error);
data.dataListLoading = false;
rows.value = [];
data.total = 0;
});
};
// 新增装车订单
@@ -224,6 +276,319 @@ const del = (id) => {
});
};
// 导入数据相关
const handleImportCommand = (command) => {
if (command === 'import') {
if (fileInputRef.value) {
fileInputRef.value.click();
}
} else if (command === 'download') {
downloadTemplate();
}
};
// 下载导入模板
const downloadTemplate = () => {
try {
// 创建表头
const headers = ['单价/斤', '卖方全称', '买方全称'];
// 创建工作簿
const wb = XLSX.utils.book_new();
// 创建工作表数据(只有表头)
const wsData = [headers];
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 设置列宽
ws['!cols'] = [
{ wch: 15 }, // 单价/斤
{ wch: 30 }, // 卖方全称
{ wch: 30 } // 买方全称
];
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, '订单导入模板');
// 生成Excel文件并下载
XLSX.writeFile(wb, '订单导入模板.xlsx');
ElMessage.success('模板下载成功');
} catch (error) {
console.error('下载模板失败:', error);
ElMessage.error('下载模板失败:' + (error.message || '未知错误'));
}
};
// 处理文件选择
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) {
return;
}
// 验证文件类型
const fileName = file.name;
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
if (fileExtension !== 'xlsx' && fileExtension !== 'xls') {
ElMessage.error('请选择Excel文件.xlsx或.xls格式');
event.target.value = '';
return;
}
try {
// 读取Excel文件
const excelData = await readExcelFile(file);
// 解析和转换数据
const orderDataList = await parseExcelData(excelData);
if (orderDataList.length === 0) {
ElMessage.warning('Excel文件中没有有效数据');
event.target.value = '';
return;
}
// 批量导入
await batchImportOrders(orderDataList);
// 清空文件选择
event.target.value = '';
} catch (error) {
console.error('导入失败:', error);
ElMessage.error('导入失败:' + (error.message || '未知错误'));
event.target.value = '';
}
};
// 读取Excel文件
const readExcelFile = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
resolve(jsonData);
} catch (error) {
reject(new Error('Excel文件解析失败' + error.message));
}
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
reader.readAsArrayBuffer(file);
});
};
// 解析Excel数据
const parseExcelData = async (data) => {
if (!data || data.length < 2) {
return [];
}
// 第一行是表头
const headers = data[0].map(h => String(h || '').trim());
// 查找列索引
const priceIndex = headers.findIndex(h => h.includes('单价') || h.includes('价格'));
const sellerIndex = headers.findIndex(h => h.includes('卖方'));
const buyerIndex = headers.findIndex(h => h.includes('买方'));
if (priceIndex === -1) {
throw new Error('未找到"单价/斤"列');
}
if (sellerIndex === -1) {
throw new Error('未找到"卖方全称"列');
}
if (buyerIndex === -1) {
throw new Error('未找到"买方全称"列');
}
const orderDataList = [];
// 从第二行开始读取数据
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (!row || row.length === 0) {
continue;
}
const price = row[priceIndex];
const sellerName = String(row[sellerIndex] || '').trim();
const buyerName = String(row[buyerIndex] || '').trim();
// 跳过空行
if (!sellerName && !buyerName) {
continue;
}
// 验证必填字段
if (!sellerName) {
throw new Error(`${i + 1}行:卖方全称不能为空`);
}
if (!buyerName) {
throw new Error(`${i + 1}行:买方全称不能为空`);
}
if (price === null || price === undefined || price === '') {
throw new Error(`${i + 1}行:单价/斤不能为空`);
}
// 转换价格为数字
const firmPrice = parseFloat(price);
if (isNaN(firmPrice) || firmPrice < 0) {
throw new Error(`${i + 1}行:单价/斤格式不正确必须为大于等于0的数字`);
}
// 查找或创建用户
const sellerId = await findOrCreateUser(sellerName);
const buyerId = await findOrCreateUser(buyerName);
if (!sellerId) {
throw new Error(`${i + 1}行:无法创建或找到卖方"${sellerName}"`);
}
if (!buyerId) {
throw new Error(`${i + 1}行:无法创建或找到买方"${buyerName}"`);
}
orderDataList.push({
sellerId: String(sellerId),
buyerId: String(buyerId),
settlementType: 1, // 默认结算方式为上车重量
firmPrice: firmPrice
});
}
return orderDataList;
};
// 查找或创建用户
const findOrCreateUser = async (username) => {
if (!username || !username.trim()) {
return null;
}
try {
// 先尝试查找用户(同时查询 type=2 和 type=4
const [supplierRes, purchaserRes] = await Promise.all([
memberListByType({
pageNum: 1,
pageSize: 100,
type: 2,
username: username.trim()
}),
memberListByType({
pageNum: 1,
pageSize: 100,
type: 4,
username: username.trim()
})
]);
// 合并结果并查找匹配的用户(精确匹配用户名)
const supplierList = supplierRes.data?.rows || [];
const purchaserList = purchaserRes.data?.rows || [];
const allUsers = [...supplierList, ...purchaserList];
// 精确匹配用户名
const matchedUser = allUsers.find(user => {
const userUsername = String(user.username || '').trim();
return userUsername === username.trim();
});
if (matchedUser && matchedUser.id) {
return matchedUser.id;
}
// 如果找不到创建新用户默认类型为2-供应商)
try {
const createRes = await userAdd({
username: username.trim(),
type: 2, // 默认类型为供应商
status: 0, // 启用
mobile: null,
cbkAccount: '',
remark: '通过订单导入自动创建'
});
if (createRes.code === 200) {
// 创建成功后再次查询获取新用户的ID
const newUserRes = await memberListByType({
pageNum: 1,
pageSize: 1,
type: 2,
username: username.trim()
});
const newUserList = newUserRes.data?.rows || [];
if (newUserList.length > 0) {
const newUser = newUserList.find(user => {
const userUsername = String(user.username || '').trim();
return userUsername === username.trim();
});
if (newUser && newUser.id) {
return newUser.id;
}
}
} else {
throw new Error(createRes.msg || '创建用户失败');
}
} catch (createError) {
console.error(`创建用户失败 "${username}":`, createError);
throw new Error(`创建用户"${username}"失败:${createError.message || '未知错误'}`);
}
return null;
} catch (error) {
console.error(`查找或创建用户失败 "${username}":`, error);
throw error;
}
};
// 批量导入订单
const batchImportOrders = async (orderDataList) => {
const total = orderDataList.length;
ElMessage.info(`开始导入,共${total}条数据`);
try {
const res = await orderBatchImport({
orders: orderDataList
});
if (res.code === 200) {
const successCount = res.data?.successCount || total;
const failCount = res.data?.failCount || 0;
const failMessages = res.data?.failMessages || [];
if (failCount === 0) {
ElMessage.success(`导入成功!共导入${successCount}条数据`);
} else {
ElMessage.warning(`导入完成:成功${successCount}条,失败${failCount}`);
if (failMessages.length > 0) {
ElMessageBox.alert(
failMessages.slice(0, 10).join('\n') + (failMessages.length > 10 ? `\n...还有${failMessages.length - 10}条错误` : ''),
'导入失败详情',
{ type: 'error' }
);
}
}
// 刷新列表
getDataList();
} else {
ElMessage.error(res.msg || '导入失败');
}
} catch (error) {
console.error('批量导入失败:', error);
const errorMsg = error.response?.data?.msg || error.message || '未知错误';
ElMessage.error('导入失败:' + errorMsg);
}
};
onMounted(() => {
getDataList();
@@ -290,3 +655,4 @@ onMounted(() => {
}
}
</style>

View File

@@ -7,17 +7,13 @@
clearable
filterable
remote
:remote-method="supplierRemoteMethod"
:loading="data.supplierLoading"
:remote-method="sellerRemoteMethod"
:loading="data.sellerLoading"
placeholder="请选择卖方"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
>
<el-option
v-for="item in data.supplierOptions"
v-for="item in data.sellerOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.id"
@@ -25,11 +21,11 @@
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="supplierHandleCurrentChange"
@current-change="sellerHandleCurrentChange"
:page-size="10"
:current-page="data.supplierPageNum"
:current-page="data.sellerPageNum"
layout="total, prev, pager, next"
:total="data.supplierTotal"
:total="data.sellerTotal"
>
</el-pagination>
</el-select>
@@ -41,17 +37,13 @@
clearable
filterable
remote
:remote-method="purchaserRemoteMethod"
:loading="data.purchaserLoading"
:remote-method="buyerRemoteMethod"
:loading="data.buyerLoading"
placeholder="请选择买方"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
>
<el-option
v-for="item in data.purchaserOptions"
v-for="item in data.buyerOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.id"
@@ -59,11 +51,11 @@
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="purchaserHandleCurrentChange"
@current-change="buyerHandleCurrentChange"
:page-size="10"
:current-page="data.purchaserPageNum"
:current-page="data.buyerPageNum"
layout="total, prev, pager, next"
:total="data.purchaserTotal"
:total="data.buyerTotal"
>
</el-pagination>
</el-select>
@@ -110,33 +102,52 @@ const formDataRef = ref(null);
const data = reactive({
dialogVisible: false,
saveLoading: false,
supplierOptions: [],
purchaserOptions: [],
driverLoading: false,
driverName: '',
driverPageNum: 1,
driverTotal: 0,
purchaserLoading: false,
purchaserPageNum: 1,
purchaserTotal: 0,
purchaserName: '',
supplierLoading: false,
supplierPageNum: 1,
supplierTotal: 0,
supplierName: '',
sellerOptions: [],
buyerOptions: [],
sellerLoading: false,
sellerPageNum: 1,
sellerTotal: 0,
sellerName: '',
buyerLoading: false,
buyerPageNum: 1,
buyerTotal: 0,
buyerName: '',
});
const ruleForm = reactive({
id: null, // 订单ID编辑时使用
buyerId: [], // 买方ID数组
sellerId: [], // 卖方ID数组
buyerId: null, // 买方ID(单选)
sellerId: null, // 卖方ID(单选)
settlementType: 1, // 结算方式1-上车重量2-下车重量3-按肉价结算
firmPrice: null, // 约定价格(元/斤)
});
// 验证买方和卖方不能是同一用户
const validateBuyerSeller = (rule, value, callback) => {
if (value && value === ruleForm.sellerId) {
callback(new Error('买方和卖方不能是同一用户'));
} else {
callback();
}
};
const validateSellerBuyer = (rule, value, callback) => {
if (value && value === ruleForm.buyerId) {
callback(new Error('卖方和买方不能是同一用户'));
} else {
callback();
}
};
const rules = reactive({
buyerId: [{ required: true, message: '请选择买方', trigger: 'change' }],
sellerId: [{ required: true, message: '请选择方', trigger: 'change' }],
buyerId: [
{ required: true, message: '请选择方', trigger: 'change' },
{ validator: validateBuyerSeller, trigger: 'change' }
],
sellerId: [
{ required: true, message: '请选择卖方', trigger: 'change' },
{ validator: validateSellerBuyer, trigger: 'change' }
],
settlementType: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
firmPrice: [
{ required: true, message: '请输入约定价格', trigger: 'blur' },
@@ -150,79 +161,127 @@ const handleClose = () => {
}
// 重置表单数据
ruleForm.id = null;
ruleForm.buyerId = [];
ruleForm.sellerId = [];
ruleForm.buyerId = null;
ruleForm.sellerId = null;
ruleForm.settlementType = 1;
ruleForm.firmPrice = null;
data.dialogVisible = false;
};
// ----------------
// 供应商远程搜索
const supplierRemoteMethod = (e) => {
data.supplierName = e;
data.supplierPageNum = 1;
getSupplierList();
// 卖方远程搜索(同时查询 type=2 和 type=4
const sellerRemoteMethod = (e) => {
data.sellerName = e;
data.sellerPageNum = 1;
getSellerList();
};
// 供应商 列表
const getSupplierList = () => {
data.supplierLoading = true;
const params = {
pageNum: data.supplierPageNum,
pageSize: 10,
type: 2, // 供应商类型
username: data.supplierName,
};
memberListByType(params)
.then((res) => {
data.supplierLoading = false;
data.supplierOptions = res.data.rows;
data.supplierTotal = res.data.total;
// 卖方列表(合并 type=2 和 type=4 的用户)
const getSellerList = () => {
data.sellerLoading = true;
const searchName = data.sellerName || '';
// 同时查询供应商type=2和采购商type=4
Promise.all([
memberListByType({
pageNum: data.sellerPageNum,
pageSize: 10,
type: 2,
username: searchName,
}),
memberListByType({
pageNum: data.sellerPageNum,
pageSize: 10,
type: 4,
username: searchName,
})
])
.then(([supplierRes, purchaserRes]) => {
// 合并结果并去重(根据 id
const supplierList = supplierRes.data.rows || [];
const purchaserList = purchaserRes.data.rows || [];
const mergedList = [...supplierList, ...purchaserList];
// 去重:使用 Map 根据 id 去重
const uniqueMap = new Map();
mergedList.forEach(item => {
if (!uniqueMap.has(item.id)) {
uniqueMap.set(item.id, item);
}
});
data.sellerOptions = Array.from(uniqueMap.values());
// 总数取两个查询的总和(实际可能包含重复,但用于分页显示)
data.sellerTotal = (supplierRes.data.total || 0) + (purchaserRes.data.total || 0);
data.sellerLoading = false;
})
.catch(() => {
data.supplierLoading = false;
data.sellerLoading = false;
});
};
// 选择供应商分页
const supplierHandleCurrentChange = (val) => {
data.supplierPageNum = val;
getSupplierList();
// 卖方分页
const sellerHandleCurrentChange = (val) => {
data.sellerPageNum = val;
getSellerList();
};
// ----------------
// 采购商远程搜索
const purchaserRemoteMethod = (e) => {
data.purchaserName = e;
data.purchaserPageNum = 1;
getPurchaserList();
// 买方远程搜索(同时查询 type=2 和 type=4
const buyerRemoteMethod = (e) => {
data.buyerName = e;
data.buyerPageNum = 1;
getBuyerList();
};
// 采购商 列表
const getPurchaserList = () => {
data.purchaserLoading = true;
const params = {
pageNum: data.purchaserPageNum,
pageSize: 10,
type: 4, // 采购商类型
username: data.purchaserName,
};
memberListByType(params)
.then((res) => {
data.purchaserLoading = false;
data.purchaserOptions = res.data.rows;
data.purchaserTotal = res.data.total;
// 买方列表(合并 type=2 和 type=4 的用户)
const getBuyerList = () => {
data.buyerLoading = true;
const searchName = data.buyerName || '';
// 同时查询供应商type=2和采购商type=4
Promise.all([
memberListByType({
pageNum: data.buyerPageNum,
pageSize: 10,
type: 2,
username: searchName,
}),
memberListByType({
pageNum: data.buyerPageNum,
pageSize: 10,
type: 4,
username: searchName,
})
])
.then(([supplierRes, purchaserRes]) => {
// 合并结果并去重(根据 id
const supplierList = supplierRes.data.rows || [];
const purchaserList = purchaserRes.data.rows || [];
const mergedList = [...supplierList, ...purchaserList];
// 去重:使用 Map 根据 id 去重
const uniqueMap = new Map();
mergedList.forEach(item => {
if (!uniqueMap.has(item.id)) {
uniqueMap.set(item.id, item);
}
});
data.buyerOptions = Array.from(uniqueMap.values());
// 总数取两个查询的总和(实际可能包含重复,但用于分页显示)
data.buyerTotal = (supplierRes.data.total || 0) + (purchaserRes.data.total || 0);
data.buyerLoading = false;
})
.catch(() => {
data.purchaserLoading = false;
data.buyerLoading = false;
});
};
// 采购商分页
const purchaserHandleCurrentChange = (val) => {
data.purchaserPageNum = val;
getPurchaserList();
// 买方分页
const buyerHandleCurrentChange = (val) => {
data.buyerPageNum = val;
getBuyerList();
};
// 保存
@@ -232,8 +291,8 @@ const onClickSave = () => {
if (valid) {
const params = {
id: ruleForm.id,
buyerId: ruleForm.buyerId.join(','), // 将数组转为逗号分隔的字符串
sellerId: ruleForm.sellerId.join(','), // 将数组转为逗号分隔的字符串
buyerId: ruleForm.buyerId ? String(ruleForm.buyerId) : '', // 转为字符串格式(后端可能期望字符串
sellerId: ruleForm.sellerId ? String(ruleForm.sellerId) : '', // 转为字符串格式
settlementType: ruleForm.settlementType,
firmPrice: ruleForm.firmPrice, // 约定价格(元/斤)
};
@@ -274,38 +333,45 @@ const onShowDialog = (orderData) => {
// 重置表单数据
ruleForm.id = orderData?.id || null;
// 处理buyerId支持字符串数组两种格式
// 处理buyerId支持字符串数组和数字格式,取第一个值(单选模式)
if (orderData?.buyerId) {
if (Array.isArray(orderData.buyerId)) {
if (typeof orderData.buyerId === 'number') {
ruleForm.buyerId = orderData.buyerId;
} else if (Array.isArray(orderData.buyerId) && orderData.buyerId.length > 0) {
ruleForm.buyerId = parseInt(orderData.buyerId[0]);
} else if (typeof orderData.buyerId === 'string' && orderData.buyerId.trim() !== '') {
ruleForm.buyerId = orderData.buyerId.split(',').map(id => id.trim()).filter(id => id !== '');
const ids = orderData.buyerId.split(',').map(id => id.trim()).filter(id => id !== '');
ruleForm.buyerId = ids.length > 0 ? parseInt(ids[0]) : null;
} else {
ruleForm.buyerId = [];
ruleForm.buyerId = null;
}
} else {
ruleForm.buyerId = [];
ruleForm.buyerId = null;
}
// 处理sellerId支持字符串数组两种格式
// 处理sellerId支持字符串数组和数字格式,取第一个值(单选模式)
if (orderData?.sellerId) {
if (Array.isArray(orderData.sellerId)) {
if (typeof orderData.sellerId === 'number') {
ruleForm.sellerId = orderData.sellerId;
} else if (Array.isArray(orderData.sellerId) && orderData.sellerId.length > 0) {
ruleForm.sellerId = parseInt(orderData.sellerId[0]);
} else if (typeof orderData.sellerId === 'string' && orderData.sellerId.trim() !== '') {
ruleForm.sellerId = orderData.sellerId.split(',').map(id => id.trim()).filter(id => id !== '');
const ids = orderData.sellerId.split(',').map(id => id.trim()).filter(id => id !== '');
ruleForm.sellerId = ids.length > 0 ? parseInt(ids[0]) : null;
} else {
ruleForm.sellerId = [];
ruleForm.sellerId = null;
}
} else {
ruleForm.sellerId = [];
ruleForm.sellerId = null;
}
ruleForm.settlementType = orderData?.settlementType || 1;
ruleForm.firmPrice = orderData?.firmPrice != null ? orderData.firmPrice : null;
data.dialogVisible = true;
getSupplierList();
getPurchaserList();
// 初始化时加载列表
getSellerList();
getBuyerList();
};
defineExpose({

View File

@@ -1,12 +1,25 @@
<template>
<el-dialog v-model="dialogVisible" :before-close="handleClose" :title="title" style="width: 700px; padding-bottom: 20px">
<div class="global_dialog_content">
<el-form ref="FormDataRef" :model="form.data" :rules="form.rules" label-width="100">
<el-form-item label="岗位名称" prop="name">
<el-input v-model="form.data.name" placeholder="请输入岗位名称" style="width: 320px" />
<el-form ref="FormDataRef" :model="form.data" :rules="form.rules" label-width="120px" class="post-form">
<el-form-item label="岗位名称" prop="name" class="post-name-form-item">
<el-input
v-model="form.data.name"
placeholder="请输入岗位名称"
clearable
class="post-name-input"
/>
</el-form-item>
<el-form-item label="岗位描述" prop="description">
<el-input maxlength="100" v-model="form.data.description" placeholder="请输入岗位描述" style="width: 320px" />
<el-input
v-model="form.data.description"
type="textarea"
:rows="4"
maxlength="100"
placeholder="请输入岗位描述"
show-word-limit
class="post-description-input"
/>
</el-form-item>
</el-form>
</div>
@@ -19,7 +32,8 @@
</el-dialog>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { reactive, ref, nextTick, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { sysPositionSave } from '~/api/sys.js';
const dialogVisible = ref(false);
@@ -29,6 +43,31 @@ const title = ref('');
const userId = ref('');
const emits = defineEmits();
// 修复标签宽度问题:覆盖内联样式
const fixLabelWidth = () => {
nextTick(() => {
const label = document.querySelector('.post-name-form-item .el-form-item__label');
if (label) {
// 直接设置样式属性,覆盖内联样式
label.style.width = '120px';
label.style.minWidth = '120px';
label.style.maxWidth = '120px';
label.style.whiteSpace = 'nowrap';
label.style.overflow = 'visible';
label.style.wordBreak = 'keep-all';
}
});
};
// 监听对话框打开,修复标签宽度
watch(dialogVisible, (newVal) => {
if (newVal) {
fixLabelWidth();
// 延迟再次修复,确保 DOM 完全渲染
setTimeout(fixLabelWidth, 100);
}
});
const form = reactive({
data: {
name: '',
@@ -48,6 +87,8 @@ const onShowDialog = (val, page) => {
userId.value = val.id;
form.data = val;
}
// 修复标签宽度
fixLabelWidth();
};
const onClickSave = () => {
@@ -83,4 +124,72 @@ defineExpose({
onShowDialog,
});
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.post-form {
// 针对岗位名称表单项的标签,确保不换行
// 使用更具体的选择器来覆盖内联样式
:deep(.post-name-form-item) {
.el-form-item__label {
// 使用 !important 覆盖内联样式
width: 120px !important;
min-width: 120px !important;
max-width: 120px !important;
white-space: nowrap !important;
overflow: visible !important;
text-overflow: clip !important;
padding-right: 12px !important;
line-height: 32px !important;
word-break: keep-all !important;
word-wrap: normal !important;
display: inline-block !important;
flex-shrink: 0 !important;
box-sizing: border-box !important;
// 强制覆盖任何内联样式
&[style*="width"] {
width: 120px !important;
}
}
// 确保表单项内容区域不会挤压标签
.el-form-item__content {
flex: 1;
min-width: 0;
}
}
// 全局覆盖所有标签的内联样式
:deep(.el-form-item__label[style*="width: 65px"]) {
width: 120px !important;
min-width: 120px !important;
max-width: 120px !important;
}
// 所有标签的通用样式
:deep(.el-form-item__label) {
width: 120px !important;
min-width: 120px !important;
white-space: nowrap !important;
overflow: visible !important;
word-break: keep-all !important;
word-wrap: normal !important;
}
// 优化输入框宽度
.post-name-input {
width: 100%;
max-width: 500px;
}
.post-description-input {
width: 100%;
max-width: 500px;
}
// 确保表单项布局正常
:deep(.el-form-item) {
margin-bottom: 22px;
display: flex;
align-items: flex-start;
}
}
</style>

View File

@@ -137,15 +137,15 @@ const handleRemove = (file, fileList, type) => {
const beforeAvatarUpload = (file) => {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isImage) {
ElMessage.error('上传文件只能是图片格式!');
}
if (!isLt2M) {
ElMessage.error('上传图片大小不能超过 2MB!');
if (!isLt10M) {
ElMessage.error('上传图片大小不能超过 10MB!');
}
return isImage && isLt2M;
return isImage && isLt10M;
};
const handleExceed = (number) => {

View File

@@ -1,8 +1,10 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"> </base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px; gap: 10px">
<el-button type="primary" @click="showAddDialog(null)">新增用户</el-button>
<el-button type="success" @click="handleImportClick">导入数据</el-button>
<input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display: none" @change="handleFileChange" />
</div>
<div class="main-container">
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
@@ -51,12 +53,15 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import * as XLSX from 'xlsx';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { userList, userStatus } from '@/api/userManage.js';
import { userList, userStatus, userAdd, userEdit } from '@/api/userManage.js';
import UserDialog from './userDialog.vue';
const baseSearchRef = ref();
const UserDialogRef = ref();
const fileInputRef = ref();
const formItemList = reactive([
{
label: '用户姓名',
@@ -179,6 +184,224 @@ const showAddDialog = (row) => {
UserDialogRef.value.onShowDialog(row);
}
};
// 导入数据相关
const handleImportClick = () => {
if (fileInputRef.value) {
fileInputRef.value.click();
}
};
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) {
return;
}
// 验证文件类型
const fileName = file.name;
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
if (fileExtension !== 'xlsx' && fileExtension !== 'xls') {
ElMessage.error('请选择Excel文件.xlsx或.xls格式');
event.target.value = '';
return;
}
try {
// 读取Excel文件
const data = await readExcelFile(file);
// 解析和转换数据
const userDataList = parseExcelData(data);
if (userDataList.length === 0) {
ElMessage.warning('Excel文件中没有有效数据');
event.target.value = '';
return;
}
// 批量导入
await batchImportUsers(userDataList);
// 清空文件选择
event.target.value = '';
} catch (error) {
console.error('导入失败:', error);
ElMessage.error('导入失败:' + (error.message || '未知错误'));
event.target.value = '';
}
};
// 读取Excel文件
const readExcelFile = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
resolve(jsonData);
} catch (error) {
reject(new Error('Excel文件解析失败' + error.message));
}
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
reader.readAsArrayBuffer(file);
});
};
// 解析Excel数据
const parseExcelData = (data) => {
if (!data || data.length < 2) {
return [];
}
// 第一行是表头
const headers = data[0].map(h => String(h || '').trim());
// 查找列索引
const companyNameIndex = headers.findIndex(h => h.includes('公司昵称') || h.includes('公司名称'));
const statusIndex = headers.findIndex(h => h.includes('合作状态'));
const contactPersonIndex = headers.findIndex(h => h.includes('对接人'));
const contactInfoIndex = headers.findIndex(h => h.includes('联系方式'));
if (companyNameIndex === -1) {
throw new Error('未找到"公司昵称"列');
}
if (statusIndex === -1) {
throw new Error('未找到"合作状态"列');
}
const userDataList = [];
// 从第二行开始读取数据
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (!row || row.length === 0) {
continue;
}
const companyName = String(row[companyNameIndex] || '').trim();
const status = String(row[statusIndex] || '').trim();
const contactPerson = contactPersonIndex >= 0 ? String(row[contactPersonIndex] || '').trim() : '';
const contactInfo = contactInfoIndex >= 0 ? String(row[contactInfoIndex] || '').trim() : '';
// 跳过空行
if (!companyName && !status) {
continue;
}
// 构建用户姓名:公司昵称/对接人
let username = companyName;
if (contactPerson) {
username = `${companyName}/${contactPerson}`;
}
// 映射用户类型:采购商=4供应商=2
let type = null;
if (status === '采购商') {
type = 4;
} else if (status === '供应商') {
type = 2;
} else if (status) {
// 如果合作状态不是采购商或供应商,跳过该行
console.warn(`${i + 1}行:未知的合作状态"${status}",已跳过`);
continue;
}
// 如果用户类型无法确定,跳过
if (type === null) {
console.warn(`${i + 1}行:无法确定用户类型,已跳过`);
continue;
}
userDataList.push({
username,
mobile: contactInfo || null, // 联系方式作为手机号如果为空则为null
type,
cbkAccount: '', // CBK账号为空
status: 0, // 账号状态:启用
remark: '', // 备注为空
});
}
return userDataList;
};
// 批量导入用户
const batchImportUsers = async (userDataList) => {
const total = userDataList.length;
let successCount = 0;
let failCount = 0;
const failMessages = [];
ElMessage.info(`开始导入,共${total}条数据`);
for (let i = 0; i < userDataList.length; i++) {
const userData = userDataList[i];
try {
// 如果手机号不为空,先检查是否存在
let existingUser = null;
if (userData.mobile) {
// 查询是否存在该手机号的用户
const queryResult = await userList({
pageNum: 1,
pageSize: 1,
mobile: userData.mobile,
mobileExact: true,
});
if (queryResult.data && queryResult.data.rows && queryResult.data.rows.length > 0) {
existingUser = queryResult.data.rows[0];
}
}
if (existingUser) {
// 如果存在,则更新
await userEdit({
id: existingUser.id,
...userData,
});
successCount++;
} else {
// 如果不存在,则新增
await userAdd(userData);
successCount++;
}
} catch (error) {
failCount++;
const errorMsg = error.response?.data?.msg || error.message || '未知错误';
failMessages.push(`${i + 1}行(${userData.username}${errorMsg}`);
console.error(`导入第${i + 1}行失败:`, error);
}
}
// 显示导入结果
if (failCount === 0) {
ElMessage.success(`导入成功!共导入${successCount}条数据`);
} else {
ElMessage.warning(`导入完成:成功${successCount}条,失败${failCount}`);
if (failMessages.length > 0) {
console.error('导入失败详情:', failMessages);
// 可以在这里显示详细的错误信息比如使用MessageBox
ElMessageBox.alert(
failMessages.slice(0, 10).join('\n') + (failMessages.length > 10 ? `\n...还有${failMessages.length - 10}条错误` : ''),
'导入失败详情',
{ type: 'error' }
);
}
}
// 刷新列表
getDataList();
};
onMounted(() => {
getDataList();
});

View File

@@ -92,13 +92,16 @@ const rules = reactive({
username: [{ required: true, message: '请输入用户姓名', trigger: 'blur' }],
mobile: [
{
required: true,
validator(rule, value, callback) {
if (!value) {
callback(new Error('请输入用户手机号'));
// 如果为空,直接通过验证
if (!value || value.trim() === '') {
callback();
return;
}
// 如果有值,则验证格式
if (!checkMobile(value)) {
callback(new Error('请输入正确的手机号'));
return;
}
callback();
},
@@ -106,7 +109,7 @@ const rules = reactive({
},
],
type: [{ required: true, message: '请选择用户类型', trigger: 'change' }],
cbkAccount: [{ required: true, message: '请输入CBK账号', trigger: 'blur' }],
cbkAccount: [],
status: [{ required: true, message: '请选择账户状态', trigger: 'change' }],
});
const handleClose = () => {

View File

@@ -49,41 +49,49 @@
</el-table-column>
<el-table-column label="行驶证" prop="drivingLicensePhoto" min-width="120">
<template #default="scope">
<el-image
v-if="scope.row.drivingLicensePhoto"
:src="scope.row.drivingLicensePhoto"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="[scope.row.drivingLicensePhoto]"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
<template v-if="getFirstImageUrl(scope.row.drivingLicensePhoto)">
<el-image
:src="getFirstImageUrl(scope.row.drivingLicensePhoto)"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="getImageList(scope.row.drivingLicensePhoto)"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
<div v-if="getImageCount(scope.row.drivingLicensePhoto) > 1" style="margin-top: 4px; font-size: 12px; color: #409eff">
{{ getImageCount(scope.row.drivingLicensePhoto) }}
</div>
</template>
<span v-else style="color: #999; font-size: 12px">暂无图片</span>
</template>
</el-table-column>
<el-table-column label="牧运通备案码" prop="recordCode" min-width="120">
<template #default="scope">
<el-image
v-if="scope.row.recordCode"
:src="scope.row.recordCode"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="[scope.row.recordCode]"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
<template v-if="getFirstImageUrl(scope.row.recordCode)">
<el-image
:src="getFirstImageUrl(scope.row.recordCode)"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="getImageList(scope.row.recordCode)"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
<div v-if="getImageCount(scope.row.recordCode) > 1" style="margin-top: 4px; font-size: 12px; color: #409eff">
{{ getImageCount(scope.row.recordCode) }}
</div>
</template>
<span v-else style="color: #999; font-size: 12px">暂无图片</span>
</template>
</el-table-column>
@@ -194,6 +202,69 @@ const getDataList = async () => {
}
};
// 解析图片 URL支持 JSON 数组和单图片)
const getFirstImageUrl = (value) => {
if (!value) return null;
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof value === 'string' && value.trim().startsWith('[')) {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed[0];
}
return null;
} catch (e) {
// 解析失败,按单图片处理(兼容旧数据)
return value;
}
}
// 不是 JSON 格式,按单图片处理(兼容旧数据)
return value;
};
// 获取图片列表(用于预览)
const getImageList = (value) => {
if (!value) return [];
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof value === 'string' && value.trim().startsWith('[')) {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed;
}
return [];
} catch (e) {
// 解析失败,按单图片处理(兼容旧数据)
return [value];
}
}
// 不是 JSON 格式,按单图片处理(兼容旧数据)
return [value];
};
// 获取图片数量
const getImageCount = (value) => {
if (!value) return 0;
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof value === 'string' && value.trim().startsWith('[')) {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.length;
}
return 0;
} catch (e) {
// 解析失败,按单图片处理(兼容旧数据)
return 1;
}
}
// 不是 JSON 格式,按单图片处理(兼容旧数据)
return 1;
};
onMounted(() => {
getDataList();
});

View File

@@ -38,7 +38,6 @@
</el-form-item>
<el-form-item label="行驶证照片" prop="drivingLicensePhoto">
<el-upload
:limit="1"
list-type="picture-card"
action="/api/common/upload"
:on-success="(response, file, fileList) => handleAvatarSuccess(response, file, fileList, 'drivingLicensePhoto')"
@@ -47,14 +46,12 @@
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.drivingLicensePhoto"
:headers="importHeaders"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="牧运通备案码" prop="recordCode">
<el-upload
:limit="1"
list-type="picture-card"
action="/api/common/upload"
:on-success="(response, file, fileList) => handleAvatarSuccess(response, file, fileList, 'recordCode')"
@@ -63,7 +60,6 @@
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.recordCode"
:headers="importHeaders"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
@@ -118,7 +114,6 @@ const rules = reactive({
});
const handleAvatarSuccess = (res, file, fileList, type) => {
if (ruleForm.hasOwnProperty(type)) {
let imageUrl = null;
@@ -133,8 +128,21 @@ const handleAvatarSuccess = (res, file, fileList, type) => {
}
if (imageUrl) {
ruleForm[type] = [{ url: imageUrl, uid: file.uid, name: file.name }];
// 行驶证和备案码支持多图片,其他字段保持单图片
if (type === 'drivingLicensePhoto' || type === 'recordCode') {
// 多图片:更新整个文件列表
ruleForm[type] = fileList.map(item => {
// 如果是新上传的文件,使用新的 URL
if (item.uid === file.uid) {
return { url: imageUrl, uid: file.uid, name: file.name };
}
// 保持原有文件信息
return item;
});
} else {
// 单图片:只保留最新的一张
ruleForm[type] = [{ url: imageUrl, uid: file.uid, name: file.name }];
}
} else {
console.error('无法解析图片URL:', res);
ElMessage.error('上传失败无法获取图片URL');
@@ -153,21 +161,21 @@ const handlePreview = (file) => {
const beforeAvatarUpload = (file) => {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isImage) {
ElMessage.error('上传文件只能是图片格式!');
return false;
}
if (!isLt2M) {
ElMessage.error('上传图片大小不能超过 2MB');
if (!isLt10M) {
ElMessage.error('上传图片大小不能超过 10MB');
return false;
}
return true;
};
const handleExceed = () => {
ElMessage.warning('最多只能上传一张图片!');
// 已移除单图片限制,此方法不再需要
};
const onClickSave = async () => {
@@ -176,13 +184,21 @@ const onClickSave = async () => {
data.saveLoading = true;
try {
// 将行驶证和备案码的图片数组转换为 JSON 字符串
const drivingLicensePhotoJson = ruleForm.drivingLicensePhoto.length > 0
? JSON.stringify(ruleForm.drivingLicensePhoto.map(item => item.url))
: null;
const recordCodeJson = ruleForm.recordCode.length > 0
? JSON.stringify(ruleForm.recordCode.map(item => item.url))
: null;
const formData = {
id: data.isEdit ? ruleForm.id : null,
licensePlate: ruleForm.licensePlate,
carFrontPhoto: ruleForm.carFrontPhoto.length > 0 ? ruleForm.carFrontPhoto[0].url : null,
carRearPhoto: ruleForm.carRearPhoto.length > 0 ? ruleForm.carRearPhoto[0].url : null,
drivingLicensePhoto: ruleForm.drivingLicensePhoto.length > 0 ? ruleForm.drivingLicensePhoto[0].url : null,
recordCode: ruleForm.recordCode.length > 0 ? ruleForm.recordCode[0].url : null,
drivingLicensePhoto: drivingLicensePhotoJson,
recordCode: recordCodeJson,
remark: ruleForm.remark,
};
@@ -229,8 +245,83 @@ const open = (row = null) => {
ruleForm.licensePlate = row.licensePlate || '';
ruleForm.carFrontPhoto = row.carFrontPhoto ? [{ url: row.carFrontPhoto }] : [];
ruleForm.carRearPhoto = row.carRearPhoto ? [{ url: row.carRearPhoto }] : [];
ruleForm.drivingLicensePhoto = row.drivingLicensePhoto ? [{ url: row.drivingLicensePhoto }] : [];
ruleForm.recordCode = row.recordCode ? [{ url: row.recordCode }] : [];
// 行驶证照片:尝试解析 JSON 数组,如果失败则按单图片处理
if (row.drivingLicensePhoto) {
const drivingLicenseValue = row.drivingLicensePhoto;
console.log('[VEHICLE] 行驶证照片原始数据:', drivingLicenseValue, typeof drivingLicenseValue);
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof drivingLicenseValue === 'string' && drivingLicenseValue.trim().startsWith('[')) {
try {
const parsed = JSON.parse(drivingLicenseValue);
console.log('[VEHICLE] 行驶证照片解析结果:', parsed);
if (Array.isArray(parsed) && parsed.length > 0) {
ruleForm.drivingLicensePhoto = parsed.map((url, index) => {
// 确保 url 是字符串类型
const urlStr = typeof url === 'string' ? url : String(url);
console.log(`[VEHICLE] 行驶证照片 ${index}:`, urlStr);
return {
url: urlStr,
uid: Date.now() + index,
name: `行驶证${index + 1}.jpg`
};
});
console.log('[VEHICLE] 行驶证照片最终数据:', ruleForm.drivingLicensePhoto);
} else {
ruleForm.drivingLicensePhoto = [];
}
} catch (e) {
console.error('解析行驶证照片 JSON 失败:', e, drivingLicenseValue);
// 解析失败,按单图片处理(兼容旧数据)
ruleForm.drivingLicensePhoto = [{ url: drivingLicenseValue }];
}
} else {
// 不是 JSON 格式,按单图片处理(兼容旧数据)
console.log('[VEHICLE] 行驶证照片不是 JSON 格式,按单图片处理');
ruleForm.drivingLicensePhoto = [{ url: drivingLicenseValue }];
}
} else {
ruleForm.drivingLicensePhoto = [];
}
// 牧运通备案码:尝试解析 JSON 数组,如果失败则按单图片处理
if (row.recordCode) {
const recordCodeValue = row.recordCode;
console.log('[VEHICLE] 备案码原始数据:', recordCodeValue, typeof recordCodeValue);
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof recordCodeValue === 'string' && recordCodeValue.trim().startsWith('[')) {
try {
const parsed = JSON.parse(recordCodeValue);
console.log('[VEHICLE] 备案码解析结果:', parsed);
if (Array.isArray(parsed) && parsed.length > 0) {
ruleForm.recordCode = parsed.map((url, index) => {
// 确保 url 是字符串类型
const urlStr = typeof url === 'string' ? url : String(url);
console.log(`[VEHICLE] 备案码 ${index}:`, urlStr);
return {
url: urlStr,
uid: Date.now() + index + 1000,
name: `备案码${index + 1}.jpg`
};
});
console.log('[VEHICLE] 备案码最终数据:', ruleForm.recordCode);
} else {
ruleForm.recordCode = [];
}
} catch (e) {
console.error('解析备案码 JSON 失败:', e, recordCodeValue);
// 解析失败,按单图片处理(兼容旧数据)
ruleForm.recordCode = [{ url: recordCodeValue }];
}
} else {
// 不是 JSON 格式,按单图片处理(兼容旧数据)
console.log('[VEHICLE] 备案码不是 JSON 格式,按单图片处理');
ruleForm.recordCode = [{ url: recordCodeValue }];
}
} else {
ruleForm.recordCode = [];
}
ruleForm.remark = row.remark || '';
}
};

View File

@@ -948,6 +948,11 @@
"resolved" "https://registry.npmmirror.com/acorn/-/acorn-8.12.1.tgz"
"version" "8.12.1"
"adler-32@~1.3.0":
"integrity" "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="
"resolved" "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz"
"version" "1.3.1"
"ajv@^6.12.4":
"integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="
"resolved" "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz"
@@ -1433,6 +1438,14 @@
"tslib" "^2.0.3"
"upper-case-first" "^2.0.2"
"cfb@~1.2.1":
"integrity" "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="
"resolved" "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz"
"version" "1.2.2"
dependencies:
"adler-32" "~1.3.0"
"crc-32" "~1.2.0"
"chalk@^1.1.3":
"integrity" "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="
"resolved" "https://registry.npmmirror.com/chalk/-/chalk-1.1.3.tgz"
@@ -1601,6 +1614,11 @@
"resolved" "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz"
"version" "2.1.2"
"codepage@~1.15.0":
"integrity" "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="
"resolved" "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz"
"version" "1.15.0"
"collection-visit@^1.0.0":
"integrity" "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw=="
"resolved" "https://registry.npmmirror.com/collection-visit/-/collection-visit-1.0.0.tgz"
@@ -1830,6 +1848,11 @@
"parse-json" "^5.2.0"
"path-type" "^4.0.0"
"crc-32@~1.2.0", "crc-32@~1.2.1":
"integrity" "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="
"resolved" "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz"
"version" "1.2.2"
"create-require@^1.1.0":
"integrity" "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
"resolved" "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz"
@@ -3016,6 +3039,11 @@
"combined-stream" "^1.0.8"
"mime-types" "^2.1.12"
"frac@~1.1.2":
"integrity" "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
"resolved" "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz"
"version" "1.1.2"
"fragment-cache@^0.2.1":
"integrity" "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA=="
"resolved" "https://registry.npmmirror.com/fragment-cache/-/fragment-cache-0.2.1.tgz"
@@ -5898,6 +5926,13 @@
dependencies:
"readable-stream" "^3.0.0"
"ssf@~0.11.2":
"integrity" "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="
"resolved" "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz"
"version" "0.11.2"
dependencies:
"frac" "~1.1.2"
"ssr-window@^3.0.0-alpha.1":
"integrity" "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA=="
"resolved" "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz"
@@ -6848,11 +6883,21 @@
"resolved" "https://registry.npmmirror.com/windicss/-/windicss-3.5.6.tgz"
"version" "3.5.6"
"wmf@~1.0.1":
"integrity" "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="
"resolved" "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz"
"version" "1.0.2"
"word-wrap@^1.0.3", "word-wrap@^1.2.3", "word-wrap@^1.2.5":
"integrity" "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
"resolved" "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz"
"version" "1.2.5"
"word@~0.3.0":
"integrity" "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="
"resolved" "https://registry.npmmirror.com/word/-/word-0.3.0.tgz"
"version" "0.3.0"
"wrap-ansi@^6.2.0":
"integrity" "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="
"resolved" "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
@@ -6876,6 +6921,19 @@
"resolved" "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz"
"version" "1.0.2"
"xlsx@^0.18.5":
"integrity" "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="
"resolved" "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz"
"version" "0.18.5"
dependencies:
"adler-32" "~1.3.0"
"cfb" "~1.2.1"
"codepage" "~1.15.0"
"crc-32" "~1.2.1"
"ssf" "~0.11.2"
"wmf" "~1.0.1"
"word" "~0.3.0"
"xml-name-validator@^4.0.0":
"integrity" "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="
"resolved" "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz"

View File

@@ -7,11 +7,13 @@ 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.DeliveryTrackQueryDto;
import com.aiotagro.cattletrade.business.dto.DeviceAssignDto;
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;
@@ -85,6 +87,9 @@ public class DeliveryController {
@Autowired
private OrderMapper orderMapper;
@Autowired
private BaiduYingyanService baiduYingyanService;
/**
@@ -595,6 +600,43 @@ public class DeliveryController {
return AjaxResult.error("运单不存在");
}
if (status == 2) {
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);
arrivalUpdate.setArrivalTime(new Date());
deliveryService.updateById(arrivalUpdate);
}
Delivery delivery = new Delivery();
delivery.setId(id);
@@ -614,6 +656,15 @@ public class DeliveryController {
}
}
/**
* 查询运送清单百度鹰眼轨迹/停留点
*/
@SaCheckPermission("delivery:view")
@PostMapping("/yingyan/track")
public AjaxResult getYingyanTrack(@Validated @RequestBody DeliveryTrackQueryDto dto) {
return deliveryService.queryYingyanTrack(dto.getDeliveryId());
}
/**
* 删除装车订单(物理删除)
* 删除订单时同时清空关联设备的delivery_id和weight
@@ -671,7 +722,7 @@ public class DeliveryController {
/**
* 逻辑删除运送清单
* 只标记为已删除,不清空设备绑定关系,保留历史记录
* 逻辑删除时同时清空关联设备的delivery_id和car_number
*/
@SaCheckPermission("delivery:delete")
@PostMapping("/deleteLogic")
@@ -694,12 +745,36 @@ public class DeliveryController {
return AjaxResult.error("无权限删除此运单");
}
// 1. 先清空关联设备的delivery_id和car_number
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.aiotagro.cattletrade.business.entity.IotDeviceData> queryWrapper =
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
queryWrapper.eq("delivery_id", id);
List<com.aiotagro.cattletrade.business.entity.IotDeviceData> devices = iotDeviceDataMapper.selectList(queryWrapper);
// 使用 MyBatis-Plus 的逻辑删除功能
int updatedCount = 0;
for (com.aiotagro.cattletrade.business.entity.IotDeviceData device : devices) {
// 使用LambdaUpdateWrapper确保null值也能被更新
com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<com.aiotagro.cattletrade.business.entity.IotDeviceData> updateWrapper =
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<>();
updateWrapper.eq(com.aiotagro.cattletrade.business.entity.IotDeviceData::getDeviceId, device.getDeviceId())
.set(com.aiotagro.cattletrade.business.entity.IotDeviceData::getDeliveryId, null)
.set(com.aiotagro.cattletrade.business.entity.IotDeviceData::getCarNumber, null)
.set(com.aiotagro.cattletrade.business.entity.IotDeviceData::getUpdateTime, new Date());
int result = iotDeviceDataMapper.update(null, updateWrapper);
if (result > 0) {
updatedCount++;
logger.debug("清空设备绑定信息: deviceId={}, delivery_id=null, car_number=null", device.getDeviceId());
}
}
logger.info("逻辑删除运送清单,已清空 {} 个设备的绑定信息运单ID: {}", updatedCount, id);
// 2. 使用 MyBatis-Plus 的逻辑删除功能
boolean deleted = deliveryService.removeById(id);
if (deleted) {
return AjaxResult.success("运单删除成功");
return AjaxResult.success("运单删除成功,已清空 " + updatedCount + " 个设备的绑定信息");
} else {
return AjaxResult.error("运单删除失败");
}

View File

@@ -53,13 +53,19 @@ public class IotDeviceProxyController {
// 只查询正常状态的设备is_delet=0
queryWrapper.and(wrapper -> wrapper.eq("is_delet", 0).or().isNull("is_delet"));
// 根据设备类型查询(用于创建运送清单时过滤设备)
// 根据设备类型查询
if (params.containsKey("type") && params.get("type") != null) {
Integer deviceType = (Integer) params.get("type");
queryWrapper.eq("device_type", deviceType);
// 创建运送清单时,只显示未绑定的设备(delivery_id为空
queryWrapper.isNull("delivery_id");
logger.info("查询未绑定的设备,类型: {}", deviceType);
// 只有当 allotType 参数存在且为 "unassigned" 时,才限制 delivery_id 为空
// 这是为了在创建运送清单时只显示未绑定的设备
// 硬件管理页面eartag.vue, host.vue, collar.vue不传递 allotType会显示所有设备
if (params.containsKey("allotType") && "unassigned".equals(params.get("allotType"))) {
queryWrapper.isNull("delivery_id");
logger.info("查询未绑定的设备,类型: {}", deviceType);
} else {
logger.info("查询所有设备(包括已分配的),类型: {}", deviceType);
}
}
// 根据设备ID查询

View File

@@ -237,6 +237,7 @@ public class MemberController {
*/
@SaCheckPermission("member:edit")
@PostMapping("/updateDriver")
@Transactional
public AjaxResult updateDriver(@RequestBody Map<String, Object> params) {
try {
Integer id = (Integer) params.get("id");
@@ -246,6 +247,7 @@ public class MemberController {
// 获取参数值
String username = (String) params.get("username");
String mobile = (String) params.get("mobile");
String driverLicense = (String) params.get("driverLicense");
String drivingLicense = (String) params.get("drivingLicense");
String carImg = (String) params.get("carImg");
@@ -253,7 +255,48 @@ public class MemberController {
String idCard = (String) params.get("idCard");
String remark = (String) params.get("remark");
// 执行更新不再包含carNumber参数)
// 获取司机的member_id
Map<String, Object> driverInfo = memberDriverMapper.selectDriverById(id);
if (driverInfo == null) {
return AjaxResult.error("司机信息不存在");
}
// member_id 从数据库返回时可能是Long类型需要转换为Integer
Object memberIdObj = driverInfo.get("member_id");
Integer memberId = null;
if (memberIdObj != null) {
if (memberIdObj instanceof Long) {
memberId = ((Long) memberIdObj).intValue();
} else if (memberIdObj instanceof Integer) {
memberId = (Integer) memberIdObj;
} else if (memberIdObj instanceof Number) {
memberId = ((Number) memberIdObj).intValue();
}
}
if (memberId == null) {
return AjaxResult.error("司机关联的会员ID不存在");
}
// 如果提供了新手机号,检查是否与其他用户重复
if (mobile != null && !mobile.trim().isEmpty()) {
String currentMobile = (String) driverInfo.get("mobile");
// 如果手机号有变化,检查新手机号是否已存在
if (currentMobile == null || !currentMobile.equals(mobile.trim())) {
Integer existingMemberId = memberMapper.selectMemberIdByMobile(mobile.trim());
if (existingMemberId != null && !existingMemberId.equals(memberId)) {
return AjaxResult.error("该手机号已被其他用户使用");
}
}
// 更新member表的手机号
int mobileUpdateResult = memberMapper.updateMemberMobile(memberId, mobile.trim());
if (mobileUpdateResult <= 0) {
return AjaxResult.error("更新手机号失败");
}
}
// 执行更新member_driver表
int result = memberDriverMapper.updateDriver(id, username, driverLicense,
drivingLicense, carImg, recordCode, idCard, remark);
@@ -288,6 +331,14 @@ public class MemberController {
String cbkAccount = (String) params.get("cbkAccount");
String remark = (String) params.get("remark");
// 处理mobile和cbkAccount如果为空字符串设置为null
if (mobile != null && mobile.trim().isEmpty()) {
mobile = null;
}
if (cbkAccount != null && cbkAccount.trim().isEmpty()) {
cbkAccount = null;
}
// 执行更新
int result = memberMapper.updateUserInfo(id, mobile, type, status, username, cbkAccount, remark);
@@ -318,9 +369,6 @@ public class MemberController {
String remark = (String) params.get("remark");
// 参数验证
if (mobile == null || mobile.trim().isEmpty()) {
return AjaxResult.error("手机号不能为空");
}
if (username == null || username.trim().isEmpty()) {
return AjaxResult.error("用户姓名不能为空");
}
@@ -328,10 +376,17 @@ public class MemberController {
return AjaxResult.error("用户类型不能为空");
}
// 检查手机号是否已存在直接查询member表
Integer existingMemberId = memberMapper.selectMemberIdByMobile(mobile);
if (existingMemberId != null) {
return AjaxResult.error("该手机号已存在");
// 处理mobile如果为空或空字符串设置为null
if (mobile != null && mobile.trim().isEmpty()) {
mobile = null;
}
// 检查手机号是否已存在仅在mobile不为空时检查
if (mobile != null && !mobile.trim().isEmpty()) {
Integer existingMemberId = memberMapper.selectMemberIdByMobile(mobile);
if (existingMemberId != null) {
return AjaxResult.error("该手机号已存在");
}
}
// 先插入member表
@@ -340,8 +395,15 @@ public class MemberController {
return AjaxResult.error("用户基础信息插入失败");
}
// 获取刚插入的member记录的ID(通过查询最新记录)
Integer memberId = memberMapper.selectMemberIdByMobile(mobile);
// 获取刚插入的member记录的ID
Integer memberId = null;
if (mobile != null && !mobile.trim().isEmpty()) {
// 如果mobile不为空通过mobile查询
memberId = memberMapper.selectMemberIdByMobile(mobile);
} else {
// 如果mobile为空查询最新插入的记录通过create_time和id
memberId = memberMapper.selectLatestMemberId();
}
if (memberId == null) {
return AjaxResult.error("获取新用户ID失败");
}

View File

@@ -131,5 +131,29 @@ public class OrderController {
return AjaxResult.error("查询订单详情失败:" + e.getMessage());
}
}
/**
* 批量导入订单
*/
@SaCheckPermission("order:import")
@PostMapping("/batchImport")
public AjaxResult batchImport(@RequestBody Map<String, Object> params) {
try {
logger.info("批量导入订单,参数:{}", params);
@SuppressWarnings("unchecked")
List<Map<String, Object>> orders = (List<Map<String, Object>>) params.get("orders");
if (orders == null || orders.isEmpty()) {
logger.error("批量导入失败:订单列表不能为空");
return AjaxResult.error("订单列表不能为空");
}
return orderService.batchImportOrders(orders);
} catch (Exception e) {
logger.error("批量导入订单失败:{}", e.getMessage(), e);
return AjaxResult.error("批量导入订单失败:" + e.getMessage());
}
}
}

View File

@@ -27,6 +27,9 @@ public class DeliveryEditDto {
private String driverMobile;
/** 车牌号 */
private String licensePlate;
private Integer buyerId;
private Double buyerPrice;

View File

@@ -0,0 +1,16 @@
package com.aiotagro.cattletrade.business.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 运单轨迹查询参数
*/
@Data
public class DeliveryTrackQueryDto {
@NotNull(message = "运单ID不能为空")
private Integer deliveryId;
}

View File

@@ -0,0 +1,30 @@
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

@@ -0,0 +1,30 @@
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

@@ -100,7 +100,7 @@ public class Delivery implements Serializable {
private Double firmPrice;
/**
* 状态1-待装车2-已装车/预付款已支付3-已装车/尾款待支付4-已核验/待买家付款5-尾款已付款6-发票待开/进项票7-发票待开/销项
* 状态1-准备中2-运输中3-已结束
*/
@TableField("status")
private Integer status;
@@ -179,6 +179,26 @@ public class Delivery implements Serializable {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
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")
private Date arrivalTime;
/**
* 登记智能耳标数
*/

View File

@@ -25,4 +25,12 @@ 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,
@Param("endTime") Date endTime,
@Param("limit") Integer limit);
}

View File

@@ -26,4 +26,12 @@ 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,
@Param("endTime") Date endTime,
@Param("limit") Integer limit);
}

View File

@@ -188,6 +188,12 @@ public interface MemberMapper extends BaseMapper<Member> {
@Select("SELECT id FROM member WHERE mobile = #{mobile} ORDER BY id DESC LIMIT 1")
Integer selectMemberIdByMobile(@Param("mobile") String mobile);
/**
* 查询最新插入的member记录ID用于mobile为null时获取新插入的ID
*/
@Select("SELECT id FROM member ORDER BY id DESC LIMIT 1")
Integer selectLatestMemberId();
/**
* 新增用户基础信息插入member表
*/
@@ -215,4 +221,24 @@ public interface MemberMapper extends BaseMapper<Member> {
"LEFT JOIN member_user mu ON m.id = mu.member_id " +
"WHERE m.id = #{memberId}")
Map<String, Object> selectMemberUserById(@Param("memberId") Integer memberId);
/**
* 批量根据member ID查询member和member_user关联数据
*/
@Select("<script>" +
"SELECT m.id, m.mobile, mu.username " +
"FROM member m " +
"LEFT JOIN member_user mu ON m.id = mu.member_id " +
"WHERE m.id IN " +
"<foreach collection='memberIds' item='id' open='(' separator=',' close=')'>" +
" #{id}" +
"</foreach>" +
"</script>")
List<Map<String, Object>> selectMemberUserByIds(@Param("memberIds") List<Integer> memberIds);
/**
* 更新member表的手机号
*/
@org.apache.ibatis.annotations.Update("UPDATE member SET mobile = #{mobile}, update_time = NOW() WHERE id = #{memberId}")
int updateMemberMobile(@Param("memberId") Integer memberId, @Param("mobile") String mobile);
}

View File

@@ -25,4 +25,12 @@ 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,
@Param("endTime") Date endTime,
@Param("limit") Integer limit);
}

View File

@@ -0,0 +1,611 @@
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 org.springframework.web.util.UriComponentsBuilder;
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;
}
// ✅ 验证 entityName 不是 "entity" 字符串
if ("entity".equals(entityName) || "entity_name".equals(entityName)) {
logger.error("ensureEntity 失败entityName 参数错误,值为 '{}',这可能是参数传递错误", entityName);
return false;
}
try {
MultiValueMap<String, String> form = baseForm();
form.add("entity_name", entityName);
// entity_desc 为非必填项,且命名规则限制:只支持中文、英文字母、下划线、连字符、数字
// 为避免参数错误,不传递 entity_desc 参数
logger.debug("确保终端存在 - entity={}", entityName);
JsonNode result = postForm("/entity/add", form);
int status = result.path("status").asInt(-1);
String message = result.path("message").asText();
// ✅ 修复status=0创建成功、3005终端已存在、3006其他成功状态都视为成功
if (status == 0) {
logger.info("✅ 鹰眼终端创建成功, entityName={}", entityName);
return true;
} else if (status == 3005) {
// ✅ 终端已存在,这是正常情况,视为成功
logger.info("✅ 鹰眼终端已存在, entityName={}, message={}", entityName, message);
return true;
} else if (status == 3006) {
logger.info("✅ 鹰眼终端操作成功, entityName={}, status={}", entityName, status);
return true;
}
// 其他状态码视为失败
logger.warn("❌ 鹰眼创建终端失败, entityName={}, status={}, message={}",
entityName, status, message);
} catch (Exception e) {
logger.error("❌ 鹰眼创建终端异常, entityName={}", entityName, 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;
}
/**
* 查询轨迹单次查询限制24小时内
*/
public List<YingyanTrackPoint> queryTrack(String entityName, long startTime, long endTime) {
if (StringUtils.isBlank(entityName)) {
return Collections.emptyList();
}
try {
Map<String, Object> params = new HashMap<>();
params.put("entity_name", entityName);
params.put("start_time", startTime);
params.put("end_time", endTime);
params.put("is_processed", 1);
params.put("need_denoise", 1);
params.put("need_mapmatch", 0);
params.put("coord_type_output", "bd09ll");
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();
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 pointsNode = result.path("track").path("points");
if (pointsNode == null || !pointsNode.isArray() || pointsNode.size() == 0) {
return Collections.emptyList();
}
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();
}
}
/**
* 分段查询轨迹支持超过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);
params.put("start_time", startTime);
params.put("end_time", endTime);
params.put("stay_time", 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 {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(buildUrl(path))
.queryParam("ak", BaiduYingyanConstants.AK)
.queryParam("service_id", BaiduYingyanConstants.SERVICE_ID);
if (params != null) {
params.forEach((key, value) -> {
if (value != null) {
// ✅ 验证参数名和值,避免参数传递错误
if ("entity_name".equals(key) && ("entity".equals(value) || "entity_name".equals(value))) {
logger.error("参数传递错误entity_name 的值不能是 '{}',这可能是参数名和值混淆了", value);
throw new IllegalArgumentException("entity_name 参数值错误: " + value);
}
builder.queryParam(key, value);
}
});
}
// ✅ 记录请求URL调试用生产环境可关闭
String requestUrl = builder.toUriString();
logger.debug("百度鹰眼API请求 - path={}, url={}", path, 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

@@ -0,0 +1,426 @@
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());
int successCount = 0;
int failCount = 0;
for (YingyanTrackPoint point : points) {
boolean success = baiduYingyanService.pushTrackPoint(
entityName, point.getLatitude(), point.getLongitude(), point.getLocTime());
if (success) {
successCount++;
Date locDate = new Date(point.getLocTime() * 1000L);
updateLastSync(delivery, locDate);
handleArrivalIfNeeded(delivery, point);
if (successCount <= 3 || successCount % 20 == 0) {
logger.debug("运单 {} 上传轨迹点成功 [{}/{}] - 纬度: {}, 经度: {}, 时间: {}",
delivery.getDeliveryNumber(), successCount, points.size(),
point.getLatitude(), point.getLongitude(), locDate);
}
} else {
failCount++;
if (failCount <= 3) {
logger.warn("运单 {} 上传轨迹点失败 [{}/{}] - 纬度: {}, 经度: {}, 时间: {}",
delivery.getDeliveryNumber(), failCount, points.size(),
point.getLatitude(), point.getLongitude(),
new Date(point.getLocTime() * 1000L));
}
}
if (delivery.getStatus() != null && delivery.getStatus() == 3) {
logger.info("运单 {} 状态已变为已结束,停止同步", delivery.getDeliveryNumber());
break;
}
}
logger.info("运单 {} 轨迹上传完成 - 成功: {}, 失败: {}, 总计: {}",
delivery.getDeliveryNumber(), successCount, failCount, 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

@@ -48,4 +48,9 @@ public interface IDeliveryService extends IService<Delivery> {
AjaxResult detail(Integer id);
PageResultResponse<Delivery> pageQueryListLog(DeliverListDto dto);
/**
* 查询百度鹰眼轨迹与停留点
*/
AjaxResult queryYingyanTrack(Integer deliveryId);
}

View File

@@ -5,6 +5,7 @@ import com.aiotagro.common.core.web.domain.AjaxResult;
import com.aiotagro.common.core.web.domain.PageResultResponse;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import java.util.Map;
/**
@@ -54,5 +55,13 @@ public interface IOrderService extends IService<Order> {
* @return AjaxResult
*/
AjaxResult getOrderDetail(Integer id);
/**
* 批量导入订单
*
* @param orders 订单列表
* @return AjaxResult
*/
AjaxResult batchImportOrders(List<Map<String, Object>> orders);
}

View File

@@ -50,17 +50,32 @@ public class IotDeviceLogSyncService {
@Transactional
public void syncDeviceDataToLogs() {
try {
logger.info("开始执行设备日志同步任务");
logger.info("========== 开始执行设备日志同步任务 ==========");
// 查询所有设备数据
List<IotDeviceData> allDevices = iotDeviceDataMapper.selectList(null);
// 查询绑定了运送清单的设备数据delivery_id 不为空)
QueryWrapper<IotDeviceData> queryWrapper = new QueryWrapper<>();
queryWrapper.isNotNull("delivery_id");
List<IotDeviceData> allDevices = iotDeviceDataMapper.selectList(queryWrapper);
if (allDevices.isEmpty()) {
logger.warn("未找到任何设备数据");
logger.warn("未找到任何绑定了运送清单的设备数据");
return;
}
logger.info("找到 {} 个设备,开始同步到日志表", allDevices.size());
logger.info("找到 {} 个绑定了运送清单的设备,开始同步到日志表", allDevices.size());
// ✅ 统计有经纬度数据的设备数量
long devicesWithCoordinates = allDevices.stream()
.filter(device -> device.getLatitude() != null && device.getLongitude() != null)
.filter(device -> {
String lat = sanitizeCoordinate(device.getLatitude());
String lng = sanitizeCoordinate(device.getLongitude());
return lat != null && lng != null && !lat.equals("0") && !lng.equals("0");
})
.count();
logger.info("其中 {} 个设备包含有效的经纬度坐标数据", devicesWithCoordinates);
logger.info("注意:设备日志同步任务仅将数据同步到日志表,不直接上传到百度鹰眼服务");
logger.info("百度鹰眼轨迹上传由 DeliveryYingyanSyncService 定时任务负责");
int hostCount = 0;
int earTagCount = 0;
@@ -72,6 +87,7 @@ public class IotDeviceLogSyncService {
List<XqClientLog> collarLogs = new ArrayList<>();
// 遍历所有设备,根据设备类型分组
int devicesWithCoordinatesCount = 0;
for (IotDeviceData device : allDevices) {
try {
Integer deviceType = device.getDeviceType();
@@ -81,6 +97,23 @@ public class IotDeviceLogSyncService {
continue;
}
// ✅ 记录设备经纬度信息
boolean hasValidCoordinates = false;
if (device.getLatitude() != null && device.getLongitude() != null) {
String lat = sanitizeCoordinate(device.getLatitude());
String lng = sanitizeCoordinate(device.getLongitude());
if (lat != null && lng != null && !lat.equals("0") && !lng.equals("0")) {
hasValidCoordinates = true;
devicesWithCoordinatesCount++;
logger.debug("设备 {} (类型: {}, 运单ID: {}) 包含经纬度坐标 - 纬度: {}, 经度: {}",
device.getDeviceId(), deviceType, device.getDeliveryId(), lat, lng);
}
}
if (!hasValidCoordinates) {
logger.debug("设备 {} (类型: {}, 运单ID: {}) 无有效经纬度坐标",
device.getDeviceId(), deviceType, device.getDeliveryId());
}
switch (deviceType) {
case 1: // 主机设备
hostLogs.add(convertToHostLog(device));
@@ -167,6 +200,9 @@ public class IotDeviceLogSyncService {
}
logger.info("设备日志同步完成 - 主机: {}, 耳标: {}, 项圈: {}", hostCount, earTagCount, collarCount);
logger.info("包含有效经纬度坐标的设备数量: {}", devicesWithCoordinatesCount);
logger.info("========== 设备日志同步任务执行完成 ==========");
logger.info("提示:同步到日志表的经纬度数据将由 DeliveryYingyanSyncService 定时任务上传到百度鹰眼服务");
} catch (Exception e) {
logger.error("设备日志同步任务执行失败", e);

View File

@@ -5,8 +5,11 @@ 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;
@@ -19,6 +22,7 @@ import com.aiotagro.common.core.utils.StringUtils;
import com.aiotagro.common.core.web.domain.AjaxResult;
import com.aiotagro.common.core.web.domain.PageResultResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
@@ -64,6 +68,9 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
private MemberDriverMapper memberDriverMapper;
@Autowired
private IDeliveryDeviceService deliveryDeviceService;
@Autowired
private BaiduYingyanService baiduYingyanService;
@Autowired
private IXqClientService xqClientService;
@Autowired
@@ -84,7 +91,6 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
// 调试:打印接收到的所有参数
Page<Delivery> result = PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
LambdaQueryWrapper<Delivery> wrapper = new LambdaQueryWrapper<>();
// 运输单号模糊查询
@@ -119,12 +125,20 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
}
wrapper.orderByDesc(Delivery::getId);
List<Delivery> list = this.list(wrapper);
if(CollectionUtils.isNotEmpty(list)){
}
if(CollectionUtils.isNotEmpty(list)){
list.forEach(delivery -> {
// 判断是否需要数据权限过滤
boolean needPermissionFilter = !SecurityUtil.isSuperAdmin() && StringUtils.isNotEmpty(currentUserMobile);
List<Delivery> list;
long total;
if (needPermissionFilter) {
// 需要权限过滤:先查询所有数据,填充信息,过滤后再分页
list = this.list(wrapper);
// 填充关联信息(供应商、资金方、采购商、司机等)
if(CollectionUtils.isNotEmpty(list)){
list.forEach(delivery -> {
if(userId.equals(delivery.getCheckBy())){
//判断是否需要核验1需要核验2不需要核验
delivery.setIfCheck(1);
@@ -344,58 +358,283 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
}
}
});
}
// 数据权限过滤:非超级管理员只能看到与自己手机号相关的订单
list = list.stream().filter(delivery -> {
boolean hasPermission = false;
if (!SecurityUtil.isSuperAdmin() && StringUtils.isNotEmpty(currentUserMobile)) {
// 检查是否是司机
if (StringUtils.isNotEmpty(delivery.getDriverMobile()) &&
currentUserMobile.equals(delivery.getDriverMobile())) {
hasPermission = true;
}
list = list.stream().filter(delivery -> {
boolean hasPermission = false;
// 检查是否是司机
if (StringUtils.isNotEmpty(delivery.getDriverMobile()) &&
currentUserMobile.equals(delivery.getDriverMobile())) {
hasPermission = true;
}
// 检查是否是供应商(可能有多个供应商)
if (!hasPermission && StringUtils.isNotEmpty(delivery.getSupplierMobile())) {
String[] supplierMobiles = delivery.getSupplierMobile().split(",");
for (String mobile : supplierMobiles) {
if (currentUserMobile.equals(mobile.trim())) {
hasPermission = true;
break;
}
// 检查是否是供应商(可能有多个供应商)
if (!hasPermission && StringUtils.isNotEmpty(delivery.getSupplierMobile())) {
String[] supplierMobiles = delivery.getSupplierMobile().split(",");
for (String mobile : supplierMobiles) {
if (currentUserMobile.equals(mobile.trim())) {
hasPermission = true;
break;
}
}
}
// 检查是否是资金方
if (!hasPermission && StringUtils.isNotEmpty(delivery.getFundMobile()) &&
currentUserMobile.equals(delivery.getFundMobile())) {
hasPermission = true;
}
// 检查是否是资金方
if (!hasPermission && StringUtils.isNotEmpty(delivery.getFundMobile()) &&
currentUserMobile.equals(delivery.getFundMobile())) {
hasPermission = true;
}
// 检查是否是采购商
if (!hasPermission && StringUtils.isNotEmpty(delivery.getBuyerMobile()) &&
currentUserMobile.equals(delivery.getBuyerMobile())) {
hasPermission = true;
}
// 检查是否是采购商
if (!hasPermission && StringUtils.isNotEmpty(delivery.getBuyerMobile()) &&
currentUserMobile.equals(delivery.getBuyerMobile())) {
hasPermission = true;
}
if (!hasPermission) {
}
return hasPermission;
}).collect(Collectors.toList());
return hasPermission;
}).collect(Collectors.toList());
// 获取过滤后的总数
total = list.size();
} else if (SecurityUtil.isSuperAdmin()) {
// 手动分页
int startIndex = (dto.getPageNum() - 1) * dto.getPageSize();
int endIndex = Math.min(startIndex + dto.getPageSize(), list.size());
if (startIndex < list.size()) {
list = list.subList(startIndex, endIndex);
} else {
list = new ArrayList<>();
}
} else {
// 不需要权限过滤直接使用PageHelper分页
Page<Delivery> result = PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
list = this.list(wrapper);
total = result.getTotal();
// 填充关联信息(供应商、资金方、采购商、司机等)
if(CollectionUtils.isNotEmpty(list)){
list.forEach(delivery -> {
if(userId.equals(delivery.getCheckBy())){
//判断是否需要核验1需要核验2不需要核验
delivery.setIfCheck(1);
}
// 查询四个角色的手机号(供应商、资金方、采购商、司机)
try {
// 1. 查询供应商信息supplierId是逗号分隔的字符串
if (StringUtils.isNotEmpty(delivery.getSupplierId())) {
String[] supplierIds = delivery.getSupplierId().split(",");
List<String> supplierNames = new ArrayList<>();
List<String> supplierMobiles = new ArrayList<>();
for (String supplierId : supplierIds) {
if (StringUtils.isNotEmpty(supplierId.trim())) {
try {
Integer sid = Integer.parseInt(supplierId.trim());
// 查询member和member_user表关联数据
Map<String, Object> supplierInfo = memberMapper.selectMemberUserById(sid);
if (supplierInfo != null) {
String username = (String) supplierInfo.get("username");
String mobile = (String) supplierInfo.get("mobile");
if (StringUtils.isNotEmpty(username)) {
supplierNames.add(username);
}
if (StringUtils.isNotEmpty(mobile)) {
supplierMobiles.add(mobile);
}
}
} catch (NumberFormatException e) {
}
}
}
if (!supplierNames.isEmpty()) {
delivery.setSupplierName(String.join(",", supplierNames));
} else if (!supplierMobiles.isEmpty()) {
// 如果用户名为空,使用手机号作为备选
delivery.setSupplierName(String.join(",", supplierMobiles));
}
if (!supplierMobiles.isEmpty()) {
delivery.setSupplierMobile(String.join(",", supplierMobiles));
}
}
// 2. 查询资金方信息
if (delivery.getFundId() != null) {
Map<String, Object> fundInfo = memberMapper.selectMemberUserById(delivery.getFundId());
if (fundInfo != null) {
String username = (String) fundInfo.get("username");
String mobile = (String) fundInfo.get("mobile");
if (StringUtils.isNotEmpty(username)) {
delivery.setFundName(username);
} else if (StringUtils.isNotEmpty(mobile)) {
// 如果用户名为空,使用手机号作为备选
delivery.setFundName(mobile);
}
if (StringUtils.isNotEmpty(mobile)) {
delivery.setFundMobile(mobile);
}
}
}
// 3. 查询采购商信息
if (delivery.getBuyerId() != null) {
Map<String, Object> buyerInfo = memberMapper.selectMemberUserById(delivery.getBuyerId());
if (buyerInfo != null) {
String username = (String) buyerInfo.get("username");
String mobile = (String) buyerInfo.get("mobile");
if (StringUtils.isNotEmpty(username)) {
delivery.setBuyerName(username);
} else if (StringUtils.isNotEmpty(mobile)) {
// 如果用户名为空,使用手机号作为备选
delivery.setBuyerName(mobile);
}
if (StringUtils.isNotEmpty(mobile)) {
delivery.setBuyerMobile(mobile);
}
}
}
// 4. 查询司机手机号如果有司机ID
if (delivery.getDriverId() != null) {
try {
Map<String, Object> driverInfo = memberDriverMapper.selectDriverById(delivery.getDriverId());
if (driverInfo != null) {
String driverName = (String) driverInfo.get("username");
String driverMobile = (String) driverInfo.get("mobile");
String carImg = (String) driverInfo.get("car_img");
if (StringUtils.isNotEmpty(driverMobile)) {
delivery.setDriverMobile(driverMobile);
}
if (StringUtils.isNotEmpty(driverName)) {
delivery.setDriverName(driverName);
}
// 优先从车辆表获取车身照片(根据车牌号)
// 如果车辆表中没有,再从司机信息中获取作为后备
boolean vehiclePhotoSet = false;
if (delivery.getLicensePlate() != null && StringUtils.isNotEmpty(delivery.getLicensePlate())) {
try {
Vehicle vehicle = vehicleMapper.selectByLicensePlate(delivery.getLicensePlate());
if (vehicle != null) {
String carFrontPhoto = vehicle.getCarFrontPhoto();
String carRearPhoto = vehicle.getCarRearPhoto();
if (StringUtils.isNotEmpty(carFrontPhoto) || StringUtils.isNotEmpty(carRearPhoto)) {
delivery.setCarFrontPhoto(StringUtils.isNotEmpty(carFrontPhoto) ? carFrontPhoto : null);
delivery.setCarBehindPhoto(StringUtils.isNotEmpty(carRearPhoto) ? carRearPhoto : null);
vehiclePhotoSet = true;
}
}
} catch (Exception e) {
logger.error("从车辆表获取照片失败: " + e.getMessage(), e);
}
}
// 如果车辆表中没有照片,从司机信息中获取作为后备
if (!vehiclePhotoSet && carImg != null && !carImg.isEmpty()) {
// 按逗号分割car_img字段分别映射到车头和车尾照片
String[] carImgUrls = carImg.split(",");
if (carImgUrls.length >= 2) {
// 逗号前面的URL作为车尾照片
String carBehindPhoto = carImgUrls[0].trim();
// 逗号后面的URL作为车头照片
String carFrontPhoto = carImgUrls[1].trim();
delivery.setCarBehindPhoto(carBehindPhoto);
delivery.setCarFrontPhoto(carFrontPhoto);
} else if (carImgUrls.length == 1) {
// 只有一个URL时同时设置为车头和车尾照片
String singlePhoto = carImgUrls[0].trim();
delivery.setCarFrontPhoto(singlePhoto);
delivery.setCarBehindPhoto(singlePhoto);
} else {
// 没有有效URL设置为null
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
} else if (!vehiclePhotoSet) {
// 如果车辆表和司机信息中都没有照片设置为null
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
}
} catch (Exception e) {
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 统计登记设备数量(耳标+项圈)
Integer currentDeliveryId = delivery.getId();
if (currentDeliveryId != null) {
try {
// 统计耳标设备数量
LambdaQueryWrapper<DeliveryDevice> earTagWrapper = new LambdaQueryWrapper<>();
earTagWrapper.eq(DeliveryDevice::getDeliveryId, currentDeliveryId)
.eq(DeliveryDevice::getDeviceType, 2);
long earTagCount = deliveryDeviceService.count(earTagWrapper);
// 统计项圈设备数量
LambdaQueryWrapper<DeliveryDevice> collarWrapper = new LambdaQueryWrapper<>();
collarWrapper.eq(DeliveryDevice::getDeliveryId, currentDeliveryId)
.eq(DeliveryDevice::getDeviceType, 3);
long collarCount = deliveryDeviceService.count(collarWrapper);
// 设置总设备数量和耳标数量
int totalDeviceCount = (int) (earTagCount + collarCount);
delivery.setRegisteredJbqCount(totalDeviceCount);
delivery.setEarTagCount((int) earTagCount);
// 设置已分配设备数量,与登记设备数量保持一致
delivery.setBindJbqCount(totalDeviceCount);
// 统计已佩戴设备数量bandge_status = 1
int wornDeviceCount = 0;
try {
// 统计已佩戴的耳标设备数量通过delivery_device表关联
LambdaQueryWrapper<DeliveryDevice> wornEarTagWrapper = new LambdaQueryWrapper<>();
wornEarTagWrapper.eq(DeliveryDevice::getDeliveryId, currentDeliveryId)
.eq(DeliveryDevice::getDeviceType, 2)
.eq(DeliveryDevice::getIsWare, 1); // 1表示已佩戴
long wornEarTagCount = deliveryDeviceService.count(wornEarTagWrapper);
// 统计已佩戴的项圈设备数量通过delivery_device表关联xq_client表
LambdaQueryWrapper<DeliveryDevice> wornCollarWrapper = new LambdaQueryWrapper<>();
wornCollarWrapper.eq(DeliveryDevice::getDeliveryId, currentDeliveryId)
.eq(DeliveryDevice::getDeviceType, 3);
List<DeliveryDevice> collarDevices = deliveryDeviceService.list(wornCollarWrapper);
int wornCollarCount = 0;
for (DeliveryDevice device : collarDevices) {
// 查询xq_client表中的bandge_status
LambdaQueryWrapper<XqClient> xqWrapper = new LambdaQueryWrapper<>();
xqWrapper.eq(XqClient::getSn, device.getDeviceId());
XqClient xqClient = xqClientService.getOne(xqWrapper);
if (xqClient != null && xqClient.getBandgeStatus() != null && xqClient.getBandgeStatus() == 1) {
wornCollarCount++;
}
}
wornDeviceCount = (int) (wornEarTagCount + wornCollarCount);
delivery.setWareCount(wornDeviceCount);
} catch (Exception e) {
delivery.setWareCount(0);
}
} catch (Exception e) {
delivery.setRegisteredJbqCount(0);
delivery.setBindJbqCount(0);
delivery.setWareCount(0);
}
}
});
}
}
// 更新分页信息
long filteredTotal = list.size();
return new PageResultResponse(filteredTotal, list);
// 返回分页结果
return new PageResultResponse<>(total, list);
}
/**
@@ -754,6 +993,44 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
if (dto.getDriverMobile() != null) {
delivery.setDriverMobile(dto.getDriverMobile());
}
if (dto.getLicensePlate() != null) {
String oldLicensePlate = delivery.getLicensePlate();
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());
}
@@ -1277,6 +1554,11 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
public PageResultResponse<DeliveryLogVo> pageQueryList(DeliverListDto dto) {
//获取当前登录人的信息
String currentUserMobile = SecurityUtil.getUserMobile();
boolean isSuperAdmin = SecurityUtil.isSuperAdmin();
logger.info("[预警列表查询] 开始查询,参数: pageNum={}, pageSize={}, deliveryNumber={}, licensePlate={}, warningType={}, startTime={}, endTime={}, isSuperAdmin={}",
dto.getPageNum(), dto.getPageSize(), dto.getDeliveryNumber(), dto.getLicensePlate(),
dto.getWarningType(), dto.getStartTime(), dto.getEndTime(), isSuperAdmin);
Page<Delivery> result = PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
if(StringUtils.isNotEmpty(dto.getStartTime())){
@@ -1287,16 +1569,74 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
String endTime = dto.getEndTime() + " 23:59:59";
dto.setEndTime(endTime);
}
List<Delivery> resList = this.baseMapper.getPageWarningLog(dto);
long totalBeforeFilter = result.getTotal(); // 保存过滤前的总数
logger.info("[预警列表查询] SQL查询结果: 总数={}, 当前页数据量={}", totalBeforeFilter, resList.size());
// ✅ 调试:如果查询结果为空,检查可能的原因
if (totalBeforeFilter == 0 && resList.isEmpty()) {
logger.warn("[预警列表查询] ⚠️ 查询结果为空,可能原因:");
logger.warn("[预警列表查询] 1. delivery 表和 warning_log 表的 delivery_id 不匹配");
logger.warn("[预警列表查询] 2. warning_log 表中的 delivery_id 在 delivery 表中不存在");
logger.warn("[预警列表查询] 3. SQL 查询逻辑有问题");
// 检查 warning_log 表中是否有数据
try {
long warningLogCount = warningLogMapper.selectCount(null);
logger.info("[预警列表查询] warning_log 表总记录数: {}", warningLogCount);
// 检查有 delivery_id 的记录数
QueryWrapper<WarningLog> countWrapper = new QueryWrapper<>();
countWrapper.in("warning_type", 2,3,4,5,6,7,8,9);
long validWarningCount = warningLogMapper.selectCount(countWrapper);
logger.info("[预警列表查询] warning_log 表中预警类型在(2-9)范围内的记录数: {}", validWarningCount);
// 检查 delivery 表中有多少运单
long deliveryCount = this.count();
logger.info("[预警列表查询] delivery 表总记录数: {}", deliveryCount);
// ✅ 关键调试:检查 warning_log 表中的 delivery_id 是否在 delivery 表中存在
QueryWrapper<WarningLog> deliveryIdWrapper = new QueryWrapper<>();
deliveryIdWrapper.in("warning_type", 2,3,4,5,6,7,8,9);
deliveryIdWrapper.select("DISTINCT delivery_id");
List<WarningLog> warningLogsWithDeliveryId = warningLogMapper.selectList(deliveryIdWrapper);
Set<Integer> warningLogDeliveryIds = warningLogsWithDeliveryId.stream()
.map(WarningLog::getDeliveryId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
logger.info("[预警列表查询] warning_log 表中不重复的 delivery_id 数量: {}", warningLogDeliveryIds.size());
// 检查这些 delivery_id 在 delivery 表中是否存在
if (!warningLogDeliveryIds.isEmpty()) {
List<Integer> deliveryIds = this.listByIds(new ArrayList<>(warningLogDeliveryIds))
.stream()
.map(Delivery::getId)
.collect(Collectors.toList());
logger.info("[预警列表查询] 在 delivery 表中存在的 delivery_id 数量: {}", deliveryIds.size());
logger.info("[预警列表查询] 在 delivery 表中存在的 delivery_id: {}", deliveryIds);
// 找出不匹配的 delivery_id
Set<Integer> missingDeliveryIds = new HashSet<>(warningLogDeliveryIds);
missingDeliveryIds.removeAll(deliveryIds);
if (!missingDeliveryIds.isEmpty()) {
logger.warn("[预警列表查询] ⚠️ warning_log 表中有 {} 个 delivery_id 在 delivery 表中不存在", missingDeliveryIds.size());
logger.warn("[预警列表查询] 不匹配的 delivery_id 示例前10个: {}",
missingDeliveryIds.stream().limit(10).collect(Collectors.toList()));
}
}
} catch (Exception e) {
logger.error("[预警列表查询] 调试查询失败: {}", e.getMessage(), e);
}
}
// 数据权限过滤:非超级管理员只能看到与自己手机号相关的订单
if (!SecurityUtil.isSuperAdmin() && StringUtils.isNotEmpty(currentUserMobile)) {
if (!isSuperAdmin && StringUtils.isNotEmpty(currentUserMobile)) {
int beforeFilterSize = resList.size();
resList = resList.stream().filter(delivery -> {
boolean hasPermission = false;
// 检查是否是司机
if (StringUtils.isNotEmpty(delivery.getDriverMobile()) &&
currentUserMobile.equals(delivery.getDriverMobile())) {
@@ -1326,26 +1666,61 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
hasPermission = true;
}
if (!hasPermission) {
}
return hasPermission;
}).collect(Collectors.toList());
} else if (SecurityUtil.isSuperAdmin()) {
logger.info("[预警列表查询] 权限过滤后: 过滤前={}, 过滤后={}", beforeFilterSize, resList.size());
} else if (isSuperAdmin) {
logger.info("[预警列表查询] 超级管理员,跳过权限过滤");
} else {
logger.warn("[预警列表查询] 未获取到用户手机号,跳过权限过滤");
}
resList.forEach(deliveryLogVo -> {
String warningType = deliveryLogVo.getWarningType();
if(StringUtils.isNotEmpty(warningType)){
deliveryLogVo.setWarningTypeDesc(EnumUtil.getEnumConstant(WarningStatusAdminEnum.class , Integer.parseInt(warningType)).getDescription());
}
});
// ✅ 修复:将 Delivery 实体转换为 DeliveryLogVo
List<DeliveryLogVo> voList = new ArrayList<>();
for (Delivery delivery : resList) {
DeliveryLogVo vo = new DeliveryLogVo();
vo.setId(delivery.getId());
vo.setDeliveryId(delivery.getId());
vo.setDeliveryNumber(delivery.getDeliveryNumber());
vo.setLicensePlate(delivery.getLicensePlate());
vo.setStatus(delivery.getStatus());
vo.setCarFrontPhoto(delivery.getCarFrontPhoto());
vo.setRegisteredJbqCount(delivery.getRegisteredJbqCount());
vo.setInventoryJbqCount(delivery.getInventoryJbqCount());
vo.setWarningType(delivery.getWarningType());
vo.setWarningTime(delivery.getWarningTime());
vo.setCreateByDesc(delivery.getCreateByName());
// 更新分页信息
long filteredTotal = resList.size();
return new PageResultResponse(filteredTotal, resList);
// ✅ 新增:填充起始地、目的地、创建时间、预计送达时间、司机姓名、创建人
vo.setStartLocation(delivery.getStartLocation());
vo.setEndLocation(delivery.getEndLocation());
vo.setCreateTime(delivery.getCreateTime());
vo.setEstimatedDeliveryTime(delivery.getEstimatedDeliveryTime());
vo.setDriverName(delivery.getDriverName());
vo.setCreateByName(delivery.getCreateByName());
// 设置预警类型描述
String warningType = delivery.getWarningType();
if(StringUtils.isNotEmpty(warningType)){
try {
vo.setWarningTypeDesc(EnumUtil.getEnumConstant(WarningStatusAdminEnum.class , Integer.parseInt(warningType)).getDescription());
} catch (Exception e) {
logger.warn("[预警列表查询] 预警类型解析失败: {}", warningType);
vo.setWarningTypeDesc("未知类型");
}
}
voList.add(vo);
}
// ✅ 修复:使用分页查询的总数,而不是过滤后的数量
// 如果是超级管理员,使用 SQL 查询的总数;否则使用过滤后的数量
long finalTotal = isSuperAdmin ? totalBeforeFilter : voList.size();
logger.info("[预警列表查询] 最终返回: 总数={}, 数据量={}", finalTotal, voList.size());
return new PageResultResponse<DeliveryLogVo>(finalTotal, voList);
}
@Override
@@ -1654,6 +2029,107 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
return AjaxResult.success(resMap);
}
@Override
public AjaxResult queryYingyanTrack(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;
}
logger.info("运单 {} 轨迹查询开始时间确定 - estimatedDeliveryTime: {}, estimatedDepartureTime: {}, createTime: {}, 最终使用: {}",
delivery.getDeliveryNumber(),
delivery.getEstimatedDeliveryTime(),
delivery.getEstimatedDepartureTime(),
delivery.getCreateTime(),
new Date(startTime * 1000));
long endTime = determineTrackEndTime(delivery);
// ✅ 使用分段查询方法支持超过24小时的轨迹查询
logger.info("查询运单 {} 的百度鹰眼轨迹 - entity={}, startTime={}, endTime={}, 时间跨度={}小时",
delivery.getDeliveryNumber(), entityName, startTime, endTime, (endTime - startTime) / 3600);
// ✅ 确保终端存在后再查询(重要:如果终端不存在,查询会失败)
boolean entityExists = baiduYingyanService.ensureEntity(entityName);
if (!entityExists) {
logger.error("运单 {} 终端不存在且创建失败,无法查询轨迹 - entity={}, deliveryNumber={}, deliveryId={}",
delivery.getDeliveryNumber(), entityName, delivery.getDeliveryNumber(), delivery.getId());
// 返回空结果,而不是继续查询(因为查询肯定会失败)
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", "终端不存在且创建失败,请检查百度鹰眼服务配置或终端名称是否正确");
return AjaxResult.success(data);
}
logger.debug("运单 {} 终端已确保存在,开始查询轨迹 - entity={}",
delivery.getDeliveryNumber(), entityName);
List<YingyanTrackPoint> trackPoints = baiduYingyanService.queryTrackSegmented(entityName, startTime, endTime);
List<YingyanStayPoint> stayPoints = baiduYingyanService.queryStayPointsSegmented(entityName, startTime, endTime, 900);
logger.info("运单 {} 轨迹查询完成 - 轨迹点数: {}, 停留点数: {}",
delivery.getDeliveryNumber(), trackPoints.size(), stayPoints.size());
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());
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;
}
}
return System.currentTimeMillis() / 1000;
}
private long dateToSeconds(Date date) {
if (date == null) {
return 0;
}
return date.getTime() / 1000;
}
/**
* 获取核验状态的中文描述
* @param status 状态码

View File

@@ -1,7 +1,6 @@
package com.aiotagro.cattletrade.business.service.impl;
import com.aiotagro.cattletrade.business.entity.Order;
import com.aiotagro.cattletrade.business.entity.Member;
import com.aiotagro.cattletrade.business.entity.SysUser;
import com.aiotagro.cattletrade.business.mapper.OrderMapper;
import com.aiotagro.cattletrade.business.mapper.MemberMapper;
@@ -20,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -62,9 +62,6 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
logger.info("分页查询订单列表,页码:{},每页数量:{},买方:{},卖方:{},结算方式:{}",
pageNum, pageSize, buyerName, sellerName, settlementType);
// 使用PageHelper进行分页
Page<Order> page = PageHelper.startPage(pageNum, pageSize);
// 构建查询条件
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(settlementType != null, Order::getSettlementType, settlementType);
@@ -89,28 +86,65 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
queryWrapper.orderByDesc(Order::getCreateTime);
// 执行查询
List<Order> list = orderMapper.selectList(queryWrapper);
// 判断是否需要先过滤再分页(如果提供了买方或卖方名称搜索)
boolean needFilter = (buyerName != null && !buyerName.trim().isEmpty()) ||
(sellerName != null && !sellerName.trim().isEmpty());
// 填充关联信息
list.forEach(this::fillOrderInfo);
List<Order> filteredList;
long total;
// 如果提供了买方或卖方名称搜索,进行过滤
List<Order> filteredList = list;
if (buyerName != null && !buyerName.trim().isEmpty()) {
filteredList = filteredList.stream()
.filter(order -> order.getBuyerName() != null && order.getBuyerName().contains(buyerName.trim()))
.collect(java.util.stream.Collectors.toList());
}
if (sellerName != null && !sellerName.trim().isEmpty()) {
filteredList = filteredList.stream()
.filter(order -> order.getSellerName() != null && order.getSellerName().contains(sellerName.trim()))
.collect(java.util.stream.Collectors.toList());
if (needFilter) {
// 需要过滤的情况:先查询所有数据,填充信息,过滤,然后手动分页
List<Order> allList = orderMapper.selectList(queryWrapper);
// 批量填充关联信息(优化性能)
fillOrderInfoBatch(allList);
// 进行过滤
filteredList = allList;
if (buyerName != null && !buyerName.trim().isEmpty()) {
filteredList = filteredList.stream()
.filter(order -> order.getBuyerName() != null && order.getBuyerName().contains(buyerName.trim()))
.collect(java.util.stream.Collectors.toList());
}
if (sellerName != null && !sellerName.trim().isEmpty()) {
filteredList = filteredList.stream()
.filter(order -> order.getSellerName() != null && order.getSellerName().contains(sellerName.trim()))
.collect(java.util.stream.Collectors.toList());
}
// 获取总数
total = filteredList.size();
// 手动分页
int startIndex = (pageNum - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, filteredList.size());
if (startIndex < filteredList.size()) {
filteredList = filteredList.subList(startIndex, endIndex);
} else {
filteredList = new ArrayList<>();
}
logger.info("查询到{}条订单记录,过滤后{}条,分页后{}条", allList.size(), total, filteredList.size());
} else {
// 不需要过滤的情况直接使用PageHelper分页
Page<Order> page = PageHelper.startPage(pageNum, pageSize);
// 执行查询
List<Order> list = orderMapper.selectList(queryWrapper);
// 批量填充关联信息(优化性能)
fillOrderInfoBatch(list);
// 获取总数和分页数据
total = page.getTotal();
filteredList = list;
logger.info("查询到{}条订单记录,分页后{}条", total, filteredList.size());
}
// 构建分页结果
logger.info("查询到{}条订单记录,过滤后{}条", list.size(), filteredList.size());
return new PageResultResponse<>(filteredList.size(), filteredList);
return new PageResultResponse<>(total, filteredList);
}
/**
@@ -290,8 +324,178 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
return AjaxResult.success(order);
}
/**
* 批量填充订单关联信息(优化性能,减少数据库查询次数)
*/
private void fillOrderInfoBatch(List<Order> orders) {
if (orders == null || orders.isEmpty()) {
return;
}
// 收集所有需要查询的ID
java.util.Set<Integer> buyerIds = new java.util.HashSet<>();
java.util.Set<Integer> sellerIds = new java.util.HashSet<>();
java.util.Set<Integer> creatorIds = new java.util.HashSet<>();
for (Order order : orders) {
// 收集买方ID
if (order.getBuyerId() != null && !order.getBuyerId().isEmpty()) {
Arrays.stream(order.getBuyerId().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(id -> {
try {
buyerIds.add(Integer.parseInt(id));
} catch (NumberFormatException e) {
logger.warn("无效的买方ID{}", id);
}
});
}
// 收集卖方ID
if (order.getSellerId() != null && !order.getSellerId().isEmpty()) {
Arrays.stream(order.getSellerId().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(id -> {
try {
sellerIds.add(Integer.parseInt(id));
} catch (NumberFormatException e) {
logger.warn("无效的卖方ID{}", id);
}
});
}
// 收集创建人ID
if (order.getCreatedBy() != null) {
creatorIds.add(order.getCreatedBy());
}
}
// 批量查询买方信息
java.util.Map<Integer, String> buyerNameMap = new java.util.HashMap<>();
if (!buyerIds.isEmpty()) {
List<Map<String, Object>> buyerList = memberMapper.selectMemberUserByIds(new ArrayList<>(buyerIds));
for (Map<String, Object> buyer : buyerList) {
Object idObj = buyer.get("id");
Integer id = null;
if (idObj != null) {
if (idObj instanceof Integer) {
id = (Integer) idObj;
} else if (idObj instanceof Long) {
id = ((Long) idObj).intValue();
} else if (idObj instanceof Number) {
id = ((Number) idObj).intValue();
}
}
String username = (String) buyer.get("username");
if (id != null && username != null) {
buyerNameMap.put(id, username);
}
}
}
// 批量查询卖方信息
java.util.Map<Integer, String> sellerNameMap = new java.util.HashMap<>();
if (!sellerIds.isEmpty()) {
List<Map<String, Object>> sellerList = memberMapper.selectMemberUserByIds(new ArrayList<>(sellerIds));
for (Map<String, Object> seller : sellerList) {
Object idObj = seller.get("id");
Integer id = null;
if (idObj != null) {
if (idObj instanceof Integer) {
id = (Integer) idObj;
} else if (idObj instanceof Long) {
id = ((Long) idObj).intValue();
} else if (idObj instanceof Number) {
id = ((Number) idObj).intValue();
}
}
String username = (String) seller.get("username");
if (id != null && username != null) {
sellerNameMap.put(id, username);
}
}
}
// 批量查询创建人信息
java.util.Map<Integer, String> creatorNameMap = new java.util.HashMap<>();
if (!creatorIds.isEmpty()) {
List<SysUser> creatorList = sysUserMapper.selectBatchIds(creatorIds);
for (SysUser user : creatorList) {
if (user != null && user.getId() != null && user.getName() != null) {
creatorNameMap.put(user.getId(), user.getName());
}
}
}
// 批量填充订单信息
for (Order order : orders) {
// 填充买方名称
if (order.getBuyerId() != null && !order.getBuyerId().isEmpty()) {
String buyerNames = Arrays.stream(order.getBuyerId().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(id -> {
try {
Integer buyerId = Integer.parseInt(id);
return buyerNameMap.getOrDefault(buyerId, "");
} catch (NumberFormatException e) {
return "";
}
})
.filter(name -> !name.isEmpty())
.collect(Collectors.joining(", "));
order.setBuyerName(buyerNames);
}
// 填充卖方名称
if (order.getSellerId() != null && !order.getSellerId().isEmpty()) {
String sellerNames = Arrays.stream(order.getSellerId().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(id -> {
try {
Integer sellerId = Integer.parseInt(id);
return sellerNameMap.getOrDefault(sellerId, "");
} catch (NumberFormatException e) {
return "";
}
})
.filter(name -> !name.isEmpty())
.collect(Collectors.joining(", "));
order.setSellerName(sellerNames);
}
// 填充创建人名称
if (order.getCreatedBy() != null) {
String creatorName = creatorNameMap.get(order.getCreatedBy());
if (creatorName != null) {
order.setCreatedByName(creatorName);
}
}
// 填充结算方式描述
if (order.getSettlementType() != null) {
switch (order.getSettlementType()) {
case 1:
order.setSettlementTypeDesc("上车重量");
break;
case 2:
order.setSettlementTypeDesc("下车重量");
break;
case 3:
order.setSettlementTypeDesc("按肉价结算");
break;
default:
order.setSettlementTypeDesc("未知");
break;
}
}
}
}
/**
* 填充订单关联信息(买方名称、卖方名称、创建人名称、结算方式描述)
* 用于单个订单详情查询
*/
private void fillOrderInfo(Order order) {
// 填充买方名称
@@ -358,5 +562,115 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
}
}
}
/**
* 批量导入订单
*/
@Override
@Transactional
public AjaxResult batchImportOrders(List<Map<String, Object>> orders) {
logger.info("开始批量导入订单,共{}条", orders.size());
int successCount = 0;
int failCount = 0;
List<String> failMessages = new ArrayList<>();
for (int i = 0; i < orders.size(); i++) {
Map<String, Object> orderMap = orders.get(i);
try {
// 构建Order对象
Order order = new Order();
// 设置买方ID
Object buyerIdObj = orderMap.get("buyerId");
if (buyerIdObj != null) {
order.setBuyerId(String.valueOf(buyerIdObj));
} else {
throw new RuntimeException("买方ID不能为空");
}
// 设置卖方ID
Object sellerIdObj = orderMap.get("sellerId");
if (sellerIdObj != null) {
order.setSellerId(String.valueOf(sellerIdObj));
} else {
throw new RuntimeException("卖方ID不能为空");
}
// 设置结算方式默认为1-上车重量)
Object settlementTypeObj = orderMap.get("settlementType");
if (settlementTypeObj != null) {
Integer settlementType = null;
if (settlementTypeObj instanceof Integer) {
settlementType = (Integer) settlementTypeObj;
} else if (settlementTypeObj instanceof Number) {
settlementType = ((Number) settlementTypeObj).intValue();
} else {
settlementType = Integer.parseInt(String.valueOf(settlementTypeObj));
}
if (settlementType < 1 || settlementType > 3) {
throw new RuntimeException("结算方式无效必须为1-3");
}
order.setSettlementType(settlementType);
} else {
order.setSettlementType(1); // 默认上车重量
}
// 设置约定价格
Object firmPriceObj = orderMap.get("firmPrice");
if (firmPriceObj != null) {
java.math.BigDecimal firmPrice = null;
if (firmPriceObj instanceof java.math.BigDecimal) {
firmPrice = (java.math.BigDecimal) firmPriceObj;
} else if (firmPriceObj instanceof Number) {
firmPrice = new java.math.BigDecimal(String.valueOf(firmPriceObj));
} else {
firmPrice = new java.math.BigDecimal(String.valueOf(firmPriceObj));
}
if (firmPrice.compareTo(java.math.BigDecimal.ZERO) < 0) {
throw new RuntimeException("约定价格不能小于0");
}
order.setFirmPrice(firmPrice);
} else {
throw new RuntimeException("约定价格不能为空");
}
// 设置创建人和创建时间
Integer userId = SecurityUtil.getCurrentUserId();
order.setCreatedBy(userId);
order.setCreateTime(new Date());
// 插入数据库
int result = orderMapper.insert(order);
if (result > 0) {
successCount++;
logger.info("第{}条订单导入成功订单ID{}", i + 1, order.getId());
} else {
failCount++;
failMessages.add(String.format("第%d条插入数据库失败", i + 1));
logger.error("第{}条订单导入失败:插入数据库失败", i + 1);
}
} catch (Exception e) {
failCount++;
String errorMsg = String.format("第%d条%s", i + 1, e.getMessage());
failMessages.add(errorMsg);
logger.error("第{}条订单导入失败:{}", i + 1, e.getMessage(), e);
}
}
logger.info("批量导入完成:成功{}条,失败{}条", successCount, failCount);
// 构建返回结果
Map<String, Object> result = new java.util.HashMap<>();
result.put("successCount", successCount);
result.put("failCount", failCount);
result.put("failMessages", failMessages);
if (failCount == 0) {
return AjaxResult.success("批量导入成功,共导入" + successCount + "条订单", result);
} else {
return AjaxResult.success("批量导入完成:成功" + successCount + "条,失败" + failCount + "", result);
}
}
}

View File

@@ -153,7 +153,24 @@ public class WarningLogServiceImpl extends ServiceImpl<WarningLogMapper, Warning
}
//获取当前记录的运单信息
Delivery delivery = deliveryMapper.selectById(warningLog.getDeliveryId());
Integer originalDeliveryId = warningLog.getDeliveryId();
Delivery delivery = deliveryMapper.selectById(originalDeliveryId);
// ✅ 容错处理:如果通过 delivery_id 查不到运单,尝试用预警记录的 id 作为 delivery_id 查询
// 这种情况可能是因为数据不一致,预警记录的 id 可能就是运单的 id
Integer correctDeliveryId = originalDeliveryId;
if (delivery == null && originalDeliveryId != null && !originalDeliveryId.equals(warningLog.getId())) {
log.warn("[预警详情] 通过 delivery_id={} 查询不到运单,尝试使用预警记录 id={} 作为 delivery_id 查询",
originalDeliveryId, warningLog.getId());
delivery = deliveryMapper.selectById(warningLog.getId());
if (delivery != null) {
log.info("[预警详情] ✅ 容错成功:使用预警记录 id={} 找到了运单,运单号: {}",
warningLog.getId(), delivery.getDeliveryNumber());
// 使用预警记录的 id 作为正确的 deliveryId
correctDeliveryId = warningLog.getId();
}
}
if (delivery != null) {
BeanUtils.copyProperties(delivery, warningDetailDto);
}
@@ -162,13 +179,19 @@ public class WarningLogServiceImpl extends ServiceImpl<WarningLogMapper, Warning
warningDetailDto.setInventoryJbqCount(warningLog.getInventoryJbqCount());
// ✅ 设置运单ID用于前端查询设备列表和日志
warningDetailDto.setDeliveryId(warningLog.getDeliveryId());
log.info("[预警详情] 设置运单ID: {}", warningLog.getDeliveryId());
// 使用修正后的 deliveryId如果容错成功这里会是预警记录的 id
warningDetailDto.setDeliveryId(correctDeliveryId);
log.info("[预警详情] 设置运单ID: {} (原始 delivery_id: {})", correctDeliveryId, originalDeliveryId);
//获取当前运单关联的设备信息
List<DeliveryDevice> deliveryDevices = deliveryDeviceMapper.selectList(
new LambdaQueryWrapper<DeliveryDevice>().eq(DeliveryDevice::getDeliveryId, delivery.getId())
);
List<DeliveryDevice> deliveryDevices = new ArrayList<>();
if (delivery != null) {
deliveryDevices = deliveryDeviceMapper.selectList(
new LambdaQueryWrapper<DeliveryDevice>().eq(DeliveryDevice::getDeliveryId, delivery.getId())
);
} else {
log.warn("[预警详情] 运单不存在deliveryId: {} (原始: {})", correctDeliveryId, originalDeliveryId);
}
String mainDeviceId = null;
if (CollectionUtils.isNotEmpty(deliveryDevices)) {
@@ -187,34 +210,38 @@ public class WarningLogServiceImpl extends ServiceImpl<WarningLogMapper, Warning
warningDetailDto.setJbqDeviceSn(jbqDeviceCollect);
} else {
// ✅ 如果 delivery_device 表为空,尝试从 iot_device_data 表查询(兼容旧数据)
log.info("[预警详情] delivery_device 表无数据,尝试从 iot_device_data 表查询设备");
List<IotDeviceData> iotDevices = iotDeviceDataMapper.selectList(
new LambdaQueryWrapper<IotDeviceData>().eq(IotDeviceData::getDeliveryId, delivery.getId())
);
if (delivery != null) {
log.info("[预警详情] delivery_device 表无数据,尝试从 iot_device_data 表查询设备");
List<IotDeviceData> iotDevices = iotDeviceDataMapper.selectList(
new LambdaQueryWrapper<IotDeviceData>().eq(IotDeviceData::getDeliveryId, delivery.getId())
);
if (CollectionUtils.isNotEmpty(iotDevices)) {
log.info("[预警详情] 从 iot_device_data 查询到 {} 个设备", iotDevices.size());
if (CollectionUtils.isNotEmpty(iotDevices)) {
log.info("[预警详情] 从 iot_device_data 查询到 {} 个设备", iotDevices.size());
// 查找主机设备类型1或4
List<String> serverCollect = iotDevices.stream()
.filter(device -> device.getDeviceType() != null &&
(device.getDeviceType() == 1 || device.getDeviceType() == 4))
.map(IotDeviceData::getDeviceId)
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(serverCollect)) {
mainDeviceId = serverCollect.get(0);
warningDetailDto.setServerDeviceSn(mainDeviceId);
log.info("[预警详情] 找到主机设备: {}", mainDeviceId);
// 查找主机设备类型1或4
List<String> serverCollect = iotDevices.stream()
.filter(device -> device.getDeviceType() != null &&
(device.getDeviceType() == 1 || device.getDeviceType() == 4))
.map(IotDeviceData::getDeviceId)
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(serverCollect)) {
mainDeviceId = serverCollect.get(0);
warningDetailDto.setServerDeviceSn(mainDeviceId);
log.info("[预警详情] 找到主机设备: {}", mainDeviceId);
}
// 查找耳标设备类型2
List<String> jbqDeviceCollect = iotDevices.stream()
.filter(device -> device.getDeviceType() != null && device.getDeviceType() == 2)
.map(IotDeviceData::getDeviceId)
.collect(Collectors.toList());
warningDetailDto.setJbqDeviceSn(jbqDeviceCollect);
} else {
log.warn("[预警详情] iot_device_data 表也无设备数据");
}
// 查找耳标设备类型2
List<String> jbqDeviceCollect = iotDevices.stream()
.filter(device -> device.getDeviceType() != null && device.getDeviceType() == 2)
.map(IotDeviceData::getDeviceId)
.collect(Collectors.toList());
warningDetailDto.setJbqDeviceSn(jbqDeviceCollect);
} else {
log.warn("[预警详情] iot_device_data 表也无设备数据");
log.warn("[预警详情] 运单不存在,无法查询设备信息");
}
}

View File

@@ -82,5 +82,37 @@ public class DeliveryLogVo {
*/
private String createByDesc;
/**
* 起始地
*/
private String startLocation;
/**
* 送达目的地
*/
private String endLocation;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 预计送达时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date estimatedDeliveryTime;
/**
* 司机姓名
*/
private String driverName;
/**
* 创建人(从 sys_user 表关联查询)
*/
private String createByName;
}

View File

@@ -40,10 +40,25 @@ public class XxlJobConfig {
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Value("${xxl.job.executor.connect-timeout:30000}")
private int connectTimeout;
@Value("${xxl.job.executor.read-timeout:30000}")
private int readTimeout;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
// 设置 HTTP 连接超时和读取超时时间(通过系统属性)
// XXL-Job 内部使用 HttpsURLConnection通过系统属性可以设置默认超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", String.valueOf(connectTimeout));
System.setProperty("sun.net.client.defaultReadTimeout", String.valueOf(readTimeout));
logger.info(">>>>>>>>>>> xxl-job timeout config: connectTimeout={}ms, readTimeout={}ms",
connectTimeout, readTimeout);
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);

View File

@@ -0,0 +1,34 @@
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,28 @@
package com.aiotagro.common.core.constant;
/**
* 百度鹰眼常量配置
*
* <p>注意AK 与 ServiceId 根据业务要求写死在后端,禁止透出给前端。</p>
*/
public final class BaiduYingyanConstants {
private BaiduYingyanConstants() {
}
/**
* 百度鹰眼控制台申请的 AK
*/
public static final String AK = "xITbC7jegaAAuu4m9jC2Zx6eFbQJ29Rj";
/**
* 百度鹰眼服务 ID
*/
public static final long SERVICE_ID = 242517L;
/**
* 百度鹰眼 API 基础路径
*/
public static final String BASE_URL = "https://yingyan.baidu.com/api/v3";
}

View File

@@ -99,6 +99,10 @@ xxl:
logpath: /data/applogs/xxl-job/jobhandler
# 日志保存时间
logretentiondays: 30
# 连接超时时间毫秒默认30秒
connect-timeout: 30000
# 读取超时时间毫秒默认30秒
read-timeout: 30000
address:
ip:
# 日志配置

View File

@@ -108,6 +108,10 @@ xxl:
logpath: /data/applogs/xxl-job/jobhandler
# 日志保存时间
logretentiondays: 30
# 连接超时时间毫秒默认30秒
connect-timeout: 30000
# 读取超时时间毫秒默认30秒
read-timeout: 30000
address:
ip:

View File

@@ -101,6 +101,10 @@ xxl:
logpath: /data/applogs/xxl-job/jobhandler
# 日志保存时间
logretentiondays: 30
# 连接超时时间毫秒默认30秒
connect-timeout: 30000
# 读取超时时间毫秒默认30秒
read-timeout: 30000
address:
ip:
# 日志配置 - 生产环境应使用更高的日志级别以提升性能

View File

@@ -0,0 +1,12 @@
USE cattletrade;
-- 为运送清单表新增百度鹰眼同步字段
ALTER TABLE `delivery`
ADD COLUMN `yingyan_entity_name` VARCHAR(64) NULL DEFAULT NULL COMMENT '百度鹰眼终端名称';
ALTER TABLE `delivery`
ADD COLUMN `yingyan_last_sync_time` DATETIME NULL DEFAULT NULL COMMENT '百度鹰眼最后同步时间';
ALTER TABLE `delivery`
ADD COLUMN `arrival_time` DATETIME NULL DEFAULT NULL COMMENT '自动判定的到达时间';

View File

@@ -64,31 +64,38 @@
wl.warning_time,
wl.inventory_jbq_count,
su.name as createByName
FROM delivery d inner join warning_log wl on d.id = wl.delivery_id
left join sys_user su on d.created_by = su.id
FROM delivery d
INNER JOIN warning_log wl ON d.id = wl.delivery_id
LEFT JOIN sys_user su ON d.created_by = su.id
<where>
wl.warning_type in (2,3,4,5,6,7,8,9)
and wl.id in (select max(id) from warning_log where delivery_id = d.id group by warning_type)
wl.warning_type IN (2,3,4,5,6,7,8,9)
AND wl.id IN (
SELECT MAX(id)
FROM warning_log
WHERE delivery_id = d.id
AND warning_type IN (2,3,4,5,6,7,8,9)
GROUP BY warning_type
)
<if test="dto.deliveryNumber != null and '' != dto.deliveryNumber">
and d.delivery_number like concat('%', #{dto.deliveryNumber}, '%')
AND d.delivery_number LIKE CONCAT('%', #{dto.deliveryNumber}, '%')
</if>
<if test="dto.licensePlate != null and '' != dto.licensePlate">
and d.license_plate like concat('%', #{dto.licensePlate}, '%')
AND d.license_plate LIKE CONCAT('%', #{dto.licensePlate}, '%')
</if>
<if test="dto.startTime != null and '' != dto.startTime">
and d.create_time <![CDATA[ >= ]]> #{dto.startTime}
AND d.create_time <![CDATA[ >= ]]> #{dto.startTime}
</if>
<if test="dto.endTime != null and '' != dto.endTime">
and d.create_time <![CDATA[ <= ]]> #{dto.endTime}
AND d.create_time <![CDATA[ <= ]]> #{dto.endTime}
</if>
<if test="dto.warningType != null">
and wl.warning_type = #{dto.warningType}
AND wl.warning_type = #{dto.warningType}
</if>
<if test="dto.status != null">
and d.status = #{dto.status}
AND d.status = #{dto.status}
</if>
</where>
order by wl.warning_time desc
ORDER BY wl.warning_time DESC
</select>
</mapper>

View File

@@ -55,4 +55,33 @@
</foreach>
</insert>
<select id="listLogsForYingyan" resultType="com.aiotagro.cattletrade.business.entity.JbqClientLog">
SELECT
<include refid="Base_Column_List"/>
FROM
jbq_client_log
<where>
<if test="deviceIds != null and deviceIds.size > 0">
device_id IN
<foreach collection="deviceIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="startTime != null">
AND update_time <![CDATA[>=]]> #{startTime}
</if>
<if test="endTime != null">
AND update_time <![CDATA[<=]]> #{endTime}
</if>
AND latitude IS NOT NULL
AND latitude != ''
AND longitude IS NOT NULL
AND longitude != ''
</where>
ORDER BY update_time ASC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
</mapper>

View File

@@ -46,4 +46,33 @@
</foreach>
</insert>
<select id="listLogsForYingyan" resultType="com.aiotagro.cattletrade.business.entity.JbqServerLog">
SELECT
<include refid="Base_Column_List"/>
FROM
jbq_server_log
<where>
<if test="deviceIds != null and deviceIds.size > 0">
device_id IN
<foreach collection="deviceIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="startTime != null">
AND update_time <![CDATA[>=]]> #{startTime}
</if>
<if test="endTime != null">
AND update_time <![CDATA[<=]]> #{endTime}
</if>
AND latitude IS NOT NULL
AND latitude != ''
AND longitude IS NOT NULL
AND longitude != ''
</where>
ORDER BY update_time ASC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
</mapper>

View File

@@ -57,4 +57,33 @@
</foreach>
</insert>
<select id="listLogsForYingyan" resultType="com.aiotagro.cattletrade.business.entity.XqClientLog">
SELECT
<include refid="Base_Column_List"/>
FROM
xq_client_log
<where>
<if test="deviceIds != null and deviceIds.size > 0">
device_id IN
<foreach collection="deviceIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="startTime != null">
AND update_time <![CDATA[>=]]> #{startTime}
</if>
<if test="endTime != null">
AND update_time <![CDATA[<=]]> #{endTime}
</if>
AND latitude IS NOT NULL
AND latitude != ''
AND longitude IS NOT NULL
AND longitude != ''
</where>
ORDER BY update_time ASC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
</mapper>