Files
cattleTransportation/VEHICLE_LOGICAL_DELETE.md
2025-10-29 17:33:32 +08:00

392 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 车辆管理 - 逻辑删除功能说明
## 功能概述
车辆管理已实现**逻辑删除**(软删除)功能,删除操作不会真正删除数据库中的记录,而是将 `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<Vehicle> pageQuery(Map<String, Object> params) {
// ... 省略其他代码 ...
// 构建查询条件
LambdaQueryWrapper<Vehicle> 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<Vehicle> 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("<script>" +
"SELECT * FROM vehicle WHERE is_delete = 0 " +
"<if test='licensePlate != null and licensePlate != \"\"'>" +
"AND license_plate LIKE CONCAT('%', #{licensePlate}, '%') " +
"</if>" +
"ORDER BY create_time DESC " +
"</script>")
List<Vehicle> selectVehicleList(@Param("licensePlate") String licensePlate);
```
## 前端实现
### vehicle.vue - 删除按钮
```vue
<el-table-column label="操作" width="160">
<template #default="scope">
<el-button link type="primary" @click="showAddDialog(scope.row)">编辑</el-button>
<el-button link type="danger" @click="delClick(scope.row)">删除</el-button>
</template>
</el-table-column>
```
```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`
- 审计记录:记录操作者和时间
**用户体验**:
- 删除确认对话框
- 成功/失败提示
- 自动刷新列表
- 防止重复删除