简介:这是一套面向中小企业开发者与Java学习者的开源ERP进销存系统源码,围绕采购、销售、库存与财务等核心业务,帮助读者理解企业资源计划系统的完整实现思路。压缩包共1448个文件,约14.26MB,以png、gif、jpg等界面素材,java后端代码,js、html、css、jsp前端页面,以及xml、json、properties配置和sql数据库脚本为主,另含jar依赖与少量php、aspx等文件,覆盖前后端与数据库各层。系统采用MySQL存储商品、客户、供应商与交易数据,包含进销存模块、财务核算、角色权限管理、报表分析及数据库初始化脚本,并预留API集成扩展空间。目前已有769人学习下载,适合希望二次开发或搭建进销存管理平台的读者参考,可据此掌握模块划分、表结构设计与部署要点。
1. 拿到一套 ERP 进销存源码,先别急着改代码
很多做企业信息化的朋友,第一次拿到一套 ERP 进销存系统源码时,第一反应是打开 IDE 改界面、加字段、调菜单。我见过太多这样的案例:某开发者拿到源码当天就动手改采购单逻辑,结果第三天发现库存扣减对不上,第五天发现财务凭证生成错乱,最后只能推倒重来。这套源码本质上是一套基于 MySQL 的关系型业务系统,核心链路是「采购入库 → 库存变动 → 销售出库 → 应收应付 → 报表汇总」,任何一处改动都会沿着这条链路传导。它适合中小型商贸公司、批发零售场景,也适合作为二次开发的学习底座。但前提是,你得先把它跑起来、把数据流摸清楚,再谈改。这一章不讲代码,只讲拿到源码后前两小时该干什么。
2. 环境搭建与数据库初始化:把 MySQL 跑通再谈业务
2.1 技术栈确认与依赖清单
在动手之前,先把源码包里的技术栈摸清楚。常见的 ERP 进销存源码,后端多为 Java(Spring Boot / SSM)或 PHP(ThinkPHP / Laravel),前端多为 Vue 或 jQuery 老框架,数据库统一是 MySQL 5.7 或 8.0。打开源码根目录,先找这几个文件:pom.xml或composer.json(依赖管理)、application.yml或.env(数据库配置)、sql/目录(建表脚本)。这三个文件决定了你能不能跑起来。
我一般会先看sql/目录下的文件数量和大小。如果只有一个init.sql,说明建表和数据在一起;如果有schema.sql和data.sql分开,说明结构清晰,适合二次开发。注意看建表语句里的存储引擎,InnoDB 是必须的,MyISAM 不支持事务,进销存系统里库存扣减和财务凭证必须走事务,遇到 MyISAM 直接改。
依赖版本这块,血泪经验是:不要盲目升级。源码里写死 Spring Boot 2.3.x,你就别升到 3.x,JDK 版本从 8 升到 17 看起来爽,但 MyBatis 和某些老工具类会直接翻车。常见做法是先用源码推荐的版本跑通,再考虑渐进升级。
2.2 数据库建库与导入的完整命令
假设你已经装好了 MySQL 8.0,接下来按顺序执行。先建库,字符集用utf8mb4,排序规则用utf8mb4_general_ci,不要用utf8,否则商品名称里的特殊字符会丢。
# 登录 MySQL,注意 -p 后面不要加空格 mysql -u root -p # 建库,字符集必须 utf8mb4 CREATE DATABASE erp_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 建一个专用账号,别用 root 跑业务 CREATE USER 'erp_user'@'%' IDENTIFIED BY 'Erp@2024'; GRANT ALL PRIVILEGES ON erp_db.* TO 'erp_user'@'%'; FLUSH PRIVILEGES; EXIT; # 导入建表脚本,注意路径替换成你本地的 mysql -u erp_user -p erp_db < /path/to/sql/schema.sql # 导入初始数据 mysql -u erp_user -p erp_db < /path/to/sql/data.sql这段命令的逻辑说明:先建库再建账号,权限只给erp_db,避免误操作其他库。导入时用-u erp_user而不是 root,提前验证账号权限是否正常。如果schema.sql里包含CREATE DATABASE语句,先注释掉再导入,否则会报库已存在。
参数说明:utf8mb4_general_ci比utf8mb4_unicode_ci快,进销存系统里商品名称、客户地址用 general_ci 足够。如果源码要求utf8mb4_0900_ai_ci(MySQL 8.0 默认),那就按源码来,别自作主张改排序规则,否则外键关联可能出问题。
2.3 配置文件修改与启动验证
数据库导入完成后,找到后端配置文件。Spring Boot 项目改application-dev.yml,PHP 项目改.env。重点改四个参数:数据库地址、端口、账号、密码。连接池参数也要看一眼,进销存系统并发不高,但库存扣减时会有行锁竞争,连接池最小空闲连接建议设为 5,最大 20。
# application-dev.yml 关键片段 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/erp_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: erp_user password: Erp@2024 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000改完后启动项目,观察控制台日志。如果看到Started Application in X seconds,说明后端起来了。前端如果是 Vue 项目,npm install后npm run dev,注意代理配置里的后端地址要和实际端口一致。启动成功后,用默认账号登录(常见是 admin/123456),进去先看「商品管理」「库存查询」「采购订单」三个菜单能不能正常打开,数据能不能加载。这一步过了,环境就算通了。
3. 核心表结构与库存扣减逻辑:别让并发把你坑了
3.1 进销存三张主表与关联关系
任何 ERP 进销存系统,核心都绕不开三张表:商品表、库存表、出入库流水表。商品表存基础信息(编码、名称、规格、单位),库存表存当前数量(商品 ID、仓库 ID、数量、锁定数量),流水表存每一次变动(单据号、商品 ID、变动类型、变动数量、变动后数量)。这三张表的关系是:商品表 1 对多 库存表,库存表 1 对多 流水表。
常见做法是库存表加一个version字段做乐观锁,或者直接用SELECT ... FOR UPDATE做悲观锁。我一般会先看源码用的是哪种。如果是乐观锁,扣减时 SQL 长这样:
UPDATE stock SET quantity = quantity - #{num}, version = version + 1 WHERE product_id = #{productId} AND warehouse_id = #{warehouseId} AND quantity >= #{num} AND version = #{version};这条 SQL 的逻辑是:只有库存充足且版本号匹配时才扣减,返回影响行数为 0 就说明扣减失败,需要重试或提示。参数说明:quantity >= #{num}是防止超卖的关键条件,没有这个条件,并发时库存会扣成负数。version字段是乐观锁标记,每次更新加 1。
如果是悲观锁,SQL 是SELECT quantity FROM stock WHERE ... FOR UPDATE,然后在事务里更新。悲观锁在并发高时容易死锁,进销存系统并发通常不高,两种都能用,但源码用哪种你就跟着用哪种,别混搭。
3.2 库存扣减的完整事务流程
库存扣减不是一条 SQL 就完事,它必须和单据状态、流水记录在同一个事务里。我拆过一套源码,它的销售出库流程是这样的:先校验订单状态,再锁库存,再扣减,再写流水,再改订单状态。五步必须在同一个@Transactional里,任何一步失败全部回滚。
@Transactional(rollbackFor = Exception.class) public void saleOutbound(Long orderId) { // 1. 查订单,校验状态 Order order = orderMapper.selectById(orderId); if (order.getStatus() != OrderStatus.PAID) { throw new BizException("订单状态不允许出库"); } // 2. 遍历明细,逐条扣减库存 for (OrderItem item : order.getItems()) { // 3. 乐观锁扣减,返回 0 说明库存不足或版本冲突 int affected = stockMapper.deduct(item.getProductId(), item.getWarehouseId(), item.getNum(), item.getVersion()); if (affected == 0) { throw new BizException("库存不足或并发冲突,商品:" + item.getProductId()); } // 4. 写流水 stockFlowMapper.insert(buildFlow(item, order)); } // 5. 改订单状态 order.setStatus(OrderStatus.OUTBOUND); orderMapper.updateById(order); }这段代码的关键点:rollbackFor = Exception.class确保任何异常都回滚,包括检查异常。扣减返回 0 时直接抛异常,整个事务回滚,不会出现「扣了一半」的情况。流水记录在扣减成功后写入,保证流水和库存一致。
参数说明:item.getVersion()是从库存表查出来的版本号,如果源码里没有版本号字段,说明用的是悲观锁,那deduct方法里应该先SELECT ... FOR UPDATE。两种方式不要混用,否则锁失效。
3.3 库存对账与流水追溯
系统跑一段时间后,库存表的数据和流水表汇总的数据可能对不上。常见原因是:有人直接改数据库、事务中途失败但没回滚干净、或者并发扣减时乐观锁重试逻辑有 bug。我一般会写一个对账 SQL,定期跑一遍。
-- 对账:库存表当前数量 vs 流水表汇总数量 SELECT s.product_id, s.warehouse_id, s.quantity AS stock_qty, COALESCE(SUM(f.change_num), 0) AS flow_qty, s.quantity - COALESCE(SUM(f.change_num), 0) AS diff FROM stock s LEFT JOIN stock_flow f ON s.product_id = f.product_id AND s.warehouse_id = f.warehouse_id GROUP BY s.product_id, s.warehouse_id, s.quantity HAVING diff != 0;这条 SQL 的逻辑是:把库存表的当前数量和流水表的变动汇总做对比,diff不为 0 的就是对不上的记录。参数说明:change_num是流水表的变动数量,入库为正、出库为负,汇总后应该等于当前库存。如果源码的流水表用type字段区分出入库,那汇总时要加CASE WHEN转换正负号。
发现对不上之后,不要直接改库存表,先查流水表里该商品的所有记录,按时间排序,找到第一笔对不上的流水,往前推。常见原因是某笔出库流水写了但库存没扣,或者库存扣了但流水没写。修复时补一笔调整流水,而不是直接改库存数字,这样追溯链才完整。
4. 权限模块与二次开发入口:改哪里不会崩
4.1 权限表结构与菜单加载逻辑
ERP 系统的权限模块通常由五张表组成:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。用户登录后,后端根据用户 ID 查角色,再根据角色查菜单,前端拿到菜单树后动态渲染路由。这套逻辑看起来简单,但改的时候容易崩,因为菜单表和前端路由是强绑定的。
我一般会先看菜单表里的path字段和前端路由配置文件是否一致。如果源码前端是 Vue,路由文件在src/router/index.js,菜单表里的path必须和路由里的path完全匹配,否则点击菜单会白屏。常见做法是:新增菜单时,先在菜单表插入记录,再在前端路由文件里加对应组件,两边path和component要对上。
-- 查角色对应的菜单树 SELECT m.id, m.parent_id, m.name, m.path, m.component FROM menu m JOIN role_menu rm ON m.id = rm.menu_id WHERE rm.role_id = #{roleId} AND m.status = 1 ORDER BY m.sort_order;参数说明:status = 1表示菜单启用,sort_order控制排序。如果源码没有status字段,说明菜单不支持禁用,二次开发时要注意别删菜单,只能改name和path。
4.2 新增业务模块的四个步骤
二次开发最常见的需求是加一个新模块,比如「供应商对账」。按这四步走,基本不会崩。
第一步,建表。新模块独立建表,不要往现有表里加字段。表名加前缀,比如biz_supplier_reconcile,避免和源码原有表冲突。
第二步,后端加 Controller、Service、Mapper。Controller 里加@RequiresPermissions("biz:reconcile:list")之类的权限注解,Service 里写业务逻辑,Mapper 里写 SQL。注意包名要和源码一致,否则 Spring 扫描不到。
第三步,菜单表插入记录。parent_id指向「财务管理」或「供应商管理」的菜单 ID,path设为/biz/reconcile,component设为biz/reconcile/index。
第四步,前端加路由和页面。路由文件里加{ path: '/biz/reconcile', component: () => import('@/views/biz/reconcile/index.vue') },然后在views/biz/reconcile/下建index.vue。
这四步的顺序不能乱,先建表再写后端,后端跑通了再配菜单和前端。我见过有人先改前端,结果后端接口 404,排查半天。
4.3 权限注解与数据隔离
权限注解只控制「能不能访问接口」,不控制「能看哪些数据」。进销存系统里,不同仓库的管理员只能看自己仓库的库存,这需要在 SQL 里加数据隔离条件。
// 在 Service 里获取当前用户的数据权限范围 Long userId = SecurityUtils.getUserId(); List<Long> warehouseIds = warehouseService.getWarehouseIdsByUser(userId); // 查询库存时带上仓库范围 List<Stock> stocks = stockMapper.selectByWarehouseIds(warehouseIds);参数说明:getWarehouseIdsByUser根据用户角色查关联的仓库 ID 列表,管理员返回全部,普通用户返回部分。SQL 里用IN条件过滤。如果源码没有数据隔离逻辑,二次开发时一定要补上,否则不同仓库的人能看到别人的库存,业务上会出大问题。
5. 避坑与排查:那些让我加班到凌晨的坑
5.1 库存扣成负数
现象:并发测试时,库存表出现负数,流水表里出库数量大于入库数量。
原因:扣减 SQL 里没有quantity >= #{num}条件,或者用了乐观锁但没检查影响行数,直接更新了。
解决:在扣减 SQL 里强制加quantity >= #{num},更新后检查affected rows,为 0 就抛异常回滚。如果是悲观锁,SELECT ... FOR UPDATE之后要在事务里重新校验数量。
5.2 单据编号重复
现象:采购订单号或销售订单号出现重复,数据库唯一索引报错。
原因:单据编号生成用了SELECT MAX(no) + 1,并发时两个请求拿到同一个最大值。
解决:改用数据库序列或 Redis 自增,或者用时间戳 + 随机数 + 用户 ID 组合。如果源码用的是MAX + 1,直接改成INSERT时用UUID或雪花算法生成,别在应用层拼。
5.3 事务不回滚
现象:库存扣减失败抛了异常,但订单状态还是改了,数据不一致。
原因:异常被try-catch吞了,或者抛的是检查异常但@Transactional没配rollbackFor。
解决:@Transactional(rollbackFor = Exception.class)必须加,catch块里如果要处理异常,处理完必须重新抛出RuntimeException,否则事务不会回滚。
5.4 菜单改了但前端不生效
现象:菜单表里改了path,前端点击还是跳旧页面。
原因:前端路由是静态配置的,菜单表只控制显示隐藏,不控制路由跳转。
解决:改菜单表的同时,同步改前端路由文件。如果源码支持动态路由,检查后端返回的菜单树里component字段是否正确映射到前端组件路径。
5.5 报表查询慢
现象:月度销售报表查询超过 10 秒,数据库 CPU 飙升。
原因:流水表数据量大,报表 SQL 没走索引,或者用了SELECT *全表扫描。
解决:在流水表的create_time、product_id、warehouse_id上建联合索引,报表 SQL 只查需要的字段,避免SELECT *。如果数据量超过百万,考虑按月分表或加汇总表。
6. 二次开发进阶:从能跑到好用
6.1 用定时任务做库存日结
库存对账不能靠人工跑 SQL,我一般会加一个定时任务,每天凌晨 2 点跑一次对账,把差异记录写进日志表,第二天上班直接看日志。
@Scheduled(cron = "0 0 2 * * ?") public void dailyStockReconcile() { List<StockDiff> diffs = stockMapper.selectDiff(); if (!diffs.isEmpty()) { log.error("库存对账差异:{} 条", diffs.size()); diffLogMapper.batchInsert(diffs); } }参数说明:cron = "0 0 2 * * ?"表示每天凌晨 2 点执行。selectDiff就是前面那条对账 SQL。差异记录写日志表,不要直接改库存,保留追溯依据。
6.2 用 AOP 记录关键操作日志
进销存系统里,谁改了库存、谁删了订单,必须留痕。我一般会写一个 AOP 切面,拦截所有带@Log注解的方法,把操作人、操作时间、方法名、参数写进日志表。
@Around("@annotation(logAnno)") public Object around(ProceedingJoinPoint point, Log logAnno) throws Throwable { Object result = point.proceed(); SysLog sysLog = new SysLog(); sysLog.setUserId(SecurityUtils.getUserId()); sysLog.setMethod(point.getSignature().getName()); sysLog.setParams(JSON.toJSONString(point.getArgs())); sysLog.setCreateTime(new Date()); sysLogMapper.insert(sysLog); return result; }参数说明:point.getArgs()是方法参数,序列化后存库。注意别把密码之类的敏感参数记进去,可以在切面里过滤。
6.3 验证二次开发是否成功的三个检查点
改完代码后,别急着上线,先过这三个检查点。
第一,库存对账 SQL 跑一遍,diff必须为 0。第二,并发测试跑一遍,用 JMeter 模拟 50 个并发扣减同一商品,库存不能为负,流水条数要和扣减次数一致。第三,权限测试跑一遍,用不同角色的账号登录,确认只能看到自己仓库的数据,越权接口返回 403。
这三个检查点过了,基本可以放心。从那以后我每次改完库存相关代码,都强制走一遍对账 SQL 和并发测试,再也不敢直接点「保存」就完事。希望帮到你。
本文还有配套的精品资源,点击获取