大家好我是你们的源码拆解的腻害兔今天继续 RuoYi-Vue-Pro芋道系列。前面我们已经拆了框架层、认证权限、多租户、工作流 BPM、支付、CRM 等模块今天终于来到了很多读者翘首以盼的ERP 企业资源模块。为什么说是翘首以盼因为进销存是中小企业信息化的第一步也是很多开发者接私活时最常碰到的需求。今天这篇文章我会从一个产品经理 技术分析师的双重视角带大家把这个模块看个通透。废话不多说直接上干货一、今日模块概览一句话概括ERP 模块是 RuoYi-Vue-Pro 里的轻量级进销存 简易财务系统覆盖了产品管理、采购订单→入库→退货、销售订单→出库→退货、库存入库/出库/调拨/盘点、财务收款/付款五大业务域共 23 个 Controller、23 对 Service、33 张数据库表。它不是 SAP 那种重型 ERP而是面向中小企业的够用就好方案——砍掉了生产计划、MRP、质检等重流程保留了最核心的买进来、卖出去、管库存、收付款闭环。二、技术选型分析老规矩先上结论再看原因。技术点选型替代方案为什么这么选ORM 框架MyBatis-PlusJPA/Hibernate、原生 MyBatis进销存场景大量复杂查询库存汇总、统计报表JPA 的 HQL 在这种场景下性能调优困难MyBatis-Plus 在保留 SQL 灵活性的同时提供了 CRUD 增强开发效率高单号生成Redis INCR数据库序列、UUID、雪花算法ERP 单号需要前缀日期自增序号的格式如 CGDD20260721000001Redis 原子自增天然适合且性能远高于数据库方案。UUID 不可读雪花算法太长都不适合做业务单号审批状态两态审核PROCESS/APPROVEFlowable 工作流、多态审批进销存的审批逻辑非常简单——草稿→审核两态流转上 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-PlusSpring Boot MyBatisSpring Boot MyBatis-Plus闭源 SaaS采购流程订单→入库→退货三单关联订单→入库→退货订单→入库→退货订单→入库→退货销售流程订单→出库→退货三单关联订单→出库→退货订单→出库→退货订单→出库→退货库存操作入库/出库/调拨/盘点 4 种入库/出库/调拨 3 种入库/出库 2 种入库/出库/调拨/盘点财务管理收款/付款关联具体单据简单的收付款记录无收付款对账单审批机制两态审核轻量两态审核无审批多级审批库存预警❌ 暂无✅ 有❌ 无✅ 有多仓库✅ 支持✅ 支持❌ 单仓库✅ 支持统计报表采购/销售金额汇总月度趋势简单报表无丰富报表开源协议MITApache 2.0Apache 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_producterp_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 方法里注入了大量 ServiceStockService、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支付模块✅ 已完成10CRM 客户关系✅ 已完成11ERP 企业资源✅ 本篇12商城-商品与交易 下一篇下一篇预告商城模块yudao-module-mall——商品管理、订单流程、营销活动、数据统计RuoYi 是怎么做电商的和 ERP 模块的进销存有什么异同敬请期待