Series Article

Day04 · 仓储管理系统(WMS)

配套面试准备:完成本篇后,可以继续阅读: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 - Nfrozen_qty - N(解冻)
调拨出库调拨单审核通过内部转移,计划性强quantity - Nin_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 退货入库quantitydefective_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/warehouseswms:warehouse:list
仓库管理仓库详情GET/warehouses/{id}wms:warehouse:list
仓库管理新增仓库POST/warehouseswms:warehouse:add
仓库管理编辑仓库PUT/warehouses/{id}wms:warehouse:edit
库位管理库位列表GET/warehouses/{wid}/locationswms:location:list
库位管理新增库位POST/warehouses/{wid}/locationswms:location:add
库位管理批量新增库位POST/warehouses/{wid}/locations/batchwms:location:add
库位管理空闲库位查询GET/warehouses/{wid}/locations/availablewms:location:list
库存查询SKU库存列表GET/inventorywms:inventory:list
库存查询SKU库存详情(多仓)GET/inventory/sku/{skuId}wms:inventory:list
库存查询库存预警列表GET/inventory/warningswms:inventory:list
库存查询库存流水GET/inventory/logswms:inventory:list
库存查询历史库存快照GET/inventory/snapshotwms:inventory:list
库存操作人工调整库存POST/inventory/adjustwms:inventory:adjust
入库管理入库单列表GET/inboundwms:inbound:list
入库管理创建入库单POST/inboundwms:inbound:add
入库管理确认入库PUT/inbound/{id}/confirmwms:inbound:confirm
出库管理出库单列表GET/outboundwms:outbound:list
出库管理创建出库单POST/outboundwms:outbound:add
出库管理拣货单获取GET/outbound/{id}/picklistwms:outbound:pick
出库管理更新拣货进度PUT/outbound/{id}/pickwms:outbound:pick
出库管理确认出库PUT/outbound/{id}/confirmwms:outbound:confirm
调拨管理调拨单列表GET/transferswms:transfer:list
调拨管理创建调拨单POST/transferswms:transfer:add
调拨管理审核调拨单PUT/transfers/{id}/approvewms:transfer:approve
调拨管理确认发货PUT/transfers/{id}/shipwms:transfer:ship
调拨管理确认到货PUT/transfers/{id}/receivewms:transfer:receive
盘点管理盘点任务列表GET/stocktakewms:stocktake:list
盘点管理创建盘点任务POST/stocktakewms:stocktake:add
盘点管理提交实盘数据PUT/stocktake/{id}/countwms:stocktake:count
盘点管理审核调整差异PUT/stocktake/{id}/auditwms:stocktake:audit
报表看板库存总览GET/report/overviewwms:report:view
报表看板库存健康度报告GET/report/healthwms:report:view
报表看板出入库趋势GET/report/trendwms: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:数据库执行(必做)

  1. 执行本课所有建表 SQL(11 张表)
  2. 插入测试数据:
-- 插入测试仓库
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:业务分析题(必做)

回答以下问题:

  1. 库存类型理解:一个商品从采购下单→供应商发货→到达仓库→被客户订单→打包出库,请描述在这整个过程中,quantityfrozen_qtyin_transit_qty 三个字段分别在哪些时间点发生变化?每次变化是+还是-?

  2. 先进先出的价值:假设仓库中有一批有保质期的商品(如化妆品),如果不执行先进先出(FIFO)策略,会带来什么业务风险?系统如何保证先进先出?

  3. 流水不可篡改:如果发现一条库存流水记录的数据写错了(比如把 +50 写成了 +500),应该怎么处理?(不能 UPDATE,不能 DELETE,应该怎么做?)提示:用一条新的流水记录来纠正。

  4. 盘点锁定影响:当一个仓库开始全盘时,相关库位被锁定,此时如果有订单需要从这个仓库发货,系统应该怎么处理?有哪几种方案?

参考答案:

  1. 采购下单时,商品还没有进入仓库,quantity 不变;如果系统记录在途库存,供应商确认发货后 in_transit_qty 增加。货物到达仓库并质检合格入库后,quantity 增加,in_transit_qty 减少。客户订单创建并占用库存时,quantity 不变,frozen_qty 增加。订单打包出库时,quantity 减少,frozen_qty 减少,同时写出库流水。如果订单取消且未出库,则释放冻结库存,frozen_qty 减少,quantity 不变。

  2. 如果不执行 FIFO,有保质期的商品可能出现新批次先出、老批次长期积压,最后导致过期报废、客户收到临期商品、平台差评和赔付。系统可以通过批次入库时间、生产日期、有效期维护批次库存,拣货时优先选择入库时间早、有效期更近的批次和库位,并在出库单明细中锁定具体批次,防止仓库人员随意拣货。

  3. 库存流水不能直接 UPDATE 或 DELETE。发现 +50 写成 +500 时,应新增一条反向调整流水,例如 change_qty = -450,流水类型为“差错冲正”或“库存调整”,引用原错误流水 ID,备注写明原因。这样既能把库存修正回来,又保留完整审计轨迹,后续可以看到错误发生和纠正过程。

  4. 全盘时库位被锁定,最严格的方案是暂停该仓库出库,等盘点结束后再发货;更灵活的方案是只锁定正在盘点的库位,订单可以从未锁定库位拣货;如果是紧急订单,可以由仓库主管审批后临时解锁指定库位并记录操作日志;跨仓有库存时,也可以自动切换到其他仓库发货。真实系统通常会按业务优先级组合使用,而不是简单全部停止。

作业 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 的不同批次需要分开管理,过期批次需要自动标记并停止出库。

请设计:

  1. 需要新增哪些字段或表来支持批次管理?(给出 SQL 设计)
  2. 批次出库时如何修改现有的 FIFO 策略(在先进先出的基础上,还需要考虑哪些优先级规则?)
  3. 批次过期自动处理的定时任务设计(什么时间检查?如何处理过期批次的库存?)

参考答案:

  1. 建议新增批次库存表 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='批次库存表';
  1. 批次出库不能只按入库时间 FIFO,还要加入 FEFO(First Expired First Out,先过期先出)策略。优先级可以设计为:状态正常优先、有效期更近优先、入库时间更早优先、拣货路径更短优先。临期商品是否允许出库,需要看平台和租户配置,比如距离过期 30 天内不允许发给普通客户。

  2. 批次过期任务可以每天凌晨执行,扫描 expire_date <= 当前日期 的批次,自动标记为过期,禁止销售出库,并把可售库存从普通库存中扣减或转入不良品/报废库存。同时对未来 30 天即将过期的批次标记为临期,生成预警消息,提醒仓库和运营人员优先促销、调拨或下架。


明日预告

第五天:商品管理(PIM)+ 订单管理(OMS)

明天将完成两大模块,内容密度高:

  • PIM(商品管理):SPU/SKU 两级体系、多语言内容、多平台价格、商品状态机
  • OMS(订单管理):多平台订单统一接入、超卖防护(Redis 原子操作)、订单状态机(10 个状态)、售后退款处理

预习建议:

  • 了解什么是 SPU 和 SKU 的关系(思考:一款手机有不同颜色和存储容量,SPU 是哪个,SKU 是哪个?)
  • 了解 Redis 的基础命令,特别是 DECRBY(原子减法)和 SETNX(分布式锁)
  • 思考:多平台同一时间涌入大量订单,如何保证库存不超卖?

学习提示:今天的课程中,库存流水设计入库/出库的事务原子性是最重要的两个知识点,也是 WMS 系统区别于简单库存表的核心所在。

面试中关于 WMS 的经典问题:

  • “你们系统如何保证库存数据的准确性?“(答:流水记录 + 事务保证)
  • “怎么解决并发出库时的超卖问题?“(答:quantity = quantity - N 原子操作 + 乐观锁)
  • “如果系统库存和实物库存不一致,怎么排查?“(答:通过流水还原历史快照,对比每个时间点的变化)

这三个问题,今天的课件都有完整的答案,务必能清晰地讲出来。