ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java企业级ERP系统实战:从库存并发到权限设计

Java企业级ERP系统实战:从库存并发到权限设计 简介在Java企业级开发中数据一致性和权限管控是衡量系统架构成熟度的关键指标。库存超卖问题源于并发更新时的数据竞争通过乐观锁与条件更新可有效兜底而权限模型则需从功能控制延展到数据隔离RBAC结合数据权限拦截器能避免越权访问。随着业务延伸WMS数据库设计将库存管理从台账层推进到货位级粒度支撑精细化仓储作业。同时合理的Java环境变量配置是项目快速启动的基础。这些技术点不仅构成ERP系统的核心也是面试与二次开发的高频场景。本文以赤龙企业级ERP系统源码为例剖析其模块划分、事务处理与性能优化策略。 做 Java 开发这些年我见过太多人拿着 CRUD 项目去面试被一问库存超卖怎么办权限怎么设计就卡壳。ERP 系统是最能体现 Java 工程师业务功底的载体因为它的核心不是增删改查而是业务闭环和数据一致性。这套基于 Java 的赤龙企业级 ERP 系统设计源码恰好把企业里最常见的进销存、财务、权限、报表这些场景都串起来了。它适合三类人准备 Java 面试、想拿项目经历充实战的做毕业设计需要一套完整业务系统的以及刚进项目组就要接 ERP 二次开发的新人。我先说一下整体判断这套源码走的是主流 Java 企业级技术栈核心是 Spring Boot 加持久层框架加 MySQL配 Redis 做缓存前端是前后端分离的 Vue 单页应用。这也是目前开源 Java ERP 项目最常见的形态好处是对新手友好、上手成本低同时又保留了大量可扩展的企业级细节。在下面的内容里我会把模块设计、数据库表拆分、权限模型、库存并发控制、常见坑位这些全部过一遍也会穿插我实际开发 ERP 时踩过的坑。1. 项目整体设计与技术选型思路1.1 为什么 ERP 这类系统要用 Java 写ERP 不是普通的管理系统它每时每刻都在处理有状态的数据一份采购单从草稿变成审核通过再变成入库单最后变成应付账款每一步都有状态流转状态之间不能乱跳。这种强流程、强约束、强一致性的业务模型恰好是 Java 语言和企业级框架最擅长处理的场景。先说事务能力。库存扣减、订单生成、流水写入要放在同一个数据库事务里任何一个环节异常都要求全部回滚Java 的声明式事务可以直接做到这一点代码层面只需要加一个注解。再说并发控制多人同时录单、开票、出库这是 ERP 的常态需要 JVM 锁、数据库锁、分布式锁多层级配合Java 在这块的生态非常成熟从 synchronized 到 Redisson 都有对应方案。第三是分层带来的可维护性Controller、Service、Mapper 三层各司其职ERP 业务规则复杂几个人同时开发时分层清晰能避免改一个功能影响另一个功能。很多团队也尝试过用 PHP 或 Node.js 写 ERP并不是不行但到了并发高、逻辑重、团队多人维护的阶段Java 的静态类型和成熟框架体系优势就出来了。所以选 Java 写 ERP 不是跟风是业务复杂度倒逼的选择。1.2 赤龙 ERP 的整体架构与模块划分从源码目录结构来看这套系统采用了非常典型的前后端分离架构后端提供 RESTful API前端通过 Nginx 或本地开发服务器代理访问后端接口。核心模块可以分成五组系统管理用户、角色、部门、菜单、操作日志、字典管理。这是所有业务模块的基础也几乎是所有 Java 管理系统的标配。基础资料物料档案、客户档案、供应商档案、仓库档案、计量单位。基础资料不直接产生业务数据但所有业务单据都要引用它。供应链管理采购订单、采购入库、销售订单、销售出库、库存查询、库存流水、库存盘点。财务管理应收单、应付单、收款单、付款单、对账单。财务数据和业务数据通过单据编号关联。报表中心采购报表、销售报表、库存报表、财务收支表支持按时间、部门、仓库多维度过滤。这套模块划分在企业里是经过验证的。我在实际项目里见过很多团队上来就是几十张表堆在一起结果采购、销售、库存三个模块各自为政数据根本对不上。赤龙的做法是把采购和销售作为过程库存作为结果来设计单据驱动、流水留痕这个思路我觉得是整套源码里最值得学习的地方。1.3 技术选型为什么是 Spring Boot MyBatis-Plus Redis我不去猜测源码作者的详细动机单从结果看这套选型非常稳。Spring Boot 的自动配置和 Starter 机制让项目启动成本极低不用像传统 SSM 那样写一堆 XML 配置MyBatis-Plus 在 MyBatis 基础上补上了通用 Mapper、分页插件、逻辑删除、乐观锁插件非常契合 ERP 这种大量 CRUD 又要灵活 SQL 的场景Redis 则是用来做验证码缓存、字典缓存、登录会话缓存以及分布式锁。MySQL 是绝对的性价比之选。ERP 的并发量通常不会像互联网社交业务那么爆炸大部分企业级应用的压力在几百到一两千并发左右MySQL 配好索引和事务隔离级别完全扛得住而且运维成本远低于 Oracle 和 SQL Server。整套技术栈对开发者的要求也很清晰会 Java、会 SQL、会一点前端就能在这套源码上动手改东西。2. 核心功能模块设计与数据库表结构拆解2.1 权限模型是怎么设计的RBAC 与数据权限的落地ERP 的权限和普通网站的用户权限有个明显的差别它不仅要做你能不能点这个按钮的功能控制还要做你能看哪些部门、哪些仓库的数据的数据控制。赤龙在这一块用的是标准的 RBAC 模型也就是用户-角色-菜单这种三段式同时做了数据权限的扩展。核心表结构大概是sys_user用户表字段包括用户名、密码BCrypt 加密、昵称、部门 ID、状态。sys_role角色表存角色名和角色编码。sys_user_role用户角色关联表。sys_menu菜单表包含菜单名称、父级菜单 ID、路由地址、权限标识权限标识就是接口校验用的 hasPermission 字符串。sys_role_menu角色菜单关联表。sys_dept部门表带 parent_id 做树形结构。数据权限是怎么处理的呢关键点在业务表里都留了一个 dept_id 字段。查询数据时框架会根据当前用户的角色去解析数据权限规则比如仅本人本部门本部门及以下全部数据。这个解析过程通常封装在 MyBatis 的拦截器里拼接 SQL 时自动加上 dept_id 过滤条件避免在业务代码里每次手写。这样做的好处是权限控制是全局生效的不会出现某个开发漏写过滤条件导致越权的情况。2.2 库存管理模块台账和流水为什么必须分开如果把库存模块设计错了后面所有模块都会跟着乱。赤龙的库存设计核心是一个库存现状表加一张库存流水表严格区分台账和流水这两个概念。库存台账表 inventory 记录当前每个仓库中每个物料的剩余数量字段一般有 warehouse_id、material_id、quantity、update_time。库存流水表 inventory_record 记录每一次变动比如采购入库加 100、销售出库减 30、盘点调整减 2每条流水必须包含业务单据编号、变动数量、变动时间、操作人。这里有一个新手特别容易犯的错误直接把库存表的数量字段当成计数器先查询出来在内存里计算再 update 回去。一旦并发量大两个线程同时读到旧值再写回数据就丢了。正确做法是只在库存台账上做微更新直接在 SQL 里加增量并且把每次更新的来源单据编号写入流水表。万一库存对不上能顺着流水一路倒查回去这是审计的底线。2.3 WMS 数据库表怎么设计从 ERP 库存延伸到仓库精细化热词里有 wms系统怎么设计数据库表其实 WMS 是 ERP 库存模块的深度延伸。ERP 库存偏记账WMS 偏库内作业。如果你要在赤龙源码基础上扩展 WMS重点要加这几张表wms_warehouse_area库区表一个仓库可以分成收货区、存储区、拣货区、退货区。wms_storage_location货位表每个库区下又分成多个货位字段包括货位编码、货位类型、是否启用。wms_inventory_location货位库存表记录每个货位上物料的实际数量。wms_stocktaking_task盘点任务表WMS 的盘点不是盲盘而是按库区生成盘点任务逐项记录账面数和实盘数差异。这几张表加上 ERP 的 inventory_record就能支撑按货位管理先进先出盘点差异调整这些仓库场景。设计 WMS 表一个小小的建议所有表都带上 tenant_id 或 dept_id因为仓库数据天然就有多组织隔离的需求等后面接财务和报表时这个字段能帮你省掉大量重构。2.4 采购、销售、财务单据流怎么串成闭环在企业业务里采购和销售是不直接产生钱的真正产生资金记录的是应收应付。赤龙的单据流设计是这样的采购采购订单 - 采购入库单 - 应付单 - 付款单。销售销售订单 - 销售出库单 - 应收单 - 收款单。每一步都有一个独立的单号单号之间通过 source_bill_no 或者关联表互相引用。这样设计的好处是所有业务可追溯财务要查某笔应付到底有没有对应的入库记录直接拿单号就能查业务要查某个订单走到哪一步看关联单据的状态就知道。设计单据状态机时我建议控制在五个状态以内草稿、已审核、已完成、已作废、已冲销。状态多了日常维护会失控这也是我一直强调的ERP 设计要克制。3. 实操过程与核心代码实现3.1 环境准备与项目搭建一台机器跑通前后端这个源码拿到手之后第一步不是读代码而是把环境跑起来。建议的 JDK 版本是 JDK 8 或 JDK 17Maven 3.6 以上MySQL 5.7 以上或 8.0Redis 6。JDK 版本这里特别提醒一下如果项目用的是 Spring Boot 2.xJDK 8 最稳如果项目已经升到 Spring Boot 3.x那必须 JDK 17 以上别在版本上卡壳。配置方面常见的坑有两个一个是 MySQL 连接串里的 timezone 参数不写会报时间差 8 小时另一个是 Redis 密码配置和本地不一致导致登录会话一直失败。我的建议是先把配置文件里的数据库名、账号密码、Redis 地址全部改成自己本机环境再启动服务一次跑通比边查边改效率高很多。如果你用的是 WindowsJDK 环境变量记得把 JAVA_HOME 和 Path 都配好IDEA 里 Project Structure 的 SDK 也要指到 JDK 安装目录不然代码里一行红。3.2 库存扣减的并发控制从悲观锁到乐观锁这是 ERP 里最经典的八股问题也是面试官最爱问的库存超卖怎么解决。赤龙代码里用到的方案是乐观锁加条件更新两层防护核心逻辑可以写成这样Transactional(rollbackFor Exception.class) public void stockOut(StockOutParam param) { // 1. 查询库存台账 Inventory inv inventoryMapper.selectByWarehouseAndMaterial( param.getWarehouseId(), param.getMaterialId()); if (inv.getQuantity() param.getQuantity()) { throw new BusinessException(库存不足); } // 2. 条件更新乐观锁防止并发覆盖 int rows inventoryMapper.deductStock( inv.getId(), param.getQuantity(), inv.getVersion()); if (rows 0) { throw new BusinessException(操作冲突请重试); } // 3. 写流水表 inventoryRecordMapper.insert(buildRecord(param)); }对应的 SQL 是这样的update inventory set quantity quantity - #{delta}, version version 1 where id #{id} and version #{version} and quantity #{delta}条件里带上 quantity #{delta} 是最后一道防线就算并发穿透了 version 校验数据库层面也不会让库存变成负数。这种写法在单机 MySQL 场景下足够稳如果以后要拆分布式再引入 Redis 分布式锁来保护同一个物料号的扣减操作。这套思路理解透了面试里被问到超卖问题时就能讲出层次感先说乐观锁再说条件更新兜底最后聊分布式锁的取舍。3.3 权限拦截器与登录状态校验权限这里核心是要保证每个接口都能拿到当前登录用户。赤龙的做法是前端登录后把 token 存到 localStorage每次请求带在 header 上后端用一个拦截器统一解析 token解析结果放到 ThreadLocal业务代码里通过一个工具方法就能拿到当前用户 ID 和部门 ID。拦截器核心逻辑大致是这样public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); LoginUser user loginService.getUserByToken(token); if (user null) { throw new UnauthorizedException(登录已过期); } UserContext.set(user); return true; } }这里有个细节很多人容易忽略拦截器里拿到的 user 对象不能只存 user_id得把用户角色、部门信息一起放进去不然后面做数据权限过滤时还得再查库性能和复杂度都上来了。另外还要注意在请求结束时清理 ThreadLocal否则线程池复用会带来用户数据串号的严重问题。3.4 仪表盘报表的聚合实现报表模块是 ERP 里看着简单、实际最容易翻车的部分。赤龙里报表基本不查原始明细表而是通过聚合逻辑先算好再展示。比如销售报表select date(order_time) as biz_date, dept_id, count(distinct order_no) as order_count, sum(total_amount) as sale_amount from sales_order where order_status APPROVED and order_time #{startTime} and order_time #{endTime} group by biz_date, dept_id大范围的报表查询建议加时间范围限制不要允许用户无限制地查三五年数据。更好的做法是每天凌晨用定时任务把前一天的数据汇总到报表表查询时直接查汇总表这样无论数据量怎么涨页面响应都能稳定在秒级以内。ERP 报表的性能优化是个持续过程我建议从业务端就开始控制能按日汇总就不要查明细能用汇总表就不要临时聚合。3.5 Java 环境变量配置与常见启动问题热词里一直有人搜 java环境变量配置过程这本身不难但是接 ERP 项目时经常遇到。Windows 下配好 JAVA_HOME、Path 和 CLASSPATH 之后在 IDEA 里可能还是识别不到 JDK原因是 IDEA 的 Project Structure 里的 Project SDK 选错了。这个时候把 Project SDK 指向 JDK 安装目录再在 Maven 设置里把 Runner 的 JRE 也指定一下问题就解决了。如果项目启动时提示 java: outofmemoryerror: insufficient memory八成是 Maven 的编译内存配得太小改了 Maven 的 MAVEN_OPTS 加上-Xms512m -Xmx1024m就好。这类小问题排查多了就会发现90% 的 ERP 部署问题都出在环境配置而不是代码本身。4. 常见问题与排查技巧实录4.1 事务失效同方法内部调用导致回滚失败ERP 里最常见的一类 bug在一个 Service 方法里调同一个类另一个带 Transactional 的方法第二个方法抛异常数据库却没有回滚。这不是注解没用而是 Spring 事务基于代理机制同类内部调用走的是 this.method()绕过了代理。解决方案有三种思路把需要事务的内部方法提取到另一个 Service Bean 中通过注入的 Bean 调用这是最推荐的方案。或者用 AopContext.currentProxy() 在当前类里获取代理对象再调用但是如果项目没有开启 exposeProxy 配置会报错。最粗暴但有效的方案改成 Mapper 层的事务控制但一般不建议因为事务粒度太细会失去业务语义。排查技巧是在日志里看到异常但数据没回滚先检查事务是否真的生效。Spring Boot 默认事务只对 RuntimeException 回滚对 checked exception 不回滚所以抛自定义业务异常时最好继承 RuntimeException。4.2 MyBatis 缓存导致的数据不一致MyBatis 的一级缓存是 SqlSession 级别的默认开启二级缓存是 namespace 级别的默认关闭。赤龙这种多 Service 调多 Mapper 的场景默认不开二级缓存其实是最安全的选择。如果为了性能强行开了二级缓存多个 Mapper 操作同一张表时缓存很容易出现脏读。我的建议是ERP 这种实时性要求高的业务系统二级缓存能不碰就不碰。真要缓存就用 Redis 做局部缓存的主动失效数据一变就删 key压力不大时直接全不缓存也完全能接受。很多线上故障都是开发觉得加个缓存提升性能结果缓存一致性没处理好最后只能靠重启解决得不偿失。4.3 报表慢索引没建对很多报表慢不是 SQL 写得不对是索引没建立起来。这里有一个我踩过很多次的坑在 group by 的时间字段上明明建了索引执行计划还是全表扫描。原因是查询条件里的 startTime 和 endTime 都是参数MySQL 优化器判断返回行数占比过高干脆不走索引。解决思路是这么几条强制使用覆盖索引select 只查 group by 和聚合函数用到的字段避免回表。在大表上做分区按月分区是 ERP 报表最实用的分区策略。用 EXPLAIN 看执行计划重点关注 type 是不是 range 或 ref如果出现 ALL 就说明 SQL 或索引有问题。实际排查时最有效的是先跑一次真实查询再跑 EXPLAIN对比优化前后的扫描行数。我见过一个销售报表从全表扫描 200 万行优化到覆盖索引扫描 2 万行查询时间从 8 秒降到 0.3 秒效果立竿见影。4.4 权限漏配接口被人直接调用了权限最容易出问题的不是网页按钮隐藏而是后端接口没有做二次校验。很多新人在开发时只做了按钮隐藏数据接口却没有任何权限注解别人通过 URL 直接调接口照样能拿到数据。我推荐的排查流程是把所有 Controller 接口列一个清单逐一对配置文件里的白名单。在拦截器里加统一的接口权限扫描扫描注解权限标识没有匹配到角色的接口直接拒绝。写一个自动化巡检脚本模拟不同角色的账号去访问不该访问的接口断言返回 403。这个方法虽然麻烦但确实是我在 ERP 项目里用得最顺手、效果最好的权限防漏方案。上线前跑一遍能堵掉至少八成权限漏洞。4.5 多组织/多部门数据隔离失效ERP 一定要考虑数据隔离问题。如果两个部门都能看到彼此的单据业务上会乱套。赤龙源码的通用做法是每个业务表都带 dept_id查询时框架自动拼接条件。我见过很多二次开发的同学喜欢在 SQL 里写死了部门 ID这样写死部门 ID 是巨大的隐患。正确做法是查询条件永远优先从当前登录用户上下文里取部门 IDSQL 里的过滤条件用参数绑定而不是字符串拼接测试时一定要分别用两个部门的账号验证互相看不到对方数据。数据权限这种事漏一次就是事故级别的问题。这里我把最常遇到的几个问题整理成一张速查表方便你排查时对照问题现象排查方向快速解决事务没回滚同类内部调用、异常类型拆 Bean 调用抛 RuntimeException库存变成负数并发覆盖、缺少条件更新乐观锁 quantity 条件兜底报表查询特别慢索引失效、大表全扫描覆盖索引、按月分区接口越权访问权限注解漏配、白名单不当接口清单巡检、权限规则脚本时间差 8 小时JDBC 连接串缺 timezone连接串加 serverTimezoneAsia/Shanghai登录会话不稳定Redis 地址、key 前缀不一致统一 Redis 配置检查序列化方式5. 这套源码对三类人的实战价值5.1 准备 Java 面试的这就是现成的项目经验面试官问项目经验最害怕听到我用 Spring Boot 做了个 CRUD。把赤龙 ERP 吃透之后再聊项目你可以很自然地抛出这些点权限模型讲 RBAC 和数据权限实现能展开到拦截器、SQL 过滤、防漏配。库存并发讲乐观锁、条件更新、唯一约束兜底能接上超卖这类高频八股。事务与缓存讲事务失效场景、MyBatis 缓存陷阱、MySQL 索引优化。这些都是 Java 面试技术栈里最常被追问的细节。拿这套源码当素材不是背答案而是真的在代码里看到过问题聊起来底气完全不同。面试官只要追问一句具体怎么实现你就能把表结构、SQL、拦截器一步一步讲出来这比简历上写一百个技术名词都管用。5.2 做毕业设计的模块齐全但需要裁剪毕设选题选 ERP 是聪明的选择因为系统复杂度足够评委容易认可但要注意不能把整个系统原封不动搬上去。推荐裁剪思路是保留系统管理、基础资料、采购、入库、库存查询、销售、出库这几个核心模块。财务模块可以先只做应收应付不做复杂的总账。报表只保留三到四张核心表别把功能铺得太开。文档里要写清楚数据库设计、权限模型、单据流转这三个点是最加分的答辩时刻意去讲。特别提醒一下把源码改成自己的毕设时一定要理解每一个核心表的字段含义因为答辩时评委很可能指着数据库问你为什么库存表里要有 version 字段回答不上来反而扣分。5.3 做二次开发的先跑通再扩展不要乱改表结构如果你准备在企业里把这套源码落地我给三个非常务实的原则第一次跑通流程时不要改任何表结构先按原设计把所有单据走一遍理解为什么要这样设计。扩展新需求时优先加字段不要轻易改字段含义更不要删字段。ERP 数据是持续滚动的资产删字段等于删历史。所有业务改动都要写操作日志和审计日志出了问题要能定位到人、时间、操作内容这是企业上 ERP 的底线要求。我强烈建议在动手开发之前先花半天时间梳理角色-菜单-权限标识这条线因为大多数 ERP 二次开发最后都会死在权限配置混乱上面。系统跑得再快权限是乱的业务人员用两天就会失去信任。最后再分享一点实操体会我自己在复盘这套源码时最深刻的体会是ERP 系统的复杂度不在技术而在业务规则。你写一个库存扣减用乐观锁很简单但你得想清楚这个扣减是哪个部门做的、对应哪张订单、什么时候冲销、库存流水怎么记。这些业务层面的约束才是这套源码真正值钱的地方。如果你手里拿到了这套源码我建议从库存管理模块开始读因为它是进销存的核心节点往上接采购往下接销售旁边还挂着财务。把库存模块读懂了整个 ERP 的骨架也就搭起来了。再配合我上面说的权限模型和数据流思路你会对 Java 企业级开发有完全不一样的理解。这里再教你一个我压箱底的小技巧在 IDEA 里全局搜索 Transactional 的调用链用调用链视图把每个事务方法的调用关系拉出来看一遍你会立刻明白这套系统哪些操作是强一致性的哪些地方允许最终一致。这个习惯我到现在还在用比看一百遍架构图都管用。本文还有配套的精品资源点击获取
返回列表