配套面试准备:完成本篇后,可以继续阅读:Day04_WMS核心业务面试准备 Day04_WMS仓储管理系统完整面试指南
课程目标:深度理解跨境出海场景下仓储管理的全业务链路,完整实现从仓库初始化、库位规划、多类型出入库操作、库存精细化管控到库存盘点的全流程系统设计。
今日交付物:
- 仓储模块 8 张数据库表完整 SQL
- 仓库与库位体系完整设计(含编码规范)
- 库存五种类型的精细化管控设计
- 入库全流程(采购入库 / 调拨入库 / 退货入库)
- 出库全流程(销售出库 / 调拨出库 / 报损出库)
- 库存流水不可篡改设计(审计底线)
- 三种盘点模式完整实现(全盘 / 抽盘 / 循环盘)
- 仓库间调拨流程与在途库存管理
- 库存预警与 Dashboard 看板设计
第一节 仓储管理业务全解析
1.1 仓储管理在供应链中的核心地位
仓库是整条供应链的”物理枢纽”。所有商品在这里集结、存放、分拣、发出。
graph LR
A["📦 采购入库<br>货从供应商来"] --> WMS
B["🔄 仓间调拨<br>国内仓→海外仓"] --> WMS
C["↩️ 退货入库<br>买家退回的货"] --> WMS
WMS["🏭 WMS<br>仓储管理系统<br>所有库存在这里<br>精准管理"]
WMS --> D["📤 销售出库<br>订单驱动发货"]
WMS --> E["🔄 调拨出库<br>货物跨仓转移"]
WMS --> F["🗑️ 报损出库<br>不良品处置"]
style WMS fill:#fff3e0,stroke:#ff9800,stroke-width:2px
1.2 跨境出海的仓储体系特殊性
跨境场景下的仓储比国内电商更复杂,存在多种仓库类型并行:
| 仓库类型 | 位置 | 特点 | 适用场景 |
|---|---|---|---|
| 国内备货仓 | 中国境内 | 自营,管控最强,成本低 | 存放大批货物,按需分批发往海外 |
| FBA 仓 | 亚马逊官方海外仓 | 亚马逊托管,物流快,费用高 | 亚马逊平台快速履约,Prime 标志 |
| 海外自营仓 | 自建海外仓库 | 灵活,多平台可用,成本中等 | 多平台发货,本地退货接收 |
| 第三方海外仓(3PL) | 第三方仓储服务商 | 无需自建,按量付费 | 市场试水,初期规模小时适用 |
| 保税仓 | 海关监管区内 | 进口申报前存放,享受税收优惠 | 备货提前入境,快速响应本地订单 |
3PL: DHL UPS FexEx … 菜鸟供应链 京东物流 顺丰供应链 … 不需要 自己的仓库管理员 对接 咱们的仓储系统 和 第三方的仓储系统对接 保税仓: 登记 审核 海关监管 入库 出库 海关账册 / 仓库账册 推送订单/支付/物流信息给海关 等海关审核放行 拣货/打包/发货
3PLY 保税仓 : 提供 相关的数据的API 可以让我们进行集成 自己有一套仓储系统 他们也有一套自己的仓储系统 我们需要进行集成 货架 —> 设计 —> 映射 —> 操作自己的仓库 —> 操作3PL
graph TD
subgraph 中国境内
A["🏭 国内备货仓<br>广州总仓<br>大量备货、质检、分类"]
end
subgraph 国际运输
B["✈️ 空运<br>贵重品/时效紧"]
C["🚢 海运<br>重货/大批量"]
end
subgraph 海外
D["📦 FBA仓<br>亚马逊托管<br>直接对接亚马逊平台"]
E["🏪 海外自营仓<br>多平台发货中心<br>TikTok/Shopee/独立站"]
F["🏬 3PL仓库<br>第三方仓储<br>灵活付费"]
end
A --> B & C
B & C --> D & E & F
1.3 没有 WMS 时的仓储痛点
graph TD
A["仓库没有系统管理"] --> B["库存靠人工数格子<br>盘点一次需要2天"]
A --> C["不知道货在哪个位置<br>找货靠记忆和喊人"]
A --> D["超卖:明明没货<br>却继续接订单"]
A --> E["积压:买了太多货<br>不知道还有存货"]
A --> F["出库靠感觉先拿哪个<br>导致先进先出失控"]
A --> G["库存对不上账<br>年底盘点差异巨大"]
1.4 WMS 系统解决的核心问题
| 问题 | WMS 解决方案 |
|---|---|
| 库存不知道在哪里 | 库位管理:每件商品绑定具体货架位置 |
| 库存数量不准确 | 实时流水记录:每次变动自动入账 |
| 超卖 | 冻结库存:下单时即时冻结,不允许超卖 |
| 先进先出失控 | 出库策略:系统按入库时间推荐拣货顺序 |
| 盘点效率低 | 循环盘点:按 ABC 分类每天盘一批,全年滚动 |
| 多仓库难协调 | 统一库存视图:一个界面看所有仓库实时库存 |
第二节 仓储数据库表设计
2.1 WMS 模块表结构总览
erDiagram
warehouse {
bigint id PK
bigint tenant_id
varchar warehouse_code "仓库编码"
varchar warehouse_name "仓库名称"
tinyint warehouse_type "类型"
varchar country_code "所在国家"
tinyint status "状态"
}
warehouse_location {
bigint id PK
bigint warehouse_id FK
varchar location_code "库位编码"
tinyint location_type "库位类型"
tinyint status "状态"
decimal max_weight "最大承重kg"
}
inventory {
bigint id PK
bigint tenant_id
bigint warehouse_id FK
bigint location_id FK
bigint sku_id FK
int quantity "实物库存"
int frozen_qty "冻结库存"
int in_transit_qty "在途库存"
int defective_qty "不良品库存"
int safety_stock "安全库存阈值"
}
inventory_log {
bigint id PK
bigint tenant_id
tinyint log_type "流水类型"
bigint warehouse_id
bigint sku_id
int change_qty "变动数量"
int before_qty "变动前"
int after_qty "变动后"
varchar ref_no "关联单号"
}
inbound_order {
bigint id PK
bigint tenant_id
varchar inbound_no "入库单号"
tinyint inbound_type "入库类型"
bigint warehouse_id FK
tinyint status "状态"
}
inbound_order_item {
bigint id PK
bigint inbound_id FK
bigint sku_id FK
int expected_qty "预期数量"
int actual_qty "实际入库"
bigint location_id FK "上架库位"
}
outbound_order {
bigint id PK
bigint tenant_id
varchar outbound_no "出库单号"
tinyint outbound_type "出库类型"
bigint warehouse_id FK
tinyint status "状态"
}
outbound_order_item {
bigint id PK
bigint outbound_id FK
bigint sku_id FK
int quantity "出库数量"
bigint location_id FK "拣货库位"
tinyint pick_status "拣货状态"
}
stocktake_task {
bigint id PK
bigint tenant_id
varchar task_no "盘点任务编号"
tinyint task_type "盘点类型"
bigint warehouse_id FK
tinyint status "状态"
}
stocktake_item {
bigint id PK
bigint task_id FK
bigint sku_id FK
bigint location_id FK
int book_qty "账面数量"
int actual_qty "实际盘点数"
int diff_qty "差异数量"
}
transfer_order {
bigint id PK
bigint tenant_id
varchar transfer_no "调拨单号"
bigint from_warehouse_id FK
bigint to_warehouse_id FK
tinyint status "状态"
}
warehouse ||--o{ warehouse_location : "包含多个库位"
warehouse ||--o{ inventory : "存放多个SKU库存"
warehouse_location ||--o{ inventory : "库位级库存"
inventory ||--o{ inventory_log : "产生流水记录"
warehouse ||--o{ inbound_order : "入库单属于仓库"
inbound_order ||--o{ inbound_order_item : "包含明细"
warehouse ||--o{ outbound_order : "出库单属于仓库"
outbound_order ||--o{ outbound_order_item : "包含明细"
warehouse ||--o{ stocktake_task : "盘点任务属于仓库"
stocktake_task ||--o{ stocktake_item : "包含盘点明细"
2.2 仓库主表
-- ============================================================
-- 仓库主表
-- 租户下所有仓库的基础信息,支持国内仓/海外仓/FBA仓等多类型
-- ============================================================
CREATE TABLE `warehouse`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`warehouse_code` VARCHAR(32) NOT NULL COMMENT '仓库编码,租户内唯一,如:CN-GZ-01(国家-城市-序号)',
`warehouse_name` VARCHAR(64) NOT NULL COMMENT '仓库名称,如:广州总仓、美国洛杉矶仓',
`warehouse_type` TINYINT NOT NULL COMMENT '仓库类型:1=国内备货仓 2=FBA仓 3=海外自营仓 4=第三方仓(3PL) 5=保税仓',
`country_code` CHAR(2) NOT NULL DEFAULT 'CN' COMMENT '仓库所在国家ISO代码,如CN/US/DE/JP/GB',
`country_name` VARCHAR(32) NULL COMMENT '国家名称,如:中国/美国/德国',
`province` VARCHAR(32) NULL COMMENT '省/州,如:广东省/California',
`city` VARCHAR(32) NULL COMMENT '城市,如:广州市/Los Angeles',
`address` VARCHAR(256) NULL COMMENT '详细地址',
`zip_code` VARCHAR(16) NULL COMMENT '邮政编码',
`contact_name` VARCHAR(64) NULL COMMENT '仓库负责人姓名',
`contact_phone` VARCHAR(32) NULL COMMENT '仓库负责人电话',
`contact_email` VARCHAR(128) NULL COMMENT '仓库负责人邮箱',
`area_sqm` DECIMAL(10, 2) NULL COMMENT '仓库面积(平方米)',
`total_locations` INT NOT NULL DEFAULT 0 COMMENT '总库位数量(统计值,随库位增减自动更新)',
`used_locations` INT NOT NULL DEFAULT 0 COMMENT '已使用库位数量',
`is_default` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为默认仓库(自动分配订单时优先使用)',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=停用 1=正常运营 2=盘点中(期间限制出入库)',
`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_warehouse_code` (`tenant_id`, `warehouse_code`),
KEY `idx_tenant_status` (`tenant_id`, `status`),
KEY `idx_country_code` (`country_code`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '仓库主表';
2.3 库位表
-- ============================================================
-- 仓库库位表
-- 仓库内部的精细化存储单元,每个库位有唯一编码
-- ============================================================
CREATE TABLE `warehouse_location`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`warehouse_id` BIGINT NOT NULL COMMENT '所属仓库ID',
`location_code` VARCHAR(32) NOT NULL COMMENT '库位编码,仓库内唯一,格式 区域-排-列-层,如 A-01-03-02',
`zone` VARCHAR(8) NOT NULL COMMENT '区域代码,如A/B/C(不同区域存放不同品类)',
`row_no` SMALLINT NOT NULL COMMENT '排号(第几排货架)',
`column_no` SMALLINT NOT NULL COMMENT '列号(第几列)',
`floor_no` SMALLINT NOT NULL COMMENT '层号(第几层,从下往上数)',
`location_type` TINYINT NOT NULL DEFAULT 1
COMMENT '库位类型:1=正常存储位 2=拣选位(出货用) 3=退货暂存位 4=不良品隔离区 5=待检区 6=超大件区',
`max_weight_kg` DECIMAL(8, 2) NULL COMMENT '最大承重(千克),防止货架超重',
`max_volume_l` DECIMAL(10, 2) NULL COMMENT '最大容积(升)',
`is_occupied` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否有货:0=空闲 1=已有货物(快速查询空闲库位用)',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=停用 1=正常 2=锁定(盘点中禁止出入库)',
`remark` VARCHAR(128) 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_warehouse_location_code` (`warehouse_id`, `location_code`),
KEY `idx_warehouse_zone` (`warehouse_id`, `zone`),
KEY `idx_warehouse_type` (`warehouse_id`, `location_type`),
KEY `idx_is_occupied` (`warehouse_id`, `is_occupied`) COMMENT '快速查找空闲库位'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '仓库库位表';
2.4 库存主表
-- ============================================================
-- 库存主表
-- 记录每个 SKU 在每个仓库(细化到库位)的实时库存数量
-- 这是 WMS 中查询最频繁、最核心的表
-- ============================================================
CREATE TABLE `inventory`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`warehouse_id` BIGINT NOT NULL COMMENT '仓库ID',
`location_id` BIGINT NULL COMMENT '库位ID,NULL表示仓库级汇总库存(不绑定具体库位)',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码(冗余,加速查询)',
-- ==================== 五种库存数量 ====================
`quantity` INT NOT NULL DEFAULT 0 COMMENT '实物库存:仓库中实际存在的商品数量',
`frozen_qty` INT NOT NULL DEFAULT 0 COMMENT '冻结库存:已被订单预占但尚未出库的数量',
`in_transit_qty` INT NOT NULL DEFAULT 0 COMMENT '在途库存:采购单已确认在途但尚未入库的数量',
`defective_qty` INT NOT NULL DEFAULT 0 COMMENT '不良品库存:质检不合格、无法正常销售的数量',
`reserved_qty` INT NOT NULL DEFAULT 0 COMMENT '预留库存:人工预留给指定渠道/活动,不参与正常分配',
-- ==================== 安全库存与补货 ====================
`safety_stock` INT NOT NULL DEFAULT 0 COMMENT '安全库存阈值:低于此值触发补货预警',
`max_stock` INT NULL COMMENT '最大库存上限:超过此值触发积压预警(可为NULL表示不限)',
`reorder_point` INT NULL COMMENT '补货触发点:低于此值建议采购(通常 > safety_stock)',
-- ==================== 成本信息 ====================
`avg_cost` DECIMAL(12, 4) NOT NULL DEFAULT 0 COMMENT '加权平均成本价(元):每次入库时动态计算更新',
`total_cost` DECIMAL(16, 4) NOT NULL DEFAULT 0 COMMENT '库存总成本 = quantity × avg_cost',
-- ==================== 统计信息 ====================
`last_inbound_time` DATETIME NULL COMMENT '最后一次入库时间',
`last_outbound_time` DATETIME 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,
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_warehouse_location_sku` (`warehouse_id`, `location_id`, `sku_id`) COMMENT '同一库位同一SKU只有一条记录',
KEY `idx_tenant_sku` (`tenant_id`, `sku_id`) COMMENT '查询某SKU在所有仓库的库存',
KEY `idx_warehouse_sku` (`warehouse_id`, `sku_id`) COMMENT '查询某仓库某SKU的库存',
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '库存主表(核心表,每次变动通过流水表记录)';
2.5 库存流水表
-- ============================================================
-- 库存流水表(只增不改不删,是库存数据的唯一真相来源)
-- 每次库存数量变动,必须写入一条流水记录
-- 通过流水记录可以追溯任意时刻的库存快照
-- ============================================================
CREATE TABLE `inventory_log`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`log_type` TINYINT NOT NULL
COMMENT '流水类型:
1=采购入库 2=销售出库 3=调拨入库 4=调拨出库
5=盘盈入库 6=盘亏出库 7=退货入库 8=报损出库
9=预留锁定 10=预留释放 11=冻结 12=解冻',
`warehouse_id` BIGINT NOT NULL COMMENT '仓库ID',
`location_id` BIGINT NULL COMMENT '库位ID',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码(冗余)',
`change_qty` INT NOT NULL COMMENT '变动数量:正数=增加,负数=减少',
`before_qty` INT NOT NULL COMMENT '变动前的实物库存数量(快照)',
`after_qty` INT NOT NULL COMMENT '变动后的实物库存数量(快照)',
`ref_type` VARCHAR(32) NULL COMMENT '关联单据类型:PURCHASE_ORDER/SALES_ORDER/TRANSFER/STOCKTAKE',
`ref_no` VARCHAR(64) NULL COMMENT '关联单据编号,如 PO-20250117-0001',
`ref_id` BIGINT NULL COMMENT '关联单据ID',
`batch_no` VARCHAR(64) NULL COMMENT '批次号(部分场景需要区分批次)',
`operator_id` BIGINT NOT NULL COMMENT '操作人用户ID',
`operator_name` VARCHAR(64) NOT NULL COMMENT '操作人姓名(冗余,防止用户删除后找不到)',
`operate_time` DATETIME NOT NULL COMMENT '操作时间(精确到秒)',
`remark` VARCHAR(256) NULL COMMENT '备注说明(如:盘点差异原因)',
PRIMARY KEY (`id`),
KEY `idx_tenant_sku_time` (`tenant_id`, `sku_id`, `operate_time`) COMMENT '查询某SKU的流水历史',
KEY `idx_warehouse_time` (`warehouse_id`, `operate_time`),
KEY `idx_ref_no` (`ref_no`) COMMENT '按关联单号反查流水',
KEY `idx_log_type` (`log_type`),
KEY `idx_operate_time` (`operate_time`)
-- 注意:此表不设外键,不设 is_deleted 字段,不允许 UPDATE/DELETE 操作
-- 通过数据库权限控制:应用账号只有 INSERT 和 SELECT 权限,无 UPDATE/DELETE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '库存流水记录表(只增不改不删,审计核心)';
2.6 入库单与出库单表
-- ============================================================
-- 入库单主表(统一入库管理)
-- 无论哪种入库类型,都通过此表管理
-- ============================================================
CREATE TABLE `inbound_order`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`inbound_no` VARCHAR(32) NOT NULL COMMENT '入库单编号,格式 IN-YYYYMMDD-XXXX',
`inbound_type` TINYINT NOT NULL
COMMENT '入库类型:1=采购入库 2=调拨入库 3=退货入库 4=盘盈入库 5=其他入库',
`warehouse_id` BIGINT NOT NULL COMMENT '目标仓库ID(货物将存入的仓库)',
`warehouse_name` VARCHAR(64) NOT NULL COMMENT '仓库名称(冗余)',
`ref_type` VARCHAR(32) NULL COMMENT '关联来源类型:PURCHASE_ORDER/TRANSFER/RETURN_ORDER',
`ref_id` BIGINT NULL COMMENT '关联来源单据ID',
`ref_no` VARCHAR(32) NULL COMMENT '关联来源单据编号',
`expected_date` DATE NULL COMMENT '预计到货日期',
`actual_date` DATE NULL COMMENT '实际入库完成日期',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '状态:0=待入库 1=入库中(部分SKU已入) 2=已完成 3=已取消',
`total_sku_count` INT NOT NULL DEFAULT 0 COMMENT '涉及SKU种数',
`total_qty` INT NOT NULL DEFAULT 0 COMMENT '计划入库总件数',
`actual_qty` INT NOT NULL DEFAULT 0 COMMENT '实际入库总件数',
`operator_id` BIGINT NULL COMMENT '操作人(仓储管理员)ID',
`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_inbound_no` (`tenant_id`, `inbound_no`),
KEY `idx_warehouse_status` (`warehouse_id`, `status`),
KEY `idx_ref_id` (`ref_id`),
KEY `idx_expected_date` (`expected_date`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '入库单主表';
-- 入库单明细表
CREATE TABLE `inbound_order_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`inbound_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 DEFAULT 0 COMMENT '实际入库数量(含质检合格的)',
`defective_qty` INT NOT NULL DEFAULT 0 COMMENT '质检不合格数量',
`location_id` BIGINT NULL COMMENT '上架库位ID(确认入库时填写)',
`location_code` VARCHAR(32) NULL COMMENT '库位编码(冗余显示)',
`unit_cost` DECIMAL(12, 4) NULL COMMENT '本次入库成本单价(用于更新加权均价)',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=待入库 1=已入库 2=部分入库',
`remark` VARCHAR(256) NULL COMMENT '备注(如:5件有划痕,单独放退货区)',
PRIMARY KEY (`id`),
KEY `idx_inbound_id` (`inbound_id`),
KEY `idx_sku_id` (`sku_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '入库单明细表';
-- ============================================================
-- 出库单主表(统一出库管理)
-- ============================================================
CREATE TABLE `outbound_order`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`outbound_no` VARCHAR(32) NOT NULL COMMENT '出库单编号,格式 OUT-YYYYMMDD-XXXX',
`outbound_type` TINYINT NOT NULL
COMMENT '出库类型:1=销售出库 2=调拨出库 3=退货出库(采购退供应商) 4=报损出库 5=其他',
`warehouse_id` BIGINT NOT NULL COMMENT '出库仓库ID',
`warehouse_name` VARCHAR(64) NOT NULL COMMENT '仓库名称(冗余)',
`ref_type` VARCHAR(32) NULL COMMENT '关联来源类型',
`ref_id` BIGINT NULL COMMENT '关联来源单据ID(如销售订单ID)',
`ref_no` VARCHAR(32) NULL COMMENT '关联来源单据编号',
`plan_date` DATE NULL COMMENT '计划出库日期',
`actual_date` DATE NULL COMMENT '实际出库完成日期',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '状态:0=待分配库位 1=待拣货 2=拣货中 3=待复核 4=已出库 5=已取消',
`pick_user_id` BIGINT NULL COMMENT '拣货人用户ID',
`pick_start_time` DATETIME NULL COMMENT '开始拣货时间',
`pick_end_time` DATETIME NULL COMMENT '拣货完成时间',
`total_qty` 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_outbound_no` (`tenant_id`, `outbound_no`),
KEY `idx_warehouse_status` (`warehouse_id`, `status`),
KEY `idx_ref_id` (`ref_id`),
KEY `idx_plan_date` (`plan_date`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '出库单主表';
-- 出库单明细(拣货单)
CREATE TABLE `outbound_order_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`outbound_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 '计划出库数量',
`picked_qty` INT NOT NULL DEFAULT 0 COMMENT '实际已拣货数量',
`location_id` BIGINT NULL COMMENT '系统推荐拣货库位ID(出库时分配)',
`location_code` VARCHAR(32) NULL COMMENT '库位编码(冗余,打印拣货单用)',
`pick_status` TINYINT NOT NULL DEFAULT 0 COMMENT '拣货状态:0=待拣 1=已拣 2=部分拣(库存不足时)',
`remark` VARCHAR(256) NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_outbound_id` (`outbound_id`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_location_id` (`location_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '出库单明细表(即拣货单明细)';
2.7 库存盘点表
-- ============================================================
-- 盘点任务主表
-- ============================================================
CREATE TABLE `stocktake_task`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`task_no` VARCHAR(32) NOT NULL COMMENT '盘点任务编号,格式 ST-YYYYMMDD-XXXX',
`task_type` TINYINT NOT NULL COMMENT '盘点类型:1=全盘 2=抽盘(按品类/库区) 3=循环盘(按ABC分类)',
`warehouse_id` BIGINT NOT NULL COMMENT '盘点仓库ID',
`task_name` VARCHAR(128) NOT NULL COMMENT '任务名称,如:2025年1月全盘',
`plan_date` DATE NOT NULL COMMENT '计划盘点日期',
`start_time` DATETIME NULL COMMENT '实际开始时间',
`end_time` DATETIME NULL COMMENT '实际结束时间',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '状态:0=待开始 1=进行中(相关库位锁定) 2=待审核 3=已完成 4=已取消',
`total_sku_count` INT NOT NULL DEFAULT 0 COMMENT '本次盘点涉及SKU种数',
`diff_sku_count` INT NOT NULL DEFAULT 0 COMMENT '有差异的SKU种数',
`profit_qty` INT NOT NULL DEFAULT 0 COMMENT '盘盈总数量',
`loss_qty` INT NOT NULL DEFAULT 0 COMMENT '盘亏总数量',
`profit_amount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '盘盈金额(盘盈数量×成本均价)',
`loss_amount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '盘亏金额',
`auditor_id` BIGINT NULL COMMENT '审核人ID',
`audit_time` DATETIME 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_task_no` (`tenant_id`, `task_no`),
KEY `idx_warehouse_status` (`warehouse_id`, `status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '库存盘点任务主表';
-- 盘点明细表
CREATE TABLE `stocktake_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`task_id` BIGINT NOT NULL COMMENT '关联盘点任务ID',
`warehouse_id` BIGINT NOT NULL COMMENT '仓库ID',
`location_id` BIGINT NULL COMMENT '库位ID',
`location_code` VARCHAR(32) NULL COMMENT '库位编码',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码',
`sku_name` VARCHAR(256) NOT NULL COMMENT 'SKU名称',
`book_qty` INT NOT NULL COMMENT '账面数量(盘点开始时的系统库存快照)',
`actual_qty` INT NULL COMMENT '实际盘点数量(盘点人填写,NULL表示未盘)',
`diff_qty` INT GENERATED ALWAYS AS
(CASE WHEN `actual_qty` IS NOT NULL
THEN `actual_qty` - `book_qty`
ELSE NULL END) STORED COMMENT '差异数量 = 实际 - 账面,正=盘盈,负=盘亏(自动计算)',
`diff_amount` DECIMAL(12, 2) NULL COMMENT '差异金额 = diff_qty × 成本均价(审核时计算填入)',
`diff_reason` VARCHAR(256) NULL COMMENT '差异原因说明(有差异时必填)',
`is_adjusted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已执行库存调整:0=未调整 1=已调整',
`adjust_time` DATETIME NULL COMMENT '库存调整执行时间',
`picker_id` BIGINT NULL COMMENT '盘点执行人ID',
`pick_time` DATETIME NULL COMMENT '实际盘点时间',
PRIMARY KEY (`id`),
KEY `idx_task_id` (`task_id`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_location_id` (`location_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '盘点明细表';
2.8 仓库调拨单表
-- ============================================================
-- 仓库调拨单表
-- 管理货物在不同仓库之间的转移
-- ============================================================
CREATE TABLE `transfer_order`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`transfer_no` VARCHAR(32) NOT NULL COMMENT '调拨单编号,格式 TRF-YYYYMMDD-XXXX',
`from_warehouse_id` BIGINT NOT NULL COMMENT '调出仓库ID(货物从哪里出)',
`from_warehouse_name` VARCHAR(64) NOT NULL COMMENT '调出仓库名称(冗余)',
`to_warehouse_id` BIGINT NOT NULL COMMENT '调入仓库ID(货物去哪里)',
`to_warehouse_name` VARCHAR(64) NOT NULL COMMENT '调入仓库名称(冗余)',
`transfer_reason` VARCHAR(256) NULL COMMENT '调拨原因,如:海外仓补货、仓库关闭清货',
`logistics_company` VARCHAR(64) NULL COMMENT '运输物流公司',
`tracking_no` VARCHAR(128) NULL COMMENT '运输跟踪单号',
`plan_date` DATE NULL COMMENT '计划调拨日期',
`ship_date` DATE NULL COMMENT '实际发货日期',
`arrive_date` DATE NULL COMMENT '实际到达日期',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '状态:0=草稿 1=已审核 2=调出中(出库完成,货在途) 3=已到达(入库中) 4=已完成 5=已取消',
`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_transfer_no` (`tenant_id`, `transfer_no`),
KEY `idx_from_warehouse` (`from_warehouse_id`),
KEY `idx_to_warehouse` (`to_warehouse_id`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '仓库调拨单表';
-- 调拨单明细表
CREATE TABLE `transfer_order_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`transfer_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名称',
`transfer_qty` INT NOT NULL COMMENT '计划调拨数量',
`shipped_qty` INT NOT NULL DEFAULT 0 COMMENT '实际调出数量(出库时填写)',
`received_qty` INT NOT NULL DEFAULT 0 COMMENT '实际调入数量(到达入库时填写)',
`from_location_id` BIGINT NULL COMMENT '调出库位ID',
`to_location_id` BIGINT NULL COMMENT '调入目标库位ID',
PRIMARY KEY (`id`),
KEY `idx_transfer_id` (`transfer_id`),
KEY `idx_sku_id` (`sku_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '调拨单明细表';
第三节 仓库与库位体系
3.1 库位编码规范设计
库位就是仓库里货架的精确地址,就像快递地址一样,必须唯一且有规律。
库位编码格式:{仓库代码}-{区域}-{排}-{列}-{层}
graph TD
A["CN-GZ-01-A-02-05-03<br>完整库位编码"] --> B["CN-GZ-01<br>仓库代码<br>中国-广州-01号仓"]
A --> C["A<br>区域代码<br>A区(通常按品类划分)"]
A --> D["02<br>排号<br>第2排货架"]
A --> E["05<br>列号<br>第5列"]
A --> F["03<br>层号<br>第3层(从下往上)"]
库位编码设计原则:
| 原则 | 说明 | 举例 |
|---|---|---|
| 唯一性 | 同一仓库内库位编码不能重复 | UNIQUE KEY 约束保证 |
| 可读性 | 人工扫码时能快速定位 | A-02-05-03 比 A020503 更易读 |
| 规律性 | 按物理位置有序编码,便于规划拣货路线 | 排号从入口往里递增 |
| 扩展性 | 预留足够位数,方便仓库扩容 | 排号用2位,最多支持99排 |
不同区域的功能划分:
graph LR
subgraph 仓库平面图
A["A区<br>快销品区<br>靠近出货口<br>日销量TOP20%的SKU"]
B["B区<br>普通存储区<br>常规销售SKU"]
C["C区<br>滞销品区<br>远离出货口<br>低频出库SKU"]
D["D区<br>超大件区<br>特殊货架"]
E["R区<br>退货暂存区<br>买家退回商品"]
F["Q区<br>待检区<br>刚到货未质检"]
G["X区<br>不良品隔离区<br>质检不合格商品"]
H["出货口"]
end
A --- H
B --- A
C --- B
3.2 库位分配策略
当新货物需要上架时,系统按以下优先级推荐库位:
flowchart TD
A["需要为某SKU分配上架库位"] --> B["查找该SKU是否已有<br>存储的库位(归并原则)"]
B --> C{"已有库位?"}
C -- "有且未满" --> D["推荐相同库位<br>(同SKU集中存放,方便拣货)"]
C -- "没有或已满" --> E["查找同区域的空闲库位<br>(按ABC分类选对应区)"]
E --> F{"同区域有空位?"}
F -- 有 --> G["按排列层顺序<br>推荐最近的空位<br>(先近后远,减少步行距离)"]
F -- "没有" --> H["跨区域查找空位<br>标注建议区域"]
D & G & H --> I["展示推荐库位列表<br>仓管员可手动调整"]
I --> J["确认上架库位<br>更新库位 is_occupied = 1"]
3.3 库位状态管理
stateDiagram-v2
[*] --> 空闲 : 库位创建时
空闲 --> 占用中 : 有货物上架
占用中 --> 空闲 : 货物全部取走
空闲 --> 锁定 : 盘点任务开始(禁止出入库)
占用中 --> 锁定 : 盘点任务开始
锁定 --> 空闲 : 盘点任务结束(无货)
锁定 --> 占用中 : 盘点任务结束(有货)
空闲 --> 停用 : 管理员手动停用
停用 --> 空闲 : 管理员重新启用
第四节 库存核心设计
4.1 五种库存类型详解
这是 WMS 最重要的概念,必须理解每种库存的含义和扣减时机。
graph TD
subgraph 仓库中的实物
A["📦 实物库存 quantity<br>仓库中实际存放的商品总数<br>是所有数量的基础"]
end
subgraph 不能销售的部分
B["❄️ 冻结库存 frozen_qty<br>已被订单预占的数量<br>货还在仓库,但已预留给某个订单<br>不能再卖给别人"]
C["⚠️ 不良品 defective_qty<br>质检不合格无法销售的数量<br>存放在隔离区"]
D["🔒 预留库存 reserved_qty<br>人工预留给特定渠道/活动<br>如:预留200个给双十一"]
end
subgraph 还没到仓库的
E["🚚 在途库存 in_transit_qty<br>采购单已确认,货在路上<br>还没到仓库,但可以计入补货计划"]
end
subgraph 计算可销售数量
F["✅ 可销售库存<br>= quantity - frozen_qty<br> - defective_qty - reserved_qty<br>这才是真正可以接单的数量"]
end
A --> F
B --> F
C --> F
D --> F
五种库存数量的增减时机:
| 库存类型 | 增加时机 | 减少时机 |
|---|---|---|
quantity(实物) | 入库操作确认时 | 出库操作确认时 |
frozen_qty(冻结) | 订单确认时(预占) | 出库确认时(扣减实物同时解冻);订单取消时(释放冻结) |
in_transit_qty(在途) | 采购单供应商确认时(货出发了) | 入库确认时(货到了) |
defective_qty(不良品) | 质检不合格入库时 | 退货给供应商时;报损出库时 |
reserved_qty(预留) | 手动预留操作时 | 手动释放预留时;预留商品发货后 |
4.2 可销售库存实时计算
-- 查询某SKU在某仓库的可销售库存
SELECT
i.sku_id,
i.sku_code,
i.warehouse_id,
i.quantity AS physical_stock,
i.frozen_qty,
i.defective_qty,
i.reserved_qty,
i.in_transit_qty,
i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty AS available_stock,
i.safety_stock,
CASE
WHEN i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty <= 0
THEN '无货'
WHEN i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty <= i.safety_stock
THEN '库存紧张'
ELSE '正常'
END AS stock_status
FROM inventory i
WHERE i.tenant_id = #{tenantId}
AND i.sku_id = #{skuId}
AND i.warehouse_id = #{warehouseId}
AND i.location_id IS NULL -- 查仓库级汇总,不查库位级
AND i.is_deleted = 0;
4.3 加权平均成本计算
为什么需要计算平均成本?因为同一个 SKU 可能分多次采购,每次价格不同。计算利润时需要一个准确的成本基准。
加权平均成本法:
第一次采购:100件,单价 38元
第二次采购:200件,单价 36元
第三次采购:150件,单价 37元
合计数量:100 + 200 + 150 = 450件
合计成本:100×38 + 200×36 + 150×37 = 3800 + 7200 + 5550 = 16550元
加权平均成本 = 16550 ÷ 450 = 36.78元/件
更新公式(每次入库时动态计算):
新的平均成本 = (现有总成本 + 本次入库成本) ÷ (现有库存量 + 本次入库量)
= (当前quantity × avg_cost + 本次actual_qty × unit_cost) ÷ (quantity + actual_qty)
第五节 入库全流程
5.1 三种入库类型对比
| 入库类型 | 触发来源 | 特点 | 关键操作 |
|---|---|---|---|
| 采购入库 | 采购收货单确认 | 需要质检,有不良品处理 | 质检→合格品入库→不合格品退货 |
| 调拨入库 | 调拨单到达确认 | 本公司内部,通常不需要质检 | 验收数量→直接入库 |
| 退货入库 | 买家退货到仓 | 需要二次质检,决定是否可二次销售 | 质检→好品入可售库存→差品入不良品 |
5.2 采购入库完整流程
注意:采购入库流程与第三天课程的内容有交叉,这里从 WMS 视角重点讲库位操作和库存联动。
flowchart TD
A(["采购货物到达仓库"]) --> B["门卫/收货台登记<br>记录到货时间和车牌"]
B --> C["仓管员扫描送货单<br>或查询系统确认对应采购单"]
C --> D{"采购单是否存在<br>且状态正常?"}
D -- 否 --> E["联系采购部确认<br>可能是未建采购单的急单"]
D -- 是 --> F["创建入库单<br>inbound_type = 1(采购入库)<br>关联采购单号"]
F --> G["货物移至待检区(Q区)<br>按采购单明细逐件清点"]
G --> H["质量检验<br>外观检查 + 功能抽检<br>对比采购单规格"]
H --> I{"质检结果"}
I -- "全部合格" --> J["填写入库明细:<br>每个SKU的实际数量<br>分配目标库位"]
I -- "部分合格" --> K["分别记录合格数量<br>和不合格数量及原因<br>不合格品移至X区隔离"]
I -- "全部不合格" --> L["拒收处理<br>创建采购退货单<br>通知采购部"]
J & K --> M["系统执行入库事务<br>(原子操作,见第三天详解)"]
M --> N["库存 quantity + 合格数量<br>写入库存流水记录"]
N --> O["更新库位 is_occupied = 1"]
O --> P["更新加权平均成本<br>avg_cost 重新计算"]
P --> Q["货物搬运上架<br>放置到指定库位"]
Q --> R["扫描库位条码确认<br>或 App 确认上架完成"]
R --> S(["入库完成"])
L --> T(["退货处理"])
5.3 退货入库完整流程(买家退货)
买家退货的货物与采购新品有本质区别:可能有二手损耗,需要二次质检决定处置方式。
flowchart TD
A(["买家退货到仓"]) --> B["快递员交接<br>扫描快递单号<br>关联销售退货单"]
B --> C["货物移至R区(退货暂存区)<br>登记退货数量"]
C --> D["质检人员二次检验"]
D --> E{"检验结果"}
E -- "全新/完好<br>可以再次销售" --> F["入可销售库存<br>inventory.quantity + N<br>流水类型 = 退货入库(7)"]
E -- "有轻微使用痕迹<br>可降价销售" --> G["标注为翻新品<br>建立独立SKU(如SKU加-REFURB后缀)<br>入翻新品库存"]
E -- "损坏无法销售" --> H["入不良品库存<br>inventory.defective_qty + N"]
F & G --> I["更新库位信息<br>选择合适的上架位置"]
H --> J["存放X区不良品隔离区<br>等待报损或退供应商处理"]
I & J --> K["写入库存流水记录<br>关联销售退货单号"]
K --> L(["退货入库完成"])
5.4 入库单编号规则
格式:IN-{YYYYMMDD}-{4位序号}
示例:IN-20250117-0001
不同入库类型的编号使用同一规则,通过 inbound_type 字段区分类型
第六节 出库全流程
6.1 三种出库类型对比
| 出库类型 | 触发来源 | 特点 | 库存变动 |
|---|---|---|---|
| 销售出库 | OMS 订单发货需求 | 最频繁,需要最快处理 | quantity - N,frozen_qty - N(解冻) |
| 调拨出库 | 调拨单审核通过 | 内部转移,计划性强 | quantity - N,in_transit_qty(目标仓)+N |
| 报损出库 | 不良品处置申请 | 减少不良品库存 | defective_qty - N |
6.2 销售出库完整流程(最核心)
flowchart TD
A(["OMS 推送发货需求<br>含:订单号/SKU/数量/目标仓库"]) --> B["WMS 接收发货需求<br>创建出库单<br>outbound_type = 1(销售出库)"]
B --> C["出库单分配库位<br>查询该SKU在仓库的库存分布"]
C --> D["库存分配算法(见下节)<br>确定从哪个库位拣多少件"]
D --> E["生成拣货单<br>outbound_order_item 明细<br>按库位路径排序(减少行走距离)"]
E --> F["拣货单推送给仓管员<br>(移动端 App 或打印纸质单)"]
F --> G["仓管员按拣货单<br>逐库位拣货"]
G --> H["扫描商品条码验证<br>防止拣错 SKU"]
H --> I{"条码是否匹配?"}
I -- 不匹配 --> J["提示错误,换正确的商品"]
I -- 匹配 --> K["更新拣货状态<br>pick_status = 1(已拣)"]
K --> L{"所有明细拣货完成?"}
L -- 否 --> G
L -- 是 --> M["拣货完成<br>商品送至打包区"]
M --> N["复核环节<br>再次核对 SKU/数量/订单信息"]
N --> O{"复核通过?"}
O -- 不通过 --> P["退回重新拣货"]
P --> G
O -- 通过 --> Q["打包完成<br>贴物流面单"]
Q --> R["系统执行出库事务:<br>quantity -= 拣货数量<br>frozen_qty -= 拣货数量(解冻)<br>写入库存流水(销售出库)"]
R --> S["更新库位 is_occupied<br>(该库位是否还有货)"]
S --> T["回调 OMS<br>通知订单已出库"]
T --> U["OMS 触发物流创建<br>TMS 模块生成运单"]
U --> V(["出库完成"])
6.3 库存分配算法(先进先出 FIFO)
先进先出(FIFO,First In First Out):先入库的货物先出库,避免货物在仓库中积压过久导致过期或品质下降。
flowchart TD
A["出库需求:SKU-A 需要出库 100件"] --> B["查询该SKU在当前仓库<br>所有库位的库存情况"]
B --> C["按入库时间升序排列<br>(最早入库的库位优先)"]
C --> D["库位1(A-01-02-01):入库时间 2025-01-01<br>库存:80件"]
C --> E["库位2(A-01-03-02):入库时间 2025-01-10<br>库存:60件"]
C --> F["库位3(A-02-01-01):入库时间 2025-01-15<br>库存:120件"]
D --> G["从库位1拣 80件(全部拣空)"]
G --> H["还需要 100-80=20件"]
E --> I["从库位2拣 20件(剩余40件)"]
H --> I
I --> J["共拣货:80+20=100件 ✅<br>满足出库需求"]
J --> K["生成拣货单:<br>拣货指令1:A-01-02-01 拣 80件<br>拣货指令2:A-01-03-02 拣 20件"]
拣货路径优化(减少行走距离):
graph LR
subgraph unoptimized["未优化路径"]
U1["A-01-02"] --> U2["C-03-01"] --> U3["A-02-05"] --> U4["B-01-03"]
U5["来回跑,行走距离长"]
end
subgraph optimized["优化后路径:按区域和排号排序"]
O1["A-01-02"] --> O2["A-02-05"] --> O3["B-01-03"] --> O4["C-03-01"]
O5["从A区到B区到C区,单向移动"]
end
排序规则:ORDER BY zone ASC, row_no ASC, column_no ASC, floor_no ASC
按区域→排→列→层的顺序排列拣货指令,让拣货员按固定路线走,不需要来回跑。
6.4 出库异常处理
| 异常情况 | 原因 | 处理方式 |
|---|---|---|
| 库存不足 | 可销售库存 < 出库需求数量 | 系统提示缺货,出库单挂起,通知采购补货 |
| 条码扫描不匹配 | 拣错货 | 立即提示错误,换正确商品,记录错拣次数 |
| 库位扫描不一致 | 货物被移位 | 提示位置异常,人工确认实际位置,更新库位信息 |
| 货物损坏发现 | 出库前发现商品有损坏 | 将损坏品移至不良品区,重新找良品出库 |
第七节 库存流水设计(审计核心)
7.1 库存流水是什么以及为什么重要
类比:银行账户的每一笔收款和付款都有交易流水,通过流水可以还原任意时刻的账户余额。 库存流水的作用完全相同:每一次库存变动(入库/出库/调拨/盘点调整)都记录一条流水,通过流水可以还原任意时刻的库存快照。
graph TD
A["❓ 为什么要有库存流水?"] --> B["1️⃣ 追溯性<br>某个SKU库存昨天100件<br>今天变成80件,谁动了?"]
A --> C["2️⃣ 对账<br>实物库存和系统库存不一致<br>通过流水找出差异点"]
A --> D["3️⃣ 审计<br>财务审计时需要证明<br>货物进出有据可查"]
A --> E["4️⃣ 恢复<br>系统出现Bug导致库存数据错误<br>可以通过重放流水恢复正确值"]
A --> F["5️⃣ 分析<br>统计SKU的出库频率<br>用于ABC分类和库位优化"]
7.2 库存流水的不可篡改性保证
库存流水是系统的”信任基石”,一旦可以修改,所有追溯、对账、审计都失去意义。
多层次保护机制:
graph TD
A["保护层级"] --> B["应用层:<br>Service 代码中永远不写<br>UPDATE/DELETE inventory_log 的逻辑"]
A --> C["数据库账号层:<br>应用使用的数据库账号<br>只有 INSERT + SELECT 权限<br>没有 UPDATE/DELETE 权限"]
A --> D["数据库触发器:<br>(可选)创建触发器<br>拦截任何 UPDATE/DELETE 操作并抛错"]
A --> E["审计监控:<br>定期检查流水记录的连续性<br>发现异常立即告警"]
B & C & D & E --> F["✅ 流水记录完全不可篡改<br>即使是数据库管理员<br>也需要走审批才能在特殊情况下操作"]
7.3 通过流水还原历史库存快照
-- 还原某SKU某仓库在指定时间点的库存数量
-- 思路:取该时间点之前最近一条流水的 after_qty 字段即可
SELECT
after_qty AS stock_at_that_time
FROM inventory_log
WHERE tenant_id = #{tenantId}
AND warehouse_id = #{warehouseId}
AND sku_id = #{skuId}
AND operate_time <= #{targetTime}
-- 只看影响实物库存的流水类型(1=采购入库 2=销售出库 3=调拨入 4=调拨出 5=盘盈 6=盘亏 7=退货入 8=报损)
AND log_type IN (1, 2, 3, 4, 5, 6, 7, 8)
ORDER BY operate_time DESC
LIMIT 1;
-- 还原某SKU某仓库在指定月份的出库量汇总
SELECT
DATE_FORMAT(operate_time, '%Y-%m') AS month,
SUM(ABS(change_qty)) AS total_outbound_qty,
COUNT(*) AS outbound_times
FROM inventory_log
WHERE tenant_id = #{tenantId}
AND sku_id = #{skuId}
AND log_type = 2 -- 2=销售出库
AND operate_time BETWEEN #{startTime} AND #{endTime}
GROUP BY DATE_FORMAT(operate_time, '%Y-%m')
ORDER BY month;
7.4 流水类型与库存字段对照表
| 流水类型 | 影响字段 | 变动方向 | 触发场景 |
|---|---|---|---|
| 1 采购入库 | quantity | +(增加) | 采购收货单确认 |
| 2 销售出库 | quantity | -(减少) | 拣货出库确认 |
| 3 调拨入库 | quantity | + | 调拨货物到达确认 |
| 4 调拨出库 | quantity | - | 调拨货物发出确认 |
| 5 盘盈入库 | quantity | + | 盘点发现实物多于账面 |
| 6 盘亏出库 | quantity | - | 盘点发现实物少于账面 |
| 7 退货入库 | quantity 或 defective_qty | + | 买家退货质检后入库 |
| 8 报损出库 | defective_qty | - | 不良品报损处置 |
| 11 冻结 | frozen_qty | + | 订单确认时预占库存 |
| 12 解冻 | frozen_qty | - | 订单取消或出库完成 |
第八节 仓库间调拨
8.1 调拨业务场景
graph TD
A["为什么需要调拨?"] --> B["场景1:海外仓补货<br>国内备货仓→美国FBA仓<br>提前备货应对销售旺季"]
A --> C["场景2:库存平衡<br>A仓库某SKU积压500件<br>B仓库同SKU缺货<br>内部调拨平衡库存"]
A --> D["场景3:仓库关闭<br>某仓库要停运<br>货物全部迁移到其他仓库"]
A --> E["场景4:品类集中<br>将同类商品集中到一个仓库<br>提高拣货效率"]
8.2 调拨单完整流程与在途库存管理
flowchart TD
A(["发起调拨申请"]) --> B["填写调拨信息:<br>调出仓库/调入仓库<br>SKU列表/数量<br>调拨原因/计划日期"]
B --> C["调拨审核<br>仓库负责人确认库存充足"]
C --> D{"审核结果"}
D -- 拒绝 --> E["退回修改"]
E --> B
D -- 通过 --> F["调出仓库执行出库:<br>quantity -= 调拨数量<br>写流水:类型=调拨出库(4)<br>出库单状态=已出库"]
F --> G["⚠️ 调入仓库同步增加在途库存:<br>inventory.in_transit_qty += 调拨数量<br>表示货在路上,即将到来"]
G --> H["更新调拨单状态:调出中<br>填写物流公司和运单号"]
H --> I["货物国际/国内运输中<br>可在TMS中跟踪物流轨迹"]
I --> J(["货物到达调入仓库"])
J --> K["仓管员清点到货数量<br>核对调拨单"]
K --> L{"实际到货数量<br>是否与调拨单一致?"}
L -- "一致" --> M["执行入库操作:<br>inventory.quantity += 到货数量<br>inventory.in_transit_qty -= 到货数量<br>写流水:类型=调拨入库(3)"]
L -- "有差异(运输途中损耗)" --> N["按实际到货数量入库<br>差异数量写入备注<br>通知调出仓库确认"]
M & N --> O["更新调拨单状态:已完成"]
O --> P(["调拨完成"])
在途库存的关键设计:
货物从 A 仓发出到 B 仓收到,这段时间货物处于”两个仓库都没有实物”的状态,但在 B 仓的补货计划中应该可以看到这批货即将到来。
timeline
title 调拨过程中的库存变化
section 调拨前
A仓 quantity 500件
B仓 quantity 200件
B仓 in_transit 0件
section 调拨出库后(货在路上)
A仓 quantity 300件(减少200)
B仓 quantity 200件(不变)
B仓 in_transit 200件(增加200,知道货来了)
section 调拨入库后(货到了)
A仓 quantity 300件(不变)
B仓 quantity 400件(增加200)
B仓 in_transit 0件(减少200)
第九节 库存盘点系统
9.1 为什么需要盘点
系统账面库存 ≠ 仓库实物库存,差异的来源:
- 入库时漏扫码(系统没增加,实物已入)
- 出库时多拣了1件(系统少减,实物多出)
- 货物被移位后没有更新系统
- 商品损坏但未及时报损
- 偷盗或内部拿取未记录
盘点的目标:发现差异,调整账面库存与实物保持一致,保证库存数据的准确性。
9.2 三种盘点模式对比
graph TD
subgraph 全盘 Full Count
A1["全仓所有SKU一次性盘点<br>通常年底执行"] --> A2["优点:数据最准确<br>缺点:需要停止出入库<br>2-3天,影响运营"]
end
subgraph 抽盘 Spot Check
B1["按品类/区域抽取部分SKU<br>发现问题时专项检查"] --> B2["优点:快速,不停运<br>缺点:覆盖不全面<br>有盲区"]
end
subgraph 循环盘 Cycle Count
C1["按ABC分类,每天盘一批<br>高频SKU(A类)每月盘一次<br>低频SKU(C类)每季度盘一次"] --> C2["优点:不停运,全年覆盖<br>持续发现问题<br>是企业最推荐的方式 ✅"]
end
ABC 分类标准:
| 分类 | 标准 | 盘点频率 | 约占SKU数 |
|---|---|---|---|
| A 类 | 销售额占总额前 20% | 每月盘点 | 约 20% SKU |
| B 类 | 销售额占总额 20%-50% | 每季度盘点 | 约 30% SKU |
| C 类 | 销售额占总额后 50% | 每半年盘点 | 约 50% SKU |
9.3 全盘完整流程
flowchart TD
A(["管理员发起全盘任务"]) --> B["选择盘点仓库和计划日期<br>创建盘点任务 task_type=1"]
B --> C["系统生成盘点明细清单:<br>该仓库所有SKU的当前账面库存(快照)<br>写入 stocktake_item.book_qty"]
C --> D["⚠️ 锁定相关库位:<br>warehouse_location.status = 2(锁定)<br>期间禁止该仓库出入库操作"]
D --> E["更新盘点任务状态:进行中"]
E --> F["仓管员分组<br>各自负责不同区域"]
F --> G["仓管员逐库位清点实物<br>在 App 或盘点单上填写实际数量"]
G --> H["盘点系统接收实盘数据<br>逐条更新 stocktake_item.actual_qty"]
H --> I{"所有 SKU 是否已盘完?"}
I -- 否 --> G
I -- 是 --> J["系统自动计算差异:<br>diff_qty = actual_qty - book_qty"]
J --> K{"是否有差异?"}
K -- "无差异" --> L["盘点完成,数据准确"]
K -- "有差异" --> M["生成差异报告<br>列出所有有差异的SKU"]
M --> N["仓管员填写差异原因<br>(必填)"]
N --> O["管理员审核差异报告<br>确认差异合理性"]
O --> P{"审核结果"}
P -- "差异合理,调整" --> Q["执行库存调整事务:<br>盘盈:inventory.quantity += diff_qty,写流水(5盘盈)<br>盘亏:inventory.quantity += diff_qty(负数),写流水(6盘亏)"]
P -- "差异异常,重新清点" --> G
Q & L --> R["解除库位锁定:<br>warehouse_location.status = 1(正常)"]
R --> S["生成盘点报告:<br>盘盈总量/总金额<br>盘亏总量/总金额<br>差异SKU列表"]
S --> T(["盘点完成"])
9.4 循环盘点任务自动生成
flowchart TD
A(["定时任务:每天 22:00 执行"]) --> B["查询今日循环盘点计划:<br>A类SKU:本月已盘过的跳过<br>B类SKU:本季度已盘过的跳过<br>C类SKU:本半年已盘过的跳过"]
B --> C{"今天有待盘的SKU?"}
C -- 否 --> D["今日无盘点任务<br>记录日志结束"]
C -- 是 --> E["创建循环盘点任务<br>task_type = 3<br>task_name = YYYY-MM-DD循环盘点"]
E --> F["生成盘点明细:<br>今日待盘 SKU 列表<br>记录 book_qty 快照"]
F --> G["推送盘点通知:<br>明日仓管员需盘的库位清单"]
G --> H(["定时任务结束"])
盘点覆盖率的 SQL 统计:
-- 查询各SKU最后一次盘点时间,判断是否在计划周期内
SELECT
s.sku_id,
s.sku_code,
s.abc_class,
MAX(st.end_time) AS last_stocktake_time,
DATEDIFF(NOW(), MAX(st.end_time)) AS days_since_last_stocktake,
CASE s.abc_class
WHEN 'A' THEN 30 -- A类30天盘一次
WHEN 'B' THEN 90 -- B类90天盘一次
WHEN 'C' THEN 180 -- C类180天盘一次
ELSE 365
END AS required_cycle_days,
CASE
WHEN MAX(st.end_time) IS NULL THEN '从未盘点'
WHEN DATEDIFF(NOW(), MAX(st.end_time)) > (
CASE s.abc_class WHEN 'A' THEN 30 WHEN 'B' THEN 90 ELSE 180 END
) THEN '已超期'
ELSE '在周期内'
END AS stocktake_status
FROM product_sku s
LEFT JOIN stocktake_item si ON s.sku_id = si.sku_id AND si.is_deleted = 0
LEFT JOIN stocktake_task st ON si.task_id = st.id AND st.status = 3 -- 已完成的盘点
WHERE s.tenant_id = #{tenantId}
GROUP BY s.sku_id, s.sku_code, s.abc_class
ORDER BY days_since_last_stocktake DESC NULLS FIRST;
9.5 盘点差异调整的数据完整性
sequenceDiagram
participant Admin as 仓库管理员
participant BE as 后端
participant DB as 数据库
Admin->>BE: 确认审核通过,执行库存调整
BE->>DB: BEGIN TRANSACTION(开启事务)
loop 遍历每条有差异的盘点明细
BE->>DB: UPDATE inventory<br>SET quantity = quantity + diff_qty<br>WHERE sku_id=? AND warehouse_id=?
BE->>DB: INSERT inventory_log<br>(log_type = 5盘盈 或 6盘亏,<br>change_qty = diff_qty,<br>ref_no = 盘点任务编号)
BE->>DB: UPDATE stocktake_item<br>SET is_adjusted=1, adjust_time=NOW()<br>WHERE id=?
end
BE->>DB: UPDATE stocktake_task<br>SET status=3(已完成), end_time=NOW()
BE->>DB: COMMIT(全部成功才提交)
DB-->>BE: 事务提交成功
BE-->>Admin: 盘点调整完成,生成报告
第十节 库存预警与智能补货
10.1 库存预警级别设计
graph TD
A["库存预警系统"] --> B["🟢 正常<br>可售库存 > 安全库存<br>无需操作"]
A --> C["🟡 库存紧张<br>可售库存 ≤ 安全库存<br>建议尽快补货"]
A --> D["🔴 库存不足<br>可售库存 ≤ 安全库存 × 30%<br>紧急补货,优先处理"]
A --> E["⚫ 库存为零<br>可售库存 = 0<br>立即停止销售,极速补货"]
A --> F["📦 库存积压<br>可售库存 > 最大库存上限<br>考虑降价促销"]
10.2 预警定时扫描机制
flowchart TD
A(["定时任务:每小时整点执行"]) --> B["查询所有设置了安全库存的SKU-仓库组合"]
B --> C["计算每个组合的可售库存<br>= quantity - frozen_qty - defective_qty - reserved_qty"]
C --> D{"与上次预警状态对比<br>是否发生状态变化?"}
D -- "未变化" --> E["跳过,不重复通知"]
D -- "状态变化(如:正常→紧张)" --> F["生成预警记录<br>记录预警级别/SKU/仓库/当前数量"]
F --> G["发送预警通知:<br>站内信给采购专员<br>邮件通知(高级别)"]
G --> H["自动创建补货建议单(可选)<br>采购专员确认后提交采购"]
E & H --> I{"是否还有未处理SKU?"}
I -- 是 --> C
I -- 否 --> J(["任务结束"])
10.3 库存看板设计(核心展示)
库存总览 Dashboard [刷新时间:2025-01-17 15:30]
┌──────────┬──────────┬──────────┬──────────┐
│ 实物库存量 │ 可售库存量 │ 在途库存量 │ 不良品量 │
│ 28,650件 │ 24,320件 │ 3,200件 │ 1,130件 │
│ │ │ │ 占比3.9% │
└──────────┴──────────┴──────────┴──────────┘
┌────────────────────────────────────────────┐
│ 预警 SKU 列表(需要立即处理) │
│ 🔴 蓝牙耳机Pro-黑 库存12件 安全库存50件 │
│ → [立即采购] [查看详情] │
│ 🔴 手机支架-金属 库存0件 安全库存30件 │
│ → [立即采购] [临时下架] │
│ 🟡 数据线-1m-白 库存35件 安全库存40件 │
│ → [建议采购] [查看详情] │
└────────────────────────────────────────────┘
┌───────────────────────┬────────────────────┐
│ 各仓库库存分布(饼图) │ 近7天出入库趋势 │
│ │ │
│ ■ 广州总仓 45% │ 入库 ▓▓▓▓▓ 3250 │
│ ■ 美国FBA 30% │ 出库 ▓▓▓▓▓▓ 4100 │
│ ■ 英国仓 15% │ │
│ ■ 其他 10% │ 净变化: -850件 │
└───────────────────────┴────────────────────┘
┌────────────────────────────────────────────┐
│ 库存积压 Top5 SKU(超过最大库存上限) │
│ 1. 无线充电器 实物1280件 最大库存500件 │
│ 库龄 45天 成本总值 ¥38,400 [降价促销] │
│ 2. 蓝牙音箱-白 实物680件 最大库存200件 │
│ 库龄 62天 成本总值 ¥61,200 [调拨海外] │
└────────────────────────────────────────────┘
10.4 库存健康度统计 SQL
-- 库存健康度全面统计(每天生成报告)
SELECT
w.warehouse_name,
COUNT(DISTINCT i.sku_id) AS total_sku_count, -- SKU种数
SUM(i.quantity) AS total_quantity, -- 实物总数
SUM(i.quantity - i.frozen_qty
- i.defective_qty - i.reserved_qty)
AS total_available, -- 可售总数
SUM(i.in_transit_qty) AS total_in_transit, -- 在途总数
SUM(i.defective_qty) AS total_defective, -- 不良品总数
ROUND(SUM(i.defective_qty) * 100.0
/ NULLIF(SUM(i.quantity), 0), 2) AS defective_rate, -- 不良品率
-- 各预警级别数量
SUM(CASE
WHEN i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty = 0
THEN 1 ELSE 0 END) AS zero_stock_sku_count, -- 零库存SKU数
SUM(CASE
WHEN i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty > 0
AND i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty <= i.safety_stock
THEN 1 ELSE 0 END) AS warning_sku_count, -- 预警SKU数
SUM(CASE
WHEN i.max_stock IS NOT NULL
AND i.quantity > i.max_stock
THEN 1 ELSE 0 END) AS overstock_sku_count, -- 积压SKU数
-- 库存总价值
SUM(i.quantity * i.avg_cost) AS total_inventory_value -- 库存总价值(元)
FROM inventory i
JOIN warehouse w ON i.warehouse_id = w.id
WHERE i.tenant_id = #{tenantId}
AND i.location_id IS NULL -- 仓库级汇总
AND i.is_deleted = 0
AND w.is_deleted = 0
GROUP BY w.id, w.warehouse_name
ORDER BY total_inventory_value DESC;
第十一节 接口设计规范
11.1 WMS 模块完整接口清单
基础路径前缀: /api/wms
| 模块 | 接口名称 | HTTP方法 | 路径 | 权限标识 |
|---|---|---|---|---|
| 仓库管理 | 仓库列表 | GET | /warehouses | wms:warehouse:list |
| 仓库管理 | 仓库详情 | GET | /warehouses/{id} | wms:warehouse:list |
| 仓库管理 | 新增仓库 | POST | /warehouses | wms:warehouse:add |
| 仓库管理 | 编辑仓库 | PUT | /warehouses/{id} | wms:warehouse:edit |
| 库位管理 | 库位列表 | GET | /warehouses/{wid}/locations | wms:location:list |
| 库位管理 | 新增库位 | POST | /warehouses/{wid}/locations | wms:location:add |
| 库位管理 | 批量新增库位 | POST | /warehouses/{wid}/locations/batch | wms:location:add |
| 库位管理 | 空闲库位查询 | GET | /warehouses/{wid}/locations/available | wms:location:list |
| 库存查询 | SKU库存列表 | GET | /inventory | wms:inventory:list |
| 库存查询 | SKU库存详情(多仓) | GET | /inventory/sku/{skuId} | wms:inventory:list |
| 库存查询 | 库存预警列表 | GET | /inventory/warnings | wms:inventory:list |
| 库存查询 | 库存流水 | GET | /inventory/logs | wms:inventory:list |
| 库存查询 | 历史库存快照 | GET | /inventory/snapshot | wms:inventory:list |
| 库存操作 | 人工调整库存 | POST | /inventory/adjust | wms:inventory:adjust |
| 入库管理 | 入库单列表 | GET | /inbound | wms:inbound:list |
| 入库管理 | 创建入库单 | POST | /inbound | wms:inbound:add |
| 入库管理 | 确认入库 | PUT | /inbound/{id}/confirm | wms:inbound:confirm |
| 出库管理 | 出库单列表 | GET | /outbound | wms:outbound:list |
| 出库管理 | 创建出库单 | POST | /outbound | wms:outbound:add |
| 出库管理 | 拣货单获取 | GET | /outbound/{id}/picklist | wms:outbound:pick |
| 出库管理 | 更新拣货进度 | PUT | /outbound/{id}/pick | wms:outbound:pick |
| 出库管理 | 确认出库 | PUT | /outbound/{id}/confirm | wms:outbound:confirm |
| 调拨管理 | 调拨单列表 | GET | /transfers | wms:transfer:list |
| 调拨管理 | 创建调拨单 | POST | /transfers | wms:transfer:add |
| 调拨管理 | 审核调拨单 | PUT | /transfers/{id}/approve | wms:transfer:approve |
| 调拨管理 | 确认发货 | PUT | /transfers/{id}/ship | wms:transfer:ship |
| 调拨管理 | 确认到货 | PUT | /transfers/{id}/receive | wms:transfer:receive |
| 盘点管理 | 盘点任务列表 | GET | /stocktake | wms:stocktake:list |
| 盘点管理 | 创建盘点任务 | POST | /stocktake | wms:stocktake:add |
| 盘点管理 | 提交实盘数据 | PUT | /stocktake/{id}/count | wms:stocktake:count |
| 盘点管理 | 审核调整差异 | PUT | /stocktake/{id}/audit | wms:stocktake:audit |
| 报表看板 | 库存总览 | GET | /report/overview | wms:report:view |
| 报表看板 | 库存健康度报告 | GET | /report/health | wms:report:view |
| 报表看板 | 出入库趋势 | GET | /report/trend | wms:report:view |
11.2 关键接口请求/响应示例
① 确认入库接口(含加权平均成本更新):
PUT /api/wms/inbound/1748291234567892/confirm
Content-Type: application/json
{
"inboundId": "1748291234567892",
"actualDate": "2025-01-17",
"items": [
{
"inboundItemId": "1748291234567901",
"skuId": "1000001",
"actualQty": 295,
"defectiveQty": 5,
"locationId": "2000001",
"locationCode": "A-01-03-02",
"unitCost": 38.00,
"remark": "5件外观划痕,已放X区"
}
]
}
响应:
{
"code": 200,
"msg": "入库确认成功",
"data": {
"inboundNo": "IN-20250117-0001",
"totalConfirmedQty": 295,
"totalDefectiveQty": 5,
"inventoryChanges": [
{
"skuId": "1000001",
"skuName": "蓝牙耳机Pro-黑色",
"warehouseId": "4001",
"beforeQty": 80,
"addQty": 295,
"afterQty": 375,
"newAvgCost": 37.21,
"locationCode": "A-01-03-02"
}
]
}
}
② 库存预警列表接口:
GET /api/wms/inventory/warnings?warehouseId=4001&level=2
响应:
{
"code": 200,
"data": {
"total": 3,
"records": [
{
"skuId": "1000001",
"skuCode": "SKU-EARPHONE-B",
"skuName": "蓝牙耳机Pro-黑色",
"warehouseId": "4001",
"warehouseName": "广州总仓",
"quantity": 15,
"frozenQty": 3,
"defectiveQty": 0,
"availableQty": 12,
"safetyStock": 50,
"inTransitQty": 0,
"warningLevel": 3,
"warningLevelName": "库存不足",
"avgCost": 37.21,
"totalCostValue": 558.15,
"lastOutboundTime": "2025-01-17 10:20:00",
"dailyAvgSale": 18.5,
"estimatedDaysLeft": 0.6
}
]
}
}
11.3 WMS 模块专用错误码
错误码范围:13000 - 13099
| 错误码 | 含义 | 触发场景 |
|---|---|---|
| 13001 | 仓库不存在 | 根据ID查不到 |
| 13002 | 库位不存在或已停用 | 上架时指定不存在库位 |
| 13003 | 库位已被锁定(盘点中) | 盘点期间操作被锁定库位 |
| 13004 | 库存不足,无法出库 | 可售库存 < 出库需求数量 |
| 13005 | 库存已被冻结,不可操作 | 操作冻结中的库存 |
| 13006 | 入库数量超过计划数量 | 实收超过采购单数量 |
| 13007 | 仓库正在盘点中,禁止出入库 | 全盘期间尝试出入库 |
| 13008 | 出库单已完成,不可重复操作 | 重复确认已完成的出库单 |
| 13009 | 调拨来源仓库库存不足 | 调拨数量超过可用库存 |
| 13010 | 盘点任务不存在 | 根据ID查不到任务 |
| 13011 | 该SKU未被纳入本次盘点 | 提交不在盘点范围的SKU |
| 13012 | 库存流水不允许修改或删除 | 尝试更新流水表 |
| 13013 | 实盘数量不能为负数 | 盘点数量输入错误 |
今日总结与作业
今日知识点回顾
业务层面:
- 跨境仓储的 5 种仓库类型及各自特点(国内备货/FBA/海外自营/3PL/保税仓)
- 库位编码体系:
{仓库}-{区域}-{排}-{列}-{层}格式,库位状态管理 - 库存五种类型:实物/冻结/在途/不良品/预留,以及各自的增减时机
- 可售库存公式:
= quantity - frozen_qty - defective_qty - reserved_qty - 加权平均成本法:每次入库动态更新成本均价
- 先进先出(FIFO)出库策略:按入库时间优先出库
- 拣货路径优化:按区域→排→列→层排序,减少行走距离
- 三种盘点模式:全盘/抽盘/循环盘,以及各自的适用场景
- ABC 分类盘点频率:A类每月/B类每季/C类每半年
- 调拨的在途库存管理:发出时减少来源仓,同时增加目标仓的在途数量
技术层面:
- 11 张数据库表设计(仓库/库位/库存/流水/入库单/出库单/盘点/调拨共 4 类)
- 库存流水不可篡改的三层保护:应用层+数据库账号权限+触发器
- 通过流水表还原历史库存快照的 SQL 写法
- 入库/出库/盘点调整的事务设计(任意一步失败全回滚)
- 库存更新并发安全:
quantity = quantity + N原子操作 - 盘点任务锁定库位:防止盘点期间数据被干扰
- 32 个完整接口设计(含权限标识)
- 13 个专用错误码(13000-13099)
今日作业
作业 1:数据库执行(必做)
- 执行本课所有建表 SQL(11 张表)
- 插入测试数据:
-- 插入测试仓库
INSERT INTO `warehouse` (
`id`, `tenant_id`, `warehouse_code`, `warehouse_name`, `warehouse_type`,
`country_code`, `country_name`, `province`, `city`, `address`,
`contact_name`, `contact_phone`, `is_default`, `status`
) VALUES
(4001, 101, 'CN-GZ-01', '广州总仓', 1, 'CN', '中国', '广东省', '广州市',
'天河区科技园XX路1号', '仓库负责人李主任', '13900001111', 1, 1),
(4002, 101, 'US-LA-01', '洛杉矶FBA仓', 2, 'US', '美国', 'California',
'Los Angeles', '123 Commerce Blvd, Los Angeles, CA 90001',
'Mike Johnson', '+1-213-555-0001', 0, 1);
-- 插入测试库位(广州总仓 A 区部分库位)
INSERT INTO `warehouse_location` (
`id`, `tenant_id`, `warehouse_id`, `location_code`,
`zone`, `row_no`, `column_no`, `floor_no`, `location_type`, `status`
) VALUES
(2001, 101, 4001, 'A-01-01-01', 'A', 1, 1, 1, 1, 1),
(2002, 101, 4001, 'A-01-01-02', 'A', 1, 1, 2, 1, 1),
(2003, 101, 4001, 'A-01-02-01', 'A', 1, 2, 1, 1, 1),
(2004, 101, 4001, 'A-01-03-02', 'A', 1, 3, 2, 1, 0), -- 停用状态
(2005, 101, 4001, 'R-01-01-01', 'R', 1, 1, 1, 3, 1), -- 退货暂存位
(2006, 101, 4001, 'X-01-01-01', 'X', 1, 1, 1, 4, 1); -- 不良品隔离区
-- 插入测试库存(已有一些库存)
INSERT INTO `inventory` (
`id`, `tenant_id`, `warehouse_id`, `location_id`, `sku_id`, `sku_code`,
`quantity`, `frozen_qty`, `in_transit_qty`, `defective_qty`,
`safety_stock`, `max_stock`, `avg_cost`, `total_cost`
) VALUES
-- 广州总仓,仓库级汇总库存(location_id = NULL)
(5001, 101, 4001, NULL, 1000001, 'SKU-EARPHONE-B', 375, 20, 0, 5, 50, 500, 37.21, 13953.75),
(5002, 101, 4001, NULL, 1000002, 'SKU-EARPHONE-W', 245, 10, 0, 0, 30, 400, 37.21, 9116.45),
-- 库位级库存(A-01-01-01)
(5003, 101, 4001, 2001, 1000001, 'SKU-EARPHONE-B', 200, 0, 0, 0, 0, NULL, 37.21, 7442.00),
-- 库位级库存(A-01-02-01)
(5004, 101, 4001, 2003, 1000001, 'SKU-EARPHONE-B', 175, 0, 0, 0, 0, NULL, 37.21, 6511.75);
-- 插入库存流水(模拟历史操作)
INSERT INTO `inventory_log` (
`id`, `tenant_id`, `log_type`, `warehouse_id`, `location_id`,
`sku_id`, `sku_code`, `change_qty`, `before_qty`, `after_qty`,
`ref_type`, `ref_no`, `ref_id`,
`operator_id`, `operator_name`, `operate_time`
) VALUES
-- 采购入库记录
(6001, 101, 1, 4001, 2001, 1000001, 'SKU-EARPHONE-B',
200, 0, 200, 'PURCHASE_ORDER', 'PO-20250117-0001', 3001,
501, '李仓管', '2025-01-24 09:30:00'),
-- 销售出库记录
(6002, 101, 2, 4001, 2001, 1000001, 'SKU-EARPHONE-B',
-25, 200, 175, 'SALES_ORDER', 'ORD-20250124-0001', 7001,
501, '李仓管', '2025-01-24 14:20:00'),
-- 采购入库(第二批到货)
(6003, 101, 1, 4001, 2003, 1000001, 'SKU-EARPHONE-B',
175, 0, 175, 'PURCHASE_ORDER', 'PO-20250117-0001', 3001,
501, '李仓管', '2025-01-24 15:00:00');
-- 验证查询1:查看所有库存及预警状态
SELECT
i.sku_code,
w.warehouse_name,
i.quantity,
i.frozen_qty,
i.defective_qty,
i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty AS available_qty,
i.safety_stock,
CASE
WHEN i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty = 0
THEN '⚫ 零库存'
WHEN i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty <= i.safety_stock * 0.3
THEN '🔴 严重不足'
WHEN i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty <= i.safety_stock
THEN '🟡 库存紧张'
ELSE '🟢 正常'
END AS warning_status
FROM inventory i
JOIN warehouse w ON i.warehouse_id = w.id
WHERE i.tenant_id = 101 AND i.location_id IS NULL AND i.is_deleted = 0;
-- 验证查询2:查看库存流水,理解库存变化轨迹
SELECT
log_type,
CASE log_type
WHEN 1 THEN '采购入库'
WHEN 2 THEN '销售出库'
ELSE '其他'
END AS log_type_name,
change_qty,
before_qty,
after_qty,
ref_no,
operate_time
FROM inventory_log
WHERE tenant_id = 101 AND sku_id = 1000001
ORDER BY operate_time ASC;
作业 2:业务分析题(必做)
回答以下问题:
-
库存类型理解:一个商品从采购下单→供应商发货→到达仓库→被客户订单→打包出库,请描述在这整个过程中,
quantity、frozen_qty、in_transit_qty三个字段分别在哪些时间点发生变化?每次变化是+还是-? -
先进先出的价值:假设仓库中有一批有保质期的商品(如化妆品),如果不执行先进先出(FIFO)策略,会带来什么业务风险?系统如何保证先进先出?
-
流水不可篡改:如果发现一条库存流水记录的数据写错了(比如把 +50 写成了 +500),应该怎么处理?(不能 UPDATE,不能 DELETE,应该怎么做?)提示:用一条新的流水记录来纠正。
-
盘点锁定影响:当一个仓库开始全盘时,相关库位被锁定,此时如果有订单需要从这个仓库发货,系统应该怎么处理?有哪几种方案?
参考答案:
-
采购下单时,商品还没有进入仓库,
quantity不变;如果系统记录在途库存,供应商确认发货后in_transit_qty增加。货物到达仓库并质检合格入库后,quantity增加,in_transit_qty减少。客户订单创建并占用库存时,quantity不变,frozen_qty增加。订单打包出库时,quantity减少,frozen_qty减少,同时写出库流水。如果订单取消且未出库,则释放冻结库存,frozen_qty减少,quantity不变。 -
如果不执行 FIFO,有保质期的商品可能出现新批次先出、老批次长期积压,最后导致过期报废、客户收到临期商品、平台差评和赔付。系统可以通过批次入库时间、生产日期、有效期维护批次库存,拣货时优先选择入库时间早、有效期更近的批次和库位,并在出库单明细中锁定具体批次,防止仓库人员随意拣货。
-
库存流水不能直接 UPDATE 或 DELETE。发现 +50 写成 +500 时,应新增一条反向调整流水,例如
change_qty = -450,流水类型为“差错冲正”或“库存调整”,引用原错误流水 ID,备注写明原因。这样既能把库存修正回来,又保留完整审计轨迹,后续可以看到错误发生和纠正过程。 -
全盘时库位被锁定,最严格的方案是暂停该仓库出库,等盘点结束后再发货;更灵活的方案是只锁定正在盘点的库位,订单可以从未锁定库位拣货;如果是紧急订单,可以由仓库主管审批后临时解锁指定库位并记录操作日志;跨仓有库存时,也可以自动切换到其他仓库发货。真实系统通常会按业务优先级组合使用,而不是简单全部停止。
作业 3:SQL 练习(必做)
-- 练习1:查询广州总仓(warehouse_id=4001)中
-- 所有SKU的库存状态,按可售库存升序排列
-- 要求显示:sku_code、实物库存、冻结库存、不良品、可售库存、安全库存、预警状态
-- 你的答案:
SELECT
sku_code,
quantity AS physical_qty,
frozen_qty,
defective_qty,
quantity - frozen_qty - defective_qty - reserved_qty AS available_qty,
safety_stock,
CASE
WHEN quantity - frozen_qty - defective_qty - reserved_qty = 0 THEN '零库存'
WHEN quantity - frozen_qty - defective_qty - reserved_qty <= safety_stock * 0.3 THEN '严重不足'
WHEN quantity - frozen_qty - defective_qty - reserved_qty <= safety_stock THEN '库存紧张'
ELSE '正常'
END AS warning_status
FROM inventory
WHERE tenant_id = 101
AND warehouse_id = 4001
AND location_id IS NULL
AND is_deleted = 0
ORDER BY available_qty ASC;
-- 练习2:查询 SKU-EARPHONE-B 在 2025年1月 的出入库汇总
-- 要求分别统计:入库总量(log_type=1)和出库总量(log_type=2)
-- 以及操作次数
-- 你的答案:
SELECT
sku_code,
SUM(CASE WHEN log_type = 1 THEN change_qty ELSE 0 END) AS inbound_qty,
SUM(CASE WHEN log_type = 2 THEN ABS(change_qty) ELSE 0 END) AS outbound_qty,
COUNT(*) AS operate_count
FROM inventory_log
WHERE tenant_id = 101
AND sku_code = 'SKU-EARPHONE-B'
AND operate_time >= '2025-01-01'
AND operate_time < '2025-02-01'
GROUP BY sku_code;
-- 练习3:查找所有空闲库位(is_occupied=0 且 status=1)
-- 按区域和排号排序,限制返回广州总仓的A区库位
-- 你的答案:
SELECT
id,
location_code,
zone,
row_no,
column_no,
floor_no,
location_type
FROM warehouse_location
WHERE tenant_id = 101
AND warehouse_id = 4001
AND zone = 'A'
AND is_occupied = 0
AND status = 1
AND is_deleted = 0
ORDER BY zone, row_no, column_no, floor_no;
-- 练习4:统计每个仓库的库存价值(quantity × avg_cost),
-- 按库存价值降序排列,显示仓库名称和总价值
-- 你的答案:
SELECT
w.warehouse_name,
SUM(i.quantity * i.avg_cost) AS total_inventory_value
FROM inventory i
JOIN warehouse w ON i.warehouse_id = w.id
WHERE i.tenant_id = 101
AND w.tenant_id = 101
AND i.location_id IS NULL
AND i.is_deleted = 0
GROUP BY w.id, w.warehouse_name
ORDER BY total_inventory_value DESC;
作业 4:设计题(选做)
新需求:支持商品批次管理。某些商品(如化妆品、食品补充剂)有生产批次和有效期,同一 SKU 的不同批次需要分开管理,过期批次需要自动标记并停止出库。
请设计:
- 需要新增哪些字段或表来支持批次管理?(给出 SQL 设计)
- 批次出库时如何修改现有的 FIFO 策略(在先进先出的基础上,还需要考虑哪些优先级规则?)
- 批次过期自动处理的定时任务设计(什么时间检查?如何处理过期批次的库存?)
参考答案:
- 建议新增批次库存表
inventory_batch,用于记录同一 SKU 在不同批次、不同库位下的库存。核心字段包括批次号、生产日期、有效期、批次数量、冻结数量、状态。
CREATE TABLE `inventory_batch` (
`id` BIGINT PRIMARY KEY COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`warehouse_id` BIGINT NOT NULL COMMENT '仓库ID',
`location_id` BIGINT DEFAULT NULL COMMENT '库位ID',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码',
`batch_no` VARCHAR(64) NOT NULL COMMENT '批次号',
`production_date` DATE DEFAULT NULL COMMENT '生产日期',
`expire_date` DATE DEFAULT NULL COMMENT '有效期',
`quantity` INT NOT NULL DEFAULT 0 COMMENT '批次库存',
`frozen_qty` INT NOT NULL DEFAULT 0 COMMENT '冻结数量',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1正常 2临期 3过期 4停用',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
UNIQUE KEY `uk_batch_location` (`tenant_id`, `warehouse_id`, `location_id`, `sku_id`, `batch_no`),
KEY `idx_batch_expire` (`tenant_id`, `sku_id`, `expire_date`, `status`)
) COMMENT='批次库存表';
-
批次出库不能只按入库时间 FIFO,还要加入 FEFO(First Expired First Out,先过期先出)策略。优先级可以设计为:状态正常优先、有效期更近优先、入库时间更早优先、拣货路径更短优先。临期商品是否允许出库,需要看平台和租户配置,比如距离过期 30 天内不允许发给普通客户。
-
批次过期任务可以每天凌晨执行,扫描
expire_date <= 当前日期的批次,自动标记为过期,禁止销售出库,并把可售库存从普通库存中扣减或转入不良品/报废库存。同时对未来 30 天即将过期的批次标记为临期,生成预警消息,提醒仓库和运营人员优先促销、调拨或下架。
明日预告
第五天:商品管理(PIM)+ 订单管理(OMS)
明天将完成两大模块,内容密度高:
- PIM(商品管理):SPU/SKU 两级体系、多语言内容、多平台价格、商品状态机
- OMS(订单管理):多平台订单统一接入、超卖防护(Redis 原子操作)、订单状态机(10 个状态)、售后退款处理
预习建议:
- 了解什么是 SPU 和 SKU 的关系(思考:一款手机有不同颜色和存储容量,SPU 是哪个,SKU 是哪个?)
- 了解 Redis 的基础命令,特别是
DECRBY(原子减法)和SETNX(分布式锁) - 思考:多平台同一时间涌入大量订单,如何保证库存不超卖?
学习提示:今天的课程中,库存流水设计和入库/出库的事务原子性是最重要的两个知识点,也是 WMS 系统区别于简单库存表的核心所在。
面试中关于 WMS 的经典问题:
- “你们系统如何保证库存数据的准确性?“(答:流水记录 + 事务保证)
- “怎么解决并发出库时的超卖问题?“(答:
quantity = quantity - N原子操作 + 乐观锁)- “如果系统库存和实物库存不一致,怎么排查?“(答:通过流水还原历史快照,对比每个时间点的变化)
这三个问题,今天的课件都有完整的答案,务必能清晰地讲出来。