MyBatisPlus逻辑删除:保留用户已删除的修复任务历史记录
在构建一个面向用户的图像修复系统时,我们常常面临这样一个矛盾:用户希望“删除”某个任务来清理界面,但系统又必须保留这些操作的历史痕迹,以便后续审计、分析或恢复。尤其是在像老照片智能修复这类涉及数字资产留存的场景中,一次误删可能意味着永久丢失珍贵的处理记录。
这正是逻辑删除大显身手的地方。
以DDColor 黑白老照片智能修复系统为例,每当用户上传一张黑白照片并启动修复流程,系统都会生成一条修复任务记录。无论这张照片是祖辈的合影,还是早已消失的老建筑影像,其修复过程本身就是一段值得保存的数字足迹。即便用户后来点击了“删除”,我们也绝不能真正将其从数据库中抹去——而 MyBatisPlus 的逻辑删除机制,恰好为我们提供了一种优雅且高效的解决方案。
为什么选择逻辑删除?
物理删除就像把文件扔进碎纸机,再也拼不回来;而逻辑删除更像是给文件贴上“已归档”标签,它依然存在,只是不再出现在常规视野中。这种模式在现代应用开发中已成为标配,尤其适用于需要数据追溯、合规审计或防误操作的系统。
MyBatisPlus 对逻辑删除的支持并非简单的字段标记,而是一整套贯穿 CRUD 操作的自动化拦截机制。你不需要手动拼接WHERE deleted = 0,也不必重写删除方法,只需通过注解和配置,框架就会自动完成以下转换:
- 执行
removeById(id)时,实际执行的是UPDATE repair_task SET deleted = 1 WHERE id = ? AND deleted = 0 - 查询列表时,默认自动追加
AND deleted = 0,确保不会拉出已删除的数据 - 插入和更新操作则完全不受影响,保持原有语义不变
这一切的背后,是由LogicDeleteInnerInterceptor实现的 SQL 重写能力。它作为 MyBatisPlus 拦截器链的一部分,在 SQL 构建阶段动态注入逻辑删除条件,真正做到无侵入、低耦合。
实体设计:让删除变得“可逆”
在RepairTask实体类中,我们只需要添加一个字段并标注@TableLogic:
@Data @TableName("repair_task") public class RepairTask { private Long id; private String userId; private String imageUrl; private String taskType; // PERSON / BUILDING private LocalDateTime createTime; private LocalDateTime updateTime; @TableLogic private Integer deleted; // 0: 正常, 1: 已删除 }这个deleted字段就是逻辑删除的核心开关。默认情况下,MyBatisPlus 会将0视为未删除,1视为已删除。当然,你也可以自定义映射规则,比如使用布尔值(false/true)或者字符串(”N”/”Y”),只需配合全局处理器即可。
配置即生效:一行代码开启软删能力
为了让整个系统支持逻辑删除,我们需要注册对应的拦截器:
@Configuration @MapperScan("com.example.mapper") public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new LogicDeleteInnerInterceptor()); return interceptor; } }就这么简单。一旦注册成功,所有带有@TableLogic注解的实体都将自动启用软删功能。无需修改任何业务代码,原有的save()、removeById()、list()等方法依然照常调用,底层却已经悄然完成了语义转换。
⚠️ 小贴士:如果你有多个数据源或特殊字段类型需求,还可以实现
ILogicDeleteHandler来定制不同字段的删除值映射逻辑。
查询控制:谁能看到“回收站”里的数据?
普通用户调用查询接口时,理应只能看到自己尚未删除的任务。得益于 MyBatisPlus 的自动过滤机制,以下代码天然具备此特性:
public List<RepairTask> listActiveTasks(String userId) { return this.lambdaQuery() .eq(RepairTask::getUserId, userId) .list(); }生成的 SQL 会自动变成:
SELECT * FROM repair_task WHERE user_id = ? AND deleted = 0但如果管理员需要查看“回收站”中的内容呢?这时就需要绕过默认过滤。最直接的方式是使用.last()方法强行加入条件:
public List<RepairTask> listAllIncludingDeleted(String userId) { return this.lambdaQuery() .eq(RepairTask::getUserId, userId) .last("OR deleted = 0 OR deleted = 1") .list(); }虽然有效,但.last()属于原生 SQL 拼接,存在一定的安全风险。更推荐的做法是显式指定deleted字段的查询范围:
.eq(RepairTask::getDeleted, 0).or().eq(RepairTask::getDeleted, 1)或者封装一个专用的“包含已删除”查询构造器,避免在多处重复编写逻辑。
图像修复背后的流水线:ComfyUI + DDColor 工作流
如果说逻辑删除保障了数据层面的可追溯性,那么 ComfyUI 与 DDColor 模型的结合,则让 AI 图像修复变得触手可及。
DDColor 是一种专为黑白图像着色设计的深度学习模型,能够在保留原始结构的基础上,智能还原色彩信息。它并不依赖复杂的参数调整,而是通过预设的工作流(Workflow)来标准化处理流程。这些工作流以 JSON 文件形式存在,本质上是一组节点连接图,描述了从图像输入到结果输出的完整路径。
例如,人物修复与建筑修复对分辨率的要求截然不同:
- 人物修复更注重肤色自然度和面部细节,适合使用中等分辨率(460–680),避免过度渲染导致失真;
- 建筑修复则需展现丰富的纹理和轮廓,通常采用更高分辨率(960–1280),确保砖瓦、窗户等元素清晰可见。
为此,系统提供了两个独立的 JSON 工作流文件:
DDColor人物黑白修复.jsonDDColor建筑黑白修复.json
每个文件内部都封装了最优参数组合,用户无需了解模型原理,只需选择对应类型并上传图片,即可一键完成高质量修复。
工作流长什么样?
以下是简化后的人物修复流程关键节点:
{ "nodes": [ { "id": 1, "type": "LoadImage", "widgets_values": ["input.png"] }, { "id": 2, "type": "DDColor_Preprocessor", "inputs": [[1, "IMAGE"]], "widgets_values": [460] }, { "id": 3, "type": "DDColor_Ddcolorize", "inputs": [[2, "IMAGE"]], "widgets_values": ["ddcolor_realv1", 460] }, { "id": 4, "type": "SaveImage", "inputs": [[3, "IMAGE"]], "widgets_values": ["output"] } ] }整个流程仅四个节点:
1. 加载图像;
2. 预处理并调整尺寸至 460;
3. 调用ddcolor_realv1模型进行着色;
4. 保存结果。
用户甚至可以修改widgets_values中的模型名称或分辨率,快速尝试不同效果。前提是本地已下载相应权重文件,否则会因找不到模型而报错。
⚠️ 实践建议:
- 在部署环境统一管理模型版本,避免因缺失文件导致任务失败;
- 启动前校验工作流节点连接完整性,防止中间断连;
- 对高频使用的参数组合做缓存或模板化处理,提升响应速度。
系统如何协同工作?
从前端用户操作到后台数据存储,再到 GPU 推理执行,整个系统的协作链条如下:
[前端 Web 页面] ↓ (上传图像 + 选择任务类型) [Spring Boot 后端服务] ├── 记录修复任务 → MyBatisPlus 持久化 → MySQL(含逻辑删除字段) └── 触发图像处理 → 调用 ComfyUI API 执行 DDColor 工作流 ↓ [GPU 服务器运行推理] ↓ 返回修复结果图像每一步都有明确职责:
- 用户发起请求后,后端立即创建一条repair_task记录,状态为deleted=0;
- 任务异步提交至 ComfyUI,利用 GPU 加速完成图像修复;
- 结果返回后更新任务记录,包含输出链接和处理耗时;
- 用户可在“我的任务”中查看所有未删除任务;
- 当用户点击“删除”,系统调用removeById(id),该记录的deleted字段被置为1,前端不再展示;
- 管理员可通过后台接口查询全部记录,包括已被“删除”的任务。
这套机制既尊重了用户的操作习惯,又保证了数据的完整性与安全性。
设计背后的关键考量
1. 字段命名与类型统一
我们坚持使用deleted作为逻辑删除字段名,类型为INT,取值0/1。这不是随意决定的,而是出于团队协作和长期维护的考虑:
- 统一命名降低理解成本,新人接手也能快速识别;
- 使用整型而非布尔型,兼容更多数据库类型(如 Oracle 不支持 BOOLEAN);
- 易于扩展未来可能的状态(如
2=归档、3=审核中)。
2. 性能优化:别忘了给deleted加索引
随着数据量增长,WHERE deleted = 0这类查询将成为性能瓶颈。因此,务必在deleted字段上建立索引:
ALTER TABLE repair_task ADD INDEX idx_deleted (deleted);对于复合查询(如按用户 + 状态筛选),还可创建联合索引进一步提升效率:
ALTER TABLE repair_task ADD INDEX idx_user_deleted (user_id, deleted);3. 数据生命周期管理:定期归档,释放压力
尽管逻辑删除保留了历史,但也带来了数据膨胀的问题。我们制定了如下策略:
- 所有
deleted=1且超过 6 个月的任务,自动迁移至repair_task_history历史表; - 主表仅保留最近两年的有效数据,保障查询性能;
- 历史表可离线备份或迁移到低成本存储介质。
这一过程可通过定时任务(如 Quartz 或 XXL-JOB)驱动,结合分页批量迁移,避免一次性加载过多数据造成内存溢出。
4. 权限隔离:普通人看不见“回收站”,管理员可以
前端默认只请求未删除任务,这是用户体验的基本要求。但后台管理系统应提供“查看已删除任务”的功能,供运营或技术支持人员排查问题。
实现上可通过两种方式区分权限:
- 接口层面:普通用户走/api/tasks,管理员走/admin/tasks?includeDeleted=true
- 服务层判断:根据当前登录角色决定是否忽略deleted条件
同时建议配合操作日志系统,记录每一次“删除”行为的时间、IP、用户ID等信息,形成完整的审计轨迹。
5. 工作流也要版本控制
JSON 工作流文件虽小,却是核心资产。我们将其纳入 Git 版本管理,并遵循以下规范:
- 每次参数调优都提交新版本,附带变更说明;
- 生产环境只允许使用 tagged 的稳定版本;
- 支持按任务类型动态加载对应工作流,便于灰度发布。
这样即使某次更新引入 Bug,也能迅速回滚至上一版本,最大限度减少影响。
写在最后
技术的价值,往往体现在那些“看不见的地方”。
MyBatisPlus 的逻辑删除看似只是一个小小的注解,但它守护的是用户每一次操作的真实轨迹;DDColor 工作流看似只是一串 JSON 配置,但它降低了 AI 技术的使用门槛,让更多人能亲手唤醒尘封的记忆。
在这个系统中,我们没有追求炫酷的功能堆叠,而是专注于两个朴素的目标:
-不让任何一次修复被真正抹去;
-不让任何一个用户被技术吓退。
而这,或许才是真正的“智能”所在。
当一位老人看着修复后的全家福热泪盈眶时,他不会关心背后用了什么 ORM 框架,也不会在意是不是逻辑删除保住了那条任务记录。但他知道,这张照片回来了——这就够了。