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

基于 MyBatis-Plus 拦截器实现锁定特殊数据(二)

技术博客:基于 MyBatis-Plus 拦截器实现“结账后禁止修改”的优雅方案(终极版)专用 SQL + 专用接口方案

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


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

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

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

🔹 传统做法的问题

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

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

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

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

需求说明
支持多张表ap_invoice, ap_payment, gl_voucher 等 6 张财务表
只拦截特定数据closeStatus = 'Y' 的数据才拦截
未结账数据可改closeStatus ≠ 'Y' 的数据正常更新
不改业务代码Service 层调 updateById 不加任何判断
可解锁管理员能将 'Y' 改回 'N'(关键!)
统一控制、易于维护一处配置,全局生效

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

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

✅ 核心优势

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

🎯 我们的策略

“状态字段 + 拦截器 + 专用解锁 + 通用化设计”四剑合璧

  • 数据库加 close_status 字段,标记是否已结账
  • 拦截器自动读取该字段,'Y' → 拦截,'N' → 放行
  • 但必须允许管理员通过专用接口解锁
  • 支持多表通用处理

⚠️ 关键认知:所有 UPDATE 操作都会触发 beforeUpdate,无法“绕开”拦截器,只能“识别并放行”特定操作。


四、完整实现步骤(专用 SQL + 专用接口)

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

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':已结账(禁止修改)

✅ 第二步:定义统一接口

// LockableEntity.java
public interface LockableEntity {Long getId();String getCloseStatus();void setCloseStatus(String closeStatus);
}

✅ 第三步:实体类实现接口

public class ApInvoice implements LockableEntity {private Long id;private String invoiceNo;private String closeStatus;// getter/setterpublic Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getCloseStatus() { return closeStatus; }public void setCloseStatus(String closeStatus) { this.closeStatus = closeStatus; }
}// 其他5个实体类同样实现 LockableEntity 接口

✅ 第四步:编写专用解锁 SQL(每张表一个)

// ApInvoiceMapper.java
@Mapper
public interface ApInvoiceMapper extends BaseMapper<ApInvoice> {@Update("UPDATE ap_invoice SET close_status = #{closeStatus} WHERE id = #{id}")int updateCloseStatus(@Param("id") Long id, @Param("closeStatus") String closeStatus);
}// ApPaymentMapper.java
@Mapper
public interface ApPaymentMapper extends BaseMapper<ApPayment> {@Update("UPDATE ap_payment SET close_status = #{closeStatus} WHERE id = #{id}")int updateCloseStatus(@Param("id") Long id, @Param("closeStatus") String closeStatus);
}// ... 其他4个 Mapper 同样添加

✅ 第五步:编写通用拦截器(核心代码)

// AccountingInterceptor.java
@Component
public class AccountingInterceptor implements InnerInterceptor {// ✅ 配置你要拦截的 Mapper 类名private static final Set<String> TARGET_MAPPERS = Set.of("ApInvoiceMapper", "ApPaymentMapper", "GlVoucherMapper","ArInvoiceMapper", "PoOrderMapper", "InvDeliveryMapper");// ✅ 专用解锁方法的后缀private static final String UNLOCK_METHOD_SUFFIX = "updateCloseStatus";// ✅ 存储所有目标 Mapperprivate final Map<String, BaseMapper<? extends LockableEntity>> mapperMap = new HashMap<>();// 通过构造函数注入所有目标 Mapperpublic AccountingInterceptor(ApInvoiceMapper apInvoiceMapper,ApPaymentMapper apPaymentMapper,GlVoucherMapper glVoucherMapper,ArInvoiceMapper arInvoiceMapper,PoOrderMapper poOrderMapper,InvDeliveryMapper invDeliveryMapper) {mapperMap.put("ApInvoiceMapper", apInvoiceMapper);mapperMap.put("ApPaymentMapper", apPaymentMapper);mapperMap.put("GlVoucherMapper", glVoucherMapper);mapperMap.put("ArInvoiceMapper", arInvoiceMapper);mapperMap.put("PoOrderMapper", poOrderMapper);mapperMap.put("InvDeliveryMapper", invDeliveryMapper);}@Overridepublic void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {// 1. 只处理 UPDATE 操作if (ms.getSqlCommandType() != SqlCommandType.UPDATE) {return;}String msId = ms.getId();// 2. ✅ 如果是专用解锁方法,直接放行if (msId.endsWith(UNLOCK_METHOD_SUFFIX)) {return;}// 3. 判断是否是目标表String mapperName = extractMapperName(msId);if (mapperName == null || !TARGET_MAPPERS.contains(mapperName)) {return;}// 4. 获取要更新的实体LockableEntity entity = getEntityFromParameter(parameter);if (entity == null || entity.getId() == null) {return;}// 5. 获取对应的 Mapper 并查询旧数据@SuppressWarnings("unchecked")BaseMapper<LockableEntity> mapper = (BaseMapper<LockableEntity>) mapperMap.get(mapperName);if (mapper == null) return;LockableEntity oldEntity = mapper.selectById(entity.getId());if (oldEntity == null) return;// 6. 核心逻辑:如果旧状态是 'Y',则禁止任何修改if ("Y".equals(oldEntity.getCloseStatus())) {throw new RuntimeException("❌ 数据已结账(closeStatus = Y),禁止修改!");}}// 从 MappedStatement ID 中提取 Mapper 类名private String extractMapperName(String msId) {int lastDot = msId.lastIndexOf(".");if (lastDot > 0) {String className = msId.substring(lastDot + 1);return TARGET_MAPPERS.stream().filter(name -> className.startsWith(name)).findFirst().orElse(null);}return null;}// 从参数中获取实体对象private LockableEntity getEntityFromParameter(Object parameter) {if (parameter instanceof LockableEntity) {return (LockableEntity) parameter;}if (parameter instanceof Map) {Object et = ((Map<?, ?>) parameter).get("et");if (et instanceof LockableEntity) {return (LockableEntity) et;}}return null;}
}

✅ 第六步:注册拦截器

@Configuration
public class MyBatisConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new AccountingInterceptor(// 注入所有目标 MapperapInvoiceMapper, apPaymentMapper, glVoucherMapper,arInvoiceMapper, poOrderMapper, invDeliveryMapper));return interceptor;}
}

五、效果演示

操作方法旧 closeStatus是否允许
修改发票金额updateByIdY❌ 拦截
修改付款金额updateByIdN✅ 允许
调用 updateCloseStatus(id, "N")专用方法Y✅ 放行(方法名匹配)
新增发票insert-✅ 允许

六、方案优势总结

优势说明
彻底无侵入日常业务代码零修改
安全解锁只能通过专用接口解锁,防止误操作
权限控制解锁操作可加入角色校验
统一管理一个拦截器管 6 张表
逻辑清晰拦截 vs 解锁 路径分离
可扩展新增表只需实现接口 + 注入 Mapper

七、注意事项(终极版)

  1. 必须查旧数据
    为了准确判断“修改前是否已锁定”,必须查询数据库旧值。

  2. 专用方法名是关键
    updateCloseStatus 这样的方法名要足够特殊,避免与其他方法冲突。

  3. 权限必须加在 Service 层
    即使拦截器放行了 updateCloseStatus,也要在 Service 中做权限校验。

  4. 性能考虑
    拦截器中查旧数据会多一次 DB 查询,建议:

    • id 字段加索引
    • 高频表可加缓存(如 Redis)
  5. Mapper 注入
    拦截器通过构造函数注入所有目标 Mapper,确保能调用 selectById


八、结语

“状态字段 + 拦截器 + 专用解锁 + 通用化”是一个简单、安全、可维护的解决方案。

它解决了:

  • 日常数据保护close_status='Y' → 禁止修改
  • 紧急解锁需求:通过专用接口安全解锁
  • 业务无侵入:日常操作完全透明
  • 多表统一管理:一套代码管所有财务表

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

加状态字段,写拦截器,配专用解锁,实现通用接口!


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


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

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

相关文章:

  • kmp 算法
  • 42-Ansible-Inventory
  • 模式组合应用-组合模式
  • SpringAI应用开发面试剧本与技术知识全解析:RAG、向量数据库、多租户与企业落地场景
  • DbVisualizer:一款功能强大的通用数据库管理开发工具
  • 1.8 Memory
  • Python 入门 Swin Transformer-T:原理、作用与代码实践
  • 05MySQL多表查询全解析
  • 使用axios封装post和get
  • RLPD——利用离线数据实现高效的在线RL:不进行离线RL预训练,直接应用离策略方法SAC,在线学习时对称采样离线数据
  • unity学习——视觉小说开发(二)
  • 【系统分析师】高分论文:论软件的系统测试及应用
  • 宽带有丢包,重传高的情况怎么优化
  • 2025板材十大品牌客观评估报告—客观分析(三方验证权威数据)
  • 【电力电子】MCP602运算放大器测交流电压(120VAC/230VAC),带直流偏置2.5V,比例:133.5:1
  • 【开题答辩全过程】以 “与我同行”中华传统历史数字化平台的设计和分析-------为例,包含答辩的问题和答案
  • 桌面GIS软件设置竖排文字标注
  • PAT 1088 Rational Arithmetic
  • Python文字识别OCR
  • 蓓韵安禧活性叶酸优生优育守护者
  • CSS基础学习第二天
  • 简说DDPM
  • 【系列07】端侧AI:构建与部署高效的本地化AI模型 第6章:知识蒸馏(Knowledge Distillation
  • 监听nacos配置中心数据的变化
  • vector的学习和模拟
  • 桌面GIS软件添加设置牵引文字标注
  • Fortran二维数组去重(unique)算法实战
  • 电子健康记录风险评分与多基因风险评分的互补性与跨系统推广性研究
  • 福彩双色球第2025100期篮球号码分析
  • GESP5级2024年03月真题解析