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_qty 和 after_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 知识体系图
下面这张图,帮你理清流水设计的核心逻辑:
4.5 避坑指南
最后,分享几个我踩过的坑:
- 不要用浮点数存金额:用 DECIMAL,否则对账时你会哭。
- 流水一旦写入,不允许修改:如果业务错了,再写一条对冲流水来修正。直接改流水,审计会找你麻烦。
- 操作时间用数据库时间:不要用应用服务器时间。分布式环境下,各服务器时间可能不一致,导致流水顺序错乱。
总结一下:流水设计,核心是「可追溯、不可篡改、查询快」。做到这三点,你的库存系统就稳了。