大家好,我是你们的源码拆解的腻害兔,今天继续 RuoYi-Vue-Pro(芋道)系列。前面我们已经拆了框架层、认证权限、多租户、工作流 BPM、支付、CRM 等模块,今天终于来到了很多读者翘首以盼的ERP 企业资源模块。
为什么说是"翘首以盼"?因为进销存是中小企业信息化的第一步,也是很多开发者接私活时最常碰到的需求。今天这篇文章,我会从一个产品经理 + 技术分析师的双重视角,带大家把这个模块看个通透。废话不多说,直接上干货!
一、今日模块概览
一句话概括:ERP 模块是 RuoYi-Vue-Pro 里的"轻量级进销存 + 简易财务"系统,覆盖了产品管理、采购(订单→入库→退货)、销售(订单→出库→退货)、库存(入库/出库/调拨/盘点)、财务(收款/付款)五大业务域,共 23 个 Controller、23 对 Service、33 张数据库表。
它不是 SAP 那种重型 ERP,而是面向中小企业的"够用就好"方案——砍掉了生产计划、MRP、质检等重流程,保留了最核心的"买进来、卖出去、管库存、收付款"闭环。
二、技术选型分析
老规矩,先上结论,再看原因。
| 技术点 | 选型 | 替代方案 | 为什么这么选 |
|---|---|---|---|
| ORM 框架 | MyBatis-Plus | JPA/Hibernate、原生 MyBatis | 进销存场景大量复杂查询(库存汇总、统计报表),JPA 的 HQL 在这种场景下性能调优困难;MyBatis-Plus 在保留 SQL 灵活性的同时提供了 CRUD 增强,开发效率高 |
| 单号生成 | Redis INCR | 数据库序列、UUID、雪花算法 | ERP 单号需要"前缀+日期+自增序号"的格式(如 CGDD20260721000001),Redis 原子自增天然适合,且性能远高于数据库方案。UUID 不可读,雪花算法太长,都不适合做业务单号 |
| 审批状态 | 两态审核(PROCESS/APPROVE) | Flowable 工作流、多态审批 | 进销存的审批逻辑非常简单——"草稿→审核"两态流转,上 Flowable 太重了。一个 status 字段 + updateByIdAndStatus 乐观锁就够了 |
| 库存更新 | 增量更新(Increment) | 全量覆盖、乐观锁 CAS | 库存是典型的并发热点,增量更新 count = count + delta 配合数据库行锁是最简洁可靠的方案 |
| 主子表更新 | diffList 算法 | 全删全插、逐条比对 | 订单明细的"新增/修改/删除"用 diffList 对比新旧列表,一次操作完成三种变更,比全删全插更安全(保留已有 ID),比逐条比对更高效 |
| 模块依赖 | 直接依赖 system 模块 | Feign 远程调用、事件驱动 | ERP 模块需要获取用户信息(AdminUserApi),但作为单体应用,直接依赖比远程调用简单得多,性能也更好 |
划重点!!!这套选型的核心思路是"够用就好,不过度设计"。很多技术团队一上来就微服务、就工作流引擎、就分布式锁,但对于 90% 的中小企业进销存场景,Redis 生成单号 + 数据库行锁更新库存 + 两态审核,已经完全够用了。
三、需求溯源推演
读完整个模块的代码,我尝试还原一下这个模块最初的产品需求。
3.1 谁在什么场景下提出的需求?
想象一下:一家年营收 500 万~5000 万的商贸公司,老板 + 3 个销售 + 2 个采购 + 1 个仓管 + 1 个财务。之前用 Excel 管进销存,经常出现这些问题:
- 采购员:下了采购订单,到货了忘了入库,导致库存数据对不上
- 仓管:盘点的时候发现实物和系统数量不一致,不知道什么时候出的错
- 财务:月底对账,付款记录和采购订单对不上,一笔一笔翻纸质单据
- 老板:想看这个月的采购额、销售额、毛利,没人能马上给出来
于是老板说:"能不能搞个系统,采购下单、仓库收货、财务付款,都在一个地方操作?"
3.2 如果写 PRD,大概长什么样?
【产品需求文档(推测还原)】 一、项目背景 公司目前使用 Excel 管理进销存,数据不一致、协作效率低, 需要一套 B/S 架构的在线进销存系统。 二、核心功能 1. 基础数据管理:产品(含分类、单位)、供应商、客户、仓库、结算账户 2. 采购管理:采购订单 → 采购入库 → 采购退货,支持审核/反审核 3. 销售管理:销售订单 → 销售出库 → 销售退货,支持审核/反审核 4. 库存管理:其他入库/出库、库存调拨、库存盘点,实时库存查询 5. 财务管理:付款单(对供应商)、收款单(对客户),关联采购/销售单据 三、非功能需求 - 支持多用户同时操作,库存数据实时一致 - 单据编号自动生成,格式:类型前缀+日期+流水号 - 已审核的单据不可修改,需要反审核才能操作 - 库存不允许为负数(可配置)从最终代码来看,芋道基本 100% 覆盖了这些需求,甚至还多做了统计报表(采购/销售金额汇总、月度趋势)和 Excel 导出,算是超预期交付了。
四、竞品对标分析
说到开源进销存/ERP,市面上有几个直接竞品值得对比:
| 对比维度 | RuoYi-Vue-Pro ERP | 华夏 ERP | 进销存(JEECG 生态) | 秦丝进销存(商业) |
|---|---|---|---|---|
| 技术栈 | Spring Boot + MyBatis-Plus | Spring Boot + MyBatis | Spring Boot + MyBatis-Plus | 闭源 SaaS |
| 采购流程 | 订单→入库→退货,三单关联 | 订单→入库→退货 | 订单→入库→退货 | 订单→入库→退货 |
| 销售流程 | 订单→出库→退货,三单关联 | 订单→出库→退货 | 订单→出库→退货 | 订单→出库→退货 |
| 库存操作 | 入库/出库/调拨/盘点 4 种 | 入库/出库/调拨 3 种 | 入库/出库 2 种 | 入库/出库/调拨/盘点 |
| 财务管理 | 收款/付款,关联具体单据 | 简单的收付款记录 | 无 | 收付款+对账单 |
| 审批机制 | 两态审核(轻量) | 两态审核 | 无审批 | 多级审批 |
| 库存预警 | ❌ 暂无 | ✅ 有 | ❌ 无 | ✅ 有 |
| 多仓库 | ✅ 支持 | ✅ 支持 | ❌ 单仓库 | ✅ 支持 |
| 统计报表 | 采购/销售金额汇总+月度趋势 | 简单报表 | 无 | 丰富报表 |
| 开源协议 | MIT | Apache 2.0 | Apache 2.0 | 商业授权 |
RuoYi ERP 的优势
- 架构最干净:基于 RuoYi-Vue-Pro 的模块化架构,ERP 模块和其他模块(系统管理、工作流等)解耦清晰,可以独立部署也可以合并部署
- 库存模型最完整:4 种库存操作(入库/出库/调拨/盘点)+ 16 种明细类型(含取消冲销),覆盖了绝大多数进销存场景
- 单据关联最紧密:采购入库单关联采购订单,付款单关联入库单,形成了完整的"订单→入库→付款"链路追溯
- 代码质量高:统一的 diffList 主从表更新模式、Redis 单号生成、乐观锁状态更新,代码风格一致性好
RuoYi ERP 的劣势
- 缺少库存预警:没有安全库存、最大库存的告警机制
- 缺少批次/序列号管理:无法追踪具体哪个批次的产品出了问题
- 缺少多单位支持:一个产品只有一个计量单位,无法处理"箱/个"换算
- 财务模块偏简单:只有收付款,缺少应收应付汇总、账龄分析
- 审批流程过于简单:只有两态审核,缺少多级审批、审批流配置
五、核心业务流程
5.1 采购全流程
采购订单(CGDD) ──审核──→ 采购入库(CGRK) ──审核──→ 库存增加 │ │ │ └──→ 付款单(FKD) ──审核──→ 更新入库单已付金额 │ └──审核──→ 采购退货(CGTH) ──审核──→ 库存减少 │ └──→ 退款(关联退货单)用 Mermaid 画出来更直观:
5.2 库存变更的核心链路
这是整个 ERP 模块最精妙的设计——所有库存变更都通过 ErpStockRecordService.createStockRecord() 统一入口:
业务Service.updateXxxStatus(id, APPROVE) │ ├── 遍历明细项 │ └──→ stockRecordService.createStockRecord(BO) │ ├── 1. stockService.updateStockCountIncrement(productId, warehouseId, count) │ → 原子更新 erp_stock 表的 count 字段 │ → 如果结果 < 0 且不允许负库存,抛异常 │ └── 2. stockRecordMapper.insert(stockRecord) → 写入 erp_stock_record 表,记录变更明细这个设计的好处是:库存变更只有一个入口,所有业务场景(采购入库、销售出库、调拨、盘点等)都走同一条路。这样保证了库存数据的一致性,也方便排查问题——任何库存变动都能在 erp_stock_record 表里找到记录。
5.3 库存调拨的双记录设计
调拨是个有意思的场景:同一个产品,从 A 仓库移到 B 仓库。代码里的处理是创建两条库存明细:
// 源仓库出库(负数) stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO( productId, fromWarehouseId, count.negate(), MOVE_OUT, ...)); // 目标仓库入库(正数) stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO( productId, toWarehouseId, count, MOVE_IN, ...));一个调拨动作产生一正一负两条记录,既保证了各仓库的库存准确,又能在明细表里追溯"这批货是从哪个仓库调过来的"。
六、数据模型解读
整个 ERP 模块共 33 张表,分为 5 个域。我来挑几个关键设计讲讲。
6.1 产品表(erp_product)
erp_product ├── id, name, barCode -- 基础信息 ├── categoryId → erp_product_category -- 分类(树形结构) ├── unitId → erp_product_unit -- 计量单位 ├── purchasePrice, salePrice, minPrice -- 采购价/销售价/最低售价 ├── weight, standard, expiryDay -- 重量/规格/保质期 └── status -- 启用/禁用产品分类用的是自关联树形结构(parentId 指向自身表的 id),这是最常见的分类方案。代码里有完整的校验逻辑:不能设自己为父分类、不能设子分类为父分类(防环)、删除前检查是否有子分类或产品引用。
6.2 采购订单主从表
erp_purchase_order (主表) ├── no (唯一单号) ├── status (审核状态) ├── supplierId → erp_supplier ├── accountId → erp_account ├── totalCount, totalProductPrice, totalTaxPrice, totalPrice, discountPrice ├── inCount (已入库总数), returnCount (已退货总数) └── depositPrice (定金) erp_purchase_order_items (从表) ├── orderId → erp_purchase_order ├── productId → erp_product ├── count, productPrice, totalPrice, taxPercent, taxPrice ├── inCount (该项已入库数), returnCount (该项已退货数) └── productUnitId (冗余存储,避免每次查产品表)设计亮点:主表的 inCount 和 returnCount 是冗余字段,记录了"已经入了多少货 / 退了多少货"。这样做的好处是:反审核订单时可以快速判断"是否已经有下游单据",而不需要 SUM 所有入库单的明细。这是典型的空间换时间策略。
6.3 库存表(erp_stock)——最精简的设计
erp_stock ├── productId -- 产品ID ├── warehouseId -- 仓库ID ├── count -- 当前库存数量 └── (productId + warehouseId 联合唯一)这张表只有 4 个字段(不算 id),是真正意义上的"产品+仓库=库存"。没有批次号、没有序列号、没有有效期——这就是"轻量级"的含义。
6.4 库存明细表(erp_stock_record)——审计追踪的核心
erp_stock_record ├── productId, warehouseId -- 哪个产品在哪个仓库 ├── count -- 本次变更数量(正数=入库,负数=出库) ├── totalCount -- 变更后的库存总量 ├── bizType -- 业务类型(16种:采购入库/销售出库/调拨...) ├── bizId, bizItemId, bizNo -- 关联的业务单据 └── createTime -- 变更时间这张表就是库存的流水账。bizType 用多态关联(同一个字段区分 16 种业务类型),bizId + bizItemId 可以精确定位到具体单据的具体行项。
6.5 财务收付款表——通过 bizType 关联
erp_finance_payment_item ├── paymentId → erp_finance_payment ├── bizType (采购入库 / 采购退货) ├── bizId → 具体的入库单或退货单 ├── totalPrice (应付总额) ├── paidPrice (已付金额) └── paymentPrice (本次付款金额)这里用 bizType + bizId 实现了付款单和入库单/退货单的关联。好处是:一张付款单可以关联多个入库单(合并付款),也可以只关联一个入库单的部分金额(分期付款)。
七、产品设计亮点与槽点
7.1 让我眼前一亮的地方
1. Redis 单号生成器——简洁优雅
public String generate(String prefix) { String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN); String key = RedisKeyConstants.NO + noPrefix; Long no = stringRedisTemplate.opsForValue().increment(key); stringRedisTemplate.expire(key, Duration.ofDays(1L)); return noPrefix + String.format("%06d", no); }5 行核心代码,解决了"高并发下唯一单号生成"的问题。中文拼音前缀(CGDD = 采购订单、XSCK = 销售出库)对中国用户非常友好,每天自动重置序号(key 过期时间 1 天),6 位序号支持每天 99 万张单据,对中小企业绰绰有余。
2. 统一的库存变更入口
所有库存变动都走 ErpStockRecordService.createStockRecord(),这个方法做两件事:更新库存数量 + 记录变更明细。这种"单一入口"设计让库存逻辑非常集中,改一处全局生效,排查问题也方便。
3. 反审核的级联保护
// 存在采购入库单,无法反审核 if (!approve && purchaseOrder.getInCount().compareTo(BigDecimal.ZERO) > 0) { throw exception(PURCHASE_ORDER_PROCESS_FAIL_EXISTS_IN); } // 存在采购退货单,无法反审核 if (!approve && purchaseOrder.getReturnCount().compareTo(BigDecimal.ZERO) > 0) { throw exception(PURCHASE_ORDER_PROCESS_FAIL_EXISTS_RETURN); }这种"有下游单据就不能反审核上游"的保护机制,防止了数据链路的断裂。你想啊,如果采购订单已经入了库,还能反审核修改订单数量,那入库单的数据就对不上了。
4. 错误码的体系化设计
整个 ERP 模块使用 1-030-XXX-YYY 的错误码段,按业务域分段(100=供应商、101=采购订单、102=采购入库...),每个错误码都有中文描述。这种设计在大型项目中非常重要——当用户看到一个错误码,运维人员可以直接定位到是哪个模块的哪个问题。
7.2 我觉得可以改进的地方
1. 缺少库存预警机制
代码里有一行注释暴露了这个问题:
// TODO 芋艿:库存低于安全库存 / 高于最大库存的告警对于实际的进销存系统,库存预警是刚需。建议在 erp_product 表增加 safetyStock(安全库存)和 maxStock(最大库存)字段,在库存变更时异步检查并发送通知。
2. 错误码有一个 Bug
我在读代码时发现,库存调拨单(Stock Move)的错误码注释写的是 1-030-403-000,但实际值用的是 1_030_402_xxx,和"其他出库单"的错误码段重叠了。虽然不影响运行(错误码值不重复就行),但会给维护带来困扰。
3. 负库存控制硬编码
// 当前是硬编码的 false,不允许负库存 private static final Boolean NEGATIVE_STOCK_COUNT_ENABLE = false;这个应该做成系统配置项,让管理员可以在界面上开关。有些行业(如预售模式)是允许负库存的。
4. Controller 层的数据组装逻辑偏重
采购/销售 Controller 的 get 和 page 方法里注入了大量 Service(StockService、ProductService、SupplierService、AdminUserApi),在 Controller 层做数据组装。更优雅的做法是在 Service 层提供一个"富查询"方法,或者用 CQRS 模式单独做一个查询 Service。
5. 缺少操作日志
代码里多处出现 // TODO 芋艿:记录操作日志 的注释。对于 ERP 系统,操作日志(谁在什么时候审核了什么单据、修改了什么数据)是审计的刚需,建议优先补上。
八、发散性思考
8.1 这个模块还能做什么?
- 增加条码/二维码支持:产品表已经有 barCode 字段,可以对接扫码枪,实现"扫码入库""扫码出库"
- 增加审批流集成:对接 BPM 模块,让采购订单超过一定金额时走多级审批
- 增加供应商/客户对账功能:基于收付款记录和订单数据,自动生成对账单
- 增加简单的 MRPII:在采购订单的基础上,根据销售订单的物料需求自动计算采购建议
- 对接电子发票:供应商/客户表已经有 taxNo(税号)字段,可以对接税务系统
8.2 如果让我重新设计
- 库存表增加批次字段:batchNo + productionDate + expiryDate,支持先进先出(FIFO)和批次追溯
- 引入事件驱动:库存变更时发布 StockChangedEvent,让预警、通知、统计等下游逻辑异步解耦
- 增加价格策略:支持不同客户等级不同售价、阶梯定价、历史价格查询
- 库存盘点增加 PDA 支持:提供移动端接口,仓管可以拿着 PDA 边扫边盘
- 财务报表增强:增加应收应付账龄分析、资金流水、利润表
8.3 技术思路的迁移
这套"轻量级进销存"的设计思路可以迁移到很多场景:
- 医院药品管理:产品→药品,仓库→药房/药库,采购入库→药品入库,销售出库→发药
- 学校资产管理:产品→资产,仓库→楼宇/房间,调拨→资产转移,盘点→资产清查
- 餐饮原料管理:产品→食材,仓库→冷库/干仓,入库→采购收货,出库→领料做菜
- 电商仓储管理:产品→SKU,仓库→前置仓,出库→发货,退货→逆向物流
核心的"主从表 + 审核状态 + 库存增量更新 + 流水记录"这套模式,几乎可以原封不动地复用。
九、关键代码导读
最后,列出 5 个最值得细读的代码文件,按推荐优先级排序:
1. ErpStockRecordServiceImpl.java —— 库存变更的"总闸门"
路径:yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordServiceImpl.java
为什么值得读:只有 53 行,却是整个 ERP 库存模块的核心。所有库存变动(采购入库、销售出库、调拨、盘点等 16 种场景)都通过这里的 createStockRecord() 方法完成。理解了这个方法,就理解了整个库存系统的运作原理。
2. ErpNoRedisDAO.java —— Redis 单号生成器
路径:yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
为什么值得读:96 行代码,展示了一个生产级的"基于 Redis 的业务单号生成器"。中文拼音前缀 + 日期 + 自增序号的方案,可以直接复用到你自己的项目里。
3. ErpPurchaseOrderServiceImpl.java —— 主从表 CRUD 的教科书
路径:yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
为什么值得读:295 行代码,完整展示了"主从表"模式的最佳实践——创建时级联插入、更新时 diffList 对比、审核时状态保护、删除时级联校验、价格计算(含税 + 折扣)。这个模式可以套用到几乎所有"订单 + 明细"的业务场景。
4. ErpStockMoveServiceImpl.java —— 调拨的双记录设计
路径:yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockMoveServiceImpl.java
为什么值得读:229 行代码,核心看点在 updateStockMoveStatus() 方法——一次调拨产生两条方向相反的库存记录(源仓库出库 + 目标仓库入库),反审核时再产生两条冲销记录。这种"正反配对"的设计思路,在处理复杂库存变动时非常值得借鉴。
5. ErrorCodeConstants.java —— 错误码的体系化设计
路径:yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
为什么值得读:168 行,定义了 ERP 模块全部错误码(约 80 个),按业务域分段编号(1-030-100=供应商、1-030-101=采购订单...)。当你需要为一个大型系统设计错误码规范时,这个文件是很好的参考。顺便也能发现前面提到的调拨单错误码段重叠的小 Bug。
写在最后
RuoYi-Vue-Pro 的 ERP 模块,是我看到的开源项目里代码质量较高的轻量级进销存实现。它没有追求大而全的功能覆盖,而是在"采购-销售-库存-财务"这条主链路上做得足够扎实。代码风格统一、设计模式一致、业务逻辑清晰,非常适合作为学习进销存系统设计的参考代码。
如果你正在做一个进销存相关的项目,我强烈建议你把这 5 个文件读透——不是因为它用了什么高深的技术,而是因为它展示了一种"恰到好处"的设计哲学:不炫技、不过度,用最简单的方式解决最核心的问题。
看到这里,觉得有用的话,点个赞支持一下呗~ 你的支持是我继续拆解源码的动力!
系列文章导航
| 序号 | 模块 | 状态 |
|---|---|---|
| 1 | 框架层 yudao-framework | ✅ 已完成 |
| 2 | 认证与权限 auth-security-permission | ✅ 已完成 |
| 3 | 用户与组织 user-dept-role | ✅ 已完成 |
| 4 | 多租户 tenant | ✅ 已完成 |
| 5 | 字典/短信/邮件/通知 | ✅ 已完成 |
| 6 | 代码生成器 codegen | ✅ 已完成 |
| 7 | 文件/配置/任务/日志 | ✅ 已完成 |
| 8 | 工作流 BPM | ✅ 已完成 |
| 9 | 支付模块 | ✅ 已完成 |
| 10 | CRM 客户关系 | ✅ 已完成 |
| 11 | ERP 企业资源 | ✅ 本篇 |
| 12 | 商城-商品与交易 | 🔜 下一篇 |
下一篇预告:商城模块(yudao-module-mall)——商品管理、订单流程、营销活动、数据统计,RuoYi 是怎么做电商的?和 ERP 模块的"进销存"有什么异同?敬请期待!