【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:ERP 企业资源模块,一个轻量级进销存的完整实现?

大家好,我是你们的源码拆解的腻害兔,今天继续 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,大概长什么样?

java 复制代码
【产品需求文档(推测还原)】

一、项目背景
公司目前使用 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 的优势

  1. 架构最干净:基于 RuoYi-Vue-Pro 的模块化架构,ERP 模块和其他模块(系统管理、工作流等)解耦清晰,可以独立部署也可以合并部署
  2. 库存模型最完整:4 种库存操作(入库/出库/调拨/盘点)+ 16 种明细类型(含取消冲销),覆盖了绝大多数进销存场景
  3. 单据关联最紧密:采购入库单关联采购订单,付款单关联入库单,形成了完整的"订单→入库→付款"链路追溯
  4. 代码质量高:统一的 diffList 主从表更新模式、Redis 单号生成、乐观锁状态更新,代码风格一致性好

RuoYi ERP 的劣势

  1. 缺少库存预警:没有安全库存、最大库存的告警机制
  2. 缺少批次/序列号管理:无法追踪具体哪个批次的产品出了问题
  3. 缺少多单位支持:一个产品只有一个计量单位,无法处理"箱/个"换算
  4. 财务模块偏简单:只有收付款,缺少应收应付汇总、账龄分析
  5. 审批流程过于简单:只有两态审核,缺少多级审批、审批流配置

五、核心业务流程

5.1 采购全流程

java 复制代码
采购订单(CGDD) ──审核──→ 采购入库(CGRK) ──审核──→ 库存增加
      │                       │
      │                       └──→ 付款单(FKD) ──审核──→ 更新入库单已付金额
      │
      └──审核──→ 采购退货(CGTH) ──审核──→ 库存减少
                                       │
                                       └──→ 退款(关联退货单)

用 Mermaid 画出来更直观:

5.2 库存变更的核心链路

这是整个 ERP 模块最精妙的设计------所有库存变更都通过 ErpStockRecordService.createStockRecord() 统一入口

java 复制代码
业务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 仓库。代码里的处理是创建两条库存明细:

java 复制代码
// 源仓库出库(负数)
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)

java 复制代码
erp_product
├── id, name, barCode          -- 基础信息
├── categoryId → erp_product_category  -- 分类(树形结构)
├── unitId → erp_product_unit          -- 计量单位
├── purchasePrice, salePrice, minPrice -- 采购价/销售价/最低售价
├── weight, standard, expiryDay        -- 重量/规格/保质期
└── status                     -- 启用/禁用

产品分类用的是自关联树形结构(parentId 指向自身表的 id),这是最常见的分类方案。代码里有完整的校验逻辑:不能设自己为父分类、不能设子分类为父分类(防环)、删除前检查是否有子分类或产品引用。

6.2 采购订单主从表

java 复制代码
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. 反审核的级联保护

java 复制代码
// 存在采购入库单,无法反审核
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 这个模块还能做什么?

  1. 增加条码/二维码支持:产品表已经有 barCode 字段,可以对接扫码枪,实现"扫码入库""扫码出库"
  2. 增加审批流集成:对接 BPM 模块,让采购订单超过一定金额时走多级审批
  3. 增加供应商/客户对账功能:基于收付款记录和订单数据,自动生成对账单
  4. 增加简单的 MRPII:在采购订单的基础上,根据销售订单的物料需求自动计算采购建议
  5. 对接电子发票:供应商/客户表已经有 taxNo(税号)字段,可以对接税务系统

8.2 如果让我重新设计

  1. 库存表增加批次字段:batchNo + productionDate + expiryDate,支持先进先出(FIFO)和批次追溯
  2. 引入事件驱动:库存变更时发布 StockChangedEvent,让预警、通知、统计等下游逻辑异步解耦
  3. 增加价格策略:支持不同客户等级不同售价、阶梯定价、历史价格查询
  4. 库存盘点增加 PDA 支持:提供移动端接口,仓管可以拿着 PDA 边扫边盘
  5. 财务报表增强:增加应收应付账龄分析、资金流水、利润表

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 模块的"进销存"有什么异同?敬请期待!

相关推荐
学习日记5251 小时前
【提示词工程系统教程 05】上下文工程:静态指令、动态检索与RAG架构
人工智能·prompt
小羊Yveesss1 小时前
模板建站哪个平台好?模板数量之外还要比较编辑与SEO能力
大数据·人工智能·小程序
程序员-李俞1 小时前
向量引擎接入 SQL 问答沙箱前:只读权限、Base URL 和费用封顶怎么验收
人工智能·大模型·接口测试·api中转·ai api
梦想的初衷~1 小时前
植被遥感反演与数据同化算法体系教程:从PROSAIL前向模拟到作物估产
人工智能·python·机器学习·作物模型·遥感数据同化·prosail·植被参数反演
LadenKiller1 小时前
近期AI协作写量化规则,要按阶段安排任务
人工智能·python
网易云信1 小时前
制造业、零售与物流的“神经末梢”:为什么企业级IM是这些行业的数字基建?
人工智能·agent
虹科网络安全1 小时前
艾体宝新闻|从 SQL 注入到服务器接管:CVE-2026-57517 暴露 Web 管理面板的供应链与安全编码风险
服务器·前端·sql
千瓜1 小时前
用户洞察:负鼠走红?解读新世代“动物人格”
大数据·人工智能·数据分析·生活·新媒体
糖果店的幽灵1 小时前
langgraph的四种state解析
java·前端·javascript·langgraph