# scm-learning **Repository Path**: leeyamaster/scm-learning ## Basic Information - **Project Name**: scm-learning - **Description**: 仓衡 · 供应链教学演示(采购/库存/销售履约) - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-11-02 - **Last Updated**: 2026-11-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 仓衡 · 供应链教学演示 一个可以在本地跑起来的小型供应链系统,用来看清三件事怎么串在一起: 1. **采购**:供应商、采购订单、收货 2. **库存**:物料、仓库、结存、出入库流水 3. **销售履约**:客户、销售订单、发货 界面是中文。没有登录。库存不被手工改数字,只跟着单据走。 ## 业务规则 草稿和「确认」都不动库存。 - **收货单**增加结存,并写一条入库流水 - **发货单**减少结存,并写一条出库流水 - 收货不能超过采购订单的未收数量 - 发货不能超过销售订单的未发数量,也不能超过该仓库的结存 - 库存不足时整笔记账回滚,结存不会变成负数 - **送达**只改销售订单状态,不会第二次扣库存 - 安全库存只用于预警。比较的是该物料在全部仓库的合计 结存应始终等于流水轧差:`结存 = 入库合计 − 出库合计`。库存台账里有一张核对表。 ### 采购订单状态 `草稿 → 已确认 → 部分收货 → 已收货 → 已关闭` 未收完也可以关闭。关闭表示剩下的不再收,已经入库的数量不回冲。 ### 销售订单状态 `草稿 → 已确认 → 部分发货 → 已发货 → 已送达 → 已关闭` 部分发货是为了和采购的部分收货对称:没发完就不能叫「已发货」,否则订单和库存各说各话。确认订单**不会预占库存**,可用量在发货时检查。 ## 启动 需要 Node.js 20 以上。 ```bash npm install npm start ``` - 前端: - 接口: 第一次启动如果数据库是空的,会自动写入课堂样本。SQLite 文件在 `apps/api/data/scm.sqlite`。 | 命令 | 作用 | | --- | --- | | `npm start` | 同时启动 API 和前端 | | `npm run dev:api` | 只启动 API(4317) | | `npm run dev:web` | 只启动前端(4327,并把 `/api` 代理到 4317) | | `npm test` | 用临时数据库跑通采购→收货→超卖拦截→发货→送达,并核对库存 | | `npm run build` | 编译 API 和前端 | | `npm run seed` | **先停掉服务**,删除本地数据库。下次启动会重新灌入样本 | 服务正在跑时,用页面右上角的「重置演示数据」,或 `POST /api/dev/reset`。这会清掉你新建的数据。 ## 样本里已经发生的事 单号里的年份是启动时的公历年。下面按 2026 年书写。 | 单据 | 状态 | 用来说明什么 | | --- | --- | --- | | PO-2026-0001 螺栓 600 | 已收货 | 上海仓入过库,后又被销售发走 100,结存 500 | | PO-2026-0002 轴承 120,实收 50 | 部分收货 | 还可以继续收剩下的 70 | | PO-2026-0003 电机 15 | 已确认 | 确认了,但库存仍是 0 | | PO-2026-0004 包装箱 | 草稿 | 草稿可改可删,不影响库存 | | PO-2026-0005 液压油 20 | 已关闭 | 苏州仓入库后关闭 | | SO-2026-0001 螺栓 100 | 已送达 | 发货扣过库存,送达没有再扣 | | SO-2026-0002 轴承 24 | 已确认 | 上海仓有 50,可以直接发 | | SO-2026-0003 电机 4 | 草稿 | 没库存,发货会被拒绝 | | SO-2026-0004 液压油 6 | 已发货 | 苏州仓结存剩下 14,还没点送达 | 低库存:轴承(50 < 80)、电机(0 < 8)、包装箱(0 < 80)。 ## 跟着界面走一遍 1. 打开仪表盘,看六步单据链和低库存。 2. 打开 **物料**,看安全库存。可以自己新建一个 SKU。 3. 打开 **采购订单**,新建一张草稿:选供应商、仓库、物料、数量、单价,保存。此时库存不变。 4. 在详情里 **确认订单**。库存仍然不变。 5. 点 **去收货**。数量可以改小,模拟分批到货。提交后结存增加,订单变成「部分收货」或「已收货」。 6. 打开 **库存台账**,看结存、流水,以及「轧差核对」。 7. 新建 **销售订单**,确认后 **去发货**。如果该仓库数量不够,提交会被拒绝。 8. 先把采购收够,再发货。结存下降。全部发完后状态是「已发货」。 9. **确认送达**,再 **关闭订单**。送达前后各看一次库存,数字应相同。 也可以直接用样本:先给 PO-2026-0003 收电机,再把 SO-2026-0003 确认并发货,体验一次「没货发不出」。 ## 目录 ``` apps/api NestJS + TypeORM + SQLite src/entities.ts 领域模型 src/procurement 采购订单与收货 src/sales 销售订单与发货 src/inventory 结存、流水、超卖校验、轧差 src/seed 样本数据 apps/web Vite + React + Ant Design ``` 类型写在各自应用里,没有再拆一个共享包,避免多一次编译。 ## 主要接口 前缀都是 `/api`。 | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/dashboard` | 数量、低库存、未完成单据、最近流水 | | GET/POST | `/products` `/suppliers` `/customers` `/warehouses` | 主数据 | | PATCH/DELETE | `/{resource}/:id` | 修改、删除。被单据引用时不能删 | | GET/POST | `/purchase-orders` | 采购订单列表、新建草稿 | | POST | `/purchase-orders/:id/confirm` | 确认 | | POST | `/purchase-orders/:id/close` | 关闭 | | GET/POST | `/goods-receipts` | 收货单。过账时加库存 | | GET/POST | `/sales-orders` | 销售订单 | | POST | `/sales-orders/:id/deliver` | 送达,不扣库存 | | GET/POST | `/shipments` | 发货单。过账时减库存 | | GET | `/stock/balances` | 结存。`belowSafety=1` 只看低库存 | | GET | `/stock/movements` | 流水 | | GET | `/stock/reconciliation` | 结存与流水是否一致 | | POST | `/dev/reset` | 清空并重建样本 | 金额在接口里用「分」表示,例如 `unitPriceCents: 82` 就是 0.82 元。创建单据时仍按元提交,最多两位小数,例如 `"unitPrice": 0.82`。