集成百度鹰眼服务AK
This commit is contained in:
1
BaiduMapApi
Submodule
1
BaiduMapApi
Submodule
Submodule BaiduMapApi added at 2e29dc1fd8
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -265,6 +265,15 @@ export function orderGetDetail(id) {
|
||||
});
|
||||
}
|
||||
|
||||
// 批量导入订单
|
||||
export function orderBatchImport(data) {
|
||||
return request({
|
||||
url: '/order/batchImport',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 运送清单管理新增 API ====================
|
||||
|
||||
// 逻辑删除运送清单
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
262
pc-cattle-transportation/src/store/deliveryForm.ts
Normal file
262
pc-cattle-transportation/src/store/deliveryForm.ts
Normal 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 = {};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || [];
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
// 详情
|
||||
|
||||
@@ -80,184 +80,188 @@
|
||||
</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>
|
||||
<!-- 重量信息分组 -->
|
||||
<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 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 v-if="hasValue(data.baseInfo.entruckWeight)" label="装车过磅重量:">
|
||||
{{ data.baseInfo.entruckWeight }}kg
|
||||
</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 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">
|
||||
<!-- 照片信息分组 -->
|
||||
<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
|
||||
v-if="data.baseInfo.destinationPoundListImg"
|
||||
style="width: 50px; height: 50px; margin-right: 10px"
|
||||
:src="data.baseInfo.destinationPoundListImg ? data.baseInfo.destinationPoundListImg : ''"
|
||||
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
|
||||
:src="data.baseInfo.quarantineTickeyUrl"
|
||||
fit="cover"
|
||||
:preview-src-list="[data.baseInfo.destinationPoundListImg] ? [data.baseInfo.destinationPoundListImg] : []"
|
||||
:preview-src-list="[data.baseInfo.quarantineTickeyUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="到地车辆过重磅车头照片:">
|
||||
<span style="vertical-align: top">
|
||||
<el-descriptions-item v-if="hasValue(data.baseInfo.poundListImg)" label="纸质磅单:">
|
||||
<div class="photo-container">
|
||||
<el-image
|
||||
v-if="data.baseInfo.destinationVehicleFrontPhoto"
|
||||
style="width: 50px; height: 50px; margin-right: 10px"
|
||||
:src="data.baseInfo.destinationVehicleFrontPhoto ? data.baseInfo.destinationVehicleFrontPhoto : ''"
|
||||
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
|
||||
:src="data.baseInfo.poundListImg"
|
||||
fit="cover"
|
||||
:preview-src-list="[data.baseInfo.destinationVehicleFrontPhoto] ? [data.baseInfo.destinationVehicleFrontPhoto] : []"
|
||||
:preview-src-list="[data.baseInfo.poundListImg]"
|
||||
preview-teleported
|
||||
/>
|
||||
</span>
|
||||
</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">
|
||||
<!-- 视频信息分组 -->
|
||||
<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="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
|
||||
style="max-width: 100%; height: 200px; border-radius: 4px"
|
||||
controls
|
||||
:src="data.baseInfo.emptyWeightVideo"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="装车过磅视频:">
|
||||
<span style="vertical-align: top" v-if="data.baseInfo.entruckWeightVideo">
|
||||
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckWeightVideo)" label="装车过磅视频:">
|
||||
<div class="video-container">
|
||||
<video
|
||||
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
|
||||
style="max-width: 100%; height: 200px; border-radius: 4px"
|
||||
controls
|
||||
:src="data.baseInfo.entruckWeightVideo"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="装车视频:">
|
||||
<span style="vertical-align: top" v-if="data.baseInfo.entruckVideo">
|
||||
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckVideo)" label="装车视频:">
|
||||
<div class="video-container">
|
||||
<video
|
||||
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
|
||||
style="max-width: 100%; height: 200px; border-radius: 4px"
|
||||
controls
|
||||
:src="data.baseInfo.entruckVideo"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="控槽视频:">
|
||||
<span style="vertical-align: top" v-if="data.baseInfo.controlSlotVideo">
|
||||
<el-descriptions-item v-if="hasValue(data.baseInfo.controlSlotVideo)" label="控槽视频:">
|
||||
<div class="video-container">
|
||||
<video
|
||||
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
|
||||
style="max-width: 100%; height: 200px; border-radius: 4px"
|
||||
controls
|
||||
:src="data.baseInfo.controlSlotVideo"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="装完牛绕车一圈视频:">
|
||||
<span style="vertical-align: top" v-if="data.baseInfo.cattleLoadingCircleVideo">
|
||||
<el-descriptions-item v-if="hasValue(data.baseInfo.cattleLoadingCircleVideo)" label="装完牛绕车一圈视频:">
|
||||
<div class="video-container">
|
||||
<video
|
||||
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
|
||||
style="max-width: 100%; height: 200px; border-radius: 4px"
|
||||
controls
|
||||
:src="data.baseInfo.cattleLoadingCircleVideo"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="卸牛视频:">
|
||||
<span style="vertical-align: top" v-if="data.baseInfo.unloadCattleVideo">
|
||||
<el-descriptions-item v-if="hasValue(data.baseInfo.unloadCattleVideo)" label="卸牛视频:">
|
||||
<div class="video-container">
|
||||
<video
|
||||
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
|
||||
style="max-width: 100%; height: 200px; border-radius: 4px"
|
||||
controls
|
||||
:src="data.baseInfo.unloadCattleVideo"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="到地过磅视频:">
|
||||
<span style="vertical-align: top" v-if="data.baseInfo.destinationWeightVideo">
|
||||
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationWeightVideo)" label="到地过磅视频:">
|
||||
<div class="video-container">
|
||||
<video
|
||||
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
|
||||
style="max-width: 100%; height: 200px; border-radius: 4px"
|
||||
controls
|
||||
:src="data.baseInfo.destinationWeightVideo"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ear-box">
|
||||
<div class="title">智能主机</div>
|
||||
<el-table
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
// 卖方列表(合并 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: data.supplierName,
|
||||
};
|
||||
memberListByType(params)
|
||||
.then((res) => {
|
||||
data.supplierLoading = false;
|
||||
data.supplierOptions = res.data.rows;
|
||||
data.supplierTotal = res.data.total;
|
||||
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,
|
||||
// 买方列表(合并 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: 4, // 采购商类型
|
||||
username: data.purchaserName,
|
||||
};
|
||||
memberListByType(params)
|
||||
.then((res) => {
|
||||
data.purchaserLoading = false;
|
||||
data.purchaserOptions = res.data.rows;
|
||||
data.purchaserTotal = res.data.total;
|
||||
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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -49,12 +49,12 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="行驶证" prop="drivingLicensePhoto" min-width="120">
|
||||
<template #default="scope">
|
||||
<template v-if="getFirstImageUrl(scope.row.drivingLicensePhoto)">
|
||||
<el-image
|
||||
v-if="scope.row.drivingLicensePhoto"
|
||||
:src="scope.row.drivingLicensePhoto"
|
||||
:src="getFirstImageUrl(scope.row.drivingLicensePhoto)"
|
||||
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
|
||||
fit="cover"
|
||||
:preview-src-list="[scope.row.drivingLicensePhoto]"
|
||||
:preview-src-list="getImageList(scope.row.drivingLicensePhoto)"
|
||||
preview-teleported
|
||||
:lazy="true"
|
||||
>
|
||||
@@ -64,17 +64,21 @@
|
||||
</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">
|
||||
<template v-if="getFirstImageUrl(scope.row.recordCode)">
|
||||
<el-image
|
||||
v-if="scope.row.recordCode"
|
||||
:src="scope.row.recordCode"
|
||||
:src="getFirstImageUrl(scope.row.recordCode)"
|
||||
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
|
||||
fit="cover"
|
||||
:preview-src-list="[scope.row.recordCode]"
|
||||
:preview-src-list="getImageList(scope.row.recordCode)"
|
||||
preview-teleported
|
||||
:lazy="true"
|
||||
>
|
||||
@@ -84,6 +88,10 @@
|
||||
</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();
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
// 行驶证和备案码支持多图片,其他字段保持单图片
|
||||
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 || '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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("运单删除失败");
|
||||
}
|
||||
|
||||
@@ -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为空)
|
||||
// 只有当 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查询
|
||||
|
||||
@@ -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,11 +376,18 @@ public class MemberController {
|
||||
return AjaxResult.error("用户类型不能为空");
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在(直接查询member表)
|
||||
// 处理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表
|
||||
int memberResult = memberMapper.insertMember(mobile, type, status);
|
||||
@@ -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失败");
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ public class DeliveryEditDto {
|
||||
|
||||
private String driverMobile;
|
||||
|
||||
/** 车牌号 */
|
||||
private String licensePlate;
|
||||
|
||||
private Integer buyerId;
|
||||
|
||||
private Double buyerPrice;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 登记智能耳标数
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,9 @@ public interface IDeliveryService extends IService<Delivery> {
|
||||
AjaxResult detail(Integer id);
|
||||
|
||||
PageResultResponse<Delivery> pageQueryListLog(DeliverListDto dto);
|
||||
|
||||
/**
|
||||
* 查询百度鹰眼轨迹与停留点
|
||||
*/
|
||||
AjaxResult queryYingyanTrack(Integer deliveryId);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,10 +125,18 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
}
|
||||
|
||||
wrapper.orderByDesc(Delivery::getId);
|
||||
List<Delivery> list = this.list(wrapper);
|
||||
if(CollectionUtils.isNotEmpty(list)){
|
||||
}
|
||||
|
||||
// 判断是否需要数据权限过滤
|
||||
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())){
|
||||
@@ -344,15 +358,12 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 数据权限过滤:非超级管理员只能看到与自己手机号相关的订单
|
||||
|
||||
if (!SecurityUtil.isSuperAdmin() && StringUtils.isNotEmpty(currentUserMobile)) {
|
||||
|
||||
list = list.stream().filter(delivery -> {
|
||||
boolean hasPermission = false;
|
||||
|
||||
|
||||
// 检查是否是司机
|
||||
if (StringUtils.isNotEmpty(delivery.getDriverMobile()) &&
|
||||
currentUserMobile.equals(delivery.getDriverMobile())) {
|
||||
@@ -382,20 +393,248 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
|
||||
hasPermission = true;
|
||||
}
|
||||
|
||||
if (!hasPermission) {
|
||||
}
|
||||
|
||||
return hasPermission;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
} else if (SecurityUtil.isSuperAdmin()) {
|
||||
// 获取过滤后的总数
|
||||
total = list.size();
|
||||
|
||||
// 手动分页
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
long filteredTotal = list.size();
|
||||
return new PageResultResponse(filteredTotal, list);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 返回分页结果
|
||||
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();
|
||||
// ✅ 修复:将 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());
|
||||
|
||||
// ✅ 新增:填充起始地、目的地、创建时间、预计送达时间、司机姓名、创建人
|
||||
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)){
|
||||
deliveryLogVo.setWarningTypeDesc(EnumUtil.getEnumConstant(WarningStatusAdminEnum.class , Integer.parseInt(warningType)).getDescription());
|
||||
try {
|
||||
vo.setWarningTypeDesc(EnumUtil.getEnumConstant(WarningStatusAdminEnum.class , Integer.parseInt(warningType)).getDescription());
|
||||
} catch (Exception e) {
|
||||
logger.warn("[预警列表查询] 预警类型解析失败: {}", warningType);
|
||||
vo.setWarningTypeDesc("未知类型");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 更新分页信息
|
||||
long filteredTotal = resList.size();
|
||||
return new PageResultResponse(filteredTotal, resList);
|
||||
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 状态码
|
||||
|
||||
@@ -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,14 +86,22 @@ 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 (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()))
|
||||
@@ -108,9 +113,38 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
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,6 +210,7 @@ public class WarningLogServiceImpl extends ServiceImpl<WarningLogMapper, Warning
|
||||
warningDetailDto.setJbqDeviceSn(jbqDeviceCollect);
|
||||
} else {
|
||||
// ✅ 如果 delivery_device 表为空,尝试从 iot_device_data 表查询(兼容旧数据)
|
||||
if (delivery != null) {
|
||||
log.info("[预警详情] delivery_device 表无数据,尝试从 iot_device_data 表查询设备");
|
||||
List<IotDeviceData> iotDevices = iotDeviceDataMapper.selectList(
|
||||
new LambdaQueryWrapper<IotDeviceData>().eq(IotDeviceData::getDeliveryId, delivery.getId())
|
||||
@@ -216,6 +240,9 @@ public class WarningLogServiceImpl extends ServiceImpl<WarningLogMapper, Warning
|
||||
} else {
|
||||
log.warn("[预警详情] iot_device_data 表也无设备数据");
|
||||
}
|
||||
} else {
|
||||
log.warn("[预警详情] 运单不存在,无法查询设备信息");
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 设置预警位置信息(优先使用预警记录中保存的经纬度)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -99,6 +99,10 @@ xxl:
|
||||
logpath: /data/applogs/xxl-job/jobhandler
|
||||
# 日志保存时间
|
||||
logretentiondays: 30
|
||||
# 连接超时时间(毫秒),默认30秒
|
||||
connect-timeout: 30000
|
||||
# 读取超时时间(毫秒),默认30秒
|
||||
read-timeout: 30000
|
||||
address:
|
||||
ip:
|
||||
# 日志配置
|
||||
|
||||
@@ -108,6 +108,10 @@ xxl:
|
||||
logpath: /data/applogs/xxl-job/jobhandler
|
||||
# 日志保存时间
|
||||
logretentiondays: 30
|
||||
# 连接超时时间(毫秒),默认30秒
|
||||
connect-timeout: 30000
|
||||
# 读取超时时间(毫秒),默认30秒
|
||||
read-timeout: 30000
|
||||
address:
|
||||
ip:
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ xxl:
|
||||
logpath: /data/applogs/xxl-job/jobhandler
|
||||
# 日志保存时间
|
||||
logretentiondays: 30
|
||||
# 连接超时时间(毫秒),默认30秒
|
||||
connect-timeout: 30000
|
||||
# 读取超时时间(毫秒),默认30秒
|
||||
read-timeout: 30000
|
||||
address:
|
||||
ip:
|
||||
# 日志配置 - 生产环境应使用更高的日志级别以提升性能
|
||||
|
||||
@@ -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 '自动判定的到达时间';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user