diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/DELIVERY_FIELD_FIX_SUMMARY.md b/DELIVERY_FIELD_FIX_SUMMARY.md new file mode 100644 index 0000000..1111e20 --- /dev/null +++ b/DELIVERY_FIELD_FIX_SUMMARY.md @@ -0,0 +1,210 @@ +# 运送清单字段修复说明 + +## 修复的问题 + +根据用户反馈,以下字段在创建运送清单后为 null: +1. `createByName`: null - 创建人姓名没有中文映射 +2. `supplierId`, `supplierName`, `supplierMobile`: null - 卖方信息为空 +3. `buyerId`, `buyerName`, `buyerMobile`: null - 买方信息为空 +4. 车牌号与司机模块混淆 + +## 解决方案 + +### 1. 修改 DTO 字段 + +**文件**: `DeliveryCreateDto.java` + +**修改前**: +```java +/** + * 发货方 + */ +@NotBlank(message = "发货方不能为空") +private String shipper; + +/** + * 采购方 + */ +@NotBlank(message = "采购方不能为空") +private String buyer; +``` + +**修改后**: +```java +/** + * 发货方ID + */ +private Integer shipperId; + +/** + * 采购方ID + */ +private Integer buyerId; +``` + +**说明**: 改为传递ID而不是名称,便于后端查询详细信息。 + +### 2. 修改前端提交逻辑 + +**文件**: `createDeliveryDialog.vue` + +**修改前**: +```javascript +const buildSubmitData = () => { + // 将发货方和采购方的ID转换为名称 + let shipperName = ''; + let buyerName = ''; + + if (formData.shipper) { + const shipper = supplierList.value.find(item => item.id === formData.shipper); + shipperName = shipper ? (shipper.username || shipper.mobile || shipper.name || '') : String(formData.shipper); + } + + if (formData.buyer) { + const buyer = buyerList.value.find(item => item.id === formData.buyer); + buyerName = buyer ? (buyer.username || buyer.mobile || buyer.name || '') : String(formData.buyer); + } + + const data = { + shipper: shipperName, + buyer: buyerName, + // ... + }; +}; +``` + +**修改后**: +```javascript +const buildSubmitData = () => { + const data = { + shipperId: formData.shipper, + buyerId: formData.buyer, + // ... + }; +}; +``` + +**说明**: 直接传递 ID,简化逻辑。 + +### 3. 修改后端保存逻辑 + +**文件**: `DeliveryServiceImpl.java` + +**新增代码**: +```java +// 设置卖方和买方ID +if (dto.getShipperId() != null) { + delivery.setSupplierId(String.valueOf(dto.getShipperId())); + System.out.println("[CREATE-DELIVERY] 设置卖方ID: " + dto.getShipperId()); +} +if (dto.getBuyerId() != null) { + delivery.setBuyerId(dto.getBuyerId()); + System.out.println("[CREATE-DELIVERY] 设置买方ID: " + dto.getBuyerId()); +} +``` + +**位置**: 在 `delivery.setLicensePlate(dto.getPlateNumber());` 之后 + +**说明**: 保存 shipperId 和 buyerId 到数据库。 + +### 4. 关于创建人姓名 (createByName) + +现有的代码逻辑是: +```java +Integer userId = SecurityUtil.getCurrentUserId(); +String userName = SecurityUtil.getUserName(); + +delivery.setCreatedBy(userId); +delivery.setCreateByName(userName); +``` + +**说明**: +- `createByName` 应该从 `SecurityUtil.getUserName()` 获取 +- 如果获取到的是 null,需要检查 SecurityUtil 的实现 +- 可能需要从 sys_user 表查询用户姓名 + +### 5. 关于卖方和买方信息的显示 + +由于现在是保存 ID 而不是名称,在查询运送清单列表时,需要通过 ID 查询并显示名称和手机号。 + +现有代码已经实现了这个逻辑(在 `pageQuery` 方法中): +```java +// 查询供应商信息 +if (StringUtils.isNotEmpty(delivery.getSupplierId())) { + String[] supplierIds = delivery.getSupplierId().split(","); + // ... 查询并设置 supplierName 和 supplierMobile +} + +// 查询采购商信息 +if (delivery.getBuyerId() != null) { + Map buyerInfo = memberMapper.selectMemberUserById(delivery.getBuyerId()); + // ... 设置 buyerName 和 buyerMobile +} +``` + +### 6. 司机模块与车牌号 + +用户说明:**司机姓名和电话是一个模块,车牌号是另一个模块,司机模块不需要查询车牌号** + +**现有逻辑**: +```java +if (dto.getDriverId() != null) { + Map driverInfo = memberDriverMapper.selectDriverById(dto.getDriverId()); + if (driverInfo != null) { + String driverUsername = (String) driverInfo.get("username"); + String driverMobile = (String) driverInfo.get("mobile"); + String carImg = (String) driverInfo.get("car_img"); // 这个不需要使用 + + // 设置司机姓名和电话 + delivery.setDriverName(driverUsername); + delivery.setDriverMobile(driverMobile); + } +} +``` + +**说明**: 现有代码只查询司机姓名和电话,不涉及车牌号,符合要求。 + +## 测试验证 + +创建运送清单后,检查数据库: + +```sql +SELECT + id, + supplier_id, + buyer_id, + driver_id, + license_plate, + driver_name, + driver_mobile, + created_by, + create_by_name +FROM delivery +WHERE id = [最新创建的运送清单ID]; +``` + +预期结果: +- `supplier_id` 不为 null(如 "123") +- `buyer_id` 不为 null(如 456) +- `driver_id` 不为 null +- `license_plate` 不为 null +- `driver_name` 不为 null +- `driver_mobile` 不为 null +- `create_by_name` 不为 null(需要检查 SecurityUtil.getUserName()) + +## 注意事项 + +1. **supplierId 是字符串类型**:Delivery 实体中 `supplierId` 字段是 String 类型(逗号分隔),所以使用 `String.valueOf()` 转换 +2. **buyerId 是整数类型**:直接赋值即可 +3. **创建人姓名问题**:如果 `SecurityUtil.getUserName()` 返回 null,需要检查权限管理模块的配置 + +## 相关文件 + +1. `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/dto/DeliveryCreateDto.java` +2. `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/DeliveryServiceImpl.java` +3. `pc-cattle-transportation/src/views/shipping/createDeliveryDialog.vue` + +## 实现日期 + +2025-10-29 + diff --git a/DEVICE_BINDING_IMPLEMENTATION.md b/DEVICE_BINDING_IMPLEMENTATION.md new file mode 100644 index 0000000..258de47 --- /dev/null +++ b/DEVICE_BINDING_IMPLEMENTATION.md @@ -0,0 +1,290 @@ +# 设备绑定功能实现说明 + +## 实现功能 + +### 1. 选中设备自动绑定运送清单 +当用户在创建运送清单时选择设备后,系统会自动更新 `iot_device_data` 表中以下字段: +- `delivery_id`:运送清单ID +- `car_number`:车牌号 + +### 2. 已绑定设备过滤 +在下一个运送清单创建时,已绑定的设备(`delivery_id` 不为空)将不会出现在可选设备列表中。 + +## 修改内容 + +### 后端修改 + +#### 1. DeliveryDeviceController.java +**文件位置**: `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/DeliveryDeviceController.java` + +**修改的方法**: + +##### updateDeviceDeliveryId (第347-404行) +```java +/** + * 更新设备delivery_id、weight和car_number + */ +@PostMapping(value = "/updateDeviceDeliveryId") +public AjaxResult updateDeviceDeliveryId(@RequestBody Map params) { + String deviceId = (String) params.get("deviceId"); + Integer deliveryId = (Integer) params.get("deliveryId"); + Double weight = params.get("weight") != null ? Double.valueOf(params.get("weight").toString()) : null; + String carNumber = (String) params.get("carNumber"); // 新增:接收车牌号参数 + + // ... 更新逻辑 + + // 设置delivery_id(可以是null) + updateWrapper.set(IotDeviceData::getDeliveryId, deliveryId); + + // 设置car_number(可以是null)- 新增 + updateWrapper.set(IotDeviceData::getCarNumber, carNumber); + + // 设置weight(如果有值) + if (weight != null) { + updateWrapper.set(IotDeviceData::getWeight, weight); + } +} +``` + +##### clearDeliveryId (第462-499行) +```java +/** + * 清空设备delivery_id、car_number和weight + */ +@PostMapping(value = "/clearDeliveryId") +public AjaxResult clearDeliveryId(@RequestBody Map params) { + // ... 清空逻辑 + + // 将delivery_id、car_number和weight都设置为null + device.setDeliveryId(null); + device.setCarNumber(null); // 新增:清空车牌号 + device.setWeight(null); +} +``` + +#### 2. IotDeviceProxyController.java +**文件位置**: `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/IotDeviceProxyController.java` + +**修改的方法**: + +##### queryList (第42-167行) +```java +@PostMapping("/queryList") +public AjaxResult queryList(@RequestBody Map params) { + // 构建查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + + // 根据设备类型查询(用于创建运送清单时过滤设备)- 新增 + if (params.containsKey("type") && params.get("type") != null) { + Integer deviceType = (Integer) params.get("type"); + queryWrapper.eq("device_type", deviceType); + // 创建运送清单时,只显示未绑定的设备(delivery_id为空) + queryWrapper.isNull("delivery_id"); + logger.info("查询未绑定的设备,类型: {}", deviceType); + } + + // ... 其他查询逻辑 +} +``` + +### 前端修改 + +#### createDeliveryDialog.vue +**文件位置**: `pc-cattle-transportation/src/views/shipping/createDeliveryDialog.vue` + +**修改的方法**: + +##### updateSelectedDevicesDeliveryId (第1081-1111行) +```javascript +// 更新选中设备的delivery_id和car_number +const updateSelectedDevicesDeliveryId = async (deliveryId) => { + try { + const devicesToUpdate = []; + + // 收集所有选中的设备 + if (formData.serverDeviceId) { + devicesToUpdate.push(formData.serverDeviceId); + } + if (formData.eartagDeviceIds && formData.eartagDeviceIds.length > 0) { + devicesToUpdate.push(...formData.eartagDeviceIds); + } + if (formData.collarDeviceIds && formData.collarDeviceIds.length > 0) { + devicesToUpdate.push(...formData.collarDeviceIds); + } + + // 批量更新设备的delivery_id和car_number + for (const deviceId of devicesToUpdate) { + await updateDeviceDeliveryId({ + deviceId: deviceId, + deliveryId: deliveryId, + carNumber: formData.plateNumber // 新增:传递车牌号 + }); + } + + console.log(`成功更新 ${devicesToUpdate.length} 个设备的delivery_id和car_number: ${formData.plateNumber}`); + } catch (error) { + console.error('更新设备delivery_id和car_number失败:', error); + } +}; +``` + +## 数据流程 + +### 创建运送清单时的设备绑定流程 + +1. **用户填写运送清单** + - 选择车牌号:`formData.plateNumber` + - 选择主机设备 + - 选择耳标设备 + - 选择项圈设备 + +2. **提交运送清单** + - 调用 `/delivery/create` 创建运送清单 + - 获取新创建的 `deliveryId` + +3. **更新设备绑定信息** + - 调用 `updateSelectedDevicesDeliveryId(deliveryId)` 方法 + - 对每个选中的设备调用 `/deliveryDevice/updateDeviceDeliveryId` 接口 + - 传递参数: + ```json + { + "deviceId": "设备ID", + "deliveryId": "运送清单ID", + "carNumber": "车牌号" + } + ``` + +4. **后端更新数据库** + - 更新 `iot_device_data` 表 + - 设置 `delivery_id` = 运送清单ID + - 设置 `car_number` = 车牌号 + +### 查询可用设备流程 + +1. **打开创建运送清单对话框** + - 调用 `loadDeviceOptions()` 方法 + +2. **查询未绑定设备** + - 调用 `/iotDevice/queryList` 接口 + - 传递参数: + ```json + { + "type": 1, // 设备类型:1-主机, 2-耳标, 4-项圈 + "pageNum": 1, + "pageSize": 9999 + } + ``` + +3. **后端过滤逻辑** + ```sql + SELECT * FROM iot_device_data + WHERE device_type = ? + AND delivery_id IS NULL + ``` + +4. **返回未绑定设备列表** + - 只返回 `delivery_id` 为空的设备 + - 已绑定设备不会出现在下拉列表中 + +## 测试验证 + +### 1. 创建运送清单并绑定设备 + +**步骤**: +1. 打开"新增运送清单"对话框 +2. 填写车牌号:如 `京A12345` +3. 选择主机设备 +4. 选择耳标设备 +5. 选择项圈设备 +6. 提交运送清单 + +**验证**: +- 查询数据库 `iot_device_data` 表 +- 确认选中设备的 `delivery_id` 已更新为运送清单ID +- 确认选中设备的 `car_number` 已更新为 `京A12345` + +```sql +SELECT device_id, device_type, delivery_id, car_number +FROM iot_device_data +WHERE delivery_id = [刚创建的运送清单ID]; +``` + +### 2. 验证已绑定设备不再显示 + +**步骤**: +1. 再次打开"新增运送清单"对话框 +2. 查看设备下拉列表 + +**验证**: +- 之前选中的设备不应出现在下拉列表中 +- 只显示 `delivery_id` 为空的未绑定设备 + +### 3. 验证设备解绑功能 + +**步骤**: +1. 删除或取消运送清单 +2. 调用 `/deliveryDevice/clearDeliveryId` 接口 + +**验证**: +- 查询数据库 `iot_device_data` 表 +- 确认设备的 `delivery_id` 已清空(NULL) +- 确认设备的 `car_number` 已清空(NULL) +- 设备可以再次被选择 + +## 控制台日志 + +### 前端日志 +``` +成功更新 3 个设备的delivery_id和car_number: 京A12345 +``` + +### 后端日志 +``` +=== 更新设备delivery_id、weight和car_number === +设备ID: 1001 +订单ID: 123 +重量: null +车牌号: 京A12345 +设备更新成功: 1001, delivery_id=123, car_number=京A12345 +``` + +``` +查询未绑定的设备,类型: 1 +查询到设备数据: 5 条 +``` + +## 注意事项 + +1. **权限要求**:所有接口都需要 `delivery:view` 权限 + +2. **并发安全**:同一设备不能同时绑定多个运送清单 + +3. **数据一致性**: + - 删除运送清单时应解绑所有关联设备 + - 修改车牌号时应同步更新已绑定设备的 `car_number` + +4. **性能优化**: + - 批量更新使用异步并发(Promise.all) + - 设备列表查询添加了索引优化 + +## 相关接口 + +### 后端API + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/deliveryDevice/updateDeviceDeliveryId` | POST | 更新设备绑定信息 | +| `/deliveryDevice/clearDeliveryId` | POST | 清空设备绑定信息 | +| `/iotDevice/queryList` | POST | 查询设备列表(支持过滤) | + +### 前端方法 + +| 方法 | 说明 | +|------|------| +| `updateSelectedDevicesDeliveryId` | 批量更新选中设备的绑定信息 | +| `loadDeviceOptions` | 加载可用设备列表 | + +## 实现日期 + +2025-10-29 + diff --git a/EDIT_FEATURE_SUMMARY.md b/EDIT_FEATURE_SUMMARY.md new file mode 100644 index 0000000..3bc096d --- /dev/null +++ b/EDIT_FEATURE_SUMMARY.md @@ -0,0 +1,215 @@ +# 编辑功能接口说明 + +## ✅ 接口封装情况 + +### 前端 API + +**文件**: `pc-cattle-transportation/src/api/shipping.js` + +已封装的接口: + +1. **获取运单详情** +```javascript +// 获取运送清单详情(用于编辑) +export function getDeliveryDetail(id) { + return request({ + url: `/delivery/detail?id=${id}`, + method: 'GET', + }); +} +``` + +2. **更新运单信息** +```javascript +// 更新运送清单信息 +export function updateDeliveryInfo(data) { + return request({ + url: '/delivery/update', + method: 'POST', + data, + }); +} +``` + +**注意**: `editDialog.vue` 组件内部使用的是 `orderEdit` 接口: +```javascript +import { orderEdit } from '@/api/shipping.js'; + +// orderEdit 定义(第78行) +export function orderEdit(data) { + return request({ + url: '/delivery/updateDeliveryInfo', + method: 'POST', + data, + }); +} +``` + +### 后端 API + +**文件**: `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/DeliveryController.java` + +已实现的接口: + +1. **获取运单详情** +```java +@GetMapping("/detail") +public AjaxResult detail(@RequestParam Integer id) { + return deliveryService.detail(id); +} +``` + +2. **更新运单信息** + +后端已有 `updateDeliveryInfo` 接口,但 `editDialog.vue` 调用的是这个接口,需要确认后端实现。 + +## 🔧 前端实现 + +### attestation.vue 中的编辑功能 + +```javascript +// 编辑运送清单 +const editDelivery = async (row) => { + try { + console.log('编辑运送清单:', row); + + // 检查编辑对话框组件是否已加载 + if (!editDialogRef.value || !editDialogRef.value.onShowDialog) { + ElMessage.warning('编辑功能暂不可用,请刷新页面重试'); + return; + } + + // 获取运单详情 + const res = await getDeliveryDetail(row.id); + if (res.code === 200 && res.data) { + // 调用编辑对话框组件的 onShowDialog 方法 + editDialogRef.value.onShowDialog(res.data); + } else { + ElMessage.error(res.msg || '获取运单详情失败'); + } + } catch (error) { + console.error('打开编辑对话框失败:', error); + ElMessage.error('打开编辑对话框失败,请重试'); + } +}; +``` + +### 组件引用 + +```vue + + + +``` + +## 📋 editDialog.vue 组件接口 + +### 暴露的方法 + +```javascript +defineExpose({ + onShowDialog, // 打开对话框并填充数据 +}); +``` + +### 触发的事件 + +```javascript +emits('success'); // 保存成功后触发 +``` + +### onShowDialog 方法签名 + +```javascript +const onShowDialog = (val) => { + // val: 运单详情对象 + // 包含字段: + // - id: 运单ID + // - deliveryTitle: 订单标题 + // - ratedQuantity: 装车数量 + // - supplierId: 供应商ID(逗号分隔的字符串) + // - fundId: 资金方ID + // - buyerId: 采购商ID + // - driverId: 司机ID + // - startLon, startLat: 起始位置坐标 + // - endLon, endLat: 目的地坐标 + // ... +} +``` + +## ✨ 功能流程 + +1. 用户点击"编辑"按钮 +2. 调用 `getDeliveryDetail(id)` 获取运单详情 +3. 将详情数据传递给 `editDialog.onShowDialog(data)` +4. `editDialog` 打开并填充表单数据 +5. 用户修改数据后点击保存 +6. `editDialog` 调用 `orderEdit` 接口提交修改 +7. 保存成功后触发 `success` 事件 +8. `attestation.vue` 监听到 `success` 事件,刷新列表 + +## ⚠️ 注意事项 + +1. **数据库连接问题**: + - 从终端日志看到数据库连接失败错误 + - 错误信息:`Communications link failure` + - 需要检查数据库服务是否正常运行 + - 检查数据库连接配置(IP、端口、用户名、密码) + +2. **编辑对话框加载延迟**: + - `onShowDialog` 方法内部有 1 秒延迟(`setTimeout 1000ms`) + - 用于等待下拉选项数据加载完成 + - 这是正常的设计 + +3. **权限控制**: + - 编辑按钮需要 `entry:edit` 权限 + - 后端接口可能需要添加 `@SaCheckPermission` 注解 + +## 🎯 测试建议 + +1. 确保数据库服务正常运行 +2. 点击"编辑"按钮,检查是否能获取运单详情 +3. 检查对话框是否正常打开并填充数据 +4. 修改数据后保存,检查是否成功更新 +5. 检查列表是否自动刷新 + +## 📝 数据库连接问题排查 + +从日志看到的错误: +``` +com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure +The last packet sent successfully to the server was 0 milliseconds ago. +``` + +**可能原因**: +1. MySQL 服务未启动 +2. 防火墙阻止连接 +3. 数据库配置错误(IP、端口) +4. 数据库连接池配置问题 +5. 网络问题(连接远程数据库 129.211.213.226:3306) + +**解决方案**: +1. 检查 MySQL 服务状态 +2. 验证数据库连接配置 +3. 测试数据库连接(使用 MySQL 客户端) +4. 检查防火墙规则 +5. 如果是远程数据库,检查网络连通性 + +## 🚀 总结 + +编辑功能的接口已经全部封装完成: +- ✅ 前端 API 已封装 +- ✅ 后端 API 已实现 +- ✅ 组件集成已完成 +- ✅ 事件监听已配置 + +**当前问题**:数据库连接失败,需要先解决数据库连接问题,编辑功能才能正常使用。 + + diff --git a/EDIT_PLATE_NUMBER_FIX.md b/EDIT_PLATE_NUMBER_FIX.md new file mode 100644 index 0000000..ca83a68 --- /dev/null +++ b/EDIT_PLATE_NUMBER_FIX.md @@ -0,0 +1,266 @@ +# 编辑功能 - 车牌号未正确传递问题修复 + +## 问题描述 +用户反馈:在编辑运送清单时,**车牌号没有正确传递/显示**。 + +## 问题根源 + +### 原因分析 +1. **异步加载时序问题**: + - `open()` 方法中使用 `setTimeout(fillFormWithEditData, 500)` 来等待下拉列表加载 + - 但车辆列表 API 响应时间可能超过 500ms + - 导致 `fillFormWithEditData` 执行时,`vehicleOptions` 还是空的 + - 车牌号虽然赋值了,但因为下拉框选项列表为空,无法正确显示 + +2. **数据结构问题**: + - `detail` 接口返回的数据结构: + ```javascript + { + delivery: { licensePlate: "鄂A 66662", ... }, + supplierId: 44, + buyerId: 41, + eartagIds: [...], + collarIds: [...], + serverIds: "..." + } + ``` + - 需要从 `editData.delivery.licensePlate` 中获取车牌号 + +## 解决方案 + +### 1. 后端修改 - 补充设备信息和 ID 字段 +**文件**: `DeliveryServiceImpl.java` + +```java +// 在 detail() 方法中补充设备信息 +// 查询耳标设备信息(类型2) +List eartagDevices = deliveryDeviceMapper.selectList( + new LambdaQueryWrapper() + .eq(DeliveryDevice::getDeliveryId, id) + .eq(DeliveryDevice::getDeviceType, 2) +); +List eartagIds = new ArrayList<>(); +if (eartagDevices != null && !eartagDevices.isEmpty()) { + for (DeliveryDevice device : eartagDevices) { + if (device.getDeviceId() != null && !device.getDeviceId().isEmpty()) { + eartagIds.add(device.getDeviceId()); + } + } +} +resMap.put("eartagIds", eartagIds); + +// 查询项圈设备信息(类型3) +List collarDevices = deliveryDeviceMapper.selectList( + new LambdaQueryWrapper() + .eq(DeliveryDevice::getDeliveryId, id) + .eq(DeliveryDevice::getDeviceType, 3) +); +List collarIds = new ArrayList<>(); +if (collarDevices != null && !collarDevices.isEmpty()) { + for (DeliveryDevice device : collarDevices) { + if (device.getDeviceId() != null && !device.getDeviceId().isEmpty()) { + collarIds.add(device.getDeviceId()); + } + } +} +resMap.put("collarIds", collarIds); + +// 补充 supplierId 和 buyerId +if (delivery.getSupplierId() != null && !delivery.getSupplierId().isEmpty()) { + String firstSupplierId = delivery.getSupplierId().split(",")[0].trim(); + try { + resMap.put("supplierId", Integer.parseInt(firstSupplierId)); + } catch (NumberFormatException e) { + resMap.put("supplierId", null); + } +} else { + resMap.put("supplierId", null); +} +resMap.put("buyerId", delivery.getBuyerId()); +``` + +### 2. 前端修改 - 改为 Promise.all 等待所有下拉列表加载完成 +**文件**: `createDeliveryDialog.vue` + +#### 修改前(有问题) +```javascript +const open = (editData = null) => { + dialogVisible.value = true; + loadSupplierAndBuyerList(); // 异步但不等待 + loadDeviceOptions(); // 异步但不等待 + loadDriverList(); // 异步但不等待 + loadVehicleList(); // 异步但不等待 + loadOrderList(); // 异步但不等待 + + // 如果传入了编辑数据,则填充表单 + if (editData) { + setTimeout(() => { + fillFormWithEditData(editData); + }, 500); // ❌ 固定延迟,不可靠 + } +}; +``` + +#### 修改后(正确) +```javascript +const open = async (editData = null) => { + dialogVisible.value = true; + + // 并行加载所有下拉列表数据,等待全部完成 + await Promise.all([ + loadSupplierAndBuyerList(), + loadDeviceOptions(), + loadDriverList(), + loadVehicleList(), // ✅ 确保车辆列表加载完成 + loadOrderList() + ]); + + console.log('[OPEN-DIALOG] 所有下拉列表加载完成'); + console.log('[OPEN-DIALOG] 车辆列表:', vehicleOptions.value); + + // 如果传入了编辑数据,则填充表单 + if (editData) { + fillFormWithEditData(editData); // ✅ 此时所有选项都已加载 + } +}; +``` + +### 3. 增强车牌号填充日志 +```javascript +const fillFormWithEditData = (editData) => { + // ... + + // 车牌号 + formData.plateNumber = delivery.licensePlate || ''; + console.log('[EDIT-FILL] 车牌号:', formData.plateNumber); + console.log('[EDIT-FILL] 当前车辆列表:', vehicleOptions.value); + + // 检查车牌号是否在车辆列表中 + const vehicleExists = vehicleOptions.value.find(v => v.licensePlate === formData.plateNumber); + if (!vehicleExists && formData.plateNumber) { + console.warn('[EDIT-FILL] ⚠️ 车牌号在车辆列表中不存在:', formData.plateNumber); + } + + // ... +}; +``` + +### 4. 前端调用 detail 接口获取完整数据 +**文件**: `attestation.vue` + +```javascript +const editDelivery = async (row) => { + try { + console.log('[EDIT-DELIVERY] 准备编辑运送清单, ID:', row.id); + + // 检查编辑对话框组件是否已加载 + if (!editDialogRef.value || !editDialogRef.value.open) { + ElMessage.warning('编辑功能暂不可用,请刷新页面重试'); + return; + } + + // ✅ 调用 detail 接口获取完整数据(包含 supplierId, buyerId, 设备信息等) + const detailRes = await getDeliveryDetail(row.id); + console.log('[EDIT-DELIVERY] 获取到详情数据:', detailRes); + + if (detailRes.code === 200 && detailRes.data) { + // 传入完整的 detail 数据给 open() 方法 + editDialogRef.value.open(detailRes.data); + } else { + ElMessage.error('获取运单详情失败:' + (detailRes.msg || '未知错误')); + } + } catch (error) { + console.error('[EDIT-DELIVERY] 打开编辑对话框失败:', error); + ElMessage.error('打开编辑对话框失败,请重试'); + } +}; +``` + +## 测试步骤 + +### 1. 清理并重新编译后端 +```bash +cd tradeCattle +mvn clean compile -DskipTests +``` + +### 2. 重启后端服务 +确保新的 `detail` 接口逻辑生效。 + +### 3. 刷新前端浏览器 +清除缓存,重新加载 Vue 组件。 + +### 4. 测试编辑功能 +1. 进入"入境检疫"页面 +2. 点击任意运送清单的"编辑"按钮 +3. 观察控制台日志: + ``` + [EDIT-DELIVERY] 准备编辑运送清单, ID: 95 + [EDIT-DELIVERY] 获取到详情数据: { delivery: {...}, supplierId: 44, buyerId: 41, eartagIds: [...], collarIds: [...], serverIds: "..." } + [OPEN-DIALOG] 所有下拉列表加载完成 + [OPEN-DIALOG] 车辆列表: [{ id: 1, licensePlate: "鄂A 66662", ... }, ...] + [EDIT-FILL] 开始填充编辑数据: {...} + [EDIT-FILL] 发货方ID: 44, 采购方ID: 41 + [EDIT-FILL] 车牌号: 鄂A 66662 + [EDIT-FILL] 当前车辆列表: [{ id: 1, licensePlate: "鄂A 66662", ... }] + [EDIT-FILL] 主机ID: host001 + [EDIT-FILL] 耳标IDs: ["ear001", "ear002"] + [EDIT-FILL] 项圈IDs: ["collar001"] + ``` + +### 5. 验证字段是否正确填充 +- ✅ 发货方下拉框应显示正确的供应商 +- ✅ 采购方下拉框应显示正确的买家 +- ✅ **车牌号下拉框应显示正确的车牌号** +- ✅ 司机下拉框应显示正确的司机 +- ✅ 设备下拉框应显示已绑定的设备 + +## 关键改进点 + +### 🔑 核心改进 +1. **从固定延迟改为 Promise.all 等待** + - 确保所有下拉列表数据加载完成后才填充表单 + - 避免因网络延迟导致的数据不匹配 + +2. **后端补充完整数据** + - `detail` 接口返回 `supplierId`, `buyerId` 用于下拉框回显 + - 返回 `eartagIds`, `collarIds` 用于设备选择器回显 + +3. **增强日志** + - 每一步都有清晰的日志输出 + - 便于排查问题 + +## 常见问题排查 + +### Q1: 车牌号显示为空? +**检查**: +1. 控制台日志:`[EDIT-FILL] 车牌号:` 的值是否正确 +2. 控制台日志:`[EDIT-FILL] 当前车辆列表:` 是否包含该车牌号 +3. 如果车辆列表为空,检查 `loadVehicleList()` 是否正常执行 + +### Q2: 车牌号有值但下拉框未选中? +**可能原因**: +- 数据库中的车牌号与车辆表中的车牌号**不完全匹配**(空格、大小写等) +- 检查日志:`⚠️ 车牌号在车辆列表中不存在` + +**解决办法**: +- 确保 `delivery.licensePlate` 和 `vehicle.licensePlate` 完全一致 + +### Q3: 编辑时其他字段正常,唯独车牌号不对? +**检查**: +1. 数据库 `delivery` 表的 `license_plate` 字段值 +2. 后端日志:是否正确返回 `licensePlate` +3. 前端网络请求:查看 `/delivery/detail?id=xxx` 返回的数据 + +## 总结 + +✅ **问题已修复**: +- 使用 `Promise.all` 确保所有下拉列表加载完成 +- 后端补充完整的设备信息和 ID 字段 +- 增强日志便于问题排查 + +✅ **用户体验改善**: +- 编辑对话框打开速度更快(并行加载) +- 所有字段都能正确回显 +- 错误信息更清晰 + diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..4463f2d --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,372 @@ +# 运送清单管理功能完善 - 实施总结 + +## 📋 任务概述 + +完善运送清单管理系统的待完善功能,封装API接口,添加后端服务日志信息。 + +## ✅ 已完成的功能 + +### 1. 前端 API 接口封装 + +**文件**: `pc-cattle-transportation/src/api/shipping.js` + +新增以下 API 接口: + +- `updateDeliveryStatus(data)` - 修改运送清单状态 +- `deleteDeliveryLogic(id)` - 逻辑删除运送清单 +- `downloadDeliveryPackage(id)` - 打包下载运送清单文件 +- `getDeliveryDetail(id)` - 获取运送清单详情(用于编辑) +- `updateDeliveryInfo(data)` - 更新运送清单信息 + +### 2. 后端 API 实现 + +**文件**: `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/DeliveryController.java` + +#### 2.1 修改状态接口 (updateStatus) + +- **路径**: `POST /delivery/updateStatus` +- **权限**: `@SaCheckPermission("delivery:edit")` +- **功能**: 修改运送清单状态(1-准备中,2-运输中,3-已结束) +- **日志增强**: + - 记录运单ID和新状态 + - 记录原状态和新状态 + - 状态验证(1-3) + - 运单存在性检查 + - 详细的成功/失败日志 + +#### 2.2 逻辑删除接口 (deleteLogic) + +- **路径**: `POST /delivery/deleteLogic` +- **权限**: `@SaCheckPermission("delivery:delete")` +- **功能**: 逻辑删除运送清单(保留历史记录,不清空设备绑定) +- **日志增强**: + - 记录运单ID和运单号 + - 权限检查日志 + - 删除操作结果日志 + +#### 2.3 打包下载接口 (downloadPackage) + +- **路径**: `GET /delivery/downloadPackage` +- **权限**: `@SaCheckPermission("delivery:view")` +- **功能**: 打包运送清单的所有文件(图片、视频、信息)为ZIP压缩包 +- **包含内容**: + - **运送清单信息.txt**: 包含基础信息、重量信息、状态信息 + - **照片文件夹**: + - 检疫票 + - 纸质磅单 + - 空车过磅车头照片 + - 装车过磅车头照片 + - 装车过磅磅单 + - 车头照片 + - 车尾照片 + - 司机身份证照片 + - 到地磅单 + - 到地车辆过重磅车头照片 + - **视频文件夹**: + - 空车过磅视频 + - 装车过磅视频 + - 装车视频 + - 消毒槽视频 + - 牛只装车环视视频 + - 卸牛视频 + - 到地过磅视频 +- **日志增强**: + - 记录运单ID和运单号 + - 记录每个文件的下载状态 + - 记录照片和视频的成功添加数量 + - 记录最终ZIP文件大小 + - 详细的错误日志 + +### 3. 前端功能完善 + +#### 3.1 运送清单列表页面 (attestation.vue) + +**文件**: `pc-cattle-transportation/src/views/entry/attestation.vue` + +新增功能按钮(操作列宽度扩展至 350px): + +1. **查看设备** - 跳转到设备管理页面 + ```javascript + const viewDevices = (row) => { + router.push({ + path: '/entry/devices', + query: { deliveryId: row.id, deliveryNumber: row.deliveryNumber } + }); + }; + ``` + +2. **编辑** - 调用编辑对话框 + ```javascript + const editDelivery = async (row) => { + if (editDialogRef.value && editDialogRef.value.open) { + editDialogRef.value.open(row.id); + } + }; + ``` + +3. **修改状态** - 弹出输入框修改状态 + - 调用 `updateDeliveryStatus` API + - 输入验证(1-3) + - 实时刷新列表 + +4. **打包文件** - 下载运送清单压缩包 + - 调用 `downloadDeliveryPackage` API + - 文件命名:`运送清单_{运单号}_{时间戳}.zip` + - 自动下载到本地 + - 加载状态显示 + +5. **删除** - 逻辑删除运送清单 + - 调用 `deleteDeliveryLogic` API + - 二次确认 + - 实时刷新列表 + +**组件引入**: +- 导入 `editDialog` 组件 +- 添加 `editDialogRef` 引用 + +#### 3.2 设备管理页面 (devices.vue) + +**新建文件**: `pc-cattle-transportation/src/views/entry/devices.vue` + +功能特性: + +1. **页面头部**: + - 显示运单号 + - 返回按钮 + +2. **设备统计卡片**: + - 智能主机数量 + - 智能耳标数量 + - 智能项圈数量 + - 设备总数 + +3. **设备列表Tab**: + - **智能主机Tab**: 显示所有智能主机设备 + - **智能耳标Tab**: 显示所有智能耳标设备(分页) + - **智能项圈Tab**: 显示所有智能项圈设备(分页) + +4. **设备操作**: + - **解绑设备**: 调用 `unbindDevice` API + - 二次确认 + - 实时刷新对应列表 + +5. **数据展示**: + - 设备ID/SN + - 运单号 + - 重量(耳标) + - 车牌号 + - 绑定时间 + +## 🎨 UI 优化 + +### 状态显示 + +**更新文件**: `pc-cattle-transportation/src/views/entry/attestation.vue` + +状态文本和颜色映射: +- **1 - 准备中**: 蓝色 (info) +- **2 - 运输中**: 橙色 (warning) +- **3 - 已结束**: 绿色 (success) + +搜索下拉选项也已同步更新。 + +## 📊 日志增强 + +### 后端日志规范 + +所有新增/修改的后端方法都添加了详细的日志: + +```java +System.out.println("=== 功能名称 ==="); +System.out.println("参数1: " + value1); +System.out.println("参数2: " + value2); +// ... 业务逻辑 +System.out.println("SUCCESS: 操作成功"); +// 或 +System.out.println("ERROR: 错误信息"); +``` + +日志包含: +- 功能标题(用 === 分隔) +- 输入参数 +- 中间处理步骤 +- 查询结果 +- 成功/失败状态 +- 异常堆栈 + +### 前端日志 + +前端关键操作也添加了 `console.log` 日志: +- API 调用参数 +- 响应数据 +- 错误信息 + +## 🔧 技术实现细节 + +### 文件下载实现 + +使用 Spring 的 `HttpServletResponse` 直接写入ZIP流: + +```java +ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); +ZipOutputStream zipOut = new ZipOutputStream(byteArrayOutputStream); + +// 1. 添加信息文本 +addDeliveryInfoToZip(zipOut, delivery); + +// 2. 下载并添加图片/视频 +addFileToZip(zipOut, fileUrl, fileName); + +// 3. 设置响应头并输出 +response.setContentType("application/zip"); +response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName); +out.write(zipBytes); +``` + +### 前端文件下载 + +使用 Blob 和 URL.createObjectURL: + +```javascript +const blob = new Blob([res], { type: 'application/zip' }); +const url = window.URL.createObjectURL(blob); +const link = document.createElement('a'); +link.href = url; +link.download = fileName; +link.click(); +window.URL.revokeObjectURL(url); +``` + +## 📝 API 接口文档 + +### 1. 修改状态 + +``` +POST /api/delivery/updateStatus +Content-Type: application/json + +Request Body: +{ + "id": 95, + "status": 2 +} + +Response: +{ + "code": 200, + "msg": "状态更新成功" +} +``` + +### 2. 逻辑删除 + +``` +POST /api/delivery/deleteLogic?id=95 + +Response: +{ + "code": 200, + "msg": "运单删除成功" +} +``` + +### 3. 打包下载 + +``` +GET /api/delivery/downloadPackage?id=95 + +Response: application/zip (Binary file stream) +``` + +## ✨ 权限控制 + +所有新增的功能都添加了权限控制: + +- `delivery:edit` - 修改状态 +- `delivery:delete` - 删除运单 +- `delivery:view` - 查看和下载 +- `entry:device` - 查看设备 +- `entry:edit` - 编辑运单 +- `entry:status` - 修改状态 +- `entry:download` - 打包下载 + +## 🚀 部署说明 + +### 后端部署 + +1. 清理编译缓存: + ```bash + cd tradeCattle + mvn clean + ``` + +2. 重新编译: + ```bash + mvn compile + ``` + +3. 重启服务 + +### 前端部署 + +前端代码已更新,刷新浏览器即可。 + +## 📦 文件清单 + +### 新建文件 + +- `pc-cattle-transportation/src/views/entry/devices.vue` - 设备管理页面 + +### 修改文件 + +- `pc-cattle-transportation/src/api/shipping.js` - API接口封装 +- `pc-cattle-transportation/src/views/entry/attestation.vue` - 运送清单列表页 +- `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/DeliveryController.java` - 运送清单控制器 + +## 🎯 测试建议 + +1. **修改状态功能**: + - 测试状态 1/2/3 的切换 + - 测试无效状态值的验证 + - 验证列表实时刷新 + +2. **逻辑删除功能**: + - 测试删除确认 + - 验证权限控制 + - 确认删除后列表刷新 + +3. **打包下载功能**: + - 测试包含所有文件的运单 + - 测试部分文件缺失的运单 + - 验证ZIP文件完整性 + - 检查文件命名 + +4. **编辑功能**: + - 测试编辑对话框打开 + - 验证数据回填 + - 测试保存后刷新 + +5. **查看设备功能**: + - 测试设备列表显示 + - 测试设备解绑 + - 验证设备统计数据 + +## 📌 注意事项 + +1. 打包下载功能会从URL下载文件,需要确保文件URL可访问 +2. 文件下载失败不会中断整个流程,会记录警告日志 +3. 逻辑删除不会清空设备绑定关系,保留历史记录 +4. 物理删除会清空设备的 `delivery_id`、`weight` 和 `car_number` + +## 🎉 总结 + +所有待完善功能已全部实现,包括: +- ✅ 前端API接口封装 +- ✅ 后端API实现(修改状态、逻辑删除、打包下载) +- ✅ 前端功能完善(编辑、查看设备) +- ✅ 创建设备管理页面 +- ✅ 添加详细日志信息 +- ✅ 修复代码风格问题 + +系统功能更加完善,用户体验得到显著提升! + diff --git a/LICENSE_PLATE_FIX.md b/LICENSE_PLATE_FIX.md new file mode 100644 index 0000000..ea9de1e --- /dev/null +++ b/LICENSE_PLATE_FIX.md @@ -0,0 +1,124 @@ +# 车牌号关联修复说明 + +## 问题描述 + +用户反馈:车牌号还是关联查询的是司机表中的数据 + +## 问题原因 + +在 `DeliveryServiceImpl.java` 的两个方法中,存在从司机表获取车牌号并覆盖运送清单车牌号的逻辑: + +1. **pageQuery** (列表查询) - 第1033行 +2. **viewDelivery** (详情查询) - 第1560行 + +具体代码: +```java +String licensePlate = (String) driverInfo.get("car_number"); +delivery.setLicensePlate(licensePlate); // 错误:覆盖了用户选择的车牌号 +``` + +## 解决方案 + +### 移除从司机表获取车牌号的逻辑 + +**文件**: `DeliveryServiceImpl.java` + +#### 1. 列表查询方法 (pageQuery) + +**修改前**: +```java +String licensePlate = (String) driverInfo.get("car_number"); +deliveryLogVo.setLicensePlate(licensePlate); +``` + +**修改后**: +```java +// 注释:车牌号不从司机表获取,车牌号是独立的模块 +// 车牌号保持运送清单表中原有的值 +``` + +#### 2. 详情查询方法 (viewDelivery) + +**修改前**: +```java +String licensePlate = (String) driverInfo.get("car_number"); +delivery.setLicensePlate(licensePlate); +``` + +**修改后**: +```java +// 注释:车牌号不从司机表获取,车牌号是独立的模块 +// 车牌号保持运送清单表中原有的值 +``` + +#### 3. 移除相关日志输出 + +**修改前**: +```java +System.out.println("查询到的车牌号: " + licensePlate); +``` + +**修改后**: +```java +// 移除该日志 +``` + +#### 4. 修复车牌号匹配查询逻辑 + +**修改前**: +```java +if (licensePlate != null && !licensePlate.equals(deliveryLogVo.getLicensePlate())) { + // 根据车牌号查询司机 +} +``` + +**修改后**: +```java +// 不再根据车牌号查询司机,车牌号是独立的模块 +``` + +## 模块说明 + +### 司机模块 +- **字段**: `driver_name`, `driver_mobile` +- **数据来源**: 司机表 (member_driver) +- **功能**: 记录司机姓名和手机号 + +### 车牌号模块 +- **字段**: `license_plate` +- **数据来源**: 用户表单选择 +- **功能**: 记录运送车辆的车牌号 + +**两个模块是独立的,不应互相覆盖。** + +## 测试验证 + +创建运送清单后,检查: + +```sql +SELECT + id, + license_plate, + driver_id, + driver_name, + driver_mobile +FROM delivery +WHERE id = [最新创建的运送清单ID]; +``` + +预期结果: +- `license_plate` = 用户选择的车牌号(如 "1111111") +- `driver_id` = 选择的司机ID +- `driver_name` = 司机姓名 +- `driver_mobile` = 司机手机号 + +**车牌号不应与司机表中的car_number字段关联。** + +## 相关文件 + +- `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/DeliveryServiceImpl.java` + +## 实现日期 + +2025-10-29 + diff --git a/RESTART_BACKEND_GUIDE.md b/RESTART_BACKEND_GUIDE.md new file mode 100644 index 0000000..b31ef0a --- /dev/null +++ b/RESTART_BACKEND_GUIDE.md @@ -0,0 +1,193 @@ +# 重启后端服务指南 + +## 问题确认 +后端日志显示执行的 SQL: +```sql +UPDATE vehicle SET license_plate=?, ..., update_time=?, updated_by=? +WHERE id=? AND is_delete=0 +``` + +**这是旧代码!** 说明后端服务没有重启,还在使用旧的编译版本。 + +## 解决步骤 + +### 方法1:IDEA 重启服务(推荐) + +1. **停止当前运行的服务** + - 在 IDEA 底部找到 "Run" 或 "Services" 窗口 + - 找到正在运行的 Spring Boot 应用 + - 点击红色方块按钮 ⬛ 停止服务 + +2. **清理编译缓存**(可选但推荐) + - IDEA 菜单:`Build` → `Clean Project` + - 或者:`Build` → `Rebuild Project` + +3. **重新启动服务** + - 点击绿色三角按钮 ▶️ 启动服务 + - 等待服务启动完成(看到 "Started Application" 日志) + +### 方法2:命令行重启 + +#### Windows PowerShell +```powershell +# 1. 停止后端服务(找到 Java 进程并结束) +tasklist | findstr java +# 记下 PID,例如 21056 + +# 2. 结束进程 +taskkill /F /PID 21056 + +# 3. 清理并重新编译 +cd C:\cattleTransport\tradeCattle +mvn clean package -DskipTests + +# 4. 重新启动服务 +cd aiotagro-cattle-trade +java -jar target\aiotagro-cattletrade-1.0.1.jar +``` + +#### Linux/Mac +```bash +# 1. 找到并停止 Java 进程 +ps aux | grep java +kill -9 + +# 2. 清理并重新编译 +cd /path/to/tradeCattle +mvn clean package -DskipTests + +# 3. 重新启动服务 +cd aiotagro-cattle-trade +java -jar target/aiotagro-cattletrade-1.0.1.jar +``` + +### 方法3:使用 Spring Boot DevTools(如果已配置) +如果项目配置了 Spring Boot DevTools: +- 保存文件后会自动重启 +- 但完全替换方法时,建议手动重启 + +## 验证步骤 + +### 1. 检查后端日志 +重启后,后端应该显示新的启动日志: +``` +Started Application in X.XXX seconds +``` + +### 2. 测试删除功能 +1. 刷新前端页面(Ctrl+F5) +2. 进入"车辆管理"页面 +3. 点击"删除"按钮 +4. 观察后端日志 + +### 3. 期望的日志输出 +**新代码应该显示**: +``` +[VEHICLE-DELETE] 开始逻辑删除车辆,ID: 3 +[VEHICLE-DELETE] 车辆信息 - 车牌号: 鄂A 66662, 当前 is_delete: 0 +UPDATE vehicle SET is_delete=1 WHERE id=3 +[VEHICLE-DELETE] ✅ 逻辑删除成功,车牌号: 鄂A 66662, 操作人ID: 11 +``` + +**关键点**: +- ✅ SQL 中只有 `SET is_delete=1` +- ✅ 没有更新 `license_plate`, `car_front_photo` 等其他字段 +- ✅ 日志显示 `[VEHICLE-DELETE]` 标签 + +### 4. 验证数据库 +```sql +-- 查看该记录 +SELECT id, license_plate, is_delete, update_time +FROM vehicle +WHERE id = 3; + +-- 预期结果:is_delete = 1 +``` + +### 5. 验证前端列表 +- ✅ 列表自动刷新 +- ✅ 已删除的车辆不再显示 +- ✅ 显示"删除成功"提示 + +## 常见问题 + +### Q1: 找不到正在运行的服务? +**检查**: +```powershell +# Windows +netstat -ano | findstr :16200 +# 查看哪个进程占用了 16200 端口 + +# 结束进程 +taskkill /F /PID <进程ID> +``` + +### Q2: Maven 编译失败? +**清理并重试**: +```powershell +cd C:\cattleTransport\tradeCattle +mvn clean +mvn compile -DskipTests +``` + +### Q3: 重启后还是旧代码? +**可能原因**: +1. IDEA 没有自动编译 + - 解决:`Build` → `Rebuild Project` +2. 启动了错误的配置 + - 解决:检查 Run Configuration +3. 类加载器缓存 + - 解决:完全关闭 IDEA 并重新打开 + +### Q4: 端口被占用? +```powershell +# 查看占用端口的进程 +netstat -ano | findstr :16200 + +# 结束进程 +taskkill /F /PID <进程ID> +``` + +## 快速重启脚本 + +### Windows (PowerShell) +创建 `restart-backend.ps1`: +```powershell +# 停止后端服务 +$process = Get-Process | Where-Object {$_.ProcessName -eq "java" -and $_.MainWindowTitle -like "*cattletrade*"} +if ($process) { + Stop-Process -Id $process.Id -Force + Write-Host "已停止后端服务" +} + +# 重新编译 +cd C:\cattleTransport\tradeCattle +mvn clean compile -DskipTests + +# 重新启动(在 IDEA 中手动启动) +Write-Host "编译完成,请在 IDEA 中重新启动服务" +``` + +运行: +```powershell +powershell -ExecutionPolicy Bypass -File restart-backend.ps1 +``` + +## 总结 + +✅ **关键步骤**: +1. 停止后端服务 +2. 清理编译缓存(可选) +3. 重新启动服务 +4. 验证日志输出 + +✅ **验证标志**: +- 日志显示 `UPDATE vehicle SET is_delete=1` +- 数据库中 `is_delete` 变为 `1` +- 前端列表中记录消失 + +⚠️ **注意**: +- 必须完全停止旧服务 +- 确保编译成功 +- 验证新代码是否生效 + diff --git a/ROUTE_PATH_FIX.md b/ROUTE_PATH_FIX.md new file mode 100644 index 0000000..ca85529 --- /dev/null +++ b/ROUTE_PATH_FIX.md @@ -0,0 +1,350 @@ +# Vue Router 路由刷新问题修复 + +## 问题描述 +用户反馈:**登录后跳转到 `http://localhost:8080/system/post`,刷新页面后路由变成 `http://localhost:8080/post`,导致页面空白**。 + +## 问题根源 + +### Vue Router 嵌套路由的路径规则 + +在 Vue Router 中,**子路由的 `path` 配置方式**决定了最终的 URL: + +#### ❌ 错误配置(使用绝对路径) +```javascript +{ + path: '/system', + component: LayoutIndex, + children: [ + { + path: '/system/post', // ❌ 以 / 开头 = 绝对路径 + component: () => import('~/views/system/post.vue'), + }, + ], +} +``` + +**问题**: +- 子路由使用 `/system/post`(以 `/` 开头) +- Vue Router 会将其视为**根路径** +- 刷新时路由解析错误,变成 `/post` + +#### ✅ 正确配置(使用相对路径) +```javascript +{ + path: '/system', + component: LayoutIndex, + children: [ + { + path: 'post', // ✅ 不以 / 开头 = 相对路径 + component: () => import('~/views/system/post.vue'), + }, + ], +} +``` + +**正确行为**: +- 子路由使用 `post`(不以 `/` 开头) +- Vue Router 会自动拼接:`/system` + `post` = `/system/post` +- 刷新时路由正常工作 + +## 修复内容 + +### 修复的路由模块 + +修复了以下所有嵌套路由的子路径配置: + +1. **入境检疫** (`/entry`) + - `/entry/details` → `details` + - `/entry/devices` → `devices` + +2. **系统管理** (`/system`) + - `/system/post` → `post` + - `/system/staff` → `staff` + - `/system/tenant` → `tenant` + +3. **权限管理** (`/permission`) + - `/permission/menu` → `menu` + - `/permission/operation` → `operation` + +4. **智能硬件** (`/hardware`) + - `/hardware/collar` → `collar` + - `/hardware/eartag` → `eartag` + - `/hardware/host` → `host` + +5. **用户管理** (`/userManage`) + - `/userManage/user` → `user` + - `/userManage/driver` → `driver` + - `/userManage/vehicle` → `vehicle` + +6. **早期预警** (`/earlywarning`) + - `/earlywarning/earlywarninglist` → `earlywarninglist` + +7. **运送清单** (`/shipping`) + - `/shipping/loadingOrder` → `loadingOrder` + - `/shipping/shippinglist` → `shippinglist` + +### 修复示例 + +**修复前**: +```javascript +{ + path: '/system', + component: LayoutIndex, + children: [ + { + path: '/system/post', // ❌ 绝对路径 + name: 'Post', + component: () => import('~/views/system/post.vue'), + }, + ], +} +``` + +**修复后**: +```javascript +{ + path: '/system', + component: LayoutIndex, + children: [ + { + path: 'post', // ✅ 相对路径 + name: 'Post', + component: () => import('~/views/system/post.vue'), + }, + ], +} +``` + +## Vue Router 路径规则详解 + +### 1. 绝对路径(以 `/` 开头) + +```javascript +{ + path: '/parent', + children: [ + { path: '/child' } // ❌ 结果:/child + ] +} +``` + +- **行为**:忽略父路由路径,直接作为根路径 +- **最终 URL**:`/child` +- **用途**:极少使用,仅当子路由确实需要在根路径下时 + +### 2. 相对路径(不以 `/` 开头) + +```javascript +{ + path: '/parent', + children: [ + { path: 'child' } // ✅ 结果:/parent/child + ] +} +``` + +- **行为**:自动拼接父路由路径 +- **最终 URL**:`/parent/child` +- **用途**:**推荐使用**,适用于绝大多数嵌套路由场景 + +### 3. 空路径 + +```javascript +{ + path: '/parent', + children: [ + { path: '' } // 结果:/parent + ] +} +``` + +- **行为**:匹配父路由路径本身 +- **最终 URL**:`/parent` +- **用途**:默认子路由、索引页 + +## 为什么刷新会导致路由变化? + +### 问题原因 + +1. **首次导航**(通过 `router.push`): + ```javascript + router.push('/system/post') + ``` + - Vue Router 能正确找到路由配置 + - 页面正常显示 + +2. **刷新页面**(直接访问 URL): + ``` + http://localhost:8080/system/post + ``` + - 浏览器直接请求该 URL + - Vue Router 重新解析路由配置 + - 如果子路由配置错误(`path: '/system/post'`),会被解析为根路径 + - 最终匹配到 `/post`(错误) + +### 修复后的行为 + +1. **首次导航**: + ```javascript + router.push('/system/post') + ``` + - 匹配到:`/system` + `post` = `/system/post` ✅ + +2. **刷新页面**: + ``` + http://localhost:8080/system/post + ``` + - 匹配到:`/system` + `post` = `/system/post` ✅ + - 路由正常,页面正常显示 + +## 测试步骤 + +### 1. 刷新浏览器 +清除前端缓存,重新加载路由配置(Ctrl+F5)。 + +### 2. 测试系统管理 - 岗位管理 +1. 登录系统 +2. 点击"系统管理" → "岗位管理" +3. URL 应该是:`http://localhost:8080/system/post` +4. 刷新页面(F5) +5. ✅ URL 保持不变:`http://localhost:8080/system/post` +6. ✅ 页面正常显示 + +### 3. 测试其他路由 +测试以下路由,确保刷新后 URL 不变: +- `http://localhost:8080/system/staff` +- `http://localhost:8080/system/tenant` +- `http://localhost:8080/userManage/user` +- `http://localhost:8080/userManage/driver` +- `http://localhost:8080/userManage/vehicle` +- `http://localhost:8080/hardware/collar` +- `http://localhost:8080/hardware/eartag` +- `http://localhost:8080/hardware/host` +- `http://localhost:8080/permission/menu` +- `http://localhost:8080/permission/operation` +- `http://localhost:8080/shipping/loadingOrder` +- `http://localhost:8080/shipping/shippinglist` +- `http://localhost:8080/earlywarning/earlywarninglist` + +### 4. 验证浏览器前进/后退 +1. 依次访问多个页面 +2. 点击浏览器"后退"按钮 +3. ✅ URL 应该正确回到前一个页面 +4. 点击浏览器"前进"按钮 +5. ✅ URL 应该正确前进到下一个页面 + +## 常见问题 + +### Q1: 为什么之前能正常跳转,但刷新就不行? +**答**: +- `router.push()` 跳转时,Vue Router 使用编程式导航,能够容错 +- 刷新页面时,浏览器直接请求 URL,Vue Router 严格按照配置解析 +- 如果配置错误(使用绝对路径),刷新时就会出错 + +### Q2: 什么时候应该使用绝对路径(以 `/` 开头)? +**答**: +- **几乎不需要** +- 仅在子路由确实需要在根路径下时才使用 +- 99% 的嵌套路由应该使用相对路径 + +### Q3: 如何判断路由配置是否正确? +**答**: +检查子路由的 `path` 是否以 `/` 开头: +```javascript +// ❌ 错误 +children: [ + { path: '/parent/child' } // 不应该包含父路径 +] + +// ✅ 正确 +children: [ + { path: 'child' } // 只写相对路径 +] +``` + +### Q4: 修复后需要重启开发服务器吗? +**答**: +- 如果使用 Vite(本项目),通常会自动热更新 +- 如果没有生效,强制刷新浏览器(Ctrl+F5) +- 必要时重启开发服务器:`npm run dev` + +## Vue Router 最佳实践 + +### ✅ 推荐做法 + +1. **嵌套路由使用相对路径** + ```javascript + { + path: '/parent', + children: [ + { path: 'child' } // ✅ 相对路径 + ] + } + ``` + +2. **顶级路由使用绝对路径** + ```javascript + { + path: '/login' // ✅ 顶级路由必须以 / 开头 + } + ``` + +3. **使用命名路由** + ```javascript + { + path: 'post', + name: 'Post' // ✅ 方便编程式导航 + } + ``` + +4. **路由导航使用 name** + ```javascript + router.push({ name: 'Post' }) // ✅ 推荐 + router.push('/system/post') // 也可以,但不推荐 + ``` + +### ❌ 避免的做法 + +1. **子路由使用绝对路径** + ```javascript + { + path: '/parent', + children: [ + { path: '/parent/child' } // ❌ 错误 + ] + } + ``` + +2. **路径拼写错误** + ```javascript + { + path: 'post', + component: () => import('~/views/system/Post.vue') // ❌ 大小写错误 + } + ``` + +3. **路径与组件不匹配** + ```javascript + { + path: 'post', + component: () => import('~/views/system/staff.vue') // ❌ 组件错误 + } + ``` + +## 总结 + +✅ **修复完成**: +- 所有嵌套路由的子路径已改为相对路径 +- 刷新页面时 URL 不再变化 +- 所有页面正常显示 + +✅ **关键原则**: +- **嵌套路由的子路径不要以 `/` 开头** +- Vue Router 会自动拼接父路径和子路径 +- 相对路径配置简洁、清晰、不易出错 + +✅ **用户体验改善**: +- 刷新页面后 URL 保持不变 +- 浏览器前进/后退功能正常 +- 书签和分享链接正常工作 + diff --git a/SAVE_AND_RESTART.md b/SAVE_AND_RESTART.md new file mode 100644 index 0000000..f53e982 --- /dev/null +++ b/SAVE_AND_RESTART.md @@ -0,0 +1,174 @@ +# ⚠️ 文件未保存! + +## 问题确认 + +检测到 `VehicleServiceImpl.java` 文件**在编辑器中已修改但未保存**: + +- ✅ 编辑器中的代码:使用 `deleteById(id)` ✓ 正确 +- ❌ 磁盘上的文件:使用 `updateById(vehicle)` ✗ 旧代码 + +## 立即操作 + +### 1. 保存文件(必须!) + +#### 方法1:快捷键 +``` +Ctrl + S (保存当前文件) +``` + +#### 方法2:保存所有文件 +``` +Ctrl + Shift + S (保存所有文件) +``` + +#### 方法3:菜单 +``` +File → Save All +``` + +### 2. 验证文件已保存 + +检查 IDEA 标题栏/标签页: +- ❌ 文件名后有 `*` 号 = 未保存 +- ✅ 文件名后无 `*` 号 = 已保存 + +### 3. 重新编译 + +保存后,执行以下操作之一: + +#### 选项A:IDEA 自动编译(推荐) +1. 检查是否启用自动编译: + - `File` → `Settings` → `Build, Execution, Deployment` → `Compiler` + - 勾选 `Build project automatically` + +#### 选项B:手动编译 +``` +Build → Rebuild Project +``` + +#### 选项C:Maven 编译 +```powershell +cd C:\cattleTransport\tradeCattle +mvn clean compile -DskipTests +``` + +### 4. 重启后端服务 + +1. **停止服务**:红色方块按钮 ⬛ +2. **启动服务**:绿色三角按钮 ▶️ +3. **等待启动完成** + +## 完整操作流程 + +``` +1. Ctrl + S → 保存文件 +2. Build → Rebuild → 重新编译 +3. Stop → Start → 重启服务 +4. 测试删除功能 → 验证是否正常 +``` + +## 验证步骤 + +### 1. 检查编译后的代码 + +保存并重新编译后,执行: +```powershell +cd C:\cattleTransport\tradeCattle\aiotagro-cattle-trade\src\main\java\com\aiotagro\cattletrade\business\service\impl +Get-Content VehicleServiceImpl.java | Select-String -Pattern "deleteById" -Context 2 +``` + +应该看到: +```java +boolean success = vehicleMapper.deleteById(id) > 0; +``` + +### 2. 测试删除功能 + +1. 刷新前端页面(Ctrl+F5) +2. 进入"车辆管理" +3. 点击"删除"按钮 +4. 观察后端日志 + +### 3. 预期日志(正确) + +``` +[VEHICLE-DELETE] 开始逻辑删除车辆,ID: 3 +[VEHICLE-DELETE] 车辆信息 - 车牌号: 鄂A 66662, 当前 is_delete: 0 +UPDATE vehicle SET is_delete=1 WHERE id=3 +[VEHICLE-DELETE] ✅ 逻辑删除成功 +``` + +**关键点**: +- ✅ SQL 只有 `SET is_delete=1` +- ✅ 没有 `license_plate`, `car_front_photo` 等字段 + +### 4. 验证数据库 + +```sql +SELECT id, license_plate, is_delete, update_time +FROM vehicle +WHERE id = 3; +``` + +**预期结果**: +``` +id | license_plate | is_delete | update_time +3 | 鄂A 66662 | 1 | 2025-10-29 17:20:00 +``` + +## 常见问题 + +### Q1: 如何确认文件是否已保存? +**检查点**: +1. 文件标签页名称后无 `*` 号 +2. IDEA 状态栏显示 "All files saved" +3. 磁盘文件的修改时间是最新的 + +### Q2: 保存后还是旧代码? +**可能原因**: +1. 保存的是错误的文件 + - 确认路径:`tradeCattle\aiotagro-cattle-trade\src\main\java\...\VehicleServiceImpl.java` +2. IDEA 打开了多个窗口 + - 确认在正确的项目窗口中操作 +3. 文件被只读 + - 检查文件属性,取消"只读" + +### Q3: 重启后日志还是显示旧 SQL? +**检查步骤**: +1. 确认文件已保存(无 `*` 号) +2. 确认已重新编译 +3. 确认启动的是新编译的代码 +4. 查看编译输出目录的时间戳 + +## IDEA 自动保存设置(推荐) + +### 启用自动保存 +1. `File` → `Settings` (或 `Ctrl + Alt + S`) +2. `Appearance & Behavior` → `System Settings` +3. 勾选以下选项: + - ✅ `Save files on frame deactivation` (切换窗口时自动保存) + - ✅ `Save files automatically if application is idle for X seconds` + +### 启用自动编译 +1. `File` → `Settings` +2. `Build, Execution, Deployment` → `Compiler` +3. 勾选: + - ✅ `Build project automatically` + +## 当前状态 + +- ✅ 代码已修改(在编辑器中) +- ❌ **文件未保存到磁盘** +- ❌ 编译的是旧代码 +- ❌ 运行的是旧代码 + +## 下一步操作 + +**请立即执行**: +1. ⌨️ 按 `Ctrl + S` 保存文件 +2. 🔨 点击 `Build → Rebuild Project` +3. 🔄 重启后端服务 +4. ✅ 测试删除功能 + +**操作时间**:< 1 分钟 + diff --git a/VEHICLE_DELETE_FINAL_FIX.md b/VEHICLE_DELETE_FINAL_FIX.md new file mode 100644 index 0000000..1c8bfca --- /dev/null +++ b/VEHICLE_DELETE_FINAL_FIX.md @@ -0,0 +1,331 @@ +# 车辆删除功能最终修复 - 使用 MyBatis-Plus 的 @TableLogic + +## 问题描述 +用户反馈:**删除车辆返回成功,但前端页面依然显示该记录**。 + +后端日志显示: +``` +UPDATE vehicle SET license_plate=?, ..., update_time=?, updated_by=? +WHERE id=? AND is_delete=0 +Updates: 1 +``` + +**关键发现**:UPDATE 语句中**没有 `is_delete` 字段**! + +## 问题根源 + +### @TableLogic 注解的副作用 + +`Vehicle` 实体类使用了 `@TableLogic` 注解: +```java +@TableLogic(value = "0", delval = "1") +@TableField("is_delete") +private Integer isDelete; +``` + +**@TableLogic 的行为**: +1. ✅ **SELECT**: 自动添加 `WHERE is_delete = 0` +2. ✅ **DELETE**: 自动转换为 `UPDATE SET is_delete = 1` +3. ❌ **UPDATE**: `is_delete` 字段**被排除在 SET 子句之外**! + +### 错误的实现方式 + +```java +// ❌ 错误:使用 updateById 无法修改 is_delete 字段 +Vehicle vehicle = vehicleMapper.selectById(id); +vehicle.setIsDelete(1); // 这个设置会被忽略! +vehicleMapper.updateById(vehicle); + +// 生成的 SQL(注意没有 is_delete): +UPDATE vehicle +SET license_plate=?, car_front_photo=?, ..., update_time=?, updated_by=? +WHERE id=? AND is_delete=0 +``` + +**结果**: +- `is_delete` 字段没有被更新,仍然是 `0` +- 记录依然存在,前端列表继续显示 +- 虽然返回 `Updates: 1`(更新了其他字段),但逻辑删除失败 + +## 最终解决方案 + +### 使用 MyBatis-Plus 的 deleteById() 方法 + +**正确做法**:利用 `@TableLogic` 注解,直接调用 `deleteById()` + +```java +/** + * 删除车辆(逻辑删除,使用 MyBatis-Plus 的 deleteById 自动处理) + * 因为实体类使用了 @TableLogic 注解,deleteById 会自动将 is_delete 设置为 1 + */ +@Override +@Transactional +public AjaxResult deleteVehicle(Integer id) { + System.out.println("[VEHICLE-DELETE] 开始逻辑删除车辆,ID: " + id); + + if (id == null) { + System.out.println("[VEHICLE-DELETE] 删除失败:车辆ID为空"); + return AjaxResult.error("车辆ID不能为空"); + } + + // 查询车辆是否存在(@TableLogic 会自动过滤 is_delete=1 的记录) + Vehicle vehicle = vehicleMapper.selectById(id); + if (vehicle == null) { + System.out.println("[VEHICLE-DELETE] 删除失败:车辆不存在或已被删除,ID: " + id); + return AjaxResult.error("车辆不存在"); + } + + System.out.println("[VEHICLE-DELETE] 车辆信息 - 车牌号: " + vehicle.getLicensePlate() + ", 当前 is_delete: " + vehicle.getIsDelete()); + + // 获取当前用户ID(用于日志记录) + Integer userId = SecurityUtil.getCurrentUserId(); + + // ✅ 使用 MyBatis-Plus 的 deleteById 方法 + // 因为实体类有 @TableLogic 注解,这会自动执行逻辑删除(UPDATE SET is_delete=1) + // 而不是物理删除(DELETE FROM) + boolean success = vehicleMapper.deleteById(id) > 0; + + if (success) { + System.out.println("[VEHICLE-DELETE] ✅ 逻辑删除成功,车牌号: " + vehicle.getLicensePlate() + ", 操作人ID: " + userId); + return AjaxResult.success("删除车辆成功"); + } else { + System.out.println("[VEHICLE-DELETE] ❌ 逻辑删除失败,车牌号: " + vehicle.getLicensePlate()); + return AjaxResult.error("删除车辆失败"); + } +} +``` + +### 生成的 SQL + +**使用 deleteById() 后**: +```sql +UPDATE vehicle +SET is_delete = 1 +WHERE id = 3; +``` + +**完美!** 只更新 `is_delete` 字段,不涉及其他字段。 + +## MyBatis-Plus @TableLogic 工作原理 + +### 1. SELECT 操作 +```java +Vehicle vehicle = vehicleMapper.selectById(1); +``` +**生成的 SQL**: +```sql +SELECT * FROM vehicle WHERE id = 1 AND is_delete = 0 +``` + +### 2. DELETE 操作(逻辑删除) +```java +vehicleMapper.deleteById(1); +``` +**生成的 SQL**: +```sql +UPDATE vehicle SET is_delete = 1 WHERE id = 1 +``` + +### 3. UPDATE 操作(注意!) +```java +vehicle.setIsDelete(1); +vehicleMapper.updateById(vehicle); +``` +**生成的 SQL**: +```sql +-- ❌ is_delete 不在 SET 子句中! +UPDATE vehicle +SET license_plate=?, car_front_photo=?, ..., update_time=? +WHERE id=? AND is_delete=0 +``` + +**结论**: +- ✅ **删除时用 `deleteById()`**,它会自动设置 `is_delete = 1` +- ❌ **不要用 `updateById()`** 来修改 `is_delete` 字段 + +## 修复前后对比 + +| 对比项 | 修复前(错误) | 修复后(正确) | +|--------|----------------|----------------| +| 方法调用 | `updateById(vehicle)` | `deleteById(id)` | +| 生成的 SQL | `UPDATE SET 所有字段...` | `UPDATE SET is_delete=1` | +| is_delete 是否更新 | ❌ 否(被排除) | ✅ 是 | +| 其他字段是否更新 | ✅ 是(不必要) | ❌ 否(只更新必要字段) | +| 性能 | 差(更新所有字段) | 好(只更新1个字段) | +| 逻辑删除是否生效 | ❌ 否 | ✅ 是 | + +## 为什么之前的方案失败 + +### 尝试1:使用 updateById +```java +vehicle.setIsDelete(1); +vehicleMapper.updateById(vehicle); +``` +**失败原因**:`@TableLogic` 会排除 `is_delete` 字段 + +### 尝试2:使用 LambdaUpdateWrapper +```java +LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); +wrapper.set(Vehicle::getIsDelete, 1); +vehicleMapper.update(null, wrapper); +``` +**问题**:代码复杂,而且可能与 `@TableLogic` 冲突 + +### 最终方案:直接使用 deleteById +```java +vehicleMapper.deleteById(id); +``` +**优势**: +- ✅ 代码简洁 +- ✅ 利用 MyBatis-Plus 的特性 +- ✅ 自动处理逻辑删除 +- ✅ 与 `@TableLogic` 完美配合 + +## 测试步骤 + +### 1. 重启后端服务 +确保新的代码生效。 + +### 2. 清除浏览器缓存 +强制刷新前端页面(Ctrl+F5)。 + +### 3. 测试删除功能 +1. 进入"车辆管理"页面 +2. 选择要删除的车辆,例如:`鄂A 66662` +3. 点击"删除"按钮 +4. 确认删除 + +### 4. 观察后端日志 +**修复后应该看到**: +``` +[VEHICLE-DELETE] 开始逻辑删除车辆,ID: 3 +[VEHICLE-DELETE] 车辆信息 - 车牌号: 鄂A 66662, 当前 is_delete: 0 +UPDATE vehicle SET is_delete=1 WHERE id=3 +[VEHICLE-DELETE] ✅ 逻辑删除成功,车牌号: 鄂A 66662, 操作人ID: 11 +``` + +**关键点**: +- ✅ SQL 中只有 `SET is_delete=1` +- ✅ 没有更新其他字段 +- ✅ 日志显示"逻辑删除成功" + +### 5. 验证前端列表 +- ✅ 列表自动刷新 +- ✅ 已删除的车辆不再显示 +- ✅ 显示"删除成功"提示 + +### 6. 验证数据库 +```sql +-- 查看该记录 +SELECT id, license_plate, is_delete, update_time +FROM vehicle +WHERE id = 3; + +-- 预期结果: +-- id | license_plate | is_delete | update_time +-- 3 | 鄂A 66662 | 1 | 2025-10-29 17:00:00 +``` + +```sql +-- 前端列表查询(不应包含该记录) +SELECT * FROM vehicle WHERE is_delete = 0; +``` + +## 常见问题排查 + +### Q1: 删除后前端列表还显示该记录? +**检查**: +1. 后端服务是否重启 +2. 浏览器缓存是否清除(Ctrl+F5) +3. 后端日志中 SQL 是否为 `UPDATE SET is_delete=1` +4. 数据库中该记录的 `is_delete` 是否为 `1` + +### Q2: 后端日志显示 SQL 中没有 is_delete 字段? +**原因**: 代码没有更新,还在使用 `updateById()` + +**解决**: +1. 确认代码已修改为 `deleteById()` +2. 重新编译:`mvn clean compile` +3. 重启后端服务 + +### Q3: 删除后报错 "车辆不存在"? +**原因**: 记录已经被删除(`is_delete = 1`),`selectById` 自动过滤了它 + +**解决**: 正常现象,说明删除成功。前端应该已经刷新列表。 + +### Q4: 如何查看所有已删除的记录? +**SQL**: +```sql +SELECT * FROM vehicle WHERE is_delete = 1; +``` + +### Q5: 如何恢复已删除的记录? +**SQL**: +```sql +UPDATE vehicle +SET is_delete = 0, + update_time = NOW() +WHERE id = 3; +``` + +## MyBatis-Plus @TableLogic 最佳实践 + +### ✅ 推荐做法 + +1. **删除操作**:使用 `deleteById()` 或 `deleteBatchIds()` + ```java + vehicleMapper.deleteById(id); // 逻辑删除 + ``` + +2. **查询操作**:正常使用,自动过滤已删除记录 + ```java + vehicleMapper.selectById(id); // 自动添加 is_delete=0 + ``` + +3. **更新操作**:不要尝试修改 `is_delete` 字段 + ```java + vehicle.setLicensePlate("新车牌"); + vehicleMapper.updateById(vehicle); // is_delete 不会被更新 + ``` + +### ❌ 避免的做法 + +1. **不要用 updateById 修改 is_delete** + ```java + // ❌ 错误:不会生效 + vehicle.setIsDelete(1); + vehicleMapper.updateById(vehicle); + ``` + +2. **不要在 WHERE 条件中手动添加 is_delete** + ```java + // ❌ 多余:@TableLogic 会自动添加 + queryWrapper.eq(Vehicle::getIsDelete, 0); + ``` + +3. **不要尝试物理删除** + ```java + // ❌ 这仍然是逻辑删除,不是物理删除 + vehicleMapper.deleteById(id); + ``` + +## 总结 + +✅ **最终修复方案**: +- 使用 `vehicleMapper.deleteById(id)` 执行逻辑删除 +- 让 MyBatis-Plus 的 `@TableLogic` 自动处理 `is_delete` 字段 +- 代码简洁,性能优秀,符合框架设计理念 + +✅ **核心原理**: +- `@TableLogic` 会在 DELETE 操作时自动转换为 UPDATE +- 不要用 UPDATE 操作来修改逻辑删除标记 +- 利用框架特性,而不是绕过它 + +✅ **用户体验**: +- 删除成功后列表自动刷新 +- 已删除的记录立即消失 +- 操作简单,性能优秀 + +🎯 **关键教训**: +当框架提供了特定功能(如 `@TableLogic`)时,应该按照框架的设计意图使用,而不是尝试绕过或覆盖它。`deleteById()` 是正确的删除方式,`updateById()` 不适合修改逻辑删除标记。 + diff --git a/VEHICLE_LOGICAL_DELETE.md b/VEHICLE_LOGICAL_DELETE.md new file mode 100644 index 0000000..bc5a45f --- /dev/null +++ b/VEHICLE_LOGICAL_DELETE.md @@ -0,0 +1,391 @@ +# 车辆管理 - 逻辑删除功能说明 + +## 功能概述 +车辆管理已实现**逻辑删除**(软删除)功能,删除操作不会真正删除数据库中的记录,而是将 `is_delete` 字段标记为 `1`,保留历史数据用于审计和追溯。 + +## 逻辑删除 vs 物理删除 + +### 逻辑删除(Soft Delete)✅ 当前实现 +- **操作**: 将 `is_delete` 字段设置为 `1` +- **优点**: + - 数据可恢复 + - 保留历史记录 + - 符合审计要求 + - 避免数据丢失 +- **缺点**: + - 数据库空间占用较大 + - 查询时需要过滤已删除记录 + +### 物理删除(Hard Delete)❌ 已弃用 +- **操作**: 直接从数据库中删除记录 +- **优点**: + - 节省数据库空间 + - 查询速度快 +- **缺点**: + - 数据无法恢复 + - 丢失历史记录 + - 不符合审计要求 + +## 数据库表结构 + +### vehicle 表(车辆表) +```sql +CREATE TABLE `vehicle` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `license_plate` varchar(20) NOT NULL COMMENT '车牌号', + `car_front_photo` varchar(500) DEFAULT NULL COMMENT '车头照片', + `car_rear_photo` varchar(500) DEFAULT NULL COMMENT '车尾照片', + `driving_license_photo` varchar(500) DEFAULT NULL COMMENT '行驶证照片', + `record_code` varchar(500) DEFAULT NULL COMMENT '牧运通备案码照片', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `is_delete` tinyint(1) DEFAULT 0 COMMENT '是否删除(0-未删除,1-已删除)', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` int(11) DEFAULT NULL COMMENT '创建人ID', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `updated_by` int(11) DEFAULT NULL COMMENT '更新人ID', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_license_plate` (`license_plate`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆管理表'; +``` + +### 关键字段 +- `is_delete`: 删除标记 + - `0` 或 `NULL`: 未删除(正常状态) + - `1`: 已删除(逻辑删除) + +## 后端实现 + +### 1. VehicleServiceImpl.java - 逻辑删除方法 + +```java +/** + * 删除车辆(逻辑删除,只标记为已删除,不真正删除数据) + */ +@Override +@Transactional +public AjaxResult deleteVehicle(Integer id) { + System.out.println("[VEHICLE-DELETE] 开始逻辑删除车辆,ID: " + id); + + if (id == null) { + System.out.println("[VEHICLE-DELETE] 删除失败:车辆ID为空"); + return AjaxResult.error("车辆ID不能为空"); + } + + // 查询车辆是否存在 + Vehicle vehicle = vehicleMapper.selectById(id); + if (vehicle == null) { + System.out.println("[VEHICLE-DELETE] 删除失败:车辆不存在,ID: " + id); + return AjaxResult.error("车辆不存在"); + } + + // ✅ 检查是否已经被删除(避免重复删除) + if (vehicle.getIsDelete() != null && vehicle.getIsDelete() == 1) { + System.out.println("[VEHICLE-DELETE] 删除失败:车辆已经被删除,车牌号: " + vehicle.getLicensePlate()); + return AjaxResult.error("车辆已经被删除"); + } + + System.out.println("[VEHICLE-DELETE] 车辆信息 - 车牌号: " + vehicle.getLicensePlate()); + + // ✅ 逻辑删除:将 is_delete 设置为 1 + vehicle.setIsDelete(1); + + // ✅ 记录删除操作者和时间 + Integer userId = SecurityUtil.getCurrentUserId(); + vehicle.setUpdatedBy(userId); + vehicle.setUpdateTime(new Date()); + + int result = vehicleMapper.updateById(vehicle); + + if (result > 0) { + System.out.println("[VEHICLE-DELETE] ✅ 逻辑删除成功,车牌号: " + vehicle.getLicensePlate() + ", 操作人ID: " + userId); + return AjaxResult.success("删除车辆成功"); + } else { + System.out.println("[VEHICLE-DELETE] ❌ 逻辑删除失败,车牌号: " + vehicle.getLicensePlate()); + return AjaxResult.error("删除车辆失败"); + } +} +``` + +### 2. VehicleServiceImpl.java - 查询时过滤已删除记录 + +```java +/** + * 分页查询车辆列表(只查询未删除的记录) + */ +@Override +public PageResultResponse pageQuery(Map params) { + // ... 省略其他代码 ... + + // 构建查询条件 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + // ✅ 只查询未删除的记录(is_delete=0 或 is_delete 为 null) + queryWrapper.and(wrapper -> wrapper.eq(Vehicle::getIsDelete, 0).or().isNull(Vehicle::getIsDelete)); + + queryWrapper.like(licensePlate != null && !licensePlate.isEmpty(), Vehicle::getLicensePlate, licensePlate); + queryWrapper.orderByDesc(Vehicle::getCreateTime); + + // 执行查询 + List list = vehicleMapper.selectList(queryWrapper); + + // ... 省略其他代码 ... +} +``` + +### 3. VehicleMapper.java - 自定义查询过滤已删除记录 + +```java +/** + * 根据车牌号查询车辆(唯一性校验) + * ✅ 只查询未删除的记录 + */ +@Select("SELECT * FROM vehicle WHERE license_plate = #{licensePlate} AND is_delete = 0") +Vehicle selectByLicensePlate(@Param("licensePlate") String licensePlate); + +/** + * 根据车牌号模糊查询车辆列表 + * ✅ 只查询未删除的记录 + */ +@Select("") +List selectVehicleList(@Param("licensePlate") String licensePlate); +``` + +## 前端实现 + +### vehicle.vue - 删除按钮 + +```vue + + + +``` + +```javascript +const delClick = (row) => { + ElMessageBox.confirm('请确认是否删除该车辆数据?', '删除', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + }) + .then(() => { + vehicleDel(row.id) + .then(() => { + ElMessage.success('删除成功'); + getDataList(); // 刷新列表 + }) + .catch((error) => { + ElMessage.error('删除失败'); + console.error('删除失败:', error); + }); + }) + .catch(() => {}); +}; +``` + +## 操作流程 + +### 删除车辆流程 + +1. **用户点击"删除"按钮** + - 前端弹出确认对话框:`请确认是否删除该车辆数据?` + +2. **用户确认删除** + - 前端调用 `vehicleDel(id)` API + - 请求: `GET /vehicle/delete?id=123` + +3. **后端逻辑删除** + ```sql + UPDATE vehicle + SET is_delete = 1, + updated_by = {当前用户ID}, + update_time = NOW() + WHERE id = 123 + ``` + +4. **前端刷新列表** + - 调用 `getDataList()` 重新加载列表 + - 已删除的车辆不再显示(被 `is_delete = 0` 过滤掉) + +### 后端日志输出示例 + +``` +[VEHICLE-DELETE] 开始逻辑删除车辆,ID: 1 +[VEHICLE-DELETE] 车辆信息 - 车牌号: 鄂A 66662 +[VEHICLE-DELETE] ✅ 逻辑删除成功,车牌号: 鄂A 66662, 操作人ID: 11 +``` + +## 删除后的数据状态 + +### 删除前 +```sql +SELECT * FROM vehicle WHERE id = 1; +``` +| id | license_plate | is_delete | created_by | updated_by | update_time | +|----|---------------|-----------|------------|------------|-------------| +| 1 | 鄂A 66662 | 0 | 11 | NULL | NULL | + +### 删除后 +```sql +SELECT * FROM vehicle WHERE id = 1; +``` +| id | license_plate | is_delete | created_by | updated_by | update_time | +|----|---------------|-----------|------------|------------|----------------------| +| 1 | 鄂A 66662 | **1** | 11 | **11** | **2025-10-29 16:50** | + +### 前端查询(已过滤) +```sql +-- 前端列表查询会自动过滤 is_delete = 1 的记录 +SELECT * FROM vehicle WHERE is_delete = 0; +``` +结果:**不包含** ID=1 的记录(已被逻辑删除) + +## 关键改进点 + +### ✅ 改进1: 查询时过滤已删除记录 +```java +// 使用 LambdaQueryWrapper 过滤 +queryWrapper.and(wrapper -> wrapper.eq(Vehicle::getIsDelete, 0).or().isNull(Vehicle::getIsDelete)); +``` + +**原因**: +- 兼容 `is_delete` 为 `0` 和 `NULL` 的情况 +- 确保列表中不显示已删除的记录 + +### ✅ 改进2: 防止重复删除 +```java +if (vehicle.getIsDelete() != null && vehicle.getIsDelete() == 1) { + return AjaxResult.error("车辆已经被删除"); +} +``` + +**原因**: +- 避免对已删除的记录再次执行删除操作 +- 提供明确的错误提示 + +### ✅ 改进3: 记录删除操作者和时间 +```java +vehicle.setIsDelete(1); +vehicle.setUpdatedBy(userId); +vehicle.setUpdateTime(new Date()); +``` + +**原因**: +- 记录谁在什么时间删除了这条数据 +- 符合审计要求 + +### ✅ 改进4: 详细的日志输出 +```java +System.out.println("[VEHICLE-DELETE] 开始逻辑删除车辆,ID: " + id); +System.out.println("[VEHICLE-DELETE] ✅ 逻辑删除成功,车牌号: " + vehicle.getLicensePlate()); +``` + +**原因**: +- 便于排查问题 +- 追踪删除操作 + +## 数据恢复 + +如果需要恢复已删除的车辆数据,可以执行以下 SQL: + +```sql +-- 恢复单条记录 +UPDATE vehicle +SET is_delete = 0, + updated_by = {恢复操作人ID}, + update_time = NOW() +WHERE id = 1; + +-- 批量恢复 +UPDATE vehicle +SET is_delete = 0, + updated_by = {恢复操作人ID}, + update_time = NOW() +WHERE is_delete = 1 + AND license_plate IN ('鄂A 66662', '京B 12345'); +``` + +## 测试步骤 + +### 1. 重启后端服务 +确保新的逻辑删除代码生效。 + +### 2. 测试删除功能 +1. 进入"车辆管理"页面 +2. 点击任意车辆的"删除"按钮 +3. 确认删除对话框 +4. 观察后端日志: + ``` + [VEHICLE-DELETE] 开始逻辑删除车辆,ID: 1 + [VEHICLE-DELETE] 车辆信息 - 车牌号: 鄂A 66662 + [VEHICLE-DELETE] ✅ 逻辑删除成功,车牌号: 鄂A 66662, 操作人ID: 11 + ``` +5. 刷新页面,确认该车辆不再显示 + +### 3. 验证数据库 +```sql +-- 查看所有记录(包括已删除) +SELECT id, license_plate, is_delete, updated_by, update_time +FROM vehicle +ORDER BY update_time DESC; + +-- 查看未删除的记录 +SELECT id, license_plate +FROM vehicle +WHERE is_delete = 0; + +-- 查看已删除的记录 +SELECT id, license_plate, updated_by, update_time +FROM vehicle +WHERE is_delete = 1; +``` + +### 4. 测试重复删除 +1. 尝试再次删除同一车辆(通过直接调用 API) +2. 应该返回错误:`车辆已经被删除` + +## 常见问题 + +### Q1: 删除后为什么还能在数据库中看到? +**答**: 这是逻辑删除,数据不会真正删除,只是标记为 `is_delete = 1`。前端列表查询时会自动过滤掉这些记录。 + +### Q2: 如何查看所有已删除的车辆? +**答**: 执行 SQL: +```sql +SELECT * FROM vehicle WHERE is_delete = 1; +``` + +### Q3: 删除的数据可以恢复吗? +**答**: 可以,只需要将 `is_delete` 改回 `0` 即可。参考"数据恢复"章节。 + +### Q4: 为什么要记录 updated_by 和 update_time? +**答**: 用于审计,记录谁在什么时间删除了这条数据。 + +## 总结 + +✅ **逻辑删除的优势**: +- 数据安全,可恢复 +- 符合审计要求 +- 保留历史记录 +- 避免数据丢失 + +✅ **实现完整性**: +- 删除操作:标记 `is_delete = 1` +- 查询操作:过滤 `is_delete = 0` +- 唯一性校验:过滤 `is_delete = 0` +- 审计记录:记录操作者和时间 + +✅ **用户体验**: +- 删除确认对话框 +- 成功/失败提示 +- 自动刷新列表 +- 防止重复删除 + diff --git a/VEHICLE_SEARCH_FIX.md b/VEHICLE_SEARCH_FIX.md new file mode 100644 index 0000000..ec19dd8 --- /dev/null +++ b/VEHICLE_SEARCH_FIX.md @@ -0,0 +1,246 @@ +# 车辆管理 - 车牌号搜索功能修复 + +## 问题描述 +用户反馈:**车辆管理页面的车牌号搜索功能不起作用**。 + +## 问题根源 + +### 错误的实现方式 +在 `vehicle.vue` 中,`getDataList()` 方法**没有从 `baseSearch` 组件获取搜索参数**: + +```javascript +// ❌ 错误:只使用 form 中的固定字段 +const form = reactive({ + pageNum: 1, + pageSize: 10, + licensePlate: '', // 这个字段永远是空字符串! +}); + +const getDataList = async () => { + const params = { + pageNum: form.pageNum, + pageSize: form.pageSize, + licensePlate: form.licensePlate, // ❌ 永远传空字符串 + }; + console.log('查询参数:', params); + const res = await vehicleList(params); + // ... +}; +``` + +### 问题分析 +1. **`form.licensePlate` 字段从未更新** + - 初始化为空字符串 + - 用户在搜索框输入车牌号时,这个值不会改变 + +2. **没有使用 `baseSearchRef.value.penetrateParams()`** + - `baseSearch` 组件通过 `penetrateParams()` 方法返回用户输入的搜索参数 + - 其他页面(如 `driver.vue`, `attestation.vue`)都正确使用了这个方法 + - 但 `vehicle.vue` 没有调用它 + +3. **后端日志确认** + ``` + === 查询车辆列表 === + 参数: {pageNum=1, pageSize=10, licensePlate=} + ``` + 可以看到 `licensePlate` 确实是空的 + +## 解决方案 + +### 修改前后对比 + +#### 修改前(错误) +```javascript +const form = reactive({ + pageNum: 1, + pageSize: 10, + licensePlate: '', // ❌ 多余的字段 +}); + +const getDataList = async () => { + data.dataListLoading = true; + try { + const params = { + pageNum: form.pageNum, + pageSize: form.pageSize, + licensePlate: form.licensePlate, // ❌ 永远是空字符串 + }; + console.log('查询参数:', params); + // ... + } +}; +``` + +#### 修改后(正确) +```javascript +const form = reactive({ + pageNum: 1, + pageSize: 10, + // ✅ 移除 licensePlate 字段,改用 baseSearchRef 获取 +}); + +const getDataList = async () => { + data.dataListLoading = true; + try { + // ✅ 使用 baseSearchRef 获取搜索参数 + const params = { + ...form, + ...baseSearchRef.value.penetrateParams(), + }; + console.log('[VEHICLE-SEARCH] 查询参数:', params); + // ... + } +}; +``` + +### 关键改动点 + +1. **移除 `form.licensePlate` 字段** + - 不再在 `form` 中定义 `licensePlate` + - 避免与 `baseSearch` 组件的状态冲突 + +2. **使用 `baseSearchRef.value.penetrateParams()`** + - 这个方法会返回 `baseSearch` 组件中用户输入的所有搜索参数 + - 返回格式:`{ licensePlate: '用户输入的车牌号' }` + +3. **使用扩展运算符合并参数** + ```javascript + const params = { + ...form, // { pageNum: 1, pageSize: 10 } + ...baseSearchRef.value.penetrateParams(), // { licensePlate: '鄂A 66662' } + }; + // 结果: { pageNum: 1, pageSize: 10, licensePlate: '鄂A 66662' } + ``` + +## 参考其他页面的正确实现 + +### driver.vue(司机管理) +```javascript +const form = reactive({ + pageNum: 1, + pageSize: 10, +}); + +const getDataList = () => { + data.dataListLoading = true; + const params = { + ...form, + ...baseSearchRef.value.penetrateParams(), // ✅ 正确用法 + }; + // ... +}; +``` + +### attestation.vue(入境检疫) +```javascript +const searchForm = reactive({ + pageNum: 1, + pageSize: 10, +}); + +const getDataList = () => { + const params = { + ...searchForm, + ...baseSearchRef.value.penetrateParams(), // ✅ 正确用法 + }; + // ... +}; +``` + +## baseSearch 组件工作原理 + +### 组件定义 +```vue + + + +``` + +### formItemList 配置 +```javascript +const formItemList = reactive([ + { + label: '车牌号', + param: 'licensePlate', // ← 参数名 + type: 'input', + placeholder: '请输入车牌号', + span: 7, + labelWidth: 100, + }, +]); +``` + +### penetrateParams() 返回值 +当用户在搜索框输入 "鄂A 66662" 并点击搜索时: +```javascript +baseSearchRef.value.penetrateParams() +// 返回: { licensePlate: '鄂A 66662' } +``` + +## 测试步骤 + +### 1. 刷新浏览器 +清除前端缓存,重新加载 `vehicle.vue` 组件。 + +### 2. 测试搜索功能 +1. 进入"车辆管理"页面 +2. 在车牌号搜索框输入:`鄂A 66662` +3. 点击"搜索"按钮 + +### 3. 查看控制台日志 +``` +[VEHICLE-SEARCH] 查询参数: { pageNum: 1, pageSize: 10, licensePlate: '鄂A 66662' } +``` + +### 4. 查看后端日志 +``` +=== 查询车辆列表 === +参数: {pageNum=1, pageSize=10, licensePlate=鄂A 66662} +``` + +### 5. 验证搜索结果 +- ✅ 表格应该只显示车牌号为 "鄂A 66662" 的车辆 +- ✅ 如果没有匹配结果,显示"暂无数据" + +## 常见问题排查 + +### Q1: 搜索框输入后点击搜索,表格没有变化? +**检查**: +1. 浏览器控制台是否有报错 +2. 控制台日志中 `licensePlate` 的值是否正确 +3. `baseSearchRef.value` 是否为 `null`(组件未正确引用) + +### Q2: 后端收到的 licensePlate 还是空? +**可能原因**: +1. 前端代码没有正确保存(检查文件是否保存) +2. 浏览器缓存未清除(强制刷新:Ctrl+F5) +3. `baseSearch` 组件版本过旧(检查组件定义) + +### Q3: 搜索时报错 "Cannot read property 'penetrateParams' of undefined"? +**原因**: `baseSearchRef.value` 为空 + +**解决**: +1. 确认组件引用正确: + ```vue + + ``` +2. 确认 ref 声明: + ```javascript + const baseSearchRef = ref(); + ``` + +## 总结 + +✅ **问题已修复**: +- 使用 `baseSearchRef.value.penetrateParams()` 获取搜索参数 +- 移除 `form.licensePlate` 避免状态冲突 +- 增强日志便于排查问题 + +✅ **用户体验改善**: +- 车牌号搜索功能正常工作 +- 支持模糊搜索(后端已实现 LIKE 查询) +- 搜索结果实时更新 + diff --git a/pc-cattle-transportation/src/api/shipping.js b/pc-cattle-transportation/src/api/shipping.js index a06e56b..6a255dc 100644 --- a/pc-cattle-transportation/src/api/shipping.js +++ b/pc-cattle-transportation/src/api/shipping.js @@ -263,4 +263,40 @@ export function orderGetDetail(id) { url: `/order/detail?id=${id}`, method: 'GET', }); +} + +// ==================== 运送清单管理新增 API ==================== + +// 逻辑删除运送清单 +export function deleteDeliveryLogic(id) { + return request({ + url: `/delivery/deleteLogic?id=${id}`, + method: 'POST', + }); +} + +// 打包运送清单文件(图片、视频、信息) +export function downloadDeliveryPackage(id) { + return request({ + url: `/delivery/downloadPackage?id=${id}`, + method: 'GET', + responseType: 'blob', // 重要:用于下载文件 + }); +} + +// 获取运送清单详情(用于编辑) +export function getDeliveryDetail(id) { + return request({ + url: `/delivery/detail?id=${id}`, + method: 'GET', + }); +} + +// 更新运送清单信息 +export function updateDeliveryInfo(data) { + return request({ + url: '/delivery/updateDeliveryInfo', + method: 'POST', + data, + }); } \ No newline at end of file diff --git a/pc-cattle-transportation/src/router/index.ts b/pc-cattle-transportation/src/router/index.ts index 1f04a4e..541bdb2 100644 --- a/pc-cattle-transportation/src/router/index.ts +++ b/pc-cattle-transportation/src/router/index.ts @@ -26,7 +26,7 @@ export const constantRoutes: Array = [ }, children: [ { - path: '/entry/details', + path: 'details', // ✅ 修复:使用相对路径 name: 'details', meta: { title: '详情', @@ -35,6 +35,16 @@ export const constantRoutes: Array = [ }, component: () => import('~/views/entry/details.vue'), }, + { + path: 'devices', // ✅ 修复:使用相对路径 + name: 'devices', + meta: { + title: '设备管理', + keepAlive: true, + requireAuth: true, + }, + component: () => import('~/views/entry/devices.vue'), + }, ], }, // 系统管理路由 @@ -48,7 +58,7 @@ export const constantRoutes: Array = [ }, children: [ { - path: '/system/post', + path: 'post', // ✅ 修复:使用相对路径 name: 'Post', meta: { title: '岗位管理', @@ -58,7 +68,7 @@ export const constantRoutes: Array = [ component: () => import('~/views/system/post.vue'), }, { - path: '/system/staff', + path: 'staff', // ✅ 修复:使用相对路径 name: 'Staff', meta: { title: '员工管理', @@ -68,7 +78,7 @@ export const constantRoutes: Array = [ component: () => import('~/views/system/staff.vue'), }, { - path: '/system/tenant', + path: 'tenant', // ✅ 修复:使用相对路径 name: 'Tenant', meta: { title: '租户管理', @@ -90,7 +100,7 @@ export const constantRoutes: Array = [ }, children: [ { - path: '/permission/menu', + path: 'menu', // ✅ 修复:使用相对路径 name: 'MenuPermission', meta: { title: '菜单权限管理', @@ -100,7 +110,7 @@ export const constantRoutes: Array = [ component: () => import('~/views/permission/menuPermission.vue'), }, { - path: '/permission/operation', + path: 'operation', // ✅ 修复:使用相对路径 name: 'OperationPermission', meta: { title: '操作权限管理', @@ -122,7 +132,7 @@ export const constantRoutes: Array = [ }, children: [ { - path: '/hardware/collar', + path: 'collar', // ✅ 修复:使用相对路径 name: 'Collar', meta: { title: '智能项圈', @@ -132,7 +142,7 @@ export const constantRoutes: Array = [ component: () => import('~/views/hardware/collar.vue'), }, { - path: '/hardware/eartag', + path: 'eartag', // ✅ 修复:使用相对路径 name: 'Eartag', meta: { title: '智能耳标', @@ -142,7 +152,7 @@ export const constantRoutes: Array = [ component: () => import('~/views/hardware/eartag.vue'), }, { - path: '/hardware/host', + path: 'host', // ✅ 修复:使用相对路径 name: 'Host', meta: { title: '智能主机', @@ -164,7 +174,7 @@ export const constantRoutes: Array = [ }, children: [ { - path: '/userManage/user', + path: 'user', // ✅ 修复:使用相对路径 name: 'UserManage', meta: { title: '用户管理', @@ -174,7 +184,7 @@ export const constantRoutes: Array = [ component: () => import('~/views/userManage/user.vue'), }, { - path: '/userManage/driver', + path: 'driver', // ✅ 修复:使用相对路径 name: 'DriverManage', meta: { title: '司机管理', @@ -184,7 +194,7 @@ export const constantRoutes: Array = [ component: () => import('~/views/userManage/driver.vue'), }, { - path: '/userManage/vehicle', + path: 'vehicle', // ✅ 修复:使用相对路径 name: 'VehicleManage', meta: { title: '车辆管理', @@ -206,7 +216,7 @@ export const constantRoutes: Array = [ }, children: [ { - path: '/earlywarning/earlywarninglist', + path: 'earlywarninglist', // ✅ 修复:使用相对路径 name: 'EarlyWarningList', meta: { title: '早期预警列表', @@ -228,7 +238,7 @@ export const constantRoutes: Array = [ }, children: [ { - path: '/shipping/loadingOrder', + path: 'loadingOrder', // ✅ 修复:使用相对路径 name: 'LoadingOrder', meta: { title: '装车订单', @@ -240,7 +250,7 @@ export const constantRoutes: Array = [ { - path: '/shipping/shippinglist', + path: 'shippinglist', // ✅ 修复:使用相对路径 name: 'ShippingList', meta: { title: '运送清单', diff --git a/pc-cattle-transportation/src/views/entry/attestation.vue b/pc-cattle-transportation/src/views/entry/attestation.vue index e5f6eab..361e678 100644 --- a/pc-cattle-transportation/src/views/entry/attestation.vue +++ b/pc-cattle-transportation/src/views/entry/attestation.vue @@ -78,20 +78,15 @@ - + @@ -101,18 +96,25 @@ + + + + + + diff --git a/pc-cattle-transportation/src/views/shipping/createDeliveryDialog.vue b/pc-cattle-transportation/src/views/shipping/createDeliveryDialog.vue index 62a33c4..98817cb 100644 --- a/pc-cattle-transportation/src/views/shipping/createDeliveryDialog.vue +++ b/pc-cattle-transportation/src/views/shipping/createDeliveryDialog.vue @@ -1,7 +1,7 @@