拓冰建站拓冰建站
首页 / 资讯中心 / 正文

电商库存管理系统设计:数据库建模、流水追溯与防超卖实践

每年毕业季计算机相关专业的选题清单里电商库存管理系统出现的频率高得离谱。这个题目看起来传统、稳当但正因为它太常见了很多学生做着做着就变成了商品增删改查一个库存数字答辩时老师随便追问两句超卖怎么处理库存流水怎么对账现场就冷场了。这篇文章我结合自己做毕设指导、也帮人救急改过项目的经验聊聊怎么把一个库存管理系统做出真正的技术含量同时把源码、数据库、演示数据整理到拿到就能跑、跑完能答辩的状态给准备做这个方向或者正在中期阶段的同学一个完整参考。1. 需求边界电商库存系统到底要管住哪些事1.1 先捋业务链路别急着打开IDE很多人拿到这个题目第一反应是建表写接口但我强烈建议先花半天把业务链路画清楚。电商库存系统不是记录一个数字那么简单它核心要回答三个问题货在哪、货还剩多少、货的变动凭什么发生。围绕这三个问题去拆系统天然就分成几块商品管理负责货是什么仓库管理负责货在哪库存台账负责还剩多少出入库单和流水负责变动凭什么发生。再加一层辅助能力比如库存预警、盘点、调拨、操作日志整个系统的业务闭环就完整了。我见过不少同学一上来就设计十几个表什么用户表、角色表、权限表、菜单表一套后台管理系统全家桶全上。不是说不能做而是你要分清主次。库存系统的主线永远是商品-仓库-库存数量-流水权限和用户管理是外围支撑先想清楚主线外围再慢慢补。1.2 功能清单与优先级划分把功能按优先级排一遍你会发现及格和优秀之间的差距其实很清晰优先级功能模块具体内容说明P0商品管理商品SPU/SKU、品牌、分类、上下架一切库存的前提P0仓库管理多仓库维护、仓库地址、状态库存必须落在具体仓库P0库存台账各仓库各SKU实时库存、冻结库存系统的核心数据P0入库管理采购入库、退货入库、入库单审核库存从哪来P0出库管理销售出库、调拨出库、出库单审核库存到哪去P1库存流水每一次变动的前值/后值/类型/单据号对账与审计的基础P1库存预警低于下限、高于上限提醒业务价值很明显P1盘点管理盘点单、盘点差异、库存调整账实不符的修正手段P2调拨管理仓库间调拨、在途状态多仓场景的进阶功能P2操作日志登录日志、关键操作记录答辩容易加分这里要说明一下P0是没有就根本不算库存系统P1是有了就像个正经系统P2是时间充裕再做。很多同学做毕设最大的问题不是功能少而是P0的细节没做扎实就忙着堆P2的架子。一个入库单审核状态都理不清楚的系统加再多花哨功能也经不起答辩追问。2. 数据库建模库存台账与流水表的设计思路2.1 一张余额配一本账本数据库设计是整个系统的地基也是最容易被答辩老师抓住问的地方。我个人的经验是库存系统里一定要有两张核心表——库存台账表当前库存余额和库存流水表每一次变动的明细记录。你可以把库存台账想象成银行账户余额,把流水表想象成交易明细。余额是结果明细是过程。只有余额没有明细的系统出了问题根本没法追溯只有明细没有余额每次查询都要全量汇总性能扛不住。所以标准做法是保留余额同时每笔变动写流水两者通过单据号关联。库存台账的设计上建议按SKU 仓库粒度存一行而不是把数量堆在一个字段里。举个例子一件商品在华东仓和华南仓各有库存如果只维护一个总库存数那么华东仓缺货但华南仓有货这个信息就丢了。按仓库拆行后每个仓的库存、预警线、冻结数量各自独立逻辑清楚查询也快。2.2 核心表结构和建表SQL参考下面这套表结构是我帮一个学生改项目时定下来的方案覆盖了毕设常用的场景你可以直接参考调整-- 商品表简化 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(64) NOT NULL COMMENT SKU编码, sku_name VARCHAR(128) NOT NULL COMMENT SKU名称, category_id BIGINT, price DECIMAL(10,2), status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 仓库表 CREATE TABLE warehouse ( id BIGINT PRIMARY KEY AUTO_INCREMENT, warehouse_code VARCHAR(64) NOT NULL COMMENT 仓库编码, warehouse_name VARCHAR(128) NOT NULL, address VARCHAR(255), status TINYINT DEFAULT 1 ); -- 库存台账表 CREATE TABLE inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(64) NOT NULL, warehouse_code VARCHAR(64) NOT NULL, quantity INT NOT NULL DEFAULT 0 COMMENT 可用库存, locked_quantity INT NOT NULL DEFAULT 0 COMMENT 锁定库存, warning_line INT NOT NULL DEFAULT 10 COMMENT 预警下限, upper_line INT NOT NULL DEFAULT 1000 COMMENT 预警上限, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku_warehouse (sku_code, warehouse_code) ); -- 库存流水表 CREATE TABLE inventory_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flow_no VARCHAR(64) NOT NULL COMMENT 流水单号, sku_code VARCHAR(64) NOT NULL, warehouse_code VARCHAR(64) NOT NULL, change_type TINYINT COMMENT 1入库 2出库 3盘点调整 4调拨, change_qty INT NOT NULL COMMENT 变动数量正负表示方向, before_qty INT NOT NULL, after_qty INT NOT NULL, ref_bill_no VARCHAR(64) COMMENT 关联单据号, operator VARCHAR(64), remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 出入库单据表简化 CREATE TABLE stock_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(64) NOT NULL COMMENT 单据号, bill_type TINYINT COMMENT 1采购入库 2销售出库 3调拨出库, warehouse_code VARCHAR(64), status TINYINT COMMENT 1待审核 2已审核 3已驳回, total_qty INT, create_by VARCHAR(64), audit_by VARCHAR(64), audit_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里有个容易犯的错把quantity字段做成INT UNSIGNED觉得库存不可能为负。实际上在并发出库场景下如果没有锁和条件更新保护负库存恰恰是需要被拦住而不是被数据库报错的。真上了生产环境UNSIGNED字段一旦出现负数拆台整个服务直接异常崩溃。毕设阶段我更推荐保留普通INT类型再配合后面讲到的乐观锁方案去拦超卖这样逻辑是可控的演示也不会莫名其妙MySQL报错。2.3 索引与字符串类型的选择流水表的查询高频场景是根据SKU查变动历史和根据单据号查明细所以sku_code、flow_no、ref_bill_no这些字段一定要加索引。sku_code用VARCHAR(64)并配上普通索引就够用了不用迷信BIGINT自增主键业务编码作为查询条件是更贴合实际的做法。唯一键uk_sku_warehouse一定要加这是防止同一个仓库同一条SKU出现两行数据的最后一道防线——代码里漏了判断数据库层面还能兜底。3. 出入库与流水机制让每一件货变动都有据可查3.1 入库单和出库单的状态流转做库存系统不能用户一点入库就直接把库存加了中间必须隔一层单据审核。这个设计不是毕设炫技而是真实的业务习惯操作人提交入库单审核人确认后库存才真正变动。单据状态建议四态待审核、已审核、已驳回、已作废。待审核状态下可以编辑和撤回已审核后不可再修改。在已审核单据上做任何更改都应该是冲红——也就是生成一笔负数的反向流水来冲销而不是直接改原单。这个思路你写进设计文档里答辩时老师会觉得你不是在做作业而是真的理解业务。3.2 流水写入的连贯性每次库存变动要保证改库存写流水这两个动作同时成功或同时失败。咱们来看一个标准的入库流程创建入库单状态为待审核审核通过后开启事务更新库存台账quantity quantity 入库数量写入流水记录变动前数量、变动后数量、单号、操作人更新单据状态为已审核提交事务关键在第3、4步。如果库存更新成功但流水写入失败会导致余额变了但账本没记录以后对账必然出问题。所以这两个操作必须在同一个事务里方法上直接加Transactional不要自己手动去开连接管理。说句实在话我见过好几个学生项目库存流水表建了但压根不写数据追问之下说不知道什么时候该写。其实规则非常机械凡是inventory表的数量字段发生了增删改就必须同时往inventory_flow插一条记录。把这个规则放在所有库存变动方法的底层就不会漏。3.3 单据号生成看起来简单其实有讲究单据号建议做成类型前缀日期自增序号比如RK20250612001代表2025年6月12日第1张入库单CK代表出库PD代表盘点。这样不用查数据库也能从单号里读出基本信息演示的时候观感很好答辩时讲到也显得职业化。我之前帮一个学生改项目时他用的单号是全局自增ID直接从表里取虽然没毛病但演示时老师看到一串127、128、129会立刻觉得没用心。库存单据、流水号这种编号永远是带着业务含义比纯数字自增更专业。4. 并发扣减与超卖问题毕设中最容易翻车也最容易加分的环节4.1 超卖是怎么发生的两件库存只剩最后一件商品两个用户几乎同时提交订单。如果代码逻辑是先查库存够不够再扣库存那么两个请求都查到了还剩1件都认为可以扣最终库存变成-1——这就是超卖。很多毕设项目在演示时没有并发场景所以这个问题被掩盖了。但答辩老师非常喜欢问高并发下怎么保证库存不超卖答不上来或者只会说加锁但说不清楚加在哪分数就会受影响。反过来如果这个点你答得很透简直是送分题。4.2 用乐观锁做条件更新毕设级项目我推荐用乐观锁。不需要引入额外的中间件一张表加一个version字段就能解决。核心SQL是条件更新UPDATE inventory SET quantity quantity - #{buyNum}, version version 1 WHERE sku_code #{skuCode} AND warehouse_code #{warehouseCode} AND version #{oldVersion} AND quantity #{buyNum};这个SQL有意思的地方在WHERE条件version #{oldVersion}保证只有在你读取库存之后没人改过更新才生效quantity #{buyNum}直接在数据库层面拦住库存不足的情况即使并发同时进来数据库的行锁也会让后到的请求看到更新后的数据而不是旧数据。如果你的表不想加version字段也可以把更新条件改为WHERE quantity #{buyNum}利用update的行锁和条件本身来防超卖。效果类似但加了version会更直观地告诉老师我知道并发控制有乐观锁这个方案。4.3 更新失败之后怎么办条件更新返回的影响行数如果是0说明扣减失败。这时候不能傻乎乎地返回系统繁忙而应该重新读取真实库存并提示用户库存不足。这里我直接给一段处理逻辑Transactional public boolean deductStock(String skuCode, String warehouseCode, int num) { Inventory inv inventoryMapper.selectBySkuAndWarehouse(skuCode, warehouseCode); if (inv null) { throw new RuntimeException(库存记录不存在); } // 乐观锁更新返回影响行数 int rows inventoryMapper.deductByVersion( skuCode, warehouseCode, num, inv.getQuantity(), inv.getVersion()); if (rows 0) { // 说明库存被改过了重新读取再判断 Inventory latest inventoryMapper.selectBySkuAndWarehouse(skuCode, warehouseCode); if (latest.getQuantity() num) { throw new RuntimeException(库存不足当前剩余 latest.getQuantity()); } // 这里可以递归重试一次也可以直接提示用户稍后重试 throw new RuntimeException(库存变动冲突请重试); } // 写流水 inventoryFlowMapper.insert(...); return true; }这里要说一个事务的小坑如果你在Transactional方法里抛了RuntimeExceptionSpring会回滚整个事务所以在方法里检测到库存不足后直接抛异常就不用自己手动回滚。有些同学习惯返回false然后调用方自己判断这在小项目里也能跑但异常事务回滚在语义上更清晰也更好维护。4.4 并发自测小技巧毕设答辩前最好自己测一把并发。最简单的办法用浏览器开两个标签页同时点提交订单或者用Postman并行发两个请求配一个只有1件库存的商品看看最终库存是不是变成负数。如果变成-1说明没防住如果有一个请求返回库存不足说明防住了。有条件的话可以装个JMeter开20个线程同时抢最后一件商品比手工点有明显的说服力。即使不做压测报告自己心里也要有数答辩被问到才能接得住。5. 库存预警、盘点与调拨把系统的差异化价值做出来5.1 预警的两种实现思路库存预警是这个系统里业务价值很直观的功能。实现上有两个思路我建议都了解一下第一是定时扫描。用一个Spring的Scheduled定时任务每天或者每小时扫一遍inventory表凡是quantity小于等于warning_line的商品生成一条预警消息。这个思路实现简单适合库存变化不频繁的情景。第二是变动时检测。在每次出入库更新库存后顺手比较一下最新库存和预警线如果触发阈值就立刻记录预警。这个思路实时性更好逻辑也分散在变动方法里。实际项目里两者往往结合但毕设用定时扫描就够了还显得你考虑了系统化任务调度。定时任务的代码如下Component public class StockWarningTask { Scheduled(cron 0 */30 * * * ?) // 每30分钟跑一次 public void checkWarning() { ListInventory lowStockList inventoryMapper.selectBelowWarningLine(); for (Inventory inv : lowStockList) { warningMapper.insert(...); // 记录预警信息 } } }预警数据要给已处理/未处理的标记加上简单的状态流转这样才是一个完整闭环而不只是把数据列出来。5.2 盘点流程账实不符的修正机制盘点这个功能很能体现系统设计的完整性。逻辑上是三步创建盘点单选定仓库和要盘点的SKU记录账面数量录入实际盘点到的数量审核盘点差异如果账实不一致自动生成一张盘点调整单把库存改成实际数量同时写一条change_type3的流水这里有个关键点盘点调整必须留痕。不能直接update inventory set quantity 实际数量就完了必须关联盘点的流水记录。否则系统里会出现库存莫名其妙变了但查不到原因的情况这在答辩时会被老师当成致命伤。另外盘点时一般建议先冻结该SKU的出入库操作避免一边盘点一边卖货导致数据更乱。在毕设阶段可以简化成盘点过程中相关SKU的出库操作不可用在页面上提示一下就行。5.3 调拨与在途库存多仓库场景下调拨是一个自然延伸。调拨的本质是三笔操作调出仓扣减、调入仓增加、中间状态在途。我建议毕设只做到调拨单审核后调出仓直接扣减调入仓直接增加不需要真的做在途物流跟踪。但设计文档里要提一句在途库存表示你了解这个场景。调拨流程创建调拨单选调出仓、调入仓和商品数量审核通过后调出仓出库调入仓入库两笔操作在同一个事务里完成并且写两笔流水一笔出库方向、一笔入库方向通过同一个调拨单号关联。这个联动做好了系统就不只是单仓自嗨而是真的具备电商多仓运营的形态。6. 源码交付的准备从能跑到拿得出手6.1 初始化数据决定第一印象标题里写着附源码这其实是一个很重要的交付信号。源码给出去第一件事就是要能跑起来而且跑起来之后要有像样的数据。我遇到过太多项目代码是好的结果启动后页面上空空荡荡连个测试账号都得现去数据库插体验分直接打对折。一份合格的初始化数据至少包括两个测试账号一个管理员一个普通操作员密码提前告诉使用者5到10个商品分别设置合理的预警线最好造一两款库存已经触及预警线的一登录就能看到预警提醒至少两个仓库方便演示调拨提前录好的几条出入库单据和流水让库存记录有历史可以翻阅一个典型的低库存场景初始化数据可以通过schema.sql和data.sql随项目一起加载也可以用MyBatis的初始化脚本。总之原则是让拿到源码的人30秒内看到有价值的数据而不是对着空表发呆。6.2 统一返回体与接口规范源码里接口风格统一会让人一眼看出你受过规范训练。前端不管用什么框架后端接口建议统一返回下面这个结构public class ResultT { private int code; // 200成功 500失败 private String msg; // 提示信息 private T data; // 业务数据 }所有接口都走这个壳前端统一处理code和msg而不是每个接口返回不同格式的JSON。这个细节看起来小但实际是团队协作的根基也是一个工程素养的体现。接口命名上尽量RESTful化商品用/api/product仓库用/api/warehouse库存查询用/api/inventory出入库单用/api/bill/inbound、/api/bill/outbound。不要出现/doSomething这种拼音式接口名。不要求教科书级别的REST规范但保持语义统一就够了。6.3 演示路线图和答辩准备最后一条也是我最想提醒的源码交付之后建议再写一个快速开始.md和演示路线.md。快速开始说明环境要求JDK版本、MySQL版本、Redis要不要装、启动步骤、默认账号密码。演示路线给出一条清晰的体验路径登录→看商品→看库存→发起入库→审核→看库存增加→发起出库→审核→看库存减少→看流水→看预警→做盘点→看差异调整。这样做的原因是你的毕设有可能被多个老师经手他们可能没有时间深入研究代码。一份清爽的文档能让你的项目在开箱体验这个维度上直接超过半数同学。答辩时高频追问可以提前预演库存和流水的数据一致性怎么保证——答事务流水写入规则多个用户同时下单怎么防超卖——答乐观锁条件更新贴出SQL预警怎么实现——答Scheduled定时扫描预警状态流转盘点流程中账面数和实盘数不一致怎么办——答盘点调整单流水留痕为什么按SKU和仓库分库存而不是一个总数——答多仓场景下总数没有业务意义这些问题不是背出来的而是在写完代码之后自然就懂了。如果你做到这一步这个毕设拿高分基本是稳的。我最后再说一个实操层面的体会库存管理系统真正的难点不在写代码而在想清楚每一笔数量变动的来龙去脉。只要你把流水和台账这层关系理透了后面的所有功能都只是在这个地基上搭积木。反过来如果这个底层逻辑是糊的后面每加一个功能都会发现数据对不上越改越乱。项目做完后我把这套源码和数据脚本打包整理的时候心里最大的感受就是好的毕设不是功能堆得多而是核心链路经得起连续追问。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门