当前位置: 首页 > ops >正文

基于 MyBatis-Plus 拦截器实现“结账后禁止修改”的优雅方案

技术博客:基于 MyBatis-Plus 拦截器实现“结账后禁止修改”的优雅方案讨论(一)

作者: 阿波
场景: 财务系统、ERP、进销存等涉及“会计期间结账”的业务
核心方案:状态字段 + 拦截器 = 无侵入式数据保护


一、业务场景:为什么需要“结账后禁止修改”?

在财务类系统中,常见的一个需求是:

每月末执行“结账”操作,结账后当月的所有业务数据(如发票、付款、凭证)不能再被修改。

🔹 传统做法的问题

  1. 在每个 Service 层写判断逻辑

    if (invoice.getCloseStatus().equals("Y")) {throw new RuntimeException("已结账,不能修改");
    }
    
    • ❌ 重复代码多
    • ❌ 容易遗漏
    • ❌ 业务代码被污染
  2. 用 AOP 切 Service 方法

    • ❌ 侵入性强
    • ❌ 难维护
    • ❌ 无法精准控制到具体数据

二、目标:我们想要什么?

需求说明
只拦截特定几张表ap_invoice, gl_voucher 等 6 张财务表
只拦截特定数据closeStatus = 'Y' 的数据才拦截
未结账数据可改closeStatus ≠ 'Y' 的数据正常更新
不改业务代码Service 层调 updateById 不加任何判断
统一控制、易于维护一处配置,全局生效

三、理论方法:为什么用 MyBatis-Plus 拦截器?

MyBatis-Plus 提供了强大的 InnerInterceptor 机制,可以在 SQL 执行前进行拦截。

✅ 核心优势

  • 底层拦截:在 SQL 执行前介入,业务无感知
  • 精准控制:可获取 MappedStatement、参数对象、SQL 类型
  • 无侵入:不需要在 Controller/Service 写判断
  • 高性能:只对目标表做判断,其他操作完全放行

🎯 我们的策略

“状态字段 + 拦截器”双剑合璧

  • 数据库加 is_close 字段(或 close_status),标记是否已结账
  • 拦截器自动读取该字段,'Y' → 拦截,'N' → 放行

四、完整实现步骤

✅ 第一步:数据库加字段(所有目标表)

为需要“结账保护”的 6 张表统一添加字段:

ALTER TABLE ap_invoice ADD COLUMN close_status VARCHAR(1) DEFAULT 'N';
ALTER TABLE ap_payment ADD COLUMN close_status VARCHAR(1) DEFAULT 'N';
ALTER TABLE gl_voucher ADD COLUMN close_status VARCHAR(1) DEFAULT 'N';
-- ... 其他3张表

💡 建议值:

  • 'N':未结账(可修改)
  • 'Y':已结账(禁止修改)

✅ 第二步:实体类加字段

public class ApInvoice {private Long id;private String invoiceNo;private BigDecimal amount;// ... 其他字段private String closeStatus; // 注意:要和数据库字段对应// getter/setterpublic String getCloseStatus() {return closeStatus;}public void setCloseStatus(String closeStatus) {this.closeStatus = closeStatus;}
}

⚠️ 其他 5 个实体类同样操作。


✅ 第三步:编写拦截器(核心代码)

// 文件:AccountingInterceptor.java
package com.yourproject.interceptor;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.util.Set;/*** 结账拦截器:自动阻止对已结账数据的修改*/
@Component
public class AccountingInterceptor implements InnerInterceptor {// ✅ 配置你要拦截的 Mapper 类名(对应6张表)private static final Set<String> TARGET_MAPPERS = Set.of("ApInvoiceMapper","ApPaymentMapper","GlVoucherMapper","ArInvoiceMapper","PoOrderMapper","InvDeliveryMapper");@Overridepublic void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {// 1. 只处理 UPDATE 操作if (ms.getSqlCommandType() != SqlCommandType.UPDATE) {return;}// 2. 判断是否是目标表String msId = ms.getId();if (TARGET_MAPPERS.stream().noneMatch(msId::contains)) {return; // 不是目标表,放行}// 3. 获取要更新的实体对象Object entity = getEntityFromParameter(parameter);if (entity == null) return;// 4. 反射获取 closeStatus 字段String closeStatus = getFieldValue(entity, "closeStatus", String.class);// 5. 如果 closeStatus == 'Y',说明已结账 → 拦截更新if ("Y".equals(closeStatus)) {throw new RuntimeException("❌ 数据已结账(closeStatus = Y),禁止修改!");}}private Object getEntityFromParameter(Object parameter) {if (parameter instanceof java.util.Map) {return ((java.util.Map<?, ?>) parameter).get("et"); // MyBatis-Plus 默认 key}return parameter;}private <T> T getFieldValue(Object entity, String fieldName, Class<T> type) {try {Field field = entity.getClass().getDeclaredField(fieldName);field.setAccessible(true);Object value = field.get(entity);return type.isInstance(value) ? type.cast(value) : null;} catch (Exception e) {return null; // 字段不存在或访问失败}}
}

✅ 第四步:注册拦截器

// 文件:MyBatisConfig.java
package com.yourproject.config;import com.yourproject.interceptor.AccountingInterceptor;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyBatisConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new AccountingInterceptor());return interceptor;}
}

五、效果演示

操作closeStatus是否允许更新
更新 ap_invoiceap_invoiceY❌ 抛异常
更新 ap_invoiceap_invoiceN✅ 成功
更新 user 表userY✅ 成功(不在拦截列表)
新增 ap_invoiceap_invoiceY✅ 成功(不是 update)

🎉 业务代码无需任何改变:

apInvoiceService.updateById(invoice); // 直接调,拦截器自动判断

六、方案优势总结

优势说明
彻底无侵入业务代码零修改
统一控制一个拦截器管 6 张表
精准拦截只拦 closeStatus='Y' 的数据
易于扩展新增表?加个 Mapper 名就行
可配置化TARGET_MAPPERS 可改为从配置中心读取

七、注意事项

  1. 字段命名统一:建议所有表用 close_statusis_close,保持一致
  2. 结账逻辑:结账时批量更新这 6 张表的 close_status = 'Y'
  3. 性能影响:拦截器只做简单判断,几乎无性能损耗
  4. 反射安全:生产环境建议加缓存(如缓存字段反射对象),但通常不需要

八、结语

“状态字段 + 拦截器”是一个简单、稳定、可维护的解决方案。

它把业务规则(已结账不能改)和技术实现(SQL 拦截)完美分离,让业务代码专注业务,让基础设施默默守护数据安全。

下次当你遇到“某种状态下禁止修改”的需求时,不妨试试这个模式:

加个状态字段,写个拦截器,业务代码一行不改,搞定!


📌 适用框架:Spring Boot + MyBatis-Plus 3.4+


✅ 一句话总结:

只要把某条数据的 close_status 改成 'Y',它就被“锁住”了,任何人、任何操作都无法修改它。
只要把 close_status 改回 'N',它就“解锁”了,可以正常修改。


🎯 举个实际例子:

数据库中有一条发票:
UPDATE ap_invoice 
SET close_status = 'Y' 
WHERE id = 1001;

👉 结果:这条发票(id=1001)立刻被锁定,任何人调 updateById(1001) 都会失败,报错:“数据已结账,禁止修改”。


这样有个问题,后来发现要修改,财务主管解锁:
UPDATE ap_invoice 
SET close_status = 'N' 
WHERE id = 1001;

👉 结果:这条发票被锁死,无法解锁,现在不可以正常编辑、保存。


✅ 你不需要做任何其他事情:

你要做的系统自动做的
执行 SQL 把 close_status = 'Y'拦截器在每次 update 前检查该字段
执行 SQL 把 close_status = 'N'拦截器发现不是 'Y',放行更新
service.updateById(invoice)拦截器判断后:能改就改,不能改就抛异常

💡 这就是“状态驱动 + 拦截器”的威力:

  • 锁住数据?UPDATE xxx SET close_status = 'Y' WHERE id = ?
  • 无法解锁数据?UPDATE xxx SET close_status = 'N' WHERE id = ?
  • 业务代码? → 一行不用改
  • 安全性? → 底层拦截,绕不过去

🚀 延伸用法(你以后也可能用到):

场景实现方式
整个组织结账UPDATE ap_invoice SET close_status = 'Y' WHERE org_id = 1001 AND period = '2025-08'
批量解锁UPDATE ap_invoice SET close_status = 'N' WHERE id IN (1001,1002,1003)
查看哪些数据被锁了SELECT * FROM ap_invoice WHERE close_status = 'Y'

✅ 总结:你只需要记住

🔒 锁住数据 = close_status = 'Y'
🗝️ 解锁数据 = close_status = 'N'
🤖 拦截器自动拦,业务代码不用管

  1. 按照当前的拦截器逻辑,当执行一条将 close_status 设置为 ‘Y’ 的 UPDATE 语句时,拦截器会检测到 close_status 的值为 ‘Y’,从而认为这条数据已经被结账,进而抛出异常阻止更新 —— 也就是说,你连“设置结账状态”这个操作本身也做不了!​​
  2. 如果执行 UPDATE 操作时,传入的数据(实体对象)中没有设置 close_status 字段(即没有调用 setCloseStatus),但表中是有这个字段的,那么这种情况下,拦截器会不会拦截这次更新​

但是这个仍有借鉴意义,略微修改拦截器就行,比如

✅ 目标

我们要实现:

  • 日常的 updateById 操作,被拦截器保护(close_status='Y' 时不能改)
  • 但管理员可以调用 /unlock 接口,把 'Y' 改成 'N'(解锁)

✅ 方案一:专用 SQL + 专用接口(推荐 ★★★★★)

下一篇专门讨论

✅ 方案二:在拦截器中加“放行标记”(备选)

1. 前端传一个特殊标记(如 forceUpdate = true

// 前端传
{"id": 1001,"closeStatus": "N","forceUpdate": true  // 特殊标记
}

2. 拦截器中判断

@Override
public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {// ... 判断表、获取实体// 如果带有 forceUpdate = true,且是管理员,就放行if (parameter instanceof Map) {Boolean force = (Boolean) ((Map<?, ?>) parameter).get("forceUpdate");if (Boolean.TRUE.equals(force) && hasAdminRole()) {return; // 放行,不拦截}}// 正常拦截逻辑if ("Y".equals(oldCloseStatus) && !"N".equals(newCloseStatus)) {throw new RuntimeException("禁止修改");}
}

⚠️ 缺点:不够安全,前端可能伪造 forceUpdate


✅ 方案三:用 Service 层事务 + 临时关闭拦截器(不推荐)

  • unlock() 方法上加 @Transactional
  • 先查,再手动改 close_status,再 updateById
  • 但依然会被拦截器拦住,除非你改拦截器逻辑

❌ 太绕,不推荐。


✅ 最终推荐:方案一(专用接口 + 专用 SQL)

graph TDA[管理员点击“解锁”] --> B[调用 /api/invoice/1001/unlock]B --> C[后端调用 unlock() 方法]C --> D[执行专用 SQL: UPDATE close_status = 'N']D --> E[成功解锁]
  • 普通更新:走 updateById → 被拦截器保护
  • 解锁操作:走 /unlock → 走专用 SQL → 绕过拦截器

✅ 总结

操作走的路径是否被拦截
修改发票金额updateById✅ 被拦截(如果 close_status='Y'
调用 /unlock专用 SQL❌ 不走拦截器,成功解锁

你只需要记住:

🔑 “通用更新”用于日常业务,受保护
🚪 “专用解锁”用于管理操作,走后门

这才是既安全又灵活的生产级设计。

点赞 + 收藏,下次结账功能直接抄作业! 💡

http://www.xdnf.cn/news/19187.html

相关文章:

  • 数据库的CURD
  • 【C++】红黑树(详解)
  • 【点云工具】CloudCompare学习记录,自用分享
  • Java对接Redis全攻略:Jedis/SpringData/Redisson三剑客对决
  • 机器人控制器开发(底层模块)——rk3588s 的 CAN 配置
  • CSS学习与心得分享
  • 码农特供版《消费者权益保护法》逆向工程指北——附源码级注释与异常处理方案
  • 轻量化模型-知识蒸馏1
  • Carrier Aggregation Enabled MIMO-OFDM Integrated Sensing and Communication
  • Spring Cache实现简化缓存功能开发
  • 内网穿透系列十二:一款基于 HTTP 传输和 SSH 加密保护的内网穿透工具 Chisel ,具备抗干扰、稳定、安全特性
  • 聊一聊 .NET 的 AssemblyLoadContext 可插拔程序集
  • HarmonyOS AppStorage:跨组件状态管理的高效解决方案
  • SW - 做装配体时,使用零件分组好处多
  • 系统架构设计师选择题精讲与解题技巧
  • STM32的内存分配与堆栈
  • compute:古老的计算之道
  • (二)设计模式(Command)
  • 为什么企业需要项目管理
  • Python Requests 爬虫案例
  • 面试问题详解十二:Qt 多线程同步:QMutex讲解
  • SystemVerilog学习【七】包(Package)详解
  • FFmpeg音视频处理解决方案
  • 【GaussDB】在逻辑复制中剔除指定用户的事务
  • 【C++】C++ const成员函数与取地址操作符重载
  • 【Leetcode hot 100】21.合并两个有序链表
  • Flutter MVVM+provider的基本示例
  • ceph配置集群
  • VGG改进(6):基于PyTorch的VGG16-SE网络实战
  • “我店模式“当下观察:三方逻辑未变,三大升级重构竞争力