开源Java ERP二次开发实战:从本地跑通到库存与权限避坑

发布时间:2026/10/26 18:27:35
开源Java ERP二次开发实战:从本地跑通到库存与权限避坑 简介这套基于 Java 开发的开源 ERP 系统源码面向 Java Web 学习者、企业信息化开发者以及需要参考 ERP 实现思路的技术人员可用于了解企业管理系统从界面、配置到业务逻辑的典型组织方式。压缩包为 rar 格式共 1444 个文件、约 2.16MB其中 Java 源文件 1361 个是阅读业务实现的主体另有 XML 配置、HTM 页面、properties 配置及少量 txt 说明能较完整地呈现源码、配置与界面资源的分层关系。已有 4004 人学习说明其在 Java 开源项目参考场景中具有一定关注度。读者可重点借鉴目录结构、实体与业务代码写法、配置文件组织以及表单重复提交错误提示等 Web 细节适合作为课程设计、毕业设计或二次开发前的参考底稿也可用于梳理 ERP 类系统的通用实现脉络按模块筛选关键代码后更易形成整体认识。1. 开箱之前先想清楚这套 ERP 开源 Java 系统能接住什么活接手一套 ERP 开源 Java 系统第一件事往往不是看代码行数而是判断它能接住多少真实业务。很多团队选型时盯着功能清单上线三个月后才发现库存对不上、单据状态回滚、权限串了部门。这类问题不是功能多少能回答的得看代码里单据流转、库存扣减和数据隔离怎么写的。这套 Java 版开源 ERP 适合有二次开发能力的中小团队采购、销售、库存、财务主链路齐全技术栈是常见的 Spring Boot MyBatis MySQL Redis前后端分离。不适合想开箱即用、零代码配置的人。下面的内容按跑通、改核心链路、避坑三段拆每一步都落到能复现的命令和参数上。2. 架构与选型Spring Boot MyBatis 这套组合为什么适合改2.1 模块拆分与依赖方向ERP 的代码量通常集中在单据和库存模块拆分如果做不好改一个采购单会牵动销售和财务。这套系统的常见做法是按业务域拆模块基础数据物料、供应商、客户、仓库、采购、销售、库存、财务、系统管理。每个模块一个 Maven 子模块依赖方向单向业务模块依赖基础数据模块基础数据模块不反向依赖业务模块。为什么强调单向依赖因为 ERP 里基础数据会被所有业务引用一旦基础模块反向依赖采购模块编译期就会形成环改一个字段要全量重编时间一长没人敢动。判断依赖方向是否干净最直接的办法是看 Maven 依赖树。# 在项目根目录执行输出依赖树并过滤业务模块之间的引用 mvn dependency:tree -Dincludescom.example.erp:* -DoutputFiledep-tree.txt这段命令把项目内模块之间的依赖关系单独导到文件里。-Dincludes的参数按你实际的 groupId 改常见是公司域名倒写加项目名-DoutputFile只是避免终端刷屏。打开文件后重点看两点基础数据模块有没有出现在业务模块的依赖列表里以及有没有模块同时被采购和销售依赖却又不属于基础层。前者说明依赖方向反了后者往往是公共工具类没抽干净。参数上真正要调的是-Dincludes的范围。如果项目用的是多级 groupId比如com.example.erp.base写成com.example.erp.*会把所有子模块都带出来文件会很长但不会漏。我一般会先跑全量依赖树再按模块逐个过滤因为第一次看这套代码时你并不知道模块命名习惯。裁剪模块时不要直接删pom.xml里的依赖先看有没有被其他模块的 mapper 或实体引用。常见做法是先用 Maven 的dependency:analyze找出声明但未使用的依赖再人工确认一遍避免删掉运行时才用到的驱动。2.2 数据访问层选型MyBatis 与 JPA 的取舍ERP 的查询有两个极端一端是单据列表的复杂条件分页另一端是库存扣减这种单行更新。MyBatis 的优势在复杂 SQL 可控JPA 的优势在单表 CRUD 省代码。这套系统选 MyBatis 为主、少量 JPA 或 MyBatis-Plus 辅助是偏工程落地的选择。为什么不是全 JPAERP 的报表查询经常要跨五六张表做聚合用 JPA 的 Criteria 或 JPQL 写出来又长又难调DBA 看到执行计划也没法直接改。MyBatis 把 SQL 放在 XML 或注解里慢查询能直接拿 SQL 去数据库里 EXPLAIN排错路径短。代价是单表增删改要手写或依赖代码生成器这部分通常用 MyBatis-Plus 的 BaseMapper 补。可执行的判断方法是看 mapper 文件的分布。如果项目里*Mapper.xml集中在resources/mapper下且每个业务模块有自己的子目录说明 SQL 是按域管理的后续加字段不容易冲突。如果所有 SQL 挤在一个大 XML 里二次开发时两个人同时改同一个文件就会频繁冲突。!-- 典型的单据分页查询注意 where 条件用 if 包裹而不是字符串拼接 -- select idselectOrderPage resultTypecom.example.erp.purchase.dto.PurchaseOrderDTO SELECT o.id, o.order_no, o.supplier_id, o.status, o.total_amount FROM pur_order o where if testsupplierId ! null AND o.supplier_id #{supplierId} /if if teststatus ! null AND o.status #{status} /if if teststartTime ! null AND o.create_time gt; #{startTime} /if /where ORDER BY o.create_time DESC /select这段 XML 里有三个参数要盯住supplierId、status、startTime。where标签会自动处理第一个AND不要手写WHERE 11后者在老代码里常见但会让索引选择变差。gt;是 XML 转义直接写在部分解析器里会报错。分页不要在这段 SQL 里写LIMIT交给分页插件统一处理否则 count 语句和列表语句容易对不上。2.3 缓存与消息中间件的介入时机ERP 不是所有查询都适合加 Redis。基础数据物料、仓库、供应商读多写少适合缓存单据列表和库存余额变化频繁缓存反而增加不一致风险。这套系统的常见做法是只缓存基础数据字典和权限树库存余额走数据库行锁或乐观锁。消息中间件在 ERP 里通常用于两件事单据审核后的异步通知邮件、站内信和跨模块的库存流水落库。如果没有这两个场景不建议为了“架构完整”硬加 MQ因为消息重复消费和顺序问题在 ERP 里会直接体现为账不平。常见做法是给每条消息带业务单据号和事件类型作为消费幂等键消费前先查流水表有没有同键记录而不是只靠 MQ 的“至少一次”语义。判断缓存该不该加看一个指标同一个 key 在业务高峰期的读请求是否远大于写请求且能容忍秒级不一致。权限树符合库存余额不符合。库存余额的正确做法是数据库层扣减缓存只做展示加速并设置短过期时间。3. 本地跑通环境、数据库与启动参数3.1 JDK/Maven/MySQL/Redis 版本对齐跑不起来的第一大原因是版本不对齐。这套 Java ERP 常见的技术栈是 JDK 8 或 JDK 17、Maven 3.6、MySQL 8、Redis 6。JDK 版本不要凭感觉选先看pom.xml里的maven.compiler.source和spring-boot-starter-parent的版本。Spring Boot 2.x 默认带 JDK 8 兼容Spring Boot 3.x 要求 JDK 17混用会在启动时报UnsupportedClassVersionError。MySQL 版本要注意的是字符集和时区。ERP 的物料名称、客户地址常有中文库和表统一用utf8mb4连接串里加characterEncodingutf8和serverTimezoneAsia/Shanghai。Redis 版本影响不大但连接池参数要改默认的max-active在批量导入物料时会不够用。# 检查本机版本三项都对上再往下走 java -version mvn -v mysql --version redis-cli --version这四行不是走形式。java -version看的是运行时mvn -v会同时输出 Maven 用的 JDK两者不一致时编译能过但启动会挂。mysql --version确认是 8.x5.7 在窗口函数和 JSON 字段上有差异部分报表 SQL 会直接报语法错。redis-cli --version主要确认能连上版本号本身不敏感。3.2 初始化 SQL 与配置文件改写数据库初始化一般分两步建库建表脚本和基础数据脚本。建表脚本在sql/或doc/sql/目录下基础数据脚本通常带init或data字样。执行顺序不能反先建表再灌数据否则外键和唯一索引会报错。配置文件重点改三处数据源、Redis、文件上传路径。数据源不要只改urlusername和password也要跟着改driver-class-name在 MySQL 8 下是com.mysql.cj.jdbc.Driver老驱动名会提示废弃。spring: datasource: url: jdbc:mysql://127.0.0.1:3306/erp_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: erp_user password: change_me driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0url里的serverTimezoneAsia/Shanghai必须加不加在部分环境会把datetime读成 UTC单据时间整体偏八小时。allowPublicKeyRetrievaltrue是 MySQL 8 默认加密插件下的连接需要本地开发加上生产环境按 DBA 要求决定是否保留。database: 0是 Redis 的库编号如果本机还有别的项目在用 Redis改成其他编号避免 key 冲突。3.3 启动顺序与首个管理员账号启动顺序影响首次登录能不能成功。常见做法是先起 MySQL 和 Redis再起后端最后起前端。后端启动时会初始化权限缓存和字典缓存如果 Redis 没起来登录接口会在校验验证码时超时。首个管理员账号一般由初始化 SQL 插入密码是加密后的固定值。不要直接改数据库里的明文密码字段先看sys_user表的password字段长度和加密方式。常见是 BCrypt密文以$2a$开头长度 60。如果你用 MD5 去改登录会一直失败。-- 查看管理员账号的加密方式不要直接改 password 字段 SELECT id, username, LENGTH(password) AS pwd_len, LEFT(password, 4) AS pwd_prefix FROM sys_user WHERE username admin;这条查询看两个值pwd_len和pwd_prefix。长度 60 且前缀$2a$说明是 BCrypt要改密码得用 BCrypt 工具生成新密文再更新。长度 32 且全是十六进制说明是 MD5这种老系统常见但二次开发时建议换成 BCrypt因为 MD5 撞库成本太低。pwd_prefix不要在生产库上随便查这里只是本地初始化时确认用。前端启动前确认vue.config.js或vite.config.js里的代理地址指向后端端口常见是 8080。代理没配时前端能打开但所有接口 404很多人会误以为是后端没起。4. 核心链路采购订单到入库的代码走读4.1 单据状态机与表结构ERP 的单据不是一张表走到底采购订单、入库单、应付单各自有状态字段。状态机设计不好会出现“已入库还能改数量”这种账务漏洞。这套系统的常见做法是状态字段用枚举值流转通过服务层方法控制不允许前端直接传状态。以采购订单为例表里通常有status字段取值为草稿、待审核、已审核、部分入库、已完成、已取消。关键约束是只有草稿能改明细只有待审核能审核已审核才能生成入库单。这些约束写在服务层不写在 Controller。// 状态流转的入口方法先校验再更新避免并发下两个请求同时审核 public void audit(Long orderId, Long auditorId) { PurchaseOrder order orderMapper.selectById(orderId); if (order null) { throw new BizException(单据不存在); } if (!OrderStatus.PENDING_AUDIT.getCode().equals(order.getStatus())) { throw new BizException(当前状态不允许审核); } // 用带状态条件的更新影响行数为 0 说明已被其他请求改过 int rows orderMapper.updateStatus(orderId, OrderStatus.PENDING_AUDIT.getCode(), OrderStatus.AUDITED.getCode(), auditorId); if (rows 0) { throw new BizException(单据状态已变化请刷新后重试); } }这段代码的重点在updateStatus的四个参数单据 ID、期望的旧状态、目标新状态、审核人。SQL 里要写成UPDATE ... SET status #{newStatus} WHERE id #{id} AND status #{oldStatus}用旧状态做乐观锁。返回 0 行不是异常是并发下另一个请求已经改了状态提示用户刷新即可。如果先查再无条件更新两个审核请求会同时通过重复生成入库单。4.2 库存扣减的并发控制库存扣减是 ERP 里最容易出账务问题的地方。常见错误是先查库存再更新查和更新之间没有锁并发下会超卖。正确做法有两种数据库乐观锁版本号或条件更新和悲观锁SELECT ... FOR UPDATE。中小团队用条件更新更简单。-- 出库扣减用 available_qty #{qty} 做条件避免扣成负数 UPDATE inv_stock SET available_qty available_qty - #{qty}, version version 1 WHERE material_id #{materialId} AND warehouse_id #{warehouseId} AND available_qty #{qty};这条 SQL 的关键是available_qty #{qty}它把校验和扣减放在同一条语句里数据库保证原子性。返回 0 行说明库存不足或并发被抢先业务层要回滚整张出库单不能只回滚这一行。version字段不是必须但加上后排查问题时能看到扣减次数。为什么不用SELECT ... FOR UPDATE行锁在库存扣减这种高频短事务里可以用但 ERP 的出库单往往一次扣多行物料按物料 ID 排序加锁能避免死锁排序逻辑写错就会互相等待。条件更新不需要显式排序数据库自己处理。4.3 权限与数据权限过滤ERP 的权限分两层功能权限能不能进采购模块和数据权限能看哪些仓库的单据。功能权限用 RBAC用户挂角色角色挂菜单和按钮。数据权限常见做法是在查询里拼部门或仓库条件不能只靠前端隐藏。这套系统的数据权限通常通过 MyBatis 拦截器或服务层参数实现。拦截器的方式对业务代码侵入小但要小心它把系统初始化的查询也拦了。服务层显式传参更可控代价是每个查询方法都要带权限参数。// 数据权限参数由登录上下文取不要从请求参数里取否则可被伪造 Long deptId SecurityContext.getCurrentDeptId(); ListPurchaseOrderDTO list orderMapper.selectOrderPage( query.getSupplierId(), query.getStatus(), query.getStartTime(), deptId);这里deptId必须从服务端登录上下文取不能从query里取。从请求参数取的话改一个 URL 参数就能看别的部门单据属于典型越权。selectOrderPage的 XML 里要加AND o.dept_id #{deptId}且这个条件不能被前端的动态查询覆盖。5. 避坑与排查五个让 ERP 上线翻车的细节5.1 时区与日期字段漂移现象本地测试单据创建时间是下午三点部署到测试环境后列表显示早上七点导出 Excel 又正常。原因数据库连接串没配serverTimezone或者 JVM 默认时区是 UTCMySQL 驱动按 UTC 解释datetime。导出正常是因为导出走的是另一条连接或另一套格式化逻辑。JVM 时区由操作系统和启动参数共同决定容器里默认 UTC 的情况很常见。解决连接串统一加serverTimezoneAsia/Shanghai启动参数加-Duser.timezoneAsia/Shanghai。已经写入的错误时间不要直接改库先确认是展示层偏移还是存储层偏移用SELECT NOW()和应用里new Date()对比差八小时就是时区问题不是数据问题。5.2 库存超卖与重复扣减现象并发压测时同一个物料库存扣成负数或者一张出库单点两次提交扣了两次。原因扣减 SQL 没有带available_qty qty条件或者接口没做幂等前端重复提交直接落到服务层。还有一种情况是扣减放在事务提交之后执行事务回滚时库存已经改了。解决扣减语句改成条件更新影响行数为 0 就抛业务异常回滚。接口幂等用单据号加唯一索引或者 Redis 的setnx加过期时间做提交锁。注意锁的 key 要带单据类型和单据号不能只用用户 ID否则同一用户不同单据会互相阻塞。5.3 多数据源下事务不生效现象采购入库方法上加了Transactional入库单写成功但库存流水没写两个操作不在同一个事务里。原因项目配了多数据源Transactional默认只认主数据源的事务管理器从库或第二数据源的操作不受管。也可能是同类内部方法调用Spring 代理没生效。解决多数据源场景显式指定事务管理器比如Transactional(transactionManager secondaryTransactionManager)。同类调用改成注入自身代理或拆到另一个 Bean。排查时在方法里打日志看连接对象两个操作拿到的Connection不是同一个就说明事务没覆盖。5.4 缓存与数据库不一致现象改了物料名称列表页还是旧名字重启 Redis 后才正常。原因更新数据库后没删缓存或者删了缓存但另一个节点又回填了旧值。基础数据缓存没设过期时间时这个问题会一直存在。解决更新逻辑统一成“先更新库再删缓存”不要更新缓存。缓存加过期时间兜底基础数据可以设 30 分钟到 2 小时。如果用了本地缓存加 Redis 两级删缓存要两级都删只删 Redis 会导致本机继续读旧值。5.5 前端打包后路由 404现象本地开发前端路由正常打包部署到 Nginx 后刷新页面 404直接访问登录页正常。原因前端用的是 history 路由模式Nginx 没配try_files刷新时请求的是真实路径服务器找不到对应文件。解决Nginx 配置里加try_files $uri $uri/ /index.html;把未匹配的路径回退到入口文件。如果部署在子路径下还要配publicPath或base否则静态资源也会 404。排查时看浏览器 Network 里失败的请求是 JS 还是 HTMLJS 失败是路径问题HTML 失败是回退规则没配。6. 二次开发验证清单改完代码怎么确认没把账做乱改 ERP 代码和改普通业务系统不一样普通系统看接口返回对不对ERP 要看账平不平。我一般在每次改动采购、库存、财务相关代码后跑一遍固定验证清单不依赖测试环境的手工点。验证项操作通过标准单据状态审核后重复审核第二次提示状态已变化不生成第二张入库单库存扣减并发 50 线程扣同一物料最终库存等于初始减扣减总量不为负数据权限用 A 部门账号查 B 部门单据列表为空直接改 URL 参数也查不到时区新建单据后查库和应用展示两边时间一致导出 Excel 一致事务入库时故意让流水表报错入库单和库存都不落库这张表不是走形式每一项都对应前面章节里的一个实现点。单据状态看乐观锁库存扣减看条件更新数据权限看参数来源时区看连接串事务看管理器配置。代码层面我习惯加一个最小的集成测试不追求覆盖率只守住“审核只成功一次”和“库存不扣负”两条// 只验证两条底线重复审核拦截、库存不扣负 Test void auditTwiceShouldFail() { Long orderId createPendingOrder(10); orderService.audit(orderId, 1L); assertThrows(BizException.class, () - orderService.audit(orderId, 1L)); }这个测试方法体里的createPendingOrder(10)是造一张数量为 10 的待审核单audit调两次第二次必须抛异常。不要在这里断言异常文案文案会变断言异常类型就够。库存扣负的测试同理造库存 5、扣 8断言抛异常且库存仍是 5。血泪经验是ERP 的 bug 很少是单个接口报错多数是“接口都成功但账不对”。从那以后我每次改完库存相关代码都会把上面这张表走一遍哪怕改动只有一行。这套开源 Java ERP 更适合当底座用开新项目时从它上面裁模块比从零搭省事你要是也在找能改得动的 ERP 底座可以按第 3 章的启动路径先跑起来再拿第 6 章这张表压一遍能过再往下深入。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询