SpringBoot+Vue3 ERP 采购管理设计:采购申请→订单→入库→付款全链路与供应商台账
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(+item) |
采购约定,跟踪 inCount/returnCount |
| 采购入库 | erp_purchase_in(+item) |
实际入库,审核后库存 +,回写 paymentPrice |
| 采购退货 | erp_purchase_return(+item) |
退回供应商,审核后库存 − |
| 付款单 | erp_finance_payment(+item) |
对入库单结算应付 |
| 结算账户 | 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 校验订单存在且已审核,否则报错。入库单还会自动从订单带出供应商、订单号,并在创建后立即回写订单的已入库数量:
@Override
@Transactional(rollbackFor = Exception.class)
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(退库存):
@Override
@Transactional(rollbackFor = Exception.class)
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 付款约束:不超额、可回写
付款单对入库单结算时,会校验付款金额不能超过入库单总金额,并把已付款金额回写入库单(供反审核校验使用):
@Override
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 |
bigint/varchar | 主键 / 采购订单号 |
status |
int | 状态(草稿/审核中/已审核) |
supplier_id / account_id |
bigint | 供应商 / 结算账户 |
total_count / total_price |
decimal | 合计数量 / 最终合计价格 |
total_product_price / total_tax_price / discount_price |
decimal | 产品价 / 税额 / 优惠 |
in_count / return_count |
decimal | 已入库数量 / 已退货数量 |
6.2 表结构:erp_purchase_in(采购入库,节选)
| 字段 | 类型 | 说明 |
|---|---|---|
id / no |
bigint/varchar | 主键 / 入库单号 |
order_id / order_no |
bigint/varchar | 关联采购订单 ID / 单号 |
supplier_id / account_id |
bigint | 供应商 / 结算账户 |
status |
int | 状态(草稿/已审核) |
total_count / total_price |
decimal | 合计数量 / 合计金额 |
other_price |
decimal | 其它费用(运费等) |
payment_price |
decimal | 已付款金额 |
6.3 表结构:erp_purchase_in_item(采购入库明细,节选)
| 字段 | 类型 | 说明 |
|---|---|---|
id / in_id |
bigint | 主键 / 入库单 ID |
product_id / warehouse_id |
bigint | 产品 / 入库仓库 |
count / product_price / total_price |
decimal | 数量 / 单价 / 金额 |
tax_percent / tax_price |
decimal | 税率 / 税额 |
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回写。
源码仓库:
| 平台 | 地址 |
|---|---|
| GitHub | https://github.com/yuqing2026/ruoyi-office |
| GitCode | https://gitcode.com/zhouzhongyan/ruoyi-office |
| Gitee | https://gitee.com/yqzy1688/ruoyi-office |
结语
采购管理的本质,是管好"约定与执行的错位"和"钱与货的对账"。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 支持一下!
更多推荐


所有评论(0)