SpringBoot+Vue3 ERP 采购管理设计:采购申请→订单→入库→付款全链路与供应商台账
🌐 演示地址:http://ruoyioffice.com | 📦 源码1·GitHub:ruoyi-office | 📦 源码2·GitCode:ruoyi-office | 📦 源码3·Gitee:ruoyi-office | 💬 微信:17156169080(备注「RuoYi Office」)
采购管理是进销存里"进"这条线的全部。它要回答四个问题:跟谁买(供应商)、买什么买多少(采购订单)、实际到了多少货(采购入库)、还欠人家多少钱(付款与往来)。听起来是流水账,但真做成系统,处处是坑:订单还没审核就入库了怎么办?一张订单分三次到货怎么记?付了款的入库单能不能撤销?RuoYi Office 的采购管理(
yudao-module-erp采购域)以供应商台账为起点,用"采购订单→采购入库→付款"三段式链路串起整个进货流程,入库必须基于已审核订单、入库审核才真正加库存、已入库数量精确回写订单、已付款单据禁止反审核——把采购全链路的防错逻辑都落进了代码。
▲ 全景图:供应商台账 → 采购订单(约定)→ 采购入库(执行·加库存·回写 inCount)→ 付款单(结算应付);采购退货反向出库,单据状态乐观校验贯穿全程
引言:采购管理到底难在哪?
“采购不就是下个单、收个货吗?”——做过的人都知道没那么简单。采购管理的复杂度来自"约定与执行的错位"和"钱货的对账":
订单是约定,入库是执行,两者会错位:采购订单说要买 100 件,供应商可能分 3 批到货,每批数量、单价、税额都可能不同。系统必须知道"这张订单还剩多少没到货",否则要么漏收、要么重复入库。
入库必须有依据:采购入库不能凭空发生,它必须挂在一张"已审核的采购订单"上。如果允许随便入库,库存和采购计划就脱节了。
钱和货要对得上:货到了要付款,付款金额不能超过入库单金额;已经付过款的入库单如果允许随便撤销,账就乱了。供应商那边的应付余额也要随时能查。
状态不能乱跳,并发不能出错:两个人同时审核同一张入库单,库存会不会加两次?已审核的订单还能不能改?这些都要靠状态机和并发控制兜住。
本文以 RuoYi Office 的 yudao-module-erp 采购域为例,基于真实源码,完整拆解供应商台账、采购订单、采购入库、付款结算、采购退货的设计与防错实现。
一、业务设计:采购三段式链路
1.1 核心抽象:订单(约定)与入库(执行)分离
结论先行:采购管理把"约定"和"执行"拆成两张单——采购订单 erp_purchase_order 记录"打算买多少",采购入库 erp_purchase_in 记录"实际收了多少"。 这种分离是采购建模的关键:一张订单可以对应多张入库单(分批到货),入库单实时把"已入库数量"回写到订单,系统因此始终清楚"这笔采购还差多少没到货"。
入库单还有一条硬规则:它必须基于一张已审核的采购订单创建。代码里 createPurchaseIn 第一步就是 validatePurchaseOrder(orderId),校验订单存在且状态为"已审核",否则直接报错——这保证了入库永远有采购依据。
1.2 三段式链路:订单 → 入库 → 付款
采购全链路是三段递进,每段都有自己的单据和状态:
① 采购订单 erp_purchase_order选供应商 → 录明细(产品/数量/单价/税率)→ 自动算总额 → 提交审核② 采购入库 erp_purchase_in(必须基于已审核订单)选已审核订单 → 录实际入库明细 → 审核→ 审核通过:库存 +(PURCHASE_IN 流水)、回写订单 inCount③ 付款单 erp_finance_payment对入库单结算应付 → 回写入库单 paymentPrice→ 已付款的入库单禁止反审核
1.3 采购退货:反向的出库
采购退货 erp_purchase_return 是采购的逆向流程——把货退回供应商,库存反向减少(PURCHASE_RETURN 出库流水),并回写采购订单的 returnCount。它与采购入库完全对称,只是库存方向相反。
二、系统设计:模块组成与核心决策
2.1 模块组成
采购域由"供应商台账 + 三段式单据 + 财务结算"组成:
|
|
|
|
|---|---|---|
|
|
erp_supplier |
|
|
|
erp_purchase_order
|
inCount/returnCount
|
|
|
erp_purchase_in
|
paymentPrice
|
|
|
erp_purchase_return
|
|
|
|
erp_finance_payment
|
|
|
|
erp_account |
|
|
|
|
|
2.2 核心设计决策
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
inCount
|
|
|
|
|
|
|
|
updateByIdAndStatus(旧→新)
|
|
|
|
|
|
|
|
BigDecimal
MoneyUtils,含税额/优惠/其它费用
|
|
三、PC 端功能实现
3.1 供应商台账
供应商是采购的起点。台账维护供应商基础信息、联系人、联系方式,采购订单与入库都从这里选择往来单位。

▲ 供应商台账:维护供应商档案,采购订单/入库单选择供应商时校验其存在与启用状态
设计要点:
- 统一往来单位
:供应商档案被采购订单、入库、付款、往来对账共用,一处维护多处引用。 - 保存即校验
:下采购单时 validateSupplier校验供应商存在,避免脏数据。
3.2 采购订单列表
采购订单是"约定",记录跟谁买、买什么、买多少、总金额,并实时显示已入库、已退货进度。
▲ 采购订单列表:每行展示订单总数量、已入库数量(inCount)、已退货数量(returnCount),一眼看出到货进度;已审核订单不可编辑
设计要点:
- 金额自动汇总
:总额 = 产品价合计 + 税额 − 优惠,由明细行自动计算,无需手填。 - 到货进度可视
: inCount/returnCount直接体现"这笔采购还差多少没到货"。 - 状态约束
:草稿可改可删,已审核不可改;有入库记录的订单不能删除。
3.3 采购入库单列表
采购入库是"执行",必须挂在已审核的采购订单上。审核通过即增加库存,并把已付款金额回写。
▲ 采购入库单列表:每张入库单关联一张采购订单(orderNo),审核通过即加库存并落"采购入库"流水;已付款金额(paymentPrice)用于反审核校验
设计要点:
- 强制关联订单
:新建入库单必须选一张已审核采购订单,自动带出供应商。 - 入库即加库存
:审核通过逐行给对应仓库加库存,反审核(作废)退回。 - 已付款锁定
:已经付过款( paymentPrice > 0)的入库单禁止反审核,保护财务一致性。
四、后端核心实现
4.1 创建入库单:必须基于已审核订单
采购入库的第一道关卡——validatePurchaseOrder 校验订单存在且已审核,否则报错。入库单还会自动从订单带出供应商、订单号,并在创建后立即回写订单的已入库数量:
public Long createPurchaseIn(ErpPurchaseInSaveReqVO createReqVO) {// 1.1 校验采购订单已审核(入库必须有依据)ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.validatePurchaseOrder(createReqVO.getOrderId());// 1.2 校验入库项有效性、1.3 校验结算账户List<ErpPurchaseInItemDO> items = validatePurchaseInItems(createReqVO.getItems());accountService.validateAccount(createReqVO.getAccountId());// 1.4 生成入库单号并校验唯一String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_IN_NO_PREFIX);if (purchaseInMapper.selectByNo(no) != null) {throw exception(PURCHASE_IN_NO_EXISTS);}// 2. 插入入库单(带出供应商/订单号)+ 入库项ErpPurchaseInDO purchaseIn = BeanUtils.toBean(createReqVO, ErpPurchaseInDO.class, in -> in.setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())).setOrderNo(purchaseOrder.getNo()).setSupplierId(purchaseOrder.getSupplierId());calculateTotalPrice(purchaseIn, items);purchaseInMapper.insert(purchaseIn);items.forEach(o -> o.setInId(purchaseIn.getId()));purchaseInItemMapper.insertBatch(items);// 3. 回写采购订单的已入库数量updatePurchaseOrderInCount(createReqVO.getOrderId());return purchaseIn.getId();}
validatePurchaseOrder 的实现很简单但很关键——状态不是"已审核"就抛异常:
public ErpPurchaseOrderDO validatePurchaseOrder(Long id) {ErpPurchaseOrderDO order = validatePurchaseOrderExists(id);if (ObjectUtil.notEqual(order.getStatus(), ErpAuditStatus.APPROVE.getStatus())) {throw exception(PURCHASE_ORDER_NOT_APPROVE);}return order;}
4.2 已入库数量回写:多张入库单汇总到订单
一张采购订单可以分多次入库。每次入库(创建/修改/删除)都重新汇总该订单下所有入库单的数量,回写到订单项与订单总数——这样订单永远知道"已经到了多少货":
private void updatePurchaseOrderInCount(Long orderId) {// 1.1 查询该采购订单对应的所有采购入库单List<ErpPurchaseInDO> purchaseIns = purchaseInMapper.selectListByOrderId(orderId);// 1.2 按订单项汇总累计入库数量Map<Long, BigDecimal> inCountMap = purchaseInItemMapper.selectOrderItemCountSumMapByInIds(convertList(purchaseIns, ErpPurchaseInDO::getId));// 2. 回写采购订单的入库数量purchaseOrderService.updatePurchaseOrderInCount(orderId, inCountMap);}
注意:修改入库单时如果换了关联订单,会同时更新"新订单"和"老订单"的入库数量;删除入库单也会重算订单入库数——保证
inCount永远等于实际入库汇总。
4.3 入库审核:乐观校验 + 驱动库存
入库审核分两步:先用 updateByIdAndStatus 做乐观状态更新(防重复审核),再按明细生成库存流水。审核用 PURCHASE_IN(加库存),反审核用 PURCHASE_IN_CANCEL(退库存):
public void updatePurchaseInStatus(Long id, Integer status) {boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);ErpPurchaseInDO purchaseIn = validatePurchaseInExists(id);// 校验:已付款的入库单不允许反审核if (!approve && purchaseIn.getPaymentPrice().compareTo(BigDecimal.ZERO) > 0) {throw exception(PURCHASE_IN_PROCESS_FAIL_EXISTS_PAYMENT);}// 乐观更新状态(旧状态作为条件,防并发重复审核)int updateCount = purchaseInMapper.updateByIdAndStatus(id, purchaseIn.getStatus(),new ErpPurchaseInDO().setStatus(status));if (updateCount == 0) {throw exception(approve ? PURCHASE_IN_APPROVE_FAIL : PURCHASE_IN_PROCESS_FAIL);}// 驱动库存:审核加库存,反审核退库存List<ErpPurchaseInItemDO> items = purchaseInItemMapper.selectListByInId(id);Integer bizType = approve ? ErpStockRecordBizTypeEnum.PURCHASE_IN.getType(): ErpStockRecordBizTypeEnum.PURCHASE_IN_CANCEL.getType();items.forEach(item -> {BigDecimal count = approve ? item.getCount() : item.getCount().negate();stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(item.getProductId(), item.getWarehouseId(), count,bizType, item.getInId(), item.getId(), purchaseIn.getNo()));});}
4.4 付款约束:不超额、可回写
付款单对入库单结算时,会校验付款金额不能超过入库单总金额,并把已付款金额回写入库单(供反审核校验使用):
public void updatePurchaseInPaymentPrice(Long id, BigDecimal paymentPrice) {ErpPurchaseInDO purchaseIn = purchaseInMapper.selectById(id);if (purchaseIn.getPaymentPrice().equals(paymentPrice)) {return;}// 已付款金额不能超过入库单总金额if (paymentPrice.compareTo(purchaseIn.getTotalPrice()) > 0) {throw exception(PURCHASE_IN_FAIL_PAYMENT_PRICE_EXCEED, paymentPrice, purchaseIn.getTotalPrice());}purchaseInMapper.updateById(new ErpPurchaseInDO().setId(id).setPaymentPrice(paymentPrice));}
4.5 金额计算:含税额、优惠、其它费用
采购入库的金额比订单多一项"其它费用"(运费等)。合计 = 产品价 + 税额 − 优惠 + 其它费用,全程 BigDecimal:
private void calculateTotalPrice(ErpPurchaseInDO in, List<ErpPurchaseInItemDO> items) {in.setTotalCount(getSumValue(items, ErpPurchaseInItemDO::getCount, BigDecimal::add));in.setTotalProductPrice(getSumValue(items, ErpPurchaseInItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));in.setTotalTaxPrice(getSumValue(items, ErpPurchaseInItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));in.setTotalPrice(in.getTotalProductPrice().add(in.getTotalTaxPrice()));in.setDiscountPrice(MoneyUtils.priceMultiplyPercent(in.getTotalPrice(), in.getDiscountPercent()));// 合计 = 产品价 + 税额 − 优惠 + 其它费用in.setTotalPrice(in.getTotalPrice().subtract(in.getDiscountPrice()).add(in.getOtherPrice()));}
五、RuoYi Office 的创新设计
5.1 订单与入库分离,天然支持分批到货
采购订单只管"约定",采购入库只管"执行",一张订单可对应多张入库单。入库数量实时汇总回写订单 inCount,系统永远清楚"还差多少没到货",彻底告别 Excel 里"靠人脑记还差几件"的混乱。
5.2 入库强制挂订单,库存与采购计划不脱节
采购入库必须基于已审核订单创建,杜绝"凭空入库"。这让库存的每一次增加都能追溯到采购计划,财务和仓储数据天然对齐。
5.3 状态机的乐观并发控制
所有单据审核都用 updateByIdAndStatus(id, 旧状态, 新状态) 条件更新,只有数据库里还是旧状态才更新成功。两个人同时点审核,只有一个成功,库存绝不会被加两次——这是最轻量却最可靠的并发防护。
5.4 已付款锁定,保护财务一致性
已经发生付款的入库单禁止反审核,付款金额不能超过入库金额。这两条约束把"钱"和"货"绑在一起,避免出现"货撤了钱还在、或付款比货还多"的对账黑洞。
5.5 采购退货与入库完全对称
采购退货复用入库的全套设计,只是库存方向相反、流水类型换成 PURCHASE_RETURN、回写订单的 returnCount。对称设计让代码复用度高、行为可预测。
六、数据结构
6.1 表结构:erp_purchase_order(采购订单,节选)
|
|
|
|
|---|---|---|
id
no
|
|
|
status |
|
|
supplier_id
account_id
|
|
|
total_count
total_price
|
|
|
total_product_price
total_tax_price / discount_price
|
|
|
in_count
return_count
|
|
|
6.2 表结构:erp_purchase_in(采购入库,节选)
|
|
|
|
|---|---|---|
id
no
|
|
|
order_id
order_no
|
|
|
supplier_id
account_id
|
|
|
status |
|
|
total_count
total_price
|
|
|
other_price |
|
|
payment_price |
|
|
6.3 表结构:erp_purchase_in_item(采购入库明细,节选)
|
|
|
|
|---|---|---|
id
in_id
|
|
|
product_id
warehouse_id
|
|
|
count
product_price / total_price
|
|
|
tax_percent
tax_price
|
|
|
6.4 设计要点
- 多租户
:所有表基于 Yudao 多租户体系,自动隔离租户数据。 - 金额全
BigDecimal:单价、税额、优惠、合计统一精度,避免浮点误差。 - 单号唯一
:单据 no由 Redis 自增生成后再查重,分布式唯一。 - 明细 diff 更新
:修改单据时用 diffList对比新老明细,精确做增/改/删,避免全删全建。
七、技术亮点总结
|
|
|
|
|---|---|---|
| 订单/入库分离 |
|
|
| 入库挂订单 | validatePurchaseOrder
|
|
| 入库数量回写 |
inCount
|
|
| 审核驱动库存 | PURCHASE_IN
PURCHASE_IN_CANCEL
|
|
| 乐观并发 | updateByIdAndStatus(旧→新) |
|
| 付款约束 |
|
|
| 金额含其它费用 |
|
|
| 采购退货对称 |
|
|
| 单号 Redis 自增 | ErpNoRedisDAO |
|
| 明细 diff 更新 | diffList
|
|
八、快速体验
在线演示:http://ruoyioffice.com/web/(账号 admin / 密码 admin123)
操作路径:ERP 进销存 → 采购管理 → 供应商 / 采购订单 / 采购入库 / 采购退货 → 财务 → 付款单
推荐体验流程:
- 建供应商
:维护一个供应商档案。 - 下采购订单
:新建采购订单,选供应商、录明细(产品/数量/单价/税率),提交并审核,观察总额自动汇总。 - 采购入库
:基于已审核订单新建入库单(试试未审核订单会被拦截),录实际入库数量并审核,回到产品库存观察数量增加。 - 查到货进度
:回到采购订单列表,观察 inCount已入库数量被回写。 - 分批到货
:对同一订单再建一张入库单,观察 inCount累加。 - 付款结算
:用付款单对入库单结算,观察 paymentPrice回写;再试着反审核该入库单,会被"已付款"拦截。 - 采购退货
:发起采购退货单审核,观察库存反向减少、订单 returnCount回写。
源码仓库:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
结语
采购管理的本质,是管好"约定与执行的错位"和"钱与货的对账"。RuoYi Office 的答案是:用采购订单管约定、采购入库管执行,两者分离又通过 inCount 紧密咬合;用"入库必须挂已审核订单"保证库存有依据;用乐观状态校验防并发;用"付款不超额、已付款锁定"守住财务底线。
这套"约定/执行分离 + 数量回写 + 状态机并发控制"的思路,不止适用于采购,也能直接复用到销售出库、生产领料、外协加工等任何"先约定后执行、要分批、要对账"的业务。
如果你正在设计采购或进销存系统,欢迎参考源码实现,也欢迎在评论区聊聊:你们公司的采购订单,分批到货时是怎么记"还差多少"的?
常见问题(FAQ)
RuoYi Office 的采购管理是开源免费的吗?
是。采购管理属于 yudao-module-erp 进销存模块,基于 RuoYi-Vue-Pro / Yudao 架构,后端 Spring Boot 3.5 + 前端 Vue3,开源可商用、无 license 限制,本地约 10 分钟即可启动体验。
采购入库必须基于采购订单吗?
是。采购入库单创建时会调用 validatePurchaseOrder 校验关联订单存在且状态为"已审核",否则报错。这保证库存的每次增加都能追溯到采购计划,不会凭空入库。
一张采购订单可以分多次入库吗?
可以。一张采购订单支持创建多张采购入库单(分批到货),每次入库都会重新汇总所有入库单数量,回写到订单的 inCount,订单因此始终知道"还差多少没到货"。
已经付款的入库单还能撤销吗?
不能直接撤销。已付款(paymentPrice > 0)的入库单禁止反审核,且付款金额不能超过入库单总金额——这两条约束保护了钱货对账的一致性。
采购退货怎么处理库存?
采购退货单审核通过后,库存按 PURCHASE_RETURN 类型反向减少(出库),并回写采购订单的 returnCount,与采购入库完全对称。
💡 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
📦 源码仓库:GitHub | GitCode | Gitee
💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!

