4. 库存流水记录:入库流水、出库流水、对冲流水

库存流水,说白了就是库存的「日记账」。

每一件货品从进来到出去,再到被对冲掉,都得留下痕迹。我见过太多系统,库存对不上账,最后查来查去,发现是流水记录没做好。嗯,今天我们就来聊聊这个核心话题。

4.1 三种流水:入库、出库、对冲

库存流水其实就三种类型。我个人习惯把它们叫做「进、出、冲」。

  • 入库流水:采购入库、生产入库、退货入库、盘盈入库等。
  • 出库流水:销售出库、领料出库、报废出库、盘亏出库等。
  • 对冲流水:库存调整、移库、库位变更、成本重算等。

你可能会问:「对冲流水是什么鬼?」

举个例子。我在项目中遇到过,两个仓库之间调拨货物。A库出库,B库入库。如果只记录两条流水,那库存总量是对的,但中间过程很难追溯。这时候就需要一条「对冲流水」来记录这个调拨动作,把A库的减少和B库的增加关联起来。

核心原则:每一条流水,都必须能追溯到业务单据。没有单据的流水,就是脏数据。

4.2 流水表结构设计

流水表的设计,我踩过不少坑。最开始我设计得特别简单,就几个字段:货品ID、数量、时间。结果后来要查「谁操作的」「哪个订单」「什么原因」,全得去翻业务表,性能差得要命。

后来我总结了一套比较通用的结构,分享给你:

CREATE TABLE inventory_ledger (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '流水ID',
    tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
    sku_id VARCHAR(64) NOT NULL COMMENT '货品SKU',
    warehouse_id VARCHAR(32) NOT NULL COMMENT '仓库ID',
    location_id VARCHAR(32) DEFAULT NULL COMMENT '库位ID',
    
    -- 流水类型
    flow_type TINYINT NOT NULL COMMENT '1=入库 2=出库 3=对冲',
    biz_type VARCHAR(32) NOT NULL COMMENT '业务类型: purchase/sale/transfer/adjust',
    
    -- 数量与金额
    quantity DECIMAL(18,4) NOT NULL COMMENT '变动数量(入库为正,出库为负)',
    before_qty DECIMAL(18,4) NOT NULL COMMENT '变动前库存',
    after_qty DECIMAL(18,4) NOT NULL COMMENT '变动后库存',
    unit_cost DECIMAL(18,4) DEFAULT NULL COMMENT '单价成本',
    total_amount DECIMAL(18,4) DEFAULT NULL COMMENT '总金额',
    
    -- 关联单据
    biz_order_no VARCHAR(64) DEFAULT NULL COMMENT '业务单号',
    ref_ledger_id BIGINT DEFAULT NULL COMMENT '关联流水ID(对冲用)',
    
    -- 操作信息
    operator VARCHAR(64) NOT NULL COMMENT '操作人',
    operate_time DATETIME NOT NULL COMMENT '操作时间',
    remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
    
    -- 索引与分区
    INDEX idx_sku_warehouse (sku_id, warehouse_id),
    INDEX idx_biz_order (biz_order_no),
    INDEX idx_operate_time (operate_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';

我的经验before_qtyafter_qty 这两个字段,很多人觉得冗余。但我在排查数据问题时,它们帮了大忙。直接能看到变动前后的库存,不用再去算一遍。

4.3 流水查询优化

流水表的数据量增长很快。我见过一个中型电商,一天就能产生几十万条流水。如果不做优化,查询会越来越慢。

这里分享几个我常用的优化手段:

4.3.1 按时间分区

流水表天然适合按时间分区。我建议按月分区,或者按周分区。这样查询某段时间的流水,MySQL 会自动只扫描对应的分区。

ALTER TABLE inventory_ledger
PARTITION BY RANGE (TO_DAYS(operate_time)) (
    PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
    PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
    PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

4.3.2 覆盖索引

查询流水时,最常见的场景是:查某个货品在某段时间的流水。所以索引要覆盖这个查询模式。

-- 推荐索引
CREATE INDEX idx_sku_time ON inventory_ledger(sku_id, operate_time, flow_type);

为什么要带上 flow_type?因为很多时候我们只想查入库流水,或者出库流水。带上这个字段,索引就能直接过滤,不用回表。

4.3.3 归档历史数据

超过一年的流水,查询频率会急剧下降。我建议把它们归档到历史表,或者迁移到 ClickHouse 这类分析型数据库。

注意:归档前一定要确认,业务上不再需要实时查询这些数据。我曾经因为归档太早,导致财务对账查不到半年前的流水,被骂得很惨。

4.4 知识体系图

下面这张图,帮你理清流水设计的核心逻辑:

库存流水核心设计 入库流水 出库流水 对冲流水 采购入库 生产入库 退货入库 销售出库 领料出库 报废出库 库存调整 库位转移 成本重算 核心字段:SKU | 仓库 | 库位 | 数量 | 成本 | 业务单号 | 操作人 | 时间 before_qty / after_qty 记录变动前后快照 按月分区 覆盖索引 历史归档

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 不要用浮点数存金额:用 DECIMAL,否则对账时你会哭。
  • 流水一旦写入,不允许修改:如果业务错了,再写一条对冲流水来修正。直接改流水,审计会找你麻烦。
  • 操作时间用数据库时间:不要用应用服务器时间。分布式环境下,各服务器时间可能不一致,导致流水顺序错乱。

总结一下:流水设计,核心是「可追溯、不可篡改、查询快」。做到这三点,你的库存系统就稳了。


专注资料整理