电商进销存系统设计:从数据建模到库存模型与状态机的完整实践

📅 2026/8/27 1:32:02
电商进销存系统设计:从数据建模到库存模型与状态机的完整实践
在实际 B 端电商项目中进销存系统从来不是一个“能加减库存”那么简单的东西。采购、销售、退货、调拨、盘点、对账、报表之间隐藏着大量业务规则和数据约束。很多团队一开始只做了一张库存表、三个 CRUD 接口结果业务跑半年后开始出现库存负数、单据重复、账实不符、取消订单后库存不回来等问题。这篇文章不谈花哨的前端效果而是围绕电商进销存系统的数据建模、库存模型、单据状态机、订单库存联动、对账报表和扩展性讲清楚一套能支撑真实业务、方便排查问题、可以逐步迭代的 B 端后台设计思路。读完后你可以直接拿其中的表结构、事务边界、状态机设计和排错清单去指导自己的项目也可以用于评审现有系统时检查设计漏洞。1. 先想清楚什么样的进销存系统算“高级”1.1 高级不等于功能多而是业务建模能力不少产品经理讲“高级进销存”时第一反应是页面要好看、报表要炫、功能要全。但对研发来说进销存系统的高级感来自另一件事当业务规则变化时系统能不能在不推倒重来的前提下接住变化。比如一个最简单的规则变化销售订单从“下单即扣库存”改成“付款后才扣库存”。如果库存扣减逻辑散落在订单 Service、购物车 Service、支付回调三个地方这个改动就非常危险。而如果库存变动收敛到库存中心的同一组方法并把扣减时机作为订单状态机的动作来编排改动范围就很可控。所以高级设计的核心是业务建模能力。判断标准包括库存的每一次变动都能追踪到来源单据。单据的状态流转有明确规则不会出现互相矛盾的状态。采购、销售、调拨复用同一套库存操作语义。报表数据可以通过流水表重新计算出来而不是依赖临时写死的数据。多仓库、多商品、多供应商扩展时不需要改表结构。1.2 设计前先定义四张主数据进销存系统的主数据主要围绕四类对象展开主数据说明常见设计问题商品与 SKU管理商品档案、规格、条码、单位把规格拼在商品名称里导致统计维度难以拆分往来单位供应商、客户、承运商等业务参与方用一个“客户表”同时装供应商和收货方字段语义混乱仓库与库位管理仓库、库区、库位和仓库类型不做库位直接按仓库汇总导致拣货和盘点困难库存账户管理不同库存状态下的数量只有一张库存表没有可用、锁定、在途的区分这里的建议是主数据设计要“窄而稳”字段宁少勿多。把商品扩展属性放到扩展表把仓库扩展信息放到仓库配置表不要把主表变成大杂烩。1.3 用核心链路图统一团队认知进销存所有功能都可以被一条核心链路串起来采购需求 - 采购订单 - 供应商发货 - 仓库收货验收 - 入库单 - 库存增加 - 销售订单 - 库存锁定 - 仓库出库 - 出库单 - 库存扣减 - 对账与报表这条链路里有两个核心动作库存增加和库存扣减。中间穿插的是各种单据状态草稿、待审核、审核中、部分收货、完成、取消。设计系统时先把这条链路画清楚再决定模块边界比一上来写接口要有效得多。2. 数据模型先做好后面才不用推倒重来2.1 商品和 SKU 的基础表结构商品是进销存系统中的核心主数据。电商环境下一个商品通常有多个 SKU所以商品表和 SKU 表要分开设计。CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_code VARCHAR(64) NOT NULL COMMENT 商品编码, product_name VARCHAR(255) NOT NULL COMMENT 商品名称, category_id BIGINT NOT NULL COMMENT 分类ID, brand VARCHAR(128) COMMENT 品牌, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态0停用 1启用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_product_code (product_code) ) COMMENT 商品主表; CREATE TABLE sku ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL COMMENT 商品ID, sku_code VARCHAR(64) NOT NULL COMMENT SKU编码, sku_name VARCHAR(255) NOT NULL COMMENT SKU名称, spec_json VARCHAR(1024) COMMENT 规格描述JSON如{颜色:红色,尺码:L}, barcode VARCHAR(128) COMMENT 条码, unit VARCHAR(32) NOT NULL DEFAULT 件 COMMENT 计量单位, weight DECIMAL(12,3) DEFAULT 0 COMMENT 重量kg, status TINYINT NOT NULL DEFAULT 1, UNIQUE KEY uk_sku_code (sku_code), KEY idx_product_id (product_id) ) COMMENT SKU表;这里要注意几个点。spec_json 用于保存规格描述不要用“红-L-”这种拼接字符串去表示规格统计时很难拆分。barcode 字段要加索引因为仓库扫码入库时按条码查 SKU 是高频操作。计量单位尽量在 SKU 层统一同一个商品下避免混用“件”和“箱”否则后续库存统计会出现数量单位不一致的问题。2.2 单据采用“头行结构”进销存里的采购订单、销售订单、入库单、出库单本质上都是“一张单 多行明细”。头行结构是行业里最稳定的建模方式。CREATE TABLE purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL COMMENT 采购单号, supplier_id BIGINT NOT NULL COMMENT 供应商ID, warehouse_id BIGINT NOT NULL COMMENT 收货仓库ID, order_status TINYINT NOT NULL DEFAULT 10 COMMENT 状态10草稿 20待审核 30已审核 40部分收货 50已完成 90已取消, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 总金额, expect_date DATE COMMENT 期望到货日期, remark VARCHAR(500), create_by BIGINT, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) COMMENT 采购订单头; CREATE TABLE purchase_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 采购单ID, sku_id BIGINT NOT NULL COMMENT SKU ID, quantity INT NOT NULL COMMENT 采购数量, received_quantity INT NOT NULL DEFAULT 0 COMMENT 累计收货数量, price DECIMAL(12,2) NOT NULL COMMENT 采购单价, amount DECIMAL(12,2) NOT NULL COMMENT 金额, KEY idx_order_id (order_id) ) COMMENT 采购订单明细;头表存公共信息单号、往来单位、仓库、总金额、状态、时间。行表存 SKU、数量、价格、已收数量。这里的关键设计是 received_quantity累计收货数量它让“部分收货”这个业务语义可以直接查询出来不需要临时汇总入库单来判断。同样的结构也可以套用到销售订单上只是把供应商换成客户把“收货数量”换成“发货数量”。2.3 单据编号与幂等性单据编号在业务里非常重要它不仅是展示给用户的单号也是排查问题和做对账时的关联键。常见生成规则是“前缀 日期 序列号”例如PO202506100001。不建议只用数据库自增 ID 作为业务单号因为自增 ID 会暴露业务量而且在导出、打印、手工录入场景中不好用。业务单号要独立生成并在表上加唯一索引。单据幂等性容易被忽略。采购入库时仓库人员可能因为网络超时重复提交入库单。如果系统不判重同一批货会被加两次库存。常见的兜底方式是在入库单表上建立“来源采购单 来源订单行 幂等批次号”的唯一索引重复请求直接命中唯一键冲突业务层捕获后返回已存在单据。2.4 参数配置表不要写死在代码里进销存系统里有很多业务参数比如“超过库存可用量的 120% 禁止销售”“低于安全库存自动生成采购建议”“退货超过 7 天不允许原路退回”等。这些参数建议放到配置表里而不是散落在代码常量中。CREATE TABLE biz_config ( id BIGINT PRIMARY KEY AUTO_INCREMENT, config_key VARCHAR(128) NOT NULL, config_value TEXT, biz_type VARCHAR(64) COMMENT 业务域如order、stock、purchase, description VARCHAR(255), update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_config_key (config_key) ) COMMENT 业务配置表;配置表注意两点一是要有 biz_type 字段方便按业务域加载二是配置变更后要能通知到本地内存缓存不要每次请求都查数据库。否则高并发场景下配置表会成为热点读。3. 库存模型是系统的心脏先把库存语义拆清楚3.1 不要把库存当成一个数字它是状态组合新手设计库存时最常见的做法是给 SKU 加一个stock_quantity字段下单就减取消就加。这种做法在单机小并发下能跑但一旦出现订单锁定、在途入库、退货待处理、残次品隔离一个数字根本表达不清楚。正确的做法是把库存拆成多个状态字段库存状态含义典型来源可用库存当前可以被销售或调拨的数量采购入库后增加锁定库存已被销售订单锁定但未出库下单或付款时占用在途库存已下单采购、尚未验收入库的数量采购订单审核后增加冻结库存质量异常、盘点差异等原因暂停销售人工冻结或质检不通过残次库存损坏、临期等不可正常销售但需要管理退货或质检转残次库存表的结构可以做成分组CREATE TABLE sku_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id BIGINT NOT NULL COMMENT SKU ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, available_qty INT NOT NULL DEFAULT 0 COMMENT 可用库存, locked_qty INT NOT NULL DEFAULT 0 COMMENT 锁定库存, in_transit_qty INT NOT NULL DEFAULT 0 COMMENT 在途库存, frozen_qty INT NOT NULL DEFAULT 0 COMMENT 冻结库存, defective_qty INT NOT NULL DEFAULT 0 COMMENT 残次库存, total_qty INT NOT NULL DEFAULT 0 COMMENT 总库存 所有状态之和, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku_warehouse (sku_id, warehouse_id) ) COMMENT SKU库存表;总库存字段不是必须的但如果报表页高频使用可以用冗余字段或宽表任务来算。使用唯一键(sku_id, warehouse_id)可以防止同一仓库同一 SKU 重复建库存记录。3.2 库存流水所有变动必须能追溯库存表保存的是结果流水表保存的是过程。任何库存变动无论是可用增加、锁定、扣减、回滚都必须写一条流水。没有流水的库存系统出问题时只能靠猜。CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, flow_type TINYINT NOT NULL COMMENT 流水类型1采购入库 2销售锁定 3销售扣减 4取消回滚 5退货入库 6调拨出库 7盘点调整 8冻结 9解冻, change_available INT NOT NULL DEFAULT 0 COMMENT 可用库存变化量正加负减, change_locked INT NOT NULL DEFAULT 0 COMMENT 锁定库存变化量, before_qty INT NOT NULL COMMENT 变动前可用库存, after_qty INT NOT NULL COMMENT 变动后可用库存, biz_type VARCHAR(32) NOT NULL COMMENT 来源业务类型PURCHASE/ORDER/REFUND/TRANSFER/STOCKTAKE, biz_order_no VARCHAR(64) NOT NULL COMMENT 来源单据号, remark VARCHAR(255), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_sku_warehouse_time (sku_id, warehouse_id, create_time), KEY idx_biz_order_no (biz_order_no) ) COMMENT 库存流水表;流水表设计的关键是 before_qty 和 after_qty。很多团队只记 change_qty出问题时无法判断当时库存快照。有了变动前和变动后数量配合 create_time可以在任意时间点重建库存历史。流水表会越来越大建议按月份做分区或定期归档到历史库。3.3 库存扣减的原子性库存扣减不能先查询再计算因为并发下会超卖。正确的做法是利用数据库行锁把“判断库存足够 扣减”合并成一条 SQL。UPDATE sku_stock SET available_qty available_qty - #{quantity}, version version 1 WHERE sku_id #{skuId} AND warehouse_id #{warehouseId} AND available_qty #{quantity};执行后判断影响行数。如果影响行数为 0说明库存不足或记录不存在业务层直接抛异常。这是最朴素也最可靠的防超卖手段。在高并发场景下还可以引入 Redis 预扣减但要注意 Redis 和数据库的一致性。推荐的做法是将 Redis 作为前置校验和热点拦截但最终以数据库扣减结果为准扣减失败时需要回补 Redis 中的预扣数量。不要为了性能丢掉数据库这层唯一权威。Transactional(rollbackFor Exception.class) public void deductStock(Long skuId, Long warehouseId, int quantity, String orderNo) { int count stockMapper.deductAvailable(skuId, warehouseId, quantity); if (count 0) { throw new BizException(库存不足或SKU不存在); } StockFlow flow new StockFlow(); flow.setSkuId(skuId); flow.setWarehouseId(warehouseId); flow.setFlowType(3); flow.setChangeAvailable(-quantity); flow.setBizType(ORDER); flow.setBizOrderNo(orderNo); stockFlowMapper.insert(flow); }这里的事务边界很关键扣减库存和写库存流水必须在同一个数据库事务里。如果扣减成功了流水没写进去后续对账会找不到这张单子的库存变动依据。3.4 库存的三种核心操作进销存系统里所有库存动作都可以抽象为三种操作占用lock从可用库存里划出锁定库存。典型场景是销售订单审核。释放unlock把锁定库存退回可用库存。典型场景是订单取消、付款超时。扣减deduct把锁定库存变成已出库数量。典型场景是仓库完成出库。设计 Service 层时这三种操作要收敛到库存中心统一实现。其它模块不允许直接 UPDATE sku_stock 表。这样做的好处是以后增加“批次库存”“序列号库存”时只改库存中心内部实现调用方感知不到变化。4. 状态机驱动单据流转别让每个接口自己改状态4.1 状态机比散落的 if else 更清晰很多进销存系统的状态字段是数据库里一个 TINYINT然后每个接口里写if (status 20 xxx) { status 30; }。系统刚上线时没问题但随着单据类型变多会出现很多匪夷所思的 bug比如已取消的采购单还能被收货已完成的销售单还能被退款。正确做法是针对每种单据设计明确的状态机。例如采购订单状态机当前状态触发事件下一状态前置条件草稿提交审核待审核至少一行明细待审核审核通过已审核审核人信息非空已审核部分收货部分收货累计收货数量大于0已审核/部分收货全部收货已完成全部明细收货完成待审核/已审核取消已取消无关联入库单或入库单已取消4.2 状态机可以落成配置如果公司内单据类型很多可以把状态机配置化。最少需要两张表状态表和流转事件表。CREATE TABLE flow_state ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_type VARCHAR(32) NOT NULL COMMENT 业务类型purchase/sale/refund, state_code TINYINT NOT NULL COMMENT 状态值, state_name VARCHAR(64) NOT NULL, UNIQUE KEY uk_biz_state (biz_type, state_code) ) COMMENT 单据状态定义表; CREATE TABLE flow_transition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_type VARCHAR(32) NOT NULL, from_state TINYINT NOT NULL, event_code VARCHAR(64) NOT NULL COMMENT 事件如submit/approve/receive/cancel, to_state TINYINT NOT NULL, UNIQUE KEY uk_biz_from_event (biz_type, from_state, event_code) ) COMMENT 状态流转配置表;实际业务判断时只需要查一次配置确认“当前状态 事件”是否能走到目标状态。不满足就抛异常避免状态错乱。4.3 使用状态机后代码长什么样核心 Service 在执行业务动作时不再手动判断多个 if而是调用状态机服务做校验和流转public void receive(PurchaseOrderReceiveCommand cmd) { PurchaseOrder order orderMapper.selectById(cmd.getOrderId()); // 校验状态只有“已审核”和“部分收货”才能执行收货 stateMachine.validateTransition(purchase, order.getStatus(), receive); // 执行业务逻辑生成入库单、增加可用库存、更新累计收货数量 doReceive(order, cmd); // 计算新状态全部收完则完成否则部分收货 int nextState isAllReceived(order) ? 50 : 40; stateMachine.applyTransition(purchase, order, receive, nextState); }这里要注意“状态校验 - 业务操作 - 状态更新”必须在同一个事务里并且要防止并发重复收货。一种做法是在更新状态时带上旧状态作为 UPDATE 条件UPDATE purchase_order SET order_status #{newStatus}, update_time NOW() WHERE id #{orderId} AND order_status #{oldStatus};影响行数为 0 时说明状态已被别人改过直接抛出“单据状态已变更请刷新后操作”。4.4 异常状态和取消逻辑进销存系统容易在“取消”这件事上出问题。取消采购单时要判断是否已经有入库单取消销售单时要判断是否已经锁定库存、是否已经出库。推荐规则草稿状态直接作废不影响库存。已审核但未部分收货取消时回滚在途库存。部分收货状态不允许整单取消只能取消未收货行。销售订单已锁定库存取消时释放锁定库存。这些规则都要放到状态机动作里而不是每个入口各自实现一遍。5. 订单与库存联动扣减时机与超卖问题5.1 扣减时机的三种方案电商进销存里销售订单扣减库存的时机不同业务后果完全不同。扣减时机优点缺点适用场景下单即扣防止超卖能力最强用户不付款也占用库存取消率高时库存利用率低秒杀、库存紧张商品付款后扣库存利用率高下单未付款阶段可能超卖正常现货销售出库时扣库存利用率最高出库时才暴露超卖用户体验差售后成本高预售、按单生产实际物流场景中更推荐“付款锁定 出库扣减”的组合订单下定时不锁库存付款成功后进入锁定状态仓库发货完再扣减。这个方案兼顾了库存利用率和买家体感。5.2 超卖如何避免超卖的本质是多个并发请求同时读到剩余库存大于 0然后一起扣减。前端按钮置灰只是体验优化不是并发控制。后端防线有两层数据库条件更新UPDATE sku_stock SET available_qty available_qty - #{qty} WHERE sku_id #{skuId} AND available_qty #{qty}。唯一约束或幂等键防止同一订单号重复扣减。如果系统里还叠加了 Redis 预扣减要注意把 Redis 回补逻辑做成可重入的。比如扣减数据库失败时要能根据订单号精确地把 Redis 预占数量加回去。这里最容易出 bug 的是直接执行incr多次失败重试会导致 Redis 数量不断变大。5.3 订单取消与库存回滚订单取消的库存回滚有严格顺序先改订单状态再释放锁定库存最后写库存流水。顺序反了可能出现库存已经释放但订单状态还是“已锁定”用户再次取消时释放了两遍。释放操作建议做幂等设计利用唯一键防止重复回滚。比如在库存流水表里增加一个业务幂等键(biz_order_no, flow_type, sku_id, warehouse_id)相同释放动作重复提交时唯一键冲突被捕获后直接返回成功。5.4 多仓与库存分配电商进销存经常会遇到多仓发货问题。库存分配常见策略有指定仓库优先根据用户地址匹配最近仓库该仓不足则提示缺货。全局共享池多个仓库共享一个可售库存池下单时按仓库库存比例拆分订单。预占模式在下单时把 SKU 在多个仓库间锁定出库时按锁定仓发货。进销存后台设计多仓时要特别关注库存表的主维度。sku_stock的主键是(sku_id, warehouse_id)而不是sku_id。报表、库存查询、锁库接口都要带 warehouse_id否则很容易查错仓库。6. 报表和对账让数据能解释业务6.1 报表不是临时查询是数据模型的二次加工进销存系统上线一段时间后业务方会不停要新报表采购金额、销售毛利、库存周转、品类销售排行、供应商交货准时率。如果这些报表都靠实时联表查询业务表数据库很快会扛不住。推荐做法是把报表能力建立在“汇总表 宽表”上。每天晚上或每半小时由定时任务从流水表和单据表聚合数据到报表汇总表报表页只查询汇总结果。CREATE TABLE daily_stock_summary ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stat_date DATE NOT NULL COMMENT 统计日期, sku_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, begin_available_qty INT NOT NULL COMMENT 期初可用库存, end_available_qty INT NOT NULL COMMENT 期末可用库存, in_qty INT NOT NULL DEFAULT 0 COMMENT 入库数量, out_qty INT NOT NULL DEFAULT 0 COMMENT 出库数量, locked_qty INT NOT NULL DEFAULT 0 COMMENT 期末锁定数量, UNIQUE KEY uk_stat_sku_warehouse (stat_date, sku_id, warehouse_id) ) COMMENT 每日库存汇总表;宽表的价值在于把多维分析需要的字段提前冗余进去例如“销售明细汇总表”里冗余了商品分类、供应商、客户、仓库、SKU 名称等字段查询时不用反复 JOIN 主数据表。缺点是数据同步任务要维护好否则宽表数据不准会引发信任危机。6.2 进销存常用指标指标计算口径用途进货金额入库单金额汇总采购成本分析销售额出库单金额或销售订单金额营收分析毛利销售额 - 出库成本商品盈利能力库存周转次数出库成本 / 平均库存库存健康度库龄按入库时间分区间统计剩余库存清理滞销和临期商品缺货率缺货订单行数 / 总订单行数履约质量计算成本时要注意采用什么计价方式移动加权平均、先进先出还是个别计价。进销存报表里的毛利率和库存成本都依赖计价方式保持一致不能不同报表用不同规则。6.3 对账逻辑怎么设计进销存系统通常需要和 ERP、财务系统、电商平台后台对账。对账的核心是“两边按同一口径计算发现差异后再下钻到流水”。常见做法是设计一张对账结果表CREATE TABLE reconciliation_result ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_date DATE NOT NULL COMMENT 对账日期, biz_type VARCHAR(32) NOT NULL COMMENT 库存/销售/采购/退款, sku_id BIGINT, order_no VARCHAR(64), local_qty INT COMMENT 本系统数量, remote_qty INT COMMENT 对方系统数量, diff_qty INT COMMENT 差异数量, check_status TINYINT COMMENT 0一致 1差异 2已处理, remark VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 对账结果表;对账流程建议分三步先对总数再对明细最后定位差异单。按日期汇总两边数量先看整体是否一致。按订单号或单据号逐条比对明细。对差异单查看本系统库存流水和对方接口返回数据定位是哪一笔变动没同步。6.4 对账排查清单差异现象可能原因排查方法本系统多对方少本系统单据重复提交或状态未同步按单号查库存流水检查是否有两条入库流水本系统少对方多库存扣减在别的系统直接操作查看是否还有其它服务绕过库存中心时间维度不一致两边统计时区或截止时间不同对齐统计口径统一以数据库时间为准金额不一致计价方式不同确认对方使用的成本计算规则7. 高级设计中的常见坑与排错路径7.1 坑一库存出现负数现象库存表 available_qty 变为负数但订单还在正常下。可能原因扣减库存时没有加available_qty quantity条件。通过后台直接改数据库或在非库存中心代码里直接扣减。多个线程并发扣减但代码先查后更新没有使用行锁。处理方式先通过库存流水反查是从哪张单据扣减的。确认扣减代码是否走库存中心的统一方法。用UPDATE ... WHERE available_qty #{qty}强制约束。对历史脏数据导出明细后由业务方确认冲正数量。7.2 坑二单据重复提交导致重复入库现象同一张采购单被重复确认收货库存翻倍。可能原因前端按钮没有做 loading 防抖。后端没有幂等判断。入库单唯一索引缺失。处理方式入库单增加(purchase_order_id, sku_id, batch_no)的唯一索引。接口层根据来源单号和幂等批次号先查一次。唯一键冲突异常单独捕获提示“该单已收货请勿重复操作”。7.3 坑三状态和库存不一致现象订单状态显示“已完成”但对应锁定库存没有释放或扣减。可能原因状态更新和库存操作不在同一个事务里。事务内调用库存接口但库存中心抛异常后外层事务没有回滚。状态机流转时把库存动作放在事务外异步执行且没有补偿逻辑。处理方式单据状态和库存变动尽量放在同一本地事务中。如果异步执行必须设计对账任务扫描长期未释放的锁定库存。增加定时任务查询“已完成但存在未扣减库存”的销售单。7.4 坑四报表越查越慢锁竞争变严重现象日报查询耗时从几百毫秒涨到几秒。可能原因报表直接查询业务表随着数据量增长全表扫描。库存表行锁竞争严重扣减操作被报表查询拖慢。流水表没有分区。处理方式报表从业务表中剥离使用汇总宽表。大表增加常用查询索引。流水表按月分区历史数据归档。这里给一个推荐的排错顺序先确认输入条件是否正确再检查单据编号和时间范围然后看关联单据状态是否正常再看库存流水是否存在最后检查事务是否回滚、日志里是否有唯一键冲突或乐观锁失败。按这个顺序能快速定位大多数进销存问题。8. 从“能用”到“好用”设计清单与扩展方向8.1 上线前设计检查清单检查项状态说明库存表是否区分可用、锁定、在途等状态必须不区分的系统无法支撑销售锁定所有库存变动是否有流水记录必须排查对账问题的唯一依据库存扣减是否使用条件更新必须防超卖底线单据是否采用头行结构建议支持部分收货、部分发货状态机是否统一管理建议防止状态散乱报表是否使用汇总宽表建议数据量上来后性能差异明显幂等键是否覆盖关键操作必须防止重复提交是否有每日对账任务建议提前发现数据不一致是否支持多仓库扩展建议电商业务基本都会走到这一步库存中心是否独立模块建议控制库存操作入口8.2 后续扩展方向上面的设计是进销存系统的基础骨架。当业务复杂到一定程度后还可以继续扩展批次管理增加批次表库存维度从 SKU 精确到“SKU 批次 仓库”支持先进先出和效期管理。序列号管理对于贵重商品增加唯一序列号出库时逐件绑定订单。调拨管理仓库之间库存调拨涉及在途库存和调拨单状态机。WMS 集成如果后端有专业仓储系统进销存系统要提供标准的库存同步接口比如入库回传、出库回传、盘点差异回传。多租户隔离如果做 SaaS 版进销存所有表都要增加 tenant_id并在唯一索引中带上租户维度。供应链协同把采购订单通过接口发送给供应商门户供应商维护发货单回传物流信息。扩展时最应该守住的原则是库存操作入口不分散。无论系统长出多少新功能只要“加库存、扣库存、锁库存、释放库存”这几个动作都收敛在库存中心后续扩展就相对安全。8.3 学习建议如果你正在设计或重构进销存系统不建议一上来就追求完整功能。先按这套思路跑通最小闭环建商品和 SKU、建仓库、做采购入库、做销售出库、记录库存流水、生成日汇总报表。把这个闭环跑稳再逐步加入状态机、对账、批次和调拨。进销存系统真正难的不是某个技术点而是数据模型是否稳定、库存语义是否清晰、状态流转是否可控。把这些底层设计想清楚系统自然会在业务复杂后体现出“高级”的价值。