大数跨境

SpringBoot+Vue3 ERP 采购管理设计:采购申请→订单→入库→付款全链路与供应商台账

SpringBoot+Vue3 ERP 采购管理设计:采购申请→订单→入库→付款全链路与供应商台账 企业软件源码
2026-06-26
1
导读:深度拆解开源 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<LongBigDecimal> 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 付款约束:不超额、可回写

付款单对入库单结算时,会校验付款金额不能超过入库单总金额,并把已付款金额回写入库单(供反审核校验使用):

@Overridepublic 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::getCountBigDecimal::add));    in.setTotalProductPrice(getSumValue(items, ErpPurchaseInItemDO::getTotalPriceBigDecimal::add, BigDecimal.ZERO));    in.setTotalTaxPrice(getSumValue(items, ErpPurchaseInItemDO::getTaxPriceBigDecimal::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 进销存 → 采购管理 → 供应商 / 采购订单 / 采购入库 / 采购退货 → 财务 → 付款单

推荐体验流程

  1. 建供应商
    :维护一个供应商档案。
  2. 下采购订单
    :新建采购订单,选供应商、录明细(产品/数量/单价/税率),提交并审核,观察总额自动汇总。
  3. 采购入库
    :基于已审核订单新建入库单(试试未审核订单会被拦截),录实际入库数量并审核,回到产品库存观察数量增加。
  4. 查到货进度
    :回到采购订单列表,观察 inCount 已入库数量被回写。
  5. 分批到货
    :对同一订单再建一张入库单,观察 inCount 累加。
  6. 付款结算
    :用付款单对入库单结算,观察 paymentPrice 回写;再试着反审核该入库单,会被"已付款"拦截。
  7. 采购退货
    :发起采购退货单审核,观察库存反向减少、订单 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
0
企业软件源码
RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
内容 38
粉丝 0
企业软件源码 RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
总阅读100
粉丝0
内容38