配套面试准备:完成本篇后,可以继续阅读:Day03_PMS核心业务面试准备 Day03_PMS采购管理系统完整面试指南
课程目标:深度理解跨境出海场景下的采购业务全链路,完整实现从采购需求产生到入库付款的闭环系统,掌握询价比价、采购状态机、库存联动、应付账款等核心业务模块的设计与实现。
今日交付物:
- 采购模块 6 张数据库表完整 SQL
- 三种采购需求来源的触发机制设计
- 询价比价完整业务流程与接口设计
- 采购订单 8 状态状态机完整实现
- 采购入库与 WMS 库存联动的事务设计
- 应付账款账期管理与到期提醒
- 采购退货逆向流程设计
- 采购数据统计报表设计
第一节 采购管理业务全解析
1.1 采购管理在供应链中的位置
采购管理系统(PMS,Purchase Management System)是连接供应商和仓储的核心枢纽,承上启下:
graph LR
A["🏭 供应商<br>SRM模块管理"] -->|"有了合格供应商"| B["🛒 采购管理<br>PMS 今天的重点"]
B -->|"货物入库后"| C["📦 仓储管理<br>WMS 第四天"]
C -->|"有库存后"| D["🏷️ 商品管理<br>PIM 第五天"]
D -->|"商品上架后"| E["📑 订单管理<br>OMS 第五天"]
style B fill:#fff3e0,stroke:#ff9800
1.2 没有采购管理系统时的真实痛点
一个没有 PMS 的跨境卖家是怎么运作的?
graph TD
A["运营发现某SKU快断货了"] --> B["微信发消息给采购:<br> 这个货快没了,让工厂发货"]
B --> C["采购在手机备忘录上<br>记下要补货这件事"]
C --> D["采购在微信里联系供应商<br>说:老王,那个耳机给我来500个"]
D --> E["供应商回复:好的<br>(没有任何书面记录)"]
E --> F["两周后货没来<br>采购问:老王货呢?<br>老王:什么货?我们没记录啊"]
F --> G["断货!平台自动下架<br>损失巨大"]
H["同时另一个采购<br>也联系了另一家供应商<br>订了同样的500个"] --> I["两批货都来了<br>库存积压1000个<br>资金压死"]
PMS 系统化后解决的问题:
| 问题 | 系统化解决方案 |
|---|---|
| 采购信息靠微信/备忘录记录 | 所有采购需求在系统中创建,有单据编号,不会遗漏 |
| 重复采购,库存积压 | 系统实时展示当前库存和在途库存,避免重复下单 |
| 不知道补货多少合适 | 智能补货建议:基于安全库存和预测销量自动计算 |
| 供应商发货没有跟踪 | 采购单状态跟踪,每个节点都有记录 |
| 对账靠人工数发票 | 系统自动生成应付账款,账期到期自动提醒 |
| 采购成本没有统计 | 多维度采购报表,品类/供应商/时间维度分析 |
1.3 采购业务核心概念
在进入系统设计之前,必须搞清楚这些业务术语:
| 术语 | 含义 | 举例 |
|---|---|---|
| 采购需求 | 需要购买某种商品的业务需求,还未形成正式订单 | ”耳机库存只剩50个,需要再采购500个” |
| 采购申请单 | 将采购需求正式化的内部单据,待审批 | 采购专员填写的申请表,等待负责人审批 |
| 询价单 | 向供应商询问价格的邀请函 | ”你们耳机500个多少钱?“ |
| 报价单 | 供应商针对询价给出的正式报价 | ”500个,单价38元,含税,7天发货” |
| 采购订单(PO) | 与供应商正式确认的采购合同(Purchase Order) | 含品名/数量/单价/交货期的正式单据 |
| 收货单 | 货物到达仓库时创建的验收记录 | 仓库人员核对实收数量后签字的单据 |
| 应付账款 | 已收到货物但尚未付款的欠款 | 采购了500个耳机,货已到,还没给供应商打款 |
| 账期 | 供应商允许的延迟付款天数 | 月结30天,即收货后30天内付款 |
| 现购 | 收到货物同时付款,无账期 | paymentDays = 0 |
| 赊购 | 先收货后付款,有账期 | paymentDays = 30/60/90 |
1.4 采购业务的参与角色
| 角色 | 职责 | 在 PMS 中的操作 |
|---|---|---|
| 采购专员 | 日常采购执行 | 创建采购申请、发询价、跟踪到货 |
| 采购负责人 | 采购策略决策 | 审批大额采购申请、确认最终供应商 |
| 仓储管理员 | 验货入库 | 创建收货单、质检确认、库存入账 |
| 财务专员 | 资金管控 | 核对发票、确认付款、管理应付账款 |
| 供应商(外部) | 履行采购合同 | 在 Portal 确认采购单、填写发货信息 |
第二节 采购数据库表设计
2.1 采购模块表结构总览
erDiagram
purchase_requisition {
bigint id PK
bigint tenant_id
varchar req_no "申请单编号"
tinyint req_source "来源1库存预警2销售预测3手动"
tinyint status "状态0草稿1待审2通过3拒绝"
}
purchase_requisition_item {
bigint id PK
bigint req_id FK
bigint sku_id FK
int quantity "申请数量"
date expected_date "期望到货日期"
}
purchase_inquiry {
bigint id PK
bigint tenant_id
varchar inquiry_no "询价单编号"
bigint req_id FK
bigint supplier_id FK
tinyint status "状态"
}
purchase_inquiry_item {
bigint id PK
bigint inquiry_id FK
bigint sku_id FK
decimal quoted_price "供应商报价"
int delivery_days "承诺交货天数"
}
purchase_order {
bigint id PK
bigint tenant_id
varchar po_no "采购订单号"
bigint supplier_id FK
decimal total_amount "总金额"
tinyint status "0草稿到7已结清"
}
purchase_order_item {
bigint id PK
bigint po_id FK
bigint sku_id FK
int quantity "采购数量"
int received_qty "已收货数量"
decimal unit_price "采购单价"
}
purchase_receipt {
bigint id PK
bigint po_id FK
varchar receipt_no "收货单编号"
tinyint status "0待质检1通过2拒收"
}
purchase_return {
bigint id PK
bigint po_id FK
varchar return_no "退货单编号"
tinyint return_reason "退货原因"
tinyint status "退货状态"
}
finance_payable {
bigint id PK
bigint po_id FK
decimal payable_amount "应付金额"
date due_date "到期日期"
tinyint status "0待付1部分付2已结清"
}
purchase_requisition ||--o{ purchase_requisition_item : "包含多个SKU明细"
purchase_requisition ||--o{ purchase_inquiry : "发起多个询价"
purchase_inquiry ||--o{ purchase_inquiry_item : "包含多个SKU报价"
purchase_inquiry }|--|| purchase_order : "确认后生成采购订单"
purchase_order ||--o{ purchase_order_item : "包含多个SKU明细"
purchase_order ||--o{ purchase_receipt : "可以多次部分收货"
purchase_order ||--o{ purchase_return : "可以发起退货"
purchase_order ||--|| finance_payable : "生成一条应付账款"
2.2 采购申请单主表
-- ============================================================
-- 采购申请单主表
-- 记录每一次采购需求的来源、申请人和审批结果
-- ============================================================
CREATE TABLE `purchase_requisition`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID,雪花算法',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`req_no` VARCHAR(32) NOT NULL COMMENT '申请单编号,格式 REQ-YYYYMMDD-XXXX',
`req_source` TINYINT NOT NULL DEFAULT 3 COMMENT '需求来源:1=库存预警自动触发 2=销售预测触发 3=人工手动申请',
`title` VARCHAR(128) NOT NULL COMMENT '申请标题,如:2025年1月蓝牙耳机补货申请',
`warehouse_id` BIGINT NOT NULL COMMENT '目标收货仓库ID',
`expect_date` DATE NULL COMMENT '期望整体到货日期',
`total_amount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '申请单估算总金额(参考价,非正式价格)',
`priority` TINYINT NOT NULL DEFAULT 2 COMMENT '优先级:1=紧急 2=普通 3=低',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '审批状态:0=草稿 1=待审批 2=审批通过 3=审批拒绝 4=已转采购单 5=已取消',
`apply_user_id` BIGINT NOT NULL COMMENT '申请人用户ID',
`apply_user_name` VARCHAR(64) NOT NULL COMMENT '申请人姓名(冗余)',£
`apply_time` DATETIME NOT NULL COMMENT '提交申请时间',
`audit_user_id` BIGINT NULL COMMENT '审批人用户ID',
`audit_time` DATETIME NULL COMMENT '审批时间',
`audit_remark` VARCHAR(512) NULL COMMENT '审批意见',
`remark` VARCHAR(512) NULL COMMENT '申请备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`update_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_req_no` (`tenant_id`, `req_no`),
KEY `idx_tenant_status` (`tenant_id`, `status`),
KEY `idx_apply_user` (`apply_user_id`),
KEY `idx_apply_time` (`apply_time`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '采购申请单主表';
2.3 采购申请单明细表
-- ============================================================
-- 采购申请单明细表
-- 一张申请单可包含多个 SKU 的采购需求
-- ============================================================
CREATE TABLE `purchase_requisition_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`req_id` BIGINT NOT NULL COMMENT '关联申请单ID',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码(冗余存储)',
`sku_name` VARCHAR(256) NOT NULL COMMENT 'SKU名称(冗余存储)',
`quantity` INT NOT NULL COMMENT '申请采购数量',
`current_stock` INT NOT NULL DEFAULT 0 COMMENT '创建申请时的当前可用库存(快照,方便审批人参考)',
`safety_stock` INT NOT NULL DEFAULT 0 COMMENT '安全库存阈值(快照)',
`in_transit_qty` INT NOT NULL DEFAULT 0 COMMENT '创建时的在途库存数量(快照)',
`ref_price` DECIMAL(10, 4) NULL COMMENT '参考单价(最近一次采购价,用于估算总金额)',
`expect_date` DATE NULL COMMENT '该SKU期望到货日期(可与主表不同)',
`remark` VARCHAR(256) NULL COMMENT '该明细项的备注',
PRIMARY KEY (`id`),
KEY `idx_req_id` (`req_id`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '采购申请单明细表';
2.4 询价单与报价明细表
-- ============================================================
-- 询价单主表
-- 每次向一家供应商发询价,生成一张询价单
-- 一个采购申请可向多家供应商发询价
-- ============================================================
CREATE TABLE `purchase_inquiry`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`inquiry_no` VARCHAR(32) NOT NULL COMMENT '询价单编号,格式 INQ-YYYYMMDD-XXXX',
`req_id` BIGINT NULL COMMENT '关联采购申请单ID(若不经申请直接询价则为NULL)',
`supplier_id` BIGINT NOT NULL COMMENT '询价的供应商ID',
`supplier_name` VARCHAR(128) NOT NULL COMMENT '供应商名称(冗余)',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=已发送 1=已报价 2=已选中(最终成交) 3=未选中 4=已过期',
`send_time` DATETIME NOT NULL COMMENT '发送询价时间',
`quote_deadline` DATETIME NULL COMMENT '要求供应商报价的截止时间',
`quoted_time` DATETIME NULL COMMENT '供应商实际回价时间',
`response_hours` DECIMAL(8, 2) NULL COMMENT '供应商响应时长(小时)= quoted_time - send_time,用于评分',
`total_quote_amt` DECIMAL(12, 2) NULL COMMENT '供应商报价总金额',
`quote_valid_days` INT NULL COMMENT '报价有效天数(从报价日起算)',
`quote_expire_date` DATE NULL COMMENT '报价到期日期 = quoted_time + quote_valid_days',
`remark` VARCHAR(512) NULL COMMENT '询价备注(如特殊要求)',
`supplier_remark` VARCHAR(512) NULL COMMENT '供应商回价时的备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_inquiry_no` (`tenant_id`, `inquiry_no`),
KEY `idx_req_id` (`req_id`),
KEY `idx_supplier_id` (`supplier_id`),
KEY `idx_tenant_status` (`tenant_id`, `status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '询价单主表';
-- ============================================================
-- 询价明细与供应商报价表
-- 记录供应商对每个SKU的具体报价
-- ============================================================
CREATE TABLE `purchase_inquiry_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`inquiry_id` BIGINT NOT NULL COMMENT '关联询价单ID',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码',
`sku_name` VARCHAR(256) NOT NULL COMMENT 'SKU名称',
`inquiry_qty` INT NOT NULL COMMENT '询价数量(我方需求量)',
`quoted_price` DECIMAL(10, 4) NULL COMMENT '供应商报价单价(填写后表示已报价)',
`quoted_qty` INT NULL COMMENT '供应商可供数量(可能小于询价数量)',
`delivery_days` INT NULL COMMENT '供应商承诺的交货天数',
`min_order_qty` INT NULL COMMENT '供应商该SKU的最小起订量',
`remark` VARCHAR(256) NULL COMMENT '供应商该项的备注',
PRIMARY KEY (`id`),
KEY `idx_inquiry_id` (`inquiry_id`),
KEY `idx_sku_id` (`sku_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '询价明细及供应商报价表';
2.5 采购订单主表与明细表
-- ============================================================
-- 采购订单主表(Purchase Order,简称 PO)
-- 与供应商签订的正式采购合同
-- ============================================================
CREATE TABLE `purchase_order`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`po_no` VARCHAR(32) NOT NULL COMMENT '采购单编号,格式 PO-YYYYMMDD-XXXX',
-- 关联信息
`req_id` BIGINT NULL COMMENT '来源申请单ID(有审批流时关联)',
`inquiry_id` BIGINT NULL COMMENT '来源询价单ID',
`supplier_id` BIGINT NOT NULL COMMENT '供应商ID',
`supplier_name` VARCHAR(128) NOT NULL COMMENT '供应商名称(冗余,防止供应商信息变更后影响历史单)',
`warehouse_id` BIGINT NOT NULL COMMENT '收货仓库ID',
`warehouse_name` VARCHAR(64) NOT NULL COMMENT '仓库名称(冗余)',
-- 金额信息
`total_amount` DECIMAL(12, 2) NOT NULL COMMENT '采购总金额(含税)',
`tax_amount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '税额',
`currency` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '结算货币',
`exchange_rate` DECIMAL(10, 6) NOT NULL DEFAULT 1 COMMENT '汇率(非人民币时使用),折算为CNY的比例',
-- 付款信息
`payment_type` TINYINT NOT NULL DEFAULT 1 COMMENT '付款方式:1=现购(现款现货) 2=赊购(有账期)',
`payment_days` INT NOT NULL DEFAULT 0 COMMENT '账期天数,现购时为0',
`paid_amount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '已付款金额',
-- 时间信息
`order_date` DATE NOT NULL COMMENT '下单日期',
`expected_date` DATE NULL COMMENT '期望到货日期(我方要求)',
`confirmed_date` DATE NULL COMMENT '供应商承诺到货日期(供应商确认后填写)',
`actual_delivery_date` DATE NULL COMMENT '实际发货日期(供应商填写)',
-- 状态
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '采购单状态:0=草稿 1=待供应商确认 2=已确认 3=发货中 4=部分到货 5=全部到货 6=已对账 7=已结清 8=已取消',
-- 物流信息
`logistics_company` VARCHAR(64) NULL COMMENT '物流公司名称(供应商填写)',
`tracking_no` VARCHAR(128) NULL COMMENT '物流运单号(供应商填写)',
-- 其他
`contract_no` VARCHAR(64) NULL COMMENT '合同编号(线下合同)',
`invoice_no` VARCHAR(64) NULL COMMENT '发票编号',
`remark` VARCHAR(512) NULL COMMENT '备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`update_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_po_no` (`tenant_id`, `po_no`),
KEY `idx_supplier_id` (`supplier_id`),
KEY `idx_tenant_status` (`tenant_id`, `status`),
KEY `idx_expected_date` (`expected_date`),
KEY `idx_create_time` (`create_time`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '采购订单主表';
-- ============================================================
-- 采购订单明细表
-- 记录每张采购单包含的 SKU 及其数量、价格
-- ============================================================
CREATE TABLE `purchase_order_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`po_id` BIGINT NOT NULL COMMENT '关联采购单ID',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码(冗余)',
`sku_name` VARCHAR(256) NOT NULL COMMENT 'SKU名称(冗余)',
`spec` VARCHAR(256) NULL COMMENT '规格描述,如:蓝色-XL码',
`unit` VARCHAR(16) NOT NULL DEFAULT '件' COMMENT '单位',
`quantity` INT NOT NULL COMMENT '采购数量',
`received_qty` INT NOT NULL DEFAULT 0 COMMENT '已累计收货数量(多次收货时逐步增加)',
`unit_price` DECIMAL(10, 4) NOT NULL COMMENT '采购单价(含税)',
`amount` DECIMAL(12, 2) NOT NULL COMMENT '小计金额 = unit_price × quantity',
`expect_date` DATE NULL COMMENT '该SKU期望到货日期',
`remark` VARCHAR(256) NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_po_id` (`po_id`),
KEY `idx_sku_id` (`sku_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '采购订单明细表';
2.6 收货单表
-- ============================================================
-- 采购收货单主表
-- 货物到达仓库时创建,一张采购单可以多次收货(分批到货)
-- ============================================================
CREATE TABLE `purchase_receipt`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`receipt_no` VARCHAR(32) NOT NULL COMMENT '收货单编号,格式 RCV-YYYYMMDD-XXXX',
`po_id` BIGINT NOT NULL COMMENT '关联采购单ID',
`po_no` VARCHAR(32) NOT NULL COMMENT '采购单编号(冗余)',
`supplier_id` BIGINT NOT NULL COMMENT '供应商ID(冗余)',
`warehouse_id` BIGINT NOT NULL COMMENT '收货仓库ID',
`receive_date` DATE NOT NULL COMMENT '实际收货日期',
`receiver_id` BIGINT NOT NULL COMMENT '收货人(仓储管理员)用户ID',
`receiver_name` VARCHAR(64) NOT NULL COMMENT '收货人姓名',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '状态:0=待质检 1=质检通过(入库中) 2=部分入库 3=全部入库 4=拒收',
`total_qty` INT NOT NULL DEFAULT 0 COMMENT '本次收货总件数',
`pass_qty` INT NOT NULL DEFAULT 0 COMMENT '质检通过件数',
`reject_qty` INT NOT NULL DEFAULT 0 COMMENT '质检拒绝件数',
`is_on_time` TINYINT(1) NULL COMMENT '是否按时到货:1=准时 0=延迟,用于供应商绩效评分',
`remark` VARCHAR(512) NULL COMMENT '收货备注(如外包装破损等情况)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`update_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_receipt_no` (`tenant_id`, `receipt_no`),
KEY `idx_po_id` (`po_id`),
KEY `idx_warehouse_id` (`warehouse_id`),
KEY `idx_receive_date` (`receive_date`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '采购收货单主表';
-- ============================================================
-- 收货单明细表
-- 记录每次收货时各 SKU 的实际收货数量和质检结果
-- ============================================================
CREATE TABLE `purchase_receipt_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`receipt_id` BIGINT NOT NULL COMMENT '关联收货单ID',
`po_item_id` BIGINT NOT NULL COMMENT '关联采购单明细ID',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码',
`sku_name` VARCHAR(256) NOT NULL COMMENT 'SKU名称',
`expected_qty` INT NOT NULL COMMENT '本次应到数量(根据采购单剩余未收数量)',
`actual_qty` INT NOT NULL COMMENT '实际收到数量',
`pass_qty` INT NOT NULL DEFAULT 0 COMMENT '质检合格数量',
`reject_qty` INT NOT NULL DEFAULT 0 COMMENT '质检不合格数量',
`reject_reason` VARCHAR(256) NULL COMMENT '不合格原因(如:外观划痕/功能异常/规格不符)',
`location_id` BIGINT NULL COMMENT '上架目标库位ID(质检通过后填写)',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=待质检 1=已入库 2=已退货',
PRIMARY KEY (`id`),
KEY `idx_receipt_id` (`receipt_id`),
KEY `idx_po_item_id` (`po_item_id`),
KEY `idx_sku_id` (`sku_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '收货单明细表';
2.7 采购退货单表
-- ============================================================
-- 采购退货单表
-- 因质量/数量/型号问题,将部分货物退还给供应商
-- ============================================================
CREATE TABLE `purchase_return`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`return_no` VARCHAR(32) NOT NULL COMMENT '退货单编号,格式 RTN-YYYYMMDD-XXXX',
`po_id` BIGINT NOT NULL COMMENT '关联采购单ID',
`po_no` VARCHAR(32) NOT NULL COMMENT '采购单编号(冗余)',
`supplier_id` BIGINT NOT NULL COMMENT '供应商ID',
`warehouse_id` BIGINT NOT NULL COMMENT '出货仓库ID',
`return_reason` TINYINT NOT NULL
COMMENT '退货原因:1=质量问题 2=数量不符 3=型号/规格错误 4=包装损坏 5=逾期未到货 6=其他',
`return_qty` INT NOT NULL COMMENT '退货总数量',
`return_amount` DECIMAL(12, 2) NOT NULL COMMENT '退货金额',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '状态:0=草稿 1=已提交(等待供应商确认) 2=供应商确认 3=已出库 4=供应商已收货 5=退款/补货完成 6=已拒绝',
`handle_type` TINYINT NULL
COMMENT '处理方式:1=退款 2=补发相同商品 3=补发替代商品',
`supplier_tracking_no` VARCHAR(128) NULL COMMENT '退货物流单号',
`evidence_urls` JSON NULL COMMENT '问题证据图片/视频URL列表',
`remark` VARCHAR(512) NULL COMMENT '退货说明',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`update_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_return_no` (`tenant_id`, `return_no`),
KEY `idx_po_id` (`po_id`),
KEY `idx_supplier_id` (`supplier_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '采购退货单表';
2.8 应付账款表
-- ============================================================
-- 应付账款表(Finance Payable)
-- 采购货物到库后生成,记录需要支付给供应商的款项
-- 属于财务模块,但与采购强相关,在此一并设计
-- ============================================================
CREATE TABLE `finance_payable`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`payable_no` VARCHAR(32) NOT NULL COMMENT '应付账款单号,格式 PAY-YYYYMMDD-XXXX',
`po_id` BIGINT NOT NULL COMMENT '关联采购单ID',
`po_no` VARCHAR(32) NOT NULL COMMENT '采购单编号(冗余)',
`supplier_id` BIGINT NOT NULL COMMENT '供应商ID',
`supplier_name` VARCHAR(128) NOT NULL COMMENT '供应商名称(冗余)',
`invoice_no` VARCHAR(64) NULL COMMENT '发票编号(对账用)',
`invoice_date` DATE NULL COMMENT '发票日期(账期从此日期开始计算)',
`payable_amount` DECIMAL(12, 2) NOT NULL COMMENT '应付总金额',
`paid_amount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '已付金额',
`remaining_amount` DECIMAL(12, 2) GENERATED ALWAYS AS (`payable_amount` - `paid_amount`) STORED
COMMENT '剩余未付金额(自动计算)',
`currency` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '货币',
`payment_days` INT NOT NULL DEFAULT 0 COMMENT '账期天数',
`due_date` DATE NOT NULL COMMENT '到期付款日 = invoice_date + payment_days',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=待付款 1=部分已付 2=已结清 3=已作废',
`overdue_days` INT NOT NULL DEFAULT 0 COMMENT '逾期天数,由定时任务每日更新',
`remark` VARCHAR(512) NULL COMMENT '备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`update_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_payable_no` (`tenant_id`, `payable_no`),
UNIQUE KEY `uk_po_id` (`po_id`) COMMENT '一张采购单只对应一条应付账款',
KEY `idx_supplier_id` (`supplier_id`),
KEY `idx_due_date` (`due_date`) COMMENT '按到期日查询,用于提醒',
KEY `idx_tenant_status` (`tenant_id`, `status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '应付账款表';
-- ============================================================
-- 付款记录表
-- 记录每一次实际付款操作
-- ============================================================
CREATE TABLE `finance_payment_record`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`payable_id` BIGINT NOT NULL COMMENT '关联应付账款ID',
`payment_amount` DECIMAL(12, 2) NOT NULL COMMENT '本次付款金额',
`payment_date` DATE NOT NULL COMMENT '付款日期',
`payment_method` TINYINT NOT NULL DEFAULT 1 COMMENT '付款方式:1=银行转账 2=支票 3=现金 4=其他',
`voucher_no` VARCHAR(64) NOT NULL COMMENT '付款凭证号(银行流水号/转账凭证)',
`operator_id` BIGINT NOT NULL COMMENT '操作人(财务专员)用户ID',
`operator_name` VARCHAR(64) NOT NULL COMMENT '操作人姓名',
`remark` VARCHAR(256) NULL COMMENT '付款备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_payable_id` (`payable_id`),
KEY `idx_payment_date` (`payment_date`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '付款记录表';

第三节 采购需求来源与触发机制
3.1 三种需求来源对比
这是柔性供应链系统的核心能力之一:需求不再只靠人工判断,系统可以智能感知并自动触发。
graph TD
subgraph 来源1 库存预警自动触发
A1["定时任务:每小时扫描一次"] --> A2["查询库存 < 安全库存的SKU"]
A2 --> A3["自动创建采购申请单<br>req_source = 1"]
A3 --> A4["推送通知给采购专员<br>请确认补货建议"]
end
subgraph 来源2 销售预测触发
B1["定时任务:每周一执行"] --> B2["基于近30天销量预测<br>未来30天需求量"]
B2 --> B3["计算:预测需求 > 当前库存+在途库存?"]
B3 --> B4["自动创建预测补货申请<br>req_source = 2"]
end
subgraph 来源3 人工手动申请
C1["采购专员手动创建"] --> C2["填写SKU/数量/<br>期望到货日期"]
C2 --> C3["保存申请单<br>req_source = 3"]
end
A4 & B4 & C3 --> D["采购申请单<br>进入审批流程"]
库存预警: 每小时扫描一次 真实库存和安全库存的对比 如果不足 就可以创建申请单 销售预测: 每周一次 基于近30天的销售数据 来进行推测 未来30天的数据 然后判断库存是否够用 如果不够 也会自动创建申请单 人工手动申请
3.2 库存预警触发机制详解
flowchart TD
A(["定时任务:每小时整点执行"]) --> B["查询该租户下所有设置了<br>安全库存阈值的SKU-仓库组合"]
B --> C["对每个SKU-仓库组合计算:<br>可用库存 = 实物库存 - 冻结库存"]
C --> D{"可用库存 ≤ 安全库存阈值?"}
D -- 否 --> E["无需处理<br>继续下一个"]
D -- 是 --> F{"当前是否已有<br>进行中的采购单<br>(含在途库存)?"}
F -- "有,且在途量充足" --> G["无需再次采购<br>跳过"]
F -- "没有或在途量不足" --> H["计算建议采购量<br>= 安全库存 × 2 - 可用库存 - 在途库存<br>(确保补货后达到安全库存的2倍)"]
H --> I{"今日是否已为<br>该SKU生成过<br>预警申请单?"}
I -- 是 --> J["避免重复生成<br>跳过(幂等)"]
I -- 否 --> K["创建采购申请单<br>req_source=1<br>priority=1(紧急)"]
K --> L["发送预警通知<br>给采购专员"]
L --> E
E --> M{"是否还有<br>未处理的SKU?"}
M -- 是 --> C
M -- 否 --> N(["任务结束"])
SaaS 只是负责生成 哪个SKU 多少个数量 价格(往期)
但是不能生成完成后 直接推给供货商
要生成后推送给 采购人员
-
要让采购人员知道该SKU 缺货了
-
要让采购人员去审查 自动生成的采购单 是否合理
建议采购量的计算公式:
建议采购量 = Max(安全库存, 预测月销量 × Lead_Time/30) - (可用库存 + 在途库存)
其中:
安全库存 = 日均销量 × 安全天数(通常设为15天)
日均销量 = 近30天总销量 ÷ 30(排除促销异常日)
Lead_Time = 该SKU供应商的交货周期(天)
可用库存 = 实物库存 - 冻结库存
在途库存 = 已确认但未到货的采购单数量之和
示例:
日均销量 = 20件/天
安全天数 = 15天,安全库存 = 300件
Lead_Time = 7天
预测月销量 = 20 × 30 = 600件
预测期需求 = 600 × 7/30 ≈ 140件
可用库存 = 80件,在途库存 = 0件
建议采购量 = Max(300, 140) - (80 + 0) = 300 - 80 = 220件
流程: 使用XXL-JOB定时任务去每小时扫描全部的SKU 找到需要进行补货的 然后生成采购单 并且推送通知给采购专员
要怎么计算是否需要进行补货 ??
建议采购量 = Max(安全库存, 预测月销量 × Lead_Time/30) - (可用库存 + 在途库存)
频率 高频 SKU —> 分片广播 同时扫描 异步处理(主要是负责计算) —> 记录 分批次去扫描 模型 推算哪些商品 慢消品(跳过) 减少扫描的频率 10W SKU 假设两台机器 一次性 limit 20 计算
3.3 销售预测触发机制
flowchart LR
A["历史销量数据<br>近90天每日销量"] --> B["数据预处理<br>识别并剔除异常值<br>(促销日、节假日)"]
B --> C["选择预测模型"]
C --> D["简单移动平均<br>近30天均值"]
C --> E["加权移动平均<br>近期权重更高"]
C --> F["指数平滑法<br>趋势感知更敏感"]
D & E & F --> G["取三种方法预测结果<br>的加权平均值"]
G --> H["预测未来30天需求量"]
H --> I["判断是否需要补货<br>预测需求 > 当前库存+在途库存?"]
I -->|是| J["生成补货建议申请单"]
简单移动平均:近 30 天均值
比如最近 30 天一共卖了 300 件:
日均销量 = 300 / 30 = 10 件/天
那么你可能预测未来每天卖 10 件。
它的特点是简单、稳定,但缺点是反应慢。
比如某商品最近 7 天突然爆单了,但前 23 天销量很低,30 天平均会把这个增长“摊平”,导致预测偏低。
加权移动平均:近期权重更高
它不是简单平均,而是给最近的数据更高权重。
例如最近 3 天销量:
| 日期 | 销量 | 权重 |
|---|---|---|
| 前天 | 8 | 20% |
| 昨天 | 12 | 30% |
| 今天 | 20 | 50% |
预测值:
8 × 20% + 12 × 30% + 20 × 50%= 1.6 + 3.6 + 10= 15.2 件/天
它比简单平均更快感知“最近卖得变好了”。
缺点是:权重需要人为设定,比如最近一天给 50% 是否合理,需要结合业务经验或历史回测。
指数平滑法:是什么意思?
指数平滑法可以理解为:
新的预测 = 一部分使用最新实际销量 + 一部分保留上一期预测
公式一般是:
下一期预测值 = α × 最新实际销量 + (1 - α) × 上一期预测值
其中 α 叫平滑系数,取值在 0 到 1 之间。
比如:
α = 0.3
昨天预测今天卖 10 件
今天实际卖了 20 件
明天预测 = 0.3 × 20 + 0.7 × 10= 6 + 7= 13 件
也就是说,今天突然卖了 20 件,模型不会立刻预测明天也卖 20 件,而是把预测从 10 调整到 13。
如果:
α = 0.8
明天预测 = 0.8 × 20 + 0.2 × 10= 18 件
这说明 α 越大,越相信最新销量,反应越快;α 越小,越保守,预测更平滑。
和加权移动平均有什么区别?
加权移动平均通常是这样:
最近 7 天分别给固定权重
比如:
今天 40%,昨天 25%,前天 15%……
而指数平滑法是:
每次用最新实际销量修正上一期预测
它不一定只看最近 7 天或 30 天,理论上所有历史数据都还在影响预测,只是越久远影响越小。
简单说:
加权移动平均:你手动规定每一天/每一段的权重。
指数平滑法:你设一个 α,模型自动让越新的数据权重越大、越旧的数据权重越小。
第四节 询价比价模块
生成采购单的需求后 不一定就是要立即采购 而是要对比 多个供应商 他们的服务(价格/响应的速度/到货的时间/最小的起订量…) 那么这些的筛选 就是要通过比价功能来进行实现
4.1 为什么需要询价流程
在没有询价系统之前,采购是怎么做的? “感觉老王家比较便宜,就找老王下单。” 这种方式的问题:采购价格完全靠经验,无法证明已经选了最优价格,容易存在利益输送。
询价比价的价值:
- 价格透明:多家供应商公开报价,有据可查
- 竞争降价:供应商知道会被比价,自然会给出竞争性价格
- 决策留痕:为什么选这家供应商、不选那家,有客观数据支撑
4.2 询价完整业务流程
flowchart TD
A["采购专员打开申请单<br>发起询价"] --> B["选择询价供应商<br>(已通过审核的同品类供应商)<br>建议选择2-5家"]
B --> C["填写询价内容:<br>SKU列表/询价数量/<br>期望价格/报价截止时间/特殊要求"]
C --> D["提交询价<br>系统生成询价单编号"]
D --> E["系统自动通知各供应商<br>站内信 + 邮件"]
E --> F["供应商在Portal查看询价单"]
F --> G["供应商填写报价:<br>每个SKU的单价/可供数量<br>/交货天数/有效期/备注"]
G --> H["供应商提交报价<br>系统记录响应时长<br>(用于绩效评分)"]
H --> I{"是否所有供应商<br>都已报价?"}
I -- 还有未报价 --> J{"报价截止时间<br>是否已到?"}
J -- 未到 --> K["等待其他供应商报价"]
K --> F
J -- 已到期 --> L["系统标记未报价供应商<br>status=已过期"]
I -- 全部已报 --> M
L --> M["采购专员打开<br>报价对比界面"]
M --> N["系统生成多维度对比表<br>价格/交期/响应速度/历史评级"]
N --> O["采购专员综合评估<br>选定最优供应商"]
O --> P["点击'选定此报价'<br>自动生成采购订单草稿"]
P --> Q["其他供应商询价单<br>自动标记为'未选中'"]
Q --> R["进入采购订单审批/确认流程"]
4.3 报价比较表设计
报价对比界面是询价模块最核心的 UI,需要让采购专员一眼看出哪家最优:
询价单 INQ-20250117-0001 报价对比
SKU:蓝牙耳机Pro-黑色 询价数量:500件
| 对比维度 | 广州鑫源 | 深圳博通 | 义乌精品 | 最优 |
|---|---|---|---|---|
| 供应商评级 | A级 ⭐⭐ | B级 ⭐ | C级 | 广州鑫源 |
| 报价单价(元) | 38.00 ✅ | 35.50 | 32.00 | 义乌精品 |
| 总金额(元) | 19,000 | 17,750 | 16,000 ✅ | 义乌精品 |
| 交货天数 | 7天 ✅ | 10天 | 15天 | 广州鑫源 |
| 可供数量 | 500件 ✅ | 300件 ⚠️ | 500件 ✅ | — |
| 响应时长 | 3.5小时 ✅ | 12小时 | 36小时 | 广州鑫源 |
| 历史合格率 | 98.5% ✅ | 91.2% | 78.3% ⚠️ | 广州鑫源 |
| 报价有效期 | 7天 | 7天 | 3天 ⚠️ | — |
| 综合评分 | 85.2 ✅ | 72.1 | 61.8 | 广州鑫源 |
| 操作 | [选定此家] | [查看] | [查看] | — |
⚠️ 深圳博通:可供 300 件,不足询价数量 500 件,选择此家需分批采购
⚠️ 义乌精品:历史质量合格率仅 78.3%,存在较高质量风险
⚠️ 义乌精品:报价有效期仅 3 天,需尽快决定
系统建议:综合评分最高为广州鑫源(虽然单价非最低),建议优先选择,原因:质量稳定、交货最快、可全量供货。
4.4 综合评分算法
系统自动计算每家供应商报价的综合评分,辅助决策:
综合评分(满分100分)= 价格评分×40% + 交期评分×30% + 质量评分×20% + 响应评分×10%
价格评分:
以最低报价为基准,计算各供应商溢价比例
溢价 0% → 100分
溢价 1%-5% → 80分
溢价 5%-10% → 60分
溢价 10%-15% → 40分
溢价 >15% → 20分
交期评分:
以最短交期为基准
最短交期 → 100分
多1-3天 → 80分
多4-7天 → 60分
多8-14天 → 40分
多15天以上 → 20分
质量评分:
直接使用供应商的历史质量合格率 × 100
响应评分:
≤2小时 → 100分
2-8小时 → 80分
8-24小时 → 60分
24-48小时 → 40分
>48小时 → 20分
4.5 历史价格查询功能
采购专员在评估报价时,可以查看该 SKU 的历史成交价格走势,判断当前报价是否合理:
graph LR
A["采购专员查看报价"] --> B["系统自动加载<br>该SKU历史采购价格"]
B --> C["展示近6次成交价<br>折线趋势图"]
C --> D["计算价格偏差:<br>当前报价 vs 历史均价"]
D --> E{"偏差判断"}
E -- "低于历史均价10%以上" --> F["🟢 标注:价格优惠<br>可能有质量风险,注意确认"]
E -- "高于历史均价10%以上" --> G["🔴 标注:价格偏高<br>需要与供应商协商或选其他家"]
E -- "在历史均价±10%以内" --> H["🟡 标注:价格正常"]
第五节 采购订单管理(核心)
5.1 采购订单状态机深度解析
状态机 技术点 如果你要在面试的时候讲解这个状态机 就要把这个状态机所有的状态 以及 状态之间的流转 要背下来 背下来的目的是为了让你给面试官讲解的时候 更流畅 更自信
采购订单是 PMS 的核心实体,有 9 个状态,是本模块最复杂的状态机:
stateDiagram-v2
[*] --> 草稿 : 手动创建或从询价自动生成
草稿 --> 待供应商确认 : 采购专员确认后发给供应商
待供应商确认 --> 已确认 : 供应商在Portal确认接单
待供应商确认 --> 草稿 : 供应商提出修改意见
已确认 --> 发货中 : 供应商更新物流单号
发货中 --> 部分到货 : 仓库首次完成收货(未全部到货)
部分到货 --> 部分到货 : 仓库再次完成收货(仍未全部到货)
部分到货 --> 全部到货 : 仓库完成全部商品的收货
发货中 --> 全部到货 : 一次性全部到货
全部到货 --> 已对账 : 财务完成发票核对
已对账 --> 已结清 : 完成付款,欠款清零
草稿 --> 已取消 : 采购专员取消
待供应商确认 --> 已取消 : 采购专员取消(通知供应商)
已取消 --> [*]
已结清 --> [*]
状态对应的业务含义与可执行操作:
| 状态码 | 状态名 | 业务含义 | 本状态可执行操作 |
|---|---|---|---|
| 0 | 草稿 | 尚未发给供应商,可自由编辑 | 编辑、发给供应商、取消 |
| 1 | 待供应商确认 | 已发给供应商,等待对方确认 | 催单、取消 |
| 2 | 已确认 | 供应商已接单,可期待发货 | 查看物流信息 |
| 3 | 发货中 | 供应商已填写物流单号,货在路上 | 查看物流轨迹 |
| 4 | 部分到货 | 仓库已收到部分货物 | 查看收货进度、催剩余 |
| 5 | 全部到货 | 全部货物已入库 | 移交财务对账 |
| 6 | 已对账 | 财务核对发票完成 | 付款操作 |
| 7 | 已结清 | 款项已全额支付 | 仅查看 |
| 8 | 已取消 | 订单已取消 | 查看原因 |
5.2 采购订单创建完整流程
flowchart TD
A(["需要创建采购订单"]) --> B{"创建方式"}
B -- "从询价单自动生成" --> C["系统读取选中的报价信息<br>自动填充:供应商/SKU/单价/数量"]
B -- "手动创建" --> D["手动填写采购信息<br>选择供应商/添加SKU/填写单价"]
C & D --> E["填写收货仓库、期望到货日期"]
E --> F{"总金额是否<br>超过审批阈值?<br>(默认10000元)"}
F -- 超过 --> G["需要提交采购负责人审批<br>先进入内部审批流程"]
F -- 未超过 --> H["直接发给供应商确认"]
G --> I["采购负责人审批"]
I --> J{"审批结果"}
J -- 拒绝 --> K["退回修改<br>注明拒绝原因"]
J -- 通过 --> H
K --> D
H --> L["系统发送通知给供应商<br>Portal站内信 + 邮件"]
L --> M["更新状态:草稿→待供应商确认"]
M --> N["等待供应商确认"]
5.3 采购订单编号生成规则
格式:PO-{YYYYMMDD}-{4位序号}
示例:PO-20250117-0001
与供应商编号类似,但需要额外注意:
同一天可能存在多个租户各自生成大量PO,
序号在租户内唯一即可(UNIQUE KEY 约束在 tenant_id + po_no)
5.4 供应商确认采购单流程
flowchart TD
A["供应商登录Portal<br>看到待确认的采购单"] --> B["查看采购单详情:<br>SKU列表/数量/单价/总金额/<br>收货地址/期望到货日期"]
B --> C{"供应商的决定"}
C -- "完全接受" --> D["点击'确认接单'<br>填写预计发货日期"]
C -- "有部分问题" --> E["在备注中说明问题<br>如:X颜色库存不足<br>可供200件,非500件"]
C -- "无法接受" --> F["点击'无法接单'<br>填写原因"]
D --> G["系统更新状态:<br>待确认→已确认"]
G --> H["发送通知给采购专员:<br>供应商已确认,预计X日发货"]
E --> I["系统标记待沟通<br>发通知给采购专员"]
I --> J["采购专员与供应商协商<br>可能需要修改数量或分批"]
J --> D
F --> K["系统通知采购专员:<br>该供应商无法接单,请更换"]
K --> L["采购专员可:<br>1.修改订单找其他供应商<br>2.重新询价"]
5.5 采购订单的主从表关联设计
主从表(Master-Detail)设计是企业项目中最常见的数据库设计模式。
主表(purchase_order)存什么?
- 整个采购单的汇总信息:总金额、供应商、仓库、状态、收货日期
- 一个采购单只有一条主表记录
从表(purchase_order_item)存什么?
- 每个 SKU 的具体信息:品名、数量、单价、小计
- 一个采购单可以包含多个 SKU,每个 SKU 一条明细记录
graph TD
A["purchase_order<br>主表:一条记录<br>PO-20250117-0001<br>总金额 19,000元<br>供应商:广州鑫源<br>状态:已确认"] --> B["purchase_order_item<br>明细1:蓝牙耳机-黑色<br>数量:300件<br>单价:38元<br>小计:11,400元"]
A --> C["purchase_order_item<br>明细2:蓝牙耳机-白色<br>数量:200件<br>单价:38元<br>小计:7,600元"]
B & C --> D["两条明细合计:<br>11,400 + 7,600 = 19,000元<br>与主表总金额一致"]
关键业务规则:
- ==主表的
total_amount必须等于所有明细的amount之和== - ==新增/修改/删除明细时,必须同步更新主表的
total_amount== - ==这个操作必须在同一个数据库事务中完成(要么全成功,要么全回滚)==
- 主表和从表一定要保持数据/业务的一致性(事务)
第六节 采购审批流程
6.1 为什么需要采购审批
如果没有审批,采购专员可以随意下大额采购单,带来以下风险:
- 采购数量超出实际需求,造成库存积压和资金占用
- 采购价格偏高,损害公司利益
- 甚至存在与供应商勾结的风险(采购专员收取回扣)
审批阈值配置:
| 金额区间 | 审批要求 |
|---|---|
| 0 - 10,000 元 | 无需审批,采购专员直接下单 |
| 10,001 - 50,000 元 | 需采购负责人审批 |
| 50,001 - 200,000 元 | 需采购负责人 + 财务负责人双签 |
| 200,000 元以上 | 需总经理审批 |
以上阈值应该是可配置的,不同租户有不同设置,存储在系统配置表中。 我们的SaaS系统主要还是提供服务的 应该尽量的考虑到每个租户的不同的需求 把系统设计的更为灵活 及 方便 我们不应该干涉租户的决定 我们只是提供数据的整合和记录的功能
6.2 审批流程设计
flowchart TD
A["采购专员提交采购申请"] --> B["系统计算申请总金额"]
B --> C{"是否超过审批阈值?"}
C -- 未超过 --> D["自动审批通过<br>直接可以发起询价/下单"]
C -- "10000-50000" --> E["创建审批任务<br>推送给采购负责人"]
C -- "50001-200000" --> F["创建审批任务<br>推送给采购负责人<br>和财务负责人"]
C -- ">200000" --> G["创建审批任务<br>推送给总经理"]
E --> H["审批人查看申请单<br>查看:申请理由/SKU/数量/<br>当前库存/估算金额/供应商建议"]
F --> H
G --> H
H --> I{"审批决定"}
I -- "通过" --> J["更新申请单状态:审批通过<br>通知采购专员可以下单"]
I -- "拒绝" --> K["填写拒绝原因<br>通知采购专员"]
I -- "修改后重提" --> L["填写修改意见<br>退回给采购专员"]
K --> M["采购专员可以<br>修改后重新提交"]
L --> M
J --> N["采购专员继续执行<br>发询价或直接下采购单"]
6.3 审批的时效性要求
flowchart LR
A["审批任务创建"] --> B["24小时内未处理"]
B --> C["系统发送提醒<br>催促审批人处理"]
C --> D["再过24小时仍未处理"]
D --> E["升级提醒:通知上级<br>避免因审批延迟影响采购"]
E --> F["超过72小时未处理"]
F --> G["自动升级审批人<br>由上级直接处理"]
定时任务 去扫描 超过 24小时还未处理的 审批 给需要进行审批的那个人 推送消息 (即时通讯)
第七节 采购入库与 WMS 联动
这是整个 PMS 中技术难度最高的部分:采购入库需要同时更新采购单状态和仓库库存,必须保证原子性,即两个操作要么同时成功,要么同时失败,不能出现”采购单显示已收货,但库存没增加”的情况。
7.1 入库业务完整流程
flowchart TD
A(["货物到达仓库门口"]) --> B["仓储管理员核对<br>实物 vs 送货单"]
B --> C{"单据匹配?"}
C -- 不匹配 --> D["联系采购专员确认<br>可能是:发货多了/少了/发错了"]
D --> E{"确认结果"}
E -- "正常,数量有出入" --> F["按实际收到数量创建收货单"]
E -- "发错货了" --> G["直接拒收<br>让供应商重新发货"]
C -- 匹配 --> F
F --> H["仓储管理员逐件扫描条码<br>或手动录入数量"]
H --> I["质量检验<br>外观/功能/规格抽检"]
I --> J{"质检结果"}
J -- "全部合格" --> K["确认收货<br>填写上架库位"]
J -- "部分合格" --> L["合格品正常入库<br>不合格品隔离放退货区"]
J -- "全部不合格" --> M["拒收整批<br>创建退货单"]
K --> N["系统执行入库事务<br>(关键步骤,见下方详解)"]
L --> N
N --> O["更新采购单状态<br>4=部分到货 或 5=全部到货"]
O --> P["生成库存流水记录<br>变动类型=采购入库"]
P --> Q["仓储管理员将货物<br>搬运到指定库位上架"]
Q --> R(["入库完成"])
G --> S(["拒收处理"])
M --> T(["退货流程"])
7.2 入库事务的原子性设计(核心重点)
sequenceDiagram
participant FE as 前端
participant BE as 后端
participant DB as 数据库
participant Redis as Redis缓存
FE->>BE: 确认收货(收货单ID,各SKU质检数量)
BE->>DB: 开启数据库事务 BEGIN TRANSACTION
BE->>DB: 1. 校验收货单状态(必须是待质检状态)
DB-->>BE: 返回收货单信息
BE->>DB: 2. 更新收货单明细(pass_qty/reject_qty)
BE->>DB: 3. 更新收货单状态 → 全部入库
BE->>DB: 4. 更新采购单明细received_qty += pass_qty
BE->>DB: 5. 重新计算采购单总received_qty
BE->>DB: 6. 判断采购单状态:部分到货 or 全部到货
BE->>DB: 7. 增加WMS库存表数量<br>UPDATE inventory SET quantity = quantity + ? WHERE sku_id=? AND warehouse_id=?
BE->>DB: 8. 写入库存流水记录(inventory_log)
DB-->>BE: 全部SQL执行成功
BE->>DB: COMMIT 提交事务(7步骤全部成功才提交)
BE->>Redis: 清除该SKU的库存缓存
BE-->>FE: 返回成功
Note over BE,DB: 任意一步失败 → ROLLBACK 回滚<br>所有操作撤销,数据保持一致
第 0 步:前端发起确认收货
前端会传:
{ "receiptId": 8001, "items": [ { "receiptItemId": 8101, "skuId": 10001, "passQty": 280, "rejectQty": 20 } ] }
含义:
passQty:质检合格数量,可以入库 rejectQty:质检不合格数量,不能进入可售库存
注意点:
- passQty + rejectQty 不能大于本次应收数量。
- passQty 不能小于 0。
- 不能重复确认同一个收货单。
- 前端传来的数量不能直接相信,后端必须重新校验。
第 1 步:开启数据库事务
BEGIN TRANSACTION
这一步的作用是把后续多个数据库操作绑定成一个整体。
也就是说:
要么全部成功 要么全部失败
如果没有事务,可能会出现:
收货单状态已更新 采购单 received_qty 已增加 但是库存没增加 或者库存增加了,但库存流水没写入
这种数据以后很难对账。
注意点:
- 事务范围不要太大,只包住必须强一致的数据库操作。
- 不要在事务里做耗时操作,比如发邮件、调外部接口。
- Redis 缓存删除通常放在事务提交之后。
第 2 步:校验收货单状态
校验收货单状态(必须是待质检状态)
后端要查询收货单:
SELECT * FROM purchase_receipt WHERE id = #{receiptId} AND tenant_id = #{tenantId} AND is_deleted = 0;
然后校验:
收货单必须存在 必须属于当前租户 状态必须是待质检 不能已经入库 不能已经拒收 关联的采购单必须存在
更严谨一点可以加锁:
SELECT * FROM purchase_receipt WHERE id = #{receiptId} AND tenant_id = #{tenantId} FOR UPDATE;
这样可以防止两个仓库人员同时确认同一张收货单。
注意点:
- 不允许前端传 status=全部入库 直接改状态。
- 状态流转必须由后端业务动作控制。
- 这里也是幂等和并发控制的入口。
第 3 步:更新收货单明细
更新收货单明细(pass_qty / reject_qty)
比如某个 SKU 本次应到 300 件,质检合格 280 件,不合格 20 件。
更新收货明细:
UPDATE purchase_receipt_item SET pass_qty = #{passQty}, reject_qty = #{rejectQty}, status = 1, update_time = NOW() WHERE id = #{receiptItemId} AND tenant_id = #{tenantId} AND status = 0;
这里的状态可以理解为:
0 = 待质检 1 = 已入库 2 = 已退货 / 待退货
注意点:
- pass_qty + reject_qty 必须等于或小于本次收货数量。
- 不合格数量不能加到可售库存。
- 不合格品后续可以进入退货流程或不良品区。
- 更新时最好带 status = 0 条件,防止重复确认。
第 4 步:更新收货单状态
更新收货单状态 → 全部入库
这里的“全部入库”指的是:这张收货单已经处理完成,不是整张采购单全部到货。
比如采购单买了 500 件,第一次只到了 300 件。这 300 件质检完成后,这张收货单可以是“全部入库”,但采购单仍然是“部分到货”。
注意点:
收货单状态:描述本次收货任务是否完成 采购单状态:描述整张采购订单是否全部完成
这两个状态不要混淆。
第 5 步:更新采购单明细 received_qty
更新采购单明细 received_qty += pass_qty
假设采购单明细是:
采购数量 quantity = 500 已入库数量 received_qty = 0
第一次收货:
本次到货 300 合格 pass_qty = 280 不合格 reject_qty = 20
如果系统定义 received_qty 表示“合格入库数量”,那么更新后:
received_qty = 0 + 280 = 280
SQL 应该写成:
UPDATE purchase_order_item SET received_qty = received_qty + #{passQty}, update_time = NOW() WHERE id = #{poItemId} AND tenant_id = #{tenantId};
重点是:
received_qty += pass_qty
而不是:
received_qty = pass_qty
因为采购单可能分多次收货。
比如第二次又合格 220 件:
received_qty = 280 + 220 = 500
这样才知道这条采购明细已经全部到货。
注意点:
- 如果 reject_qty 不入库,就不要加到 received_qty。
- 如果业务想记录“实收到货数量”,建议单独加字段:
- arrived_qty:实际到货数量
- received_qty:合格入库数量
- reject_qty:不合格数量
- 字段语义一定要清楚,否则后面库存、对账、退货都会乱。
第 6 步:重新计算采购单总 received_qty
重新计算采购单总 received_qty
这一步不是简单为了算一个数字,而是为了判断整张采购单的完成情况。
采购单可能有多条明细:
SKU-A 采购 500 件,已入库 500 件 SKU-B 采购 200 件,已入库 120 件 SKU-C 采购 100 件,已入库 100 件
这时整张采购单还不能算全部到货,因为 SKU-B 还差 80 件。
可以查询:
SELECT SUM(quantity) AS total_order_qty, SUM(received_qty) AS total_received_qty FROM purchase_order_item WHERE po_id = #{poId} AND tenant_id = #{tenantId} AND is_deleted = 0;
然后判断:
total_received_qty = 0 还没到货 0 < total_received_qty < total_order_qty 部分到货 total_received_qty >= total_order_qty 全部到货
注意点:
- 判断时应该按明细维度更严谨,避免某个 SKU 超收掩盖另一个 SKU 未收。
- 更严谨的判断是:所有明细 received_qty >= quantity,才算全部到货。
- 如果允许超收,比如供应商多发 5 件,要有单独规则:允许入库、拒收、还是生成补充采购单。
第 7 步:判断采购单状态
判断采购单状态:部分到货 or 全部到货
根据上一步结果更新采购单主表状态。
例如:
UPDATE purchase_order SET status = 4, update_time = NOW() WHERE id = #{poId} AND tenant_id = #{tenantId};
状态含义:
4 = 部分到货 5 = 全部到货
如果全部到货,后续可以触发:
生成应付账款 进入财务对账 供应商绩效评分数据沉淀
注意点:
- 状态只能从“发货中/部分到货”变成“部分到货/全部到货”。
- 不能从“已结清”回退成“部分到货”。
- 状态变更最好写操作日志。
第 8 步:增加 WMS 库存数量
增加WMS库存表数量 UPDATE inventory SET quantity = quantity + ? WHERE sku_id = ? AND warehouse_id = ?
这一步是把合格品真正加到库存里。
比如本次合格 280 件:
UPDATE inventory SET quantity = quantity + 280, update_time = NOW() WHERE tenant_id = #{tenantId} AND sku_id = #{skuId} AND warehouse_id = #{warehouseId};
注意点:
第一,必须用:
quantity = quantity + #{passQty}
不要用:
quantity = #{newQuantity}
因为两个入库操作并发时,后者可能覆盖前者。
第二,如果库存记录不存在,需要先创建:
先查 inventory 是否存在 不存在则 insert 一条库存记录 存在则 update quantity = quantity + N
真实项目里要有唯一索引:
UNIQUE KEY uk_inventory_sku_wh (tenant_id, warehouse_id, sku_id)
第三,只能把合格数量加入库存:
pass_qty 加库存 reject_qty 不加可售库存
不合格数量可以进入:
defective_qty 退货暂存区 采购退货单
第 9 步:写入库存流水
写入库存流水记录 inventory_log
库存流水是审计依据。库存只更新数量是不够的,还要记录这次库存为什么变化。
例如:
INSERT INTO inventory_log ( id, tenant_id, sku_id, warehouse_id, change_type, change_qty, before_qty, after_qty, biz_type, biz_id, remark, create_time ) VALUES ( #{id}, #{tenantId}, #{skuId}, #{warehouseId}, 'PURCHASE_IN', #{passQty}, #{beforeQty}, #{afterQty}, 'PURCHASE_RECEIPT', #{receiptId}, '采购入库', NOW() );
注意点:
- 流水要和库存更新在同一个事务里。
- 库存流水原则上只增不改,不允许随便删除。
- 流水里最好记录业务单据 ID,比如 receipt_id、po_id。
- 后续库存对账、审计、问题排查都依赖流水。
如果没有流水,未来有人问:
为什么这个 SKU 库存突然多了 280 件?
系统就查不出来。
第 10 步:提交事务
COMMIT
前面所有数据库操作都成功,才提交事务。
注意:你给的图里写的是:
COMMIT 提交事务(7步骤全部成功才提交)
严格说应该是:
8 个数据库步骤全部成功才提交
因为写库存流水也是关键步骤。
只要其中任何一步失败:
ROLLBACK
所有数据库操作撤销。
第 11 步:清除 Redis 库存缓存
BE -> Redis: 清除该SKU的库存缓存
这一步放在事务提交之后是对的。
因为如果事务还没提交就删缓存,其他请求可能马上查数据库,但数据库里的新库存还没提交,可能又把旧数据写回缓存。
常见做法:
先提交数据库事务 再删除 Redis 缓存
注意点:
- Redis 删除失败不应该回滚数据库事务。
- 删除失败可以记录日志,或者发送重试消息。
- 高一致性场景可以使用延迟双删。
- 如果订单模块使用 Redis 做库存扣减,还要考虑 Redis 库存和 MySQL 库存的同步策略。
为什么必须用事务?
场景:入库确认时,步骤7(更新库存)执行成功,但此时服务器宕机,步骤8(写流水)没有执行。
后果:库存数量增加了,但库存流水记录缺失。这会导致:
- 库存对账时数据不一致
- 无法追溯这次库存增加是哪张采购单造成的
- 供应商绩效评分缺少数据
事务的作用:用事务包裹所有操作,只要有任何一步失败,所有操作全部回滚,数据库回到操作前的状态,不会出现”半完成”的脏数据。
7.3 库存更新的并发安全
sequenceDiagram
participant W1 as 仓管员甲<br>(同时操作)
participant W2 as 仓管员乙<br>(同时操作)
participant DB as 数据库
Note over DB: 当前库存:100件
W1->>DB: UPDATE inventory<br>SET quantity = quantity + 50<br>WHERE sku_id = 1 AND warehouse_id = 1
W2->>DB: UPDATE inventory<br>SET quantity = quantity + 30<br>WHERE sku_id = 1 AND warehouse_id = 1
Note over DB: 两条UPDATE语句在数据库层面串行执行<br>(行级锁保证)<br>最终库存:100 + 50 + 30 = 180件
DB-->>W1: 更新成功,影响1行
DB-->>W2: 更新成功,影响1行
关键点:库存更新使用
quantity = quantity + N而不是quantity = 180
- 错误写法:
SET quantity = 180(先查出100,加50,直接SET 150,丢失了乙的+30)- 正确写法:
SET quantity = quantity + 50(数据库层面原子加法,自动串行处理并发)
7.4 部分收货处理
一张采购单下了500件,第一次来了300件,第二次来了200件,如何处理?
flowchart TD
A["第一次到货:300件"] --> B["创建收货单 RCV-001<br>质检300件全部通过"]
B --> C["入库事务执行:<br>库存 +300<br>purchase_order_item.received_qty = 300<br>采购单状态 → 部分到货(4)"]
C --> D["第二次到货:200件"]
D --> E["创建收货单 RCV-002<br>质检200件全部通过"]
E --> F["入库事务执行:<br>库存 +200<br>purchase_order_item.received_qty = 300+200 = 500<br>500 = 采购数量500,已全部到货"]
F --> G["采购单状态 → 全部到货(5)"]
G --> H["生成应付账款记录<br>(全部到货后才生成)"]
7.5 库存联动的完整数据流
graph LR
A["采购入库确认"] --> B["inventory 表<br>quantity +N"]
A --> C["inventory_log 表<br>新增流水记录<br>type=采购入库"]
A --> D["purchase_order_item 表<br>received_qty +N"]
A --> E["purchase_receipt_item 表<br>status=已入库"]
B --> F["Redis 缓存失效<br>下次查询重新从DB取"]
D --> G["计算采购单状态<br>部分到货 or 全部到货"]
G --> H["purchase_order 表<br>status 更新"]
第八节 采购退货处理
8.1 退货业务场景
| 退货原因 | 具体表现 | 处理优先级 |
|---|---|---|
| 质量问题 | 产品功能异常、外观瑕疵、材质不符 | 🔴 高 |
| 数量不符 | 实收数量少于采购数量 | 🟡 中 |
| 规格/型号错误 | 发货的 SKU 与采购单不一致 | 🔴 高 |
| 包装损坏 | 包装严重破损,影响销售 | 🟡 中 |
| 超期未到货 | 货物严重超期,客户取消了对应订单 | 🟠 中高 |
8.2 采购退货完整流程
flowchart TD
A(["发现问题:需要退货"]) --> B["仓储管理员拍照记录<br>问题证据(照片/视频)"]
B --> C["创建退货申请单<br>选择关联的采购单<br>填写:退货SKU/数量/原因/证据图片"]
C --> D["采购专员审核退货申请"]
D --> E{"是否需要审批?<br>(退货金额>5000时需要)"}
E -- 需要 --> F["采购负责人审批"]
E -- 不需要 --> G["直接通知供应商"]
F --> G
G --> H["供应商在Portal查看退货申请"]
H --> I{"供应商的处理"}
I -- "同意退货" --> J["供应商确认接受退货"]
I -- "拒绝退货" --> K["填写拒绝理由<br>可能:认为质量没问题"]
K --> L["进入争议处理流程<br>采购负责人介入协商<br>可要求第三方检测"]
J --> M["仓储执行退货出库<br>库存相应减少<br>生成库存流水(退货出库)"]
M --> N["安排退货物流<br>填写退货运单号"]
N --> O["供应商收到退货<br>签收确认"]
O --> P{"处理方式"}
P -- "退款" --> Q["财务冲减应付账款<br>或申请退款"]
P -- "补发货物" --> R["重新发货<br>进入正常收货流程"]
Q & R --> S(["退货处理完成"])
L --> T{"协商结果"}
T -- "达成一致" --> J
T -- "无法达成一致" --> U["上报公司处理<br>降低供应商评分"]
8.3 退货对库存的影响
flowchart LR
A["退货申请通过<br>开始执行退货出库"] --> B["检查退货的货物<br>在哪个库位"]
B --> C["从库位中拿出退货商品<br>移至退货暂存区"]
C --> D["执行库存减少事务:<br>inventory.quantity -= 退货数量<br>写入库存流水:类型=退货出库"]
D --> E{"退货商品的处置"}
E -- "退还给供应商" --> F["打包出库<br>安排物流"]
E -- "报损(无法退货)" --> G["库存报损处理<br>inventory.defective_qty减少<br>写入流水:类型=报损"]
第九节 应付账款管理
9.1 应付账款的业务意义
为什么不采购完成就立刻付款?
账期(Credit Term) 是商业信用的体现。供应商允许卖家先收货、后付款,本质上是供应商在向卖家提供短期融资。对卖家来说,账期越长,资金周转越灵活。
账期举例:
2025年1月17日:货物到仓库,采购金额 19,000 元
合同约定:月结30天(账期30天)
则应付账款到期日 = 2025年1月17日 + 30天 = 2025年2月16日
在2025年2月16日之前,这19,000元都不需要付给供应商。
这30天内,这笔钱可以用于其他经营活动。
9.2 应付账款全生命周期
flowchart TD
A["采购全部到货<br>(purchase_order.status = 5)"] --> B["系统自动生成应付账款记录<br>finance_payable"]
B --> C["初始状态:待付款<br>due_date = 开票日期 + payment_days"]
C --> D["财务专员上传发票<br>核对:发票金额 vs 采购单金额"]
D --> E{"发票金额是否一致?"}
E -- "不一致" --> F["联系供应商核实<br>可能是发票开错了"]
F --> D
E -- "一致" --> G["应付账款状态更新为已对账"]
G --> H["定时任务每天更新<br>overdue_days = today - due_date"]
H --> I{"距到期天数"}
I -- "7天前" --> J["发送提醒:7天后到期"]
I -- "3天前" --> K["发送提醒:3天后到期"]
I -- "当天" --> L["发送提醒:今天到期,请立即付款"]
I -- "已逾期" --> M["发送逾期告警<br>记录逾期天数影响供应商评分"]
J & K & L --> N["财务专员发起付款"]
N --> O["录入付款信息:<br>付款金额/付款日期/银行流水号"]
O --> P["写入付款记录表<br>finance_payment_record"]
P --> Q["更新已付金额<br>paid_amount += 本次付款额"]
Q --> R{"剩余金额 = 0?"}
R -- 否 --> S["状态:部分已付"]
S --> N
R -- 是 --> T["状态:已结清"]
T --> U["采购单状态同步更新<br>purchase_order.status = 7 已结清"]
U --> V(["账款完成"])
M --> N
9.3 应付账款汇总报表
财务专员每天需要查看应付账款的整体情况:
应付账款总览(截止今日)
未到期:
┌─────────────────────────────────────────┐
│ 7天内到期 │ 3笔 │ 合计:28,500 元 │
│ 7-30天内 │ 8笔 │ 合计:76,200 元 │
│ 30天以上 │ 5笔 │ 合计:43,000 元 │
└─────────────────────────────────────────┘
已逾期:
┌─────────────────────────────────────────┐
│ 逾期1-7天 │ 1笔 │ 合计:12,000 元 │
│ 逾期7-30天 │ 0笔 │ 合计:0 元 │
│ 逾期30天以上│0笔 │ 合计:0 元 │
└─────────────────────────────────────────┘
按供应商汇总(未结清):
广州鑫源:19,000元(PO-20250117-0001,2025-02-16到期)
深圳博通:28,500元(PO-20250115-0003,2025-02-15到期)
...
9.4 应付账款到期提醒定时任务
-- 每天 08:30 执行的到期提醒查询
-- 查询所有即将到期或已逾期的未结清应付账款
SELECT
fp.id,
fp.payable_no,
fp.supplier_id,
fp.supplier_name,
fp.payable_amount,
fp.paid_amount,
fp.payable_amount - fp.paid_amount AS remaining_amount,
fp.due_date,
DATEDIFF(fp.due_date, CURDATE()) AS days_until_due,
CASE
WHEN DATEDIFF(fp.due_date, CURDATE()) < 0 THEN '已逾期'
WHEN DATEDIFF(fp.due_date, CURDATE()) = 0 THEN '今日到期'
WHEN DATEDIFF(fp.due_date, CURDATE()) <= 3 THEN '3天内到期'
WHEN DATEDIFF(fp.due_date, CURDATE()) <= 7 THEN '7天内到期'
END AS urgency_level
FROM finance_payable fp
WHERE
fp.is_deleted = 0
AND fp.status IN (0, 1) -- 待付款或部分已付
AND fp.due_date <= DATE_ADD(CURDATE(), INTERVAL 7 DAY) -- 7天内到期或已逾期
ORDER BY
fp.due_date ASC;
第十节 采购数据统计报表
10.1 采购核心 KPI
报表统计的做法
- 如果你的数据量不大 或者 你们要求是实时是
- 就可以直接查询统计返回数据即可
- 如果对实时性要求特别高的情况下 我们也可以使用Redis进行统计
- 如果你们的数据量很大 或者 你们要求不是实时的(比如: 当天显示的是统计到前一天的数据)
- 就可以通过定时任务去扫描 把统计的结果 保存到一个统计表中
- 当需要进行展示的时候 就直接查询统计表中的单条数据
- 列存储数据库
- 统计 分析
- 数据的双重的保存 (哪些需要进行统计 分析) 只保存特定的表数据就可以了
| 指标名称 | 计算公式 | 业务意义 | 预警阈值 |
|---|---|---|---|
| 采购准时到货率 | 准时到货采购单数 / 总到货采购单数 | 衡量供应商整体履约质量 | < 90% 预警 |
| 采购质量合格率 | 质检合格件数 / 总收货件数 | 衡量采购商品整体质量 | < 95% 预警 |
| 采购单平均处理时长 | 从创建到全部到货的平均天数 | 衡量采购效率 | > 30天预警 |
| 应付账款逾期率 | 逾期账款金额 / 总未结清金额 | 衡量付款合规性 | > 5% 预警 |
| 退货率 | 退货金额 / 总采购金额 | 衡量采购质量控制水平 | > 3% 预警 |
10.2 重点报表设计
① 供应商采购金额排行榜
-- 查询指定时间段内各供应商的采购情况
SELECT
po.supplier_id,
po.supplier_name,
COUNT(po.id) AS order_count, -- 采购单数量
SUM(po.total_amount) AS total_amount, -- 总采购金额
SUM(CASE WHEN po.status = 7 THEN po.total_amount ELSE 0 END) AS paid_amount, -- 已结清金额
SUM(CASE WHEN po.status IN (5,6) THEN po.total_amount ELSE 0 END) AS unpaid_amount, -- 待付款金额
ROUND(AVG(
DATEDIFF(pr.receive_date, po.order_date)
), 1) AS avg_lead_time_days, -- 实际平均交货周期
s.grade AS supplier_grade, -- 供应商当前评级
s.score AS supplier_score
FROM purchase_order po
LEFT JOIN purchase_receipt pr ON po.id = pr.po_id AND pr.is_deleted = 0
LEFT JOIN supplier s ON po.supplier_id = s.id
WHERE
po.tenant_id = #{tenantId}
AND po.is_deleted = 0
AND po.status != 8 -- 排除已取消的采购单
AND po.order_date BETWEEN #{startDate} AND #{endDate}
GROUP BY po.supplier_id, po.supplier_name, s.grade, s.score
ORDER BY total_amount DESC
LIMIT 20;
② 采购月度趋势报表
-- 按月统计采购金额和订单数趋势
SELECT
DATE_FORMAT(po.order_date, '%Y-%m') AS month,
COUNT(po.id) AS order_count,
SUM(po.total_amount) AS total_amount,
COUNT(DISTINCT po.supplier_id) AS supplier_count, -- 涉及供应商数量
SUM(poi.quantity) AS total_qty, -- 采购总件数
ROUND(
SUM(CASE WHEN po.status IN (4,5,6,7) THEN 1 ELSE 0 END) * 100.0 / COUNT(po.id)
, 2) AS delivery_rate -- 到货率
FROM purchase_order po
LEFT JOIN purchase_order_item poi ON po.id = poi.po_id
WHERE
po.tenant_id = #{tenantId}
AND po.is_deleted = 0
AND po.order_date BETWEEN #{startDate} AND #{endDate}
GROUP BY DATE_FORMAT(po.order_date, '%Y-%m')
ORDER BY month ASC;
③ SKU 采购价格历史
-- 查询某SKU的历史采购价格变化,用于评估当前报价是否合理
SELECT
po.order_date AS order_date,
po.po_no AS po_no,
po.supplier_name AS supplier_name,
poi.unit_price AS unit_price,
poi.quantity AS quantity,
poi.amount AS amount,
s.grade AS supplier_grade
FROM purchase_order_item poi
JOIN purchase_order po ON poi.po_id = po.id
LEFT JOIN supplier s ON po.supplier_id = s.id
WHERE
poi.tenant_id = #{tenantId}
AND poi.sku_id = #{skuId}
AND po.status NOT IN (0, 8) -- 排除草稿和已取消
AND po.is_deleted = 0
AND poi.is_deleted = 0
ORDER BY po.order_date DESC
LIMIT 10;
10.3 采购报表看板布局
采购数据总览 时间范围:[本月 ▼] [自定义]
┌──────────┬──────────┬──────────┬──────────┐
│ 本月采购额 │ 本月订单数│ 准时到货率 │ 质量合格率 │
│ ¥286,500 │ 42 单 │ 94.3% │ 98.7% │
│ ↑12%较上月│ ↑5 单 │ ↓1.2% │ ↑0.3% │
└──────────┴──────────┴──────────┴──────────┘
┌────────────────────────┬───────────────────────┐
│ 采购金额月度趋势(折线图) │ 供应商占比(饼图) │
│ │ │
│ 30万 ___ │ ■ 广州鑫源 38% │
│ 20万 / \___/ │ ■ 深圳博通 27% │
│ 10万 │ ■ 义乌精品 18% │
│ 1月 2月 3月 4月 5月 │ ■ 其他 17% │
└────────────────────────┴───────────────────────┘
┌────────────────────────────────────────────────┐
│ 7天内到期应付账款 │
│ 广州鑫源 ¥19,000 → 2025-02-16(3天后)⚠️ │
│ 深圳博通 ¥28,500 → 2025-02-15(2天后)🔴 │
└────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐
│ 待处理事项 │
│ • 3 张采购申请单待审批 │
│ • 2 张采购单待供应商确认(超过48小时未回应) │
│ • 1 张收货单待质检 │
│ • 1 张退货单待供应商处理 │
└────────────────────────────────────────────────┘
第十一节 接口设计规范
11.1 采购模块完整接口清单
基础路径前缀: /api/pms
| 模块 | 接口名称 | HTTP方法 | 路径 | 权限标识 |
|---|---|---|---|---|
| 采购申请 | 申请单列表 | GET | /requisitions | pms:req:list |
| 采购申请 | 申请单详情 | GET | /requisitions/{id} | pms:req:list |
| 采购申请 | 新增申请单 | POST | /requisitions | pms:req:add |
| 采购申请 | 编辑申请单 | PUT | /requisitions/{id} | pms:req:edit |
| 采购申请 | 删除申请单 | DELETE | /requisitions/{id} | pms:req:delete |
| 采购申请 | 提交审批 | PUT | /requisitions/{id}/submit | pms:req:submit |
| 采购申请 | 审批通过 | PUT | /requisitions/{id}/approve | pms:req:audit |
| 采购申请 | 审批拒绝 | PUT | /requisitions/{id}/reject | pms:req:audit |
| 询价管理 | 发起询价 | POST | /inquiries | pms:inquiry:add |
| 询价管理 | 询价单列表 | GET | /inquiries | pms:inquiry:list |
| 询价管理 | 报价对比 | GET | /inquiries/compare | pms:inquiry:list |
| 询价管理 | 选定报价 | PUT | /inquiries/{id}/select | pms:inquiry:select |
| 采购订单 | 采购单列表 | GET | /orders | pms:order:list |
| 采购订单 | 采购单详情 | GET | /orders/{id} | pms:order:list |
| 采购订单 | 新增采购单 | POST | /orders | pms:order:add |
| 采购订单 | 编辑采购单 | PUT | /orders/{id} | pms:order:edit |
| 采购订单 | 发给供应商 | PUT | /orders/{id}/send | pms:order:send |
| 采购订单 | 取消采购单 | PUT | /orders/{id}/cancel | pms:order:cancel |
| 采购订单 | SKU价格历史 | GET | /orders/price-history | pms:order:list |
| 收货管理 | 创建收货单 | POST | /receipts | pms:receipt:add |
| 收货管理 | 收货单列表 | GET | /receipts | pms:receipt:list |
| 收货管理 | 确认入库 | PUT | /receipts/{id}/confirm | pms:receipt:confirm |
| 退货管理 | 创建退货单 | POST | /returns | pms:return:add |
| 退货管理 | 退货单列表 | GET | /returns | pms:return:list |
| 退货管理 | 确认退货出库 | PUT | /returns/{id}/ship | pms:return:ship |
| 应付账款 | 应付账款列表 | GET | /payables | pms:payable:list |
| 应付账款 | 录入付款 | POST | /payables/{id}/pay | pms:payable:pay |
| 报表统计 | 采购总览 | GET | /report/overview | pms:report:view |
| 报表统计 | 供应商排行 | GET | /report/supplier-rank | pms:report:view |
| 报表统计 | 采购月度趋势 | GET | /report/monthly-trend | pms:report:view |
11.2 关键接口请求/响应示例
① 确认入库接口(关键接口):
PUT /api/pms/receipts/1748291234567892/confirm
Content-Type: application/json
请求体:
{
"receiptId": "1748291234567892",
"items": [
{
"receiptItemId": "1748291234567901",
"skuId": "1000001",
"actualQty": 300,
"passQty": 295,
"rejectQty": 5,
"rejectReason": "5件外观有划痕,无法正常销售",
"locationId": "2000001"
},
{
"receiptItemId": "1748291234567902",
"skuId": "1000002",
"actualQty": 200,
"passQty": 200,
"rejectQty": 0,
"locationId": "2000002"
}
],
"remark": "包装完整,外箱无破损,随附装箱单与采购单核对一致"
}
响应体(成功):
{
"code": 200,
"msg": "入库成功",
"data": {
"receiptNo": "RCV-20250117-0001",
"totalPassQty": 495,
"totalRejectQty": 5,
"poStatus": 5,
"poStatusName": "全部到货",
"inventoryChanges": [
{
"skuId": "1000001",
"skuName": "蓝牙耳机Pro-黑色",
"beforeQty": 80,
"changeQty": 295,
"afterQty": 375
},
{
"skuId": "1000002",
"skuName": "蓝牙耳机Pro-白色",
"beforeQty": 45,
"changeQty": 200,
"afterQty": 245
}
]
}
}
② 录入付款接口:
POST /api/pms/payables/1748291234567893/pay
Content-Type: application/json
{
"payableId": "1748291234567893",
"paymentAmount": 19000.00,
"paymentDate": "2025-02-10",
"paymentMethod": 1,
"voucherNo": "202502100001",
"remark": "招商银行转账,实付19000元整"
}
响应体:
{
"code": 200,
"msg": "付款录入成功",
"data": {
"payableNo": "PAY-20250117-0001",
"paymentAmount": 19000.00,
"totalPaid": 19000.00,
"remaining": 0.00,
"status": 2,
"statusName": "已结清"
}
}
11.3 采购模块专用错误码
采购模块错误码范围:12000 - 12099
| 错误码 | 含义 | 触发场景 |
|---|---|---|
| 12001 | 采购申请单不存在 | 根据ID查不到数据 |
| 12002 | 申请单状态不允许此操作 | 状态流转非法 |
| 12003 | 采购数量不能为0 | 明细数量校验 |
| 12004 | SKU不存在或已下架 | 添加明细时校验 |
| 12005 | 采购单不存在 | 根据ID查不到 |
| 12006 | 采购单已发给供应商,不能修改 | 编辑时状态校验 |
| 12007 | 收货数量超过采购数量 | 收货时数量校验 |
| 12008 | 该采购单已生成应付账款 | 重复生成检查 |
| 12009 | 付款金额超过应付余额 | 付款时金额校验 |
| 12010 | 退货数量超过已收货数量 | 退货时数量校验 |
| 12011 | 询价未收到报价,无法生成采购单 | 选定报价校验 |
| 12012 | 报价已过期,请重新询价 | 报价有效期校验 |
今日总结与作业
今日知识点回顾
业务层面:
- 采购管理解决的核心问题:信息透明、避免重复采购、跟踪到货、账款管理
- 三种采购需求来源:库存预警自动触发、销售预测触发、人工手动申请
- 询价比价:为什么需要、完整流程、多维度综合评分算法
- 采购订单 9 个状态的完整状态机,以及每个状态允许的操作
- 主从表设计模式:主表存汇总信息,从表存明细信息
- 部分收货处理:多次分批到货时如何累计 received_qty
- 采购退货:6 种退货原因、完整逆向流程
- 应付账款:账期计算、到期提醒、付款记录
技术层面:
- 建议采购量公式:基于日均销量、安全天数、Lead Time 的计算
- 入库事务原子性:8 步操作必须在同一个事务中,任一失败全部回滚
- 库存并发安全:
quantity = quantity + N而非quantity = 最终值 - 幂等性设计:库存预警当日只生成一次申请单,防止重复触发
- 数据库计算字段:
remaining_amount GENERATED ALWAYS AS (payable_amount - paid_amount) - SQL 统计报表:GROUP BY、聚合函数、CASE WHEN 条件统计
今日作业
作业 1:数据库执行(必做)
-
执行本课所有建表 SQL:
purchase_requisition(申请单主表)purchase_requisition_item(申请单明细)purchase_inquiry(询价单)purchase_inquiry_item(报价明细)purchase_order(采购订单主表)purchase_order_item(采购订单明细)purchase_receipt+purchase_receipt_item(收货单)purchase_return(退货单)finance_payable+finance_payment_record(应付账款)
-
插入测试数据并验证:
-- 插入一张完整的采购订单(已全部到货状态)
INSERT INTO `purchase_order` (
`id`, `tenant_id`, `po_no`, `supplier_id`, `supplier_name`,
`warehouse_id`, `warehouse_name`, `total_amount`, `currency`,
`payment_type`, `payment_days`, `paid_amount`,
`order_date`, `expected_date`, `confirmed_date`,
`status`, `create_by`, `create_time`, `update_time`
) VALUES (
3001, 101, 'PO-20250117-0001', 1001, '广州市鑫源电子科技有限公司',
4001, '广州总仓', 19000.00, 'CNY',
2, 30, 0.00,
'2025-01-17', '2025-01-24', '2025-01-18',
5, 501, NOW(), NOW()
);
-- 采购订单明细
INSERT INTO `purchase_order_item` (
`id`, `tenant_id`, `po_id`, `sku_id`, `sku_code`, `sku_name`,
`unit`, `quantity`, `received_qty`, `unit_price`, `amount`
) VALUES
(3101, 101, 3001, 1000001, 'SKU-EARPHONE-B', '蓝牙耳机Pro-黑色',
'件', 300, 295, 38.00, 11400.00),
(3102, 101, 3001, 1000002, 'SKU-EARPHONE-W', '蓝牙耳机Pro-白色',
'件', 200, 200, 38.00, 7600.00);
-- 对应的应付账款(全部到货后自动生成)
INSERT INTO `finance_payable` (
`id`, `tenant_id`, `payable_no`, `po_id`, `po_no`,
`supplier_id`, `supplier_name`,
`payable_amount`, `paid_amount`,
`currency`, `payment_days`,
`invoice_date`, `due_date`,
`status`, `create_time`, `update_time`
) VALUES (
5001, 101, 'PAY-20250117-0001', 3001, 'PO-20250117-0001',
1001, '广州市鑫源电子科技有限公司',
19000.00, 0.00,
'CNY', 30,
'2025-01-17', '2025-02-16',
0, NOW(), NOW()
);
-- 验证查询:查看采购单与应付账款的联动
SELECT
po.po_no,
po.supplier_name,
po.total_amount AS po_amount,
po.status AS po_status,
fp.payable_no,
fp.payable_amount,
fp.remaining_amount,
fp.due_date,
DATEDIFF(fp.due_date, CURDATE()) AS days_until_due
FROM purchase_order po
LEFT JOIN finance_payable fp ON po.id = fp.po_id
WHERE po.tenant_id = 101 AND po.is_deleted = 0;
作业 2:业务分析题(必做)
请回答以下问题(用自己的话表述):
-
询价与直接下单的选择 什么情况下应该走询价流程?什么情况下可以直接创建采购订单?请结合业务场景分析(提示:考虑金额大小、紧急程度、是否有长期合作供应商等因素)
-
事务的必要性:假设采购入库时,库存更新成功了,但写入库存流水失败了,且没有事务保护。请描述会产生哪些业务影响?如果一周后要查某个 SKU 的库存增加情况,会发现什么问题?
-
部分收货的库存处理:一张采购单共 500 件,第一次收货 300 件(其中 20 件质检不合格),第二次收货 200 件。请问:
- 第一次收货后,
purchase_order.status是什么? purchase_order_item.received_qty是多少?- WMS 库存增加了多少?(质检不合格的怎么处理?)
- 第二次全部到货后,状态如何变化?
- 第一次收货后,
参考答案:
-
采购金额较大、供应商不固定、价格波动明显、需要多家比价时,应该先走询价流程,比如第一次采购某个新品,采购负责人需要比较价格、交期和供应商评级。金额较小、需求紧急、已经有长期合作供应商且价格稳定时,可以直接创建采购订单,比如固定供应商每周补货常规 SKU。
-
如果没有事务,库存数量已经增加,但库存流水没有写入成功,系统表面上库存变多了,后续却查不到这次库存变化来源。一周后排查某个 SKU 为什么库存增加,只能看到当前库存,无法通过流水还原“哪张采购单、什么时候、谁操作、增加了多少”,会影响库存审计、成本核算和问题追责。因此采购入库时,更新收货单、更新采购单、增加库存、写库存流水必须放在同一个本地事务里。
-
第一次收货 300 件,其中 20 件质检不合格,真正可入库数量是 280 件。采购单还没有全部到货,所以
purchase_order.status应该是“部分到货”。purchase_order_item.received_qty只统计合格入库数量,第一次后为 280。WMS 库存quantity增加 280,不合格的 20 件进入不良品区、待退货区或质检异常记录,不进入可售库存。第二次再收到 200 件且全部合格后,累计合格收货 480 件,如果采购单按合格入库数量判断,则仍然是部分到货,并需要对剩余 20 件发起退货、补发或关闭差异;如果业务上允许把不合格件作为已到货但不可售库存,则采购单可进入全部到货,库存中合格库存增加 480,不良品库存增加 20。实际项目中建议按“合格入库数量”和“到货数量”分开记录,避免状态判断混乱。
作业 3:SQL 练习(必做)
基于今天建立的表结构,手动编写以下 SQL:
-- 练习1:查询某供应商(supplier_id=1001)的所有采购单
-- 要求:显示 po_no、total_amount、status、order_date,按创建时间倒序
-- 你的答案:
SELECT
po_no,
total_amount,
status,
order_date
FROM purchase_order
WHERE tenant_id = 101
AND supplier_id = 1001
AND is_deleted = 0
ORDER BY create_time DESC;
-- 练习2:统计各状态的采购单数量和总金额
-- 要求:按状态分组,显示状态码、状态名称(用CASE WHEN)、数量、总金额
-- 你的答案:
SELECT
status,
CASE status
WHEN 0 THEN '待确认'
WHEN 1 THEN '已确认'
WHEN 2 THEN '部分到货'
WHEN 3 THEN '全部到货'
WHEN 4 THEN '部分付款'
WHEN 5 THEN '已完成'
WHEN 6 THEN '已取消'
ELSE CONCAT('未知状态:', status)
END AS status_name,
COUNT(*) AS order_count,
SUM(total_amount) AS total_amount
FROM purchase_order
WHERE tenant_id = 101
AND is_deleted = 0
GROUP BY status
ORDER BY status;
-- 练习3:查询7天内到期的应付账款
-- 要求:显示 payable_no、supplier_name、remaining_amount、due_date、距到期天数
-- 按到期日升序排列
-- 你的答案:
SELECT
payable_no,
supplier_name,
payable_amount - paid_amount AS remaining_amount,
due_date,
DATEDIFF(due_date, CURDATE()) AS days_until_due
FROM finance_payable
WHERE tenant_id = 101
AND is_deleted = 0
AND status IN (0, 1)
AND due_date >= CURDATE()
AND due_date < DATE_ADD(CURDATE(), INTERVAL 8 DAY)
ORDER BY due_date ASC;
-- 练习4:查询某SKU(sku_id=1000001)的最近10次采购价格
-- 要求:显示 po_no、order_date、supplier_name、unit_price、quantity
-- 按订单日期倒序
-- 你的答案:
SELECT
po.po_no,
po.order_date,
po.supplier_name,
poi.unit_price,
poi.quantity
FROM purchase_order_item poi
JOIN purchase_order po ON poi.po_id = po.id
WHERE poi.tenant_id = 101
AND po.tenant_id = 101
AND poi.sku_id = 1000001
AND po.is_deleted = 0
ORDER BY po.order_date DESC, po.create_time DESC
LIMIT 10;
作业 4:系统设计题(选做)
需求:某租户发现,同一种 SKU 经常有多个采购专员同时发起采购申请,导致重复采购、库存积压。
请设计一套”采购申请去重机制”:
- 去重的触发条件是什么?(相同 SKU、什么状态范围内算”进行中”?)
- 去重检查在哪个步骤执行?(创建申请时?提交审批时?)
- 检查发现重复时,系统如何提示?是阻止创建还是警告提示?
- 补充设计该功能需要的数据库字段或索引
- 写出去重检查的 SQL
参考答案:
-
去重条件可以定义为:同一租户、同一 SKU、同一目标仓库,在采购申请状态为“草稿、待审批、审批通过、已转采购单、采购中”的范围内,都算进行中。如果已经取消、驳回、采购完成,则不再参与去重。
-
去重检查建议分两层:创建申请时做提醒,提交审批时做强校验。创建草稿时允许保存,方便采购专员补充信息;提交审批时必须检查是否已有进行中的采购申请或采购订单,避免真正进入采购流程后重复采购。
-
如果发现重复,低风险场景可以提示“该 SKU 已存在进行中的采购申请,是否继续创建”,由负责人确认;高风险场景如同一仓库同一 SKU 已有审批通过的申请,则直接阻止提交,并给出已有单号、申请人、申请数量、当前状态,方便合并采购需求。
-
可以在
purchase_requisition_item中增加warehouse_id、requisition_status_snapshot,或者通过主表关联仓库和状态。索引建议建立:
CREATE INDEX idx_req_item_dedupe
ON purchase_requisition_item (tenant_id, sku_id, warehouse_id, requisition_id);
CREATE INDEX idx_req_status
ON purchase_requisition (tenant_id, status, is_deleted);
- 去重检查 SQL 示例:
SELECT
pr.req_no,
pr.status,
pr.create_by,
pri.sku_id,
pri.quantity,
pr.create_time
FROM purchase_requisition_item pri
JOIN purchase_requisition pr ON pri.requisition_id = pr.id
WHERE pri.tenant_id = #{tenantId}
AND pr.tenant_id = #{tenantId}
AND pri.sku_id = #{skuId}
AND pr.warehouse_id = #{warehouseId}
AND pr.status IN (0, 1, 2, 3)
AND pr.is_deleted = 0
ORDER BY pr.create_time DESC;
今日亮点
1. 智能补货 / 采购需求自动生成
业务点:采购需求不只靠人工创建,还可以由库存预警、销售预测自动触发。
技术点:
定时任务 + 安全库存模型 + Lead Time 计算 + 幂等去重
面试可以这样讲:
我负责采购需求自动生成逻辑,系统会定时扫描库存数据,当可用库存低于安全库存时,根据日均销量、安全天数、供应商 Lead Time 和在途库存计算建议采购量,自动生成采购申请。同时做了幂等控制,避免同一个 SKU 在同一天重复生成采购申请,导致重复采购和库存积压。
这个点很真实,因为采购系统的核心价值就是“什么时候补、补多少”。
2. 采购申请去重与防重复采购
业务点:多个采购专员可能同时发现同一个 SKU 快缺货,如果都创建采购申请,会造成重复采购。
技术点:
业务幂等 + 唯一约束/状态范围判断 + 在途库存校验
面试可以这样讲:
在创建采购申请时,我会判断同一租户、同一 SKU、同一仓库下是否已经存在进行中的采购申请或采购单,比如待审批、已审批、待供应商确认、发货中、部分到货等状态。如果已经有足够在途数量,就不再重复生成新的申请,避免重复采购。
这个点适合和“柔性供应链”结合起来讲。
3. 询价比价机制
业务点:采购员不是直接拍脑袋选供应商,而是向多家供应商询价,再综合比较。
技术点:
多供应商询价 + 报价有效期 + 综合评分算法 + Portal 协同
面试可以这样讲:
我实现了询价比价流程,采购申请可以向 2-5 家已审核供应商发起询价,供应商在 Portal 端提交报价、可供数量、交期和报价有效期。系统在比价时不仅比较单价,还会结合交期、可供数量、历史评级等维度辅助采购员选择最优供应商。
注意这里不要吹成“AI 智能比价”,说“综合评分辅助决策”更稳。
4. 采购订单状态机
业务点:采购订单从草稿、待确认、已确认、发货中、部分到货、全部到货、已对账、已结清、已取消逐步流转,不能随意跳状态。
技术点:
状态机 + 状态流转校验 + 操作权限控制
面试可以这样讲:
我使用状态机管理采购订单生命周期,不允许前端直接修改状态,而是通过确认、发货、收货、对账、付款等业务动作驱动状态变化。比如只有已确认的采购单才能进入发货中,只有全部到货后才能生成应付账款。
这个是 PMS 必讲亮点。
5. 采购审批流
业务点:大额采购不能让采购专员随便下单,需要按金额走不同审批层级。
技术点:
审批阈值配置 + 动态审批人 + 超时提醒
面试可以这样讲:
我实现了采购审批流程,系统会根据采购金额判断是否需要审批,以及需要哪一级审批。比如 1 万以内自动通过,1-5 万由采购负责人审批,更高金额需要财务负责人或总经理审批。审批任务超时后会触发提醒,避免采购延误。
这个点适合讲权限和流程控制。
6. 采购入库与 WMS 库存联动事务
这是PMS最核心、最值得写简历的技术亮点。
业务点:仓库确认收货后,既要更新采购单收货数量,又要增加库存,还要写库存流水,任何一步失败都不能提交。
技术点:
分布式事务 + 库存原子更新 + 库存流水 + 缓存失效
面试可以这样讲:
采购入库是 PMS 中最关键的一致性场景。我把更新收货单、累计采购明细 received_qty、判断采购单状态、增加 WMS 库存、写入库存流水放在同一个数据库事务中,任何一步失败都会回滚,避免出现采购单显示已入库但库存没增加,或者库存增加但没有流水的问题。
7. 库存并发安全
业务点:同一个 SKU 可能同时发生多次入库,不能出现库存覆盖。
技术点:
数据库原子更新 + 行级锁
面试可以这样讲:
库存增加时没有采用“先查库存,再计算最终值,再 update”的方式,而是直接使用 quantity = quantity + N。这样数据库会通过行级锁保证同一行库存更新串行执行,避免两个入库操作互相覆盖。
可以举例:
UPDATE inventory SET quantity = quantity + #{receiveQty} WHERE tenant_id = #{tenantId} AND sku_id = #{skuId} AND warehouse_id = #{warehouseId};
这是非常容易被面试官认可的点。
8. 部分收货处理
业务点:一张采购单 500 件,可能第一次到 300 件,第二次到 200 件,而且还有部分质检不合格。
技术点:
主从表设计 + received_qty 累计 + 采购单状态自动计算
面试可以这样讲:
我处理了采购单的部分收货场景。每次收货后,系统会累计采购明细中的 received_qty,再和采购数量比较,自动判断采购单是部分到货还是全部到货。质检不合格的部分不会进入可用库存,而是进入退货或不良品处理流程。
这个点业务味很强,能体现做过真实流程。
9. 应付账款自动生成与到期提醒
业务点:采购全部到货后,财务需要根据采购单生成应付账款,并按账期提醒付款。
技术点:
应付账款生命周期 + 数据库计算字段 + 定时提醒
面试可以这样讲:
采购单全部到货后,系统会自动生成应付账款记录,付款时写入付款流水,并累计已付金额。剩余未付金额使用数据库计算字段 remaining_amount = payable_amount - paid_amount 保证一致性。系统每天扫描 7 天内到期或已逾期的账款,提醒财务处理。
这个适合和财务闭环一起讲。
10. 采购退货逆向流程
业务点:采购入库后发现质量问题、数量不符、型号错误,需要退货给供应商,并联动库存和应付账款。
技术点:
逆向流程 + 库存扣减事务 + 应付账款冲减
面试可以这样讲:
我实现了采购退货流程,支持质量问题、数量不符、规格错误等退货原因。退货出库时会减少库存并写库存流水,如果涉及退款或扣款,还会冲减应付账款,保证采购、仓储、财务三方数据一致。
最推荐写进简历的版本
如果学生主打 Day03,可以这样写:
负责 PMS 采购管理模块,完成采购需求生成、询价比价、采购审批、采购订单状态机、采购入库联动、采购退货和应付账款管理。
基于安全库存、日均销量、Lead Time 和在途库存设计建议采购量计算模型,通过定时任务自动生成采购申请,并加入幂等去重机制,避免重复采购。
设计采购订单状态机,约束草稿、待确认、发货中、部分到货、全部到货、已对账、已结清等状态流转,防止前端绕过业务流程直接修改状态。
实现采购入库与 WMS 库存联动,将收货单状态更新、采购明细 received_qty 累计、采购单状态计算、库存增加、库存流水写入放在同一个事务中,保证入库数据一致性。
库存更新采用 quantity = quantity + N 的数据库原子更新方式,避免并发入库时出现库存覆盖问题。
实现应付账款自动生成和到期提醒,采购全部到货后自动生成应付账款,付款时记录付款流水并更新剩余应付金额。
优先级建议
优先讲:
采购需求自动生成 + 幂等去重 采购订单状态机 采购入库与 WMS 库存联动事务
然后再加:
询价比价综合评分 应付账款自动生成与到期提醒
关于技术点: 建议大家 抽取 一两个 你认为比较有技术含量 或者是 你能够讲清楚的业务 然后开始从头写这个逐字稿 这个业务是什么 概括的说 这个业务是如何产生的 这个业务涉及的表有哪些 这些表中核心字段有哪些 这个业务的详细的执行过程是什么 这个业务中核心的判断点有什么 这个业务使用到的技术有哪些 这些技术周边的问题有哪些 这个业务是否有跟其他的业务关联
你要做到 自己心中没有疑问
明日预告
第四天:仓储管理系统(WMS)完整实现
明天将完整实现仓储管理系统,包括:
- 多仓库和库位体系设计(库位编码规范)
- 出入库全流程(含扫码操作逻辑)
- 库存五种类型的动态管理
- 库存盘点三种模式(全盘/抽盘/循环盘)
- 仓库间调拨流程
- 库存可视化看板
预习建议:
- 回顾今天的入库事务设计(明天 WMS 会更详细地展开)
- 了解什么是”库位”(可类比超市货架的排列方式)
- 思考:一件商品从入仓到出仓,在仓库内部会经历哪些步骤?
学习提示:今天的内容是整个课程中业务最复杂的一天。采购管理的状态流转、询价比价、主从表设计、入库事务这几个知识点,是企业级项目开发的标准模式,面试时被考到的概率极高。
特别是入库事务的原子性这个设计,面试官经常会追问:“你们是怎么保证库存和采购单同时更新的?如果中间某一步失败了怎么处理?“——这就是今天讲的事务回滚机制,务必能清楚地讲出来。