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

12 KiB
Raw Blame History

车辆管理 - 逻辑删除功能说明

功能概述

车辆管理已实现逻辑删除(软删除)功能,删除操作不会真正删除数据库中的记录,而是将 is_delete 字段标记为 1,保留历史数据用于审计和追溯。

逻辑删除 vs 物理删除

逻辑删除Soft Delete 当前实现

  • 操作: 将 is_delete 字段设置为 1
  • 优点:
    • 数据可恢复
    • 保留历史记录
    • 符合审计要求
    • 避免数据丢失
  • 缺点:
    • 数据库空间占用较大
    • 查询时需要过滤已删除记录

物理删除Hard Delete 已弃用

  • 操作: 直接从数据库中删除记录
  • 优点:
    • 节省数据库空间
    • 查询速度快
  • 缺点:
    • 数据无法恢复
    • 丢失历史记录
    • 不符合审计要求

数据库表结构

vehicle 表(车辆表)

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: 删除标记
    • 0NULL: 未删除(正常状态)
    • 1: 已删除(逻辑删除)

后端实现

1. VehicleServiceImpl.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 - 查询时过滤已删除记录

/**
 * 分页查询车辆列表(只查询未删除的记录)
 */
@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 - 自定义查询过滤已删除记录

/**
 * 根据车牌号查询车辆(唯一性校验)
 * ✅ 只查询未删除的记录
 */
@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 - 删除按钮

<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>
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. 后端逻辑删除

    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

删除后的数据状态

删除前

SELECT * FROM vehicle WHERE id = 1;
id license_plate is_delete created_by updated_by update_time
1 鄂A 66662 0 11 NULL NULL

删除后

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

前端查询(已过滤)

-- 前端列表查询会自动过滤 is_delete = 1 的记录
SELECT * FROM vehicle WHERE is_delete = 0;

结果:不包含 ID=1 的记录(已被逻辑删除)

关键改进点

改进1: 查询时过滤已删除记录

// 使用 LambdaQueryWrapper 过滤
queryWrapper.and(wrapper -> wrapper.eq(Vehicle::getIsDelete, 0).or().isNull(Vehicle::getIsDelete));

原因

  • 兼容 is_delete0NULL 的情况
  • 确保列表中不显示已删除的记录

改进2: 防止重复删除

if (vehicle.getIsDelete() != null && vehicle.getIsDelete() == 1) {
    return AjaxResult.error("车辆已经被删除");
}

原因

  • 避免对已删除的记录再次执行删除操作
  • 提供明确的错误提示

改进3: 记录删除操作者和时间

vehicle.setIsDelete(1);
vehicle.setUpdatedBy(userId);
vehicle.setUpdateTime(new Date());

原因

  • 记录谁在什么时间删除了这条数据
  • 符合审计要求

改进4: 详细的日志输出

System.out.println("[VEHICLE-DELETE] 开始逻辑删除车辆ID: " + id);
System.out.println("[VEHICLE-DELETE] ✅ 逻辑删除成功,车牌号: " + vehicle.getLicensePlate());

原因

  • 便于排查问题
  • 追踪删除操作

数据恢复

如果需要恢复已删除的车辆数据,可以执行以下 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. 验证数据库

-- 查看所有记录(包括已删除)
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:

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
  • 审计记录:记录操作者和时间

用户体验:

  • 删除确认对话框
  • 成功/失败提示
  • 自动刷新列表
  • 防止重复删除