进销存逻辑流程图

下单预占 → 发货实扣 → 退回释放 → 采购入库  |  模块解耦:StockResolver ⇢ StateRouter

← 控制中心
全链路流程
模块关系
数据表结构
代理商提交订单
POST /api/submit
写订单主表 + 镜片明细
按 (sku, sph, cyl) 聚合度数
预占库存
reserveStock(sku, sph, cyl, qty)
withLock → fresh read → PATCH 预占+=qty
available = 当前库存 − 预占库存  |  toReserve = min(qty, available)
订单状态 = "已下单"
助理确认订单
POST /api/admin/confirm
StockResolver.resolveStock(lenses) → 从缓存 getStockMap() O(1) 查 available
StateRouter.routeConfirm(results) → 纯函数路由
可用 > 0
打标签
wfStep = labeled
交期: 有货1-2天
有库存路径:跳过待处理+生产中
可用 = 0(常规范围)
待处理
wfStep = confirmed
交期: 排产5-7天
→ 导出Excel → 生产中 → 打标签
超范围
待处理
wfStep = confirmed
交期: 定制7-10天
SPH < −6 或 CYL < −2
不查库存表
确认发货
POST /api/admin/ship
convertReservation(sku, sph, cyl, 1)
预约转实扣
withLock 内单次 PATCH:当前库存−1 + 预占库存−1
原子写入 → clearCache → 写出库流水
已发货(终态)
飞书发货通知卡片 + 随货同行单 + stock_movement 出库记录
退回上一步
POST /api/admin/revert
打标签→已下单  |  待处理→已下单  |  生产中→待处理
releaseReservation(sku, sph, cyl, qty)
释放预占
withLock → PATCH 预占库存 −= toRelease
toRelease = min(qty, currentReserved)  |  已是0时 no-op(安全)
创建采购单
POST /api/admin/procurement
sku + sph + cyl + qty → PO编号 → procurement表 状态=已下单
到货入库
POST .../procurement/:id/receive
withLock → 当前库存+qty → 采购状态=已到货
入库流水
stock_movement: 入库/采购到货
变动前/后库存可追溯
下单/预约
库存操作
数据表
API 端点
退回释放

StockResolver — 库存判定

lib/stock-resolver.js
输入lenses: [{sku, sph, cyl}]
输出[{stock, reserved, available, inStock}]
依赖: getStockMap() 缓存(2min TTL,O(1) 查找)
特点: 不关心库存结果意味着什么状态

StateRouter — 状态路由

lib/state-router.js
输入stockResults: [{inStock, inRange}]
输出{targetStatus, wfStep, deliveryType}
依赖: (纯函数,零 I/O)
特点: 不关心库存数据怎么来的
两模块互不知道对方存在,仅通过 {inStock: bool} 一个字段连接

lib/stock.js — 库存核心

lib/stock.js (247行)
getStockMap()全表缓存(1575行, 2min TTL)— 供 StockResolver 读
queryStockByRx()单记录过滤查询 — 供写操作锁内 fresh read
reserveStock()下单预占: withLock + fresh read + PATCH 预占+=qty
convertReservation()发货实扣: 库存−1 + 预占−1 原子写入
releaseReservation()退回释放: 预占 −= toRelease
estimateDeliveryByRx()交期预估: 三档判定(有货/排产/定制)

server.js — API 端点

server.js (4400行)
POST /api/submit写订单 → reserveStock() 预占
POST /api/admin/confirmresolveStock() → routeConfirm() → 写状态
POST /api/admin/shipconvertReservation() → 写物流 → 发通知
POST /api/admin/revert退状态 → releaseReservation() 释放预占
POST /api/admin/procurement创建成品采购单 → procurement 表
POST .../procurement/:id/receivewithLock → 库存+N → stock_movement 入库流水
GET /api/admin/stock-detail库存列表 + 预占/可用 + SKU 汇总

并发安全:withLock 每键互斥锁

key = "SKU编号|SPH|CYL"  →  Promise 链序列化同一度数的并发写
锁内流程:fresh GET 单条 → 计算新值 → PATCH 写入 → clearCache
防 lost update:两个并发请求写同一度数不会互相覆盖

stock_detail — 度数级成品库存

tbl7U79QGG4JtQev  |  1575 行 = 7 SKU × 225 度数组合(SPH 25档 × CYL 9档)
字段类型说明
SKU编号TEXT产品型号(与 SKU_CATALOG 一致)
SPH / CYLNUMBER(0.00)球镜 / 柱镜,步长 0.25
当前库存NUMBER物理在库片数
预占库存 🆕NUMBER已被订单预约但未出库的片数
可用库存—(代码计算)当前库存 − 预占库存
安全库存NUMBER低于此值触发 rule12 库存预警
最近出库DATE最后一次出库时间戳

stock_movement — 出入库流水

tblCoNeAbrz6tM9C
字段类型写入口径
单据号TEXTMOV-YYYYMMDD-XXXX
类型单选入库 / 出库
来源去向单选入库: 采购到货、生产回补、退货退回、盘点补录
出库: 订单发货、报废损耗、调拨出库、盘点差异
SKU编号 / SPH / CYLTEXT/NUMBER度数定位
数量NUMBER变动片数
变动前库存 / 变动后库存NUMBER审计追溯
关联单号TEXT订单编号 / 采购编号 / 退换单号

procurement — 采购跟踪

tblZX1qW7RvcJieg
字段类型说明
采购编号TEXTPO-YYYYMMDD-XXXX
采购类型单选模具 / 毛坯 / 成品 🆕
关联SKUTEXT产品型号
SPH / CYL 🆕NUMBER球镜/柱镜(成品采购专用,可选字段)
数量NUMBER采购片数
发起日期 / 预计到货DATE采购时间线
状态单选待下单 → 已下单 → 已到货 → 已取消
🆕 = 本次新增字段或枚举值