392 lines
12 KiB
Markdown
392 lines
12 KiB
Markdown
|
|
# 车辆管理 - 逻辑删除功能说明
|
|||
|
|
|
|||
|
|
## 功能概述
|
|||
|
|
车辆管理已实现**逻辑删除**(软删除)功能,删除操作不会真正删除数据库中的记录,而是将 `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`
|
|||
|
|
- 审计记录:记录操作者和时间
|
|||
|
|
|
|||
|
|
✅ **用户体验**:
|
|||
|
|
- 删除确认对话框
|
|||
|
|
- 成功/失败提示
|
|||
|
|
- 自动刷新列表
|
|||
|
|
- 防止重复删除
|
|||
|
|
|