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

网站设计 字体代理网店

网站设计 字体,代理网店,wordpress 扁平化响应式主题,腾讯云备案 网站名称技术博客:基于 MyBatis-Plus 拦截器实现“结账后禁止修改”的优雅方案(终极版)专用 SQL 专用接口方案作者: 阿波 场景: 财务系统、ERP、进销存等涉及“会计期间结账”的业务 核心方案:状态字段 拦截器 专…

技术博客:基于 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.dtcms.com/a/489825.html

相关文章:

  • 网站建设公司新报价淘宝网站怎样做
  • 遵义原创网站企业建网站的费用
  • 单站点网站如何创建网页模板
  • 站长网网站模板下载聊城住房建设局网站
  • 深圳手机网站制作互联网推广工资很高吗
  • nodejs的网站开发西安做网站公司8
  • 手机网站搜索优化网站建设 长春
  • jsp网站开发工资代驾小程序定制开发
  • 如何套用网站模板ui页面设计公司
  • 网络推广有哪些网站一个新手怎么做电商
  • 浙江省职业能力建设处网站重庆森林为什么叫这个名字
  • 客户端建站成都优化网站关键词
  • 微信营销网站vs 网站开发教程
  • 网站建设平台加盟网站菜单分类怎么做的
  • 烟台网站建设联系电话seo网络营销
  • 长沙网站建设的公司微商城平台有哪些
  • 制作商城网站公司湖南网站建设哪里好
  • 兴仁企业建站公司网页设计就业方向
  • 孟村做网站百度seo视频教程
  • 建一个购物网站大约多少钱哪些网站可以找到做跨境电商的公司
  • 英铭网站建设黑龙江采购网
  • 网站开发的项目流程图机箱做的网站主机怎么查看ftp
  • wordpress建站的好处免费推广网站排行榜
  • 无锡高端网站开发自助建站竹子
  • 网站建设每天的工作做视频网站的挣钱吗
  • 深圳做企业网站多少钱php mysql做网站登录
  • 手机制作企业网站wordpress otp
  • 仪征 网站建设宁河集团网站建设
  • 网站建设 好的公司移动互联网应用程序指的是什么
  • python做网站好吗佛山网页设计培训怎么学