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

数据库课程设计仓库管理系统:从ER图到存储过程实战指南

简介面向本科阶段数据库课程设计任务提供一份完整的仓库管理系统设计文档可作为实践参考。系统基于 Java 与 SQL Server 2005围绕基础信息管理、出入库管理、查询统计和系统管理四个模块展开完整给出了供应商、商品、客户、库存、操作员等表结构设计以及 Dao.java、JXCFrame.java、Login.java 等核心类代码示例可帮助读者理解数据库连接、登录验证、库存盘点等关键实现。文档从项目背景、计划进度、功能结构到设计过程均有说明并附有程序结构图和核心代码适合完成管理系统类课程设计或学习 Java 与关系型数据库结合开发的学生参考。包体为单个 docx 文档大小约 1.44MB内容紧凑、结构清晰便于按章节阅读和二次修改。目前已有 3609 人学习下载是数据库课程设计与开发实践中较有参考价值的示例报告。1. 数据库课程设计仓库管理系统到底在考察什么能力仓库管理系统是每年数据库课程设计里出现频率最高的一类题目但大多数提交上来的作业只停留在“能存能查”这个层面。这门课设真正要考察的不是你会不会写增删改查而是三个容易被忽视的能力第一能不能用约束和事务保证库存数据在并发出入库时不错乱第二能不能按第三范式完成表结构拆解而不是把所有字段堆进一张大表第三能不能写出带真实业务语义的SQL——比如先进先出的库存扣减、盘点差异处理、月度结转。本文按一条完整的落地路径展开从数据模型设计开始到存储过程封装业务规则再给出一个基于Python和MySQL的联调实现最后补上死锁排查和报表优化。这套方案覆盖了课程设计答辩时最容易被追问的技术点贴近你手头那份名为“数据库课程设计仓库管理系统.docx”的任务书。2. 仓库管理系统数据库设计从ER图到MySQL建库建表2.1 仓库管理系统需要哪几张核心表及ER关系拆分仓库管理的核心业务是入库、出库、库存实时查询和盘点。围绕这四个动作一般拆出六张基础表仓库表、供应商表、商品表、入库单表、出库单表、库存表。商品表和仓库表是多对多关系库存表就是它们的关系表同时携带数量、可用量、锁定量三个状态字段这是电商仓配场景的常见设计。按第三范式拆表的关键是“不冗余可推导数据”。比如入库单明细里不应该存商品的当前库存量因为它是可以被实时计算的同理出库单表里也不该冗余存商品名称存商品ID即可。库存表里则要保留一个冗余字段“最后变动时间”这个字段违反第二范式但在实际业务里是为了避免每次查询都去扫描出入库流水表。为了让课程设计有加分项可以把商品表拆成“商品主表”和“商品分类表”在分类表里加一个parent_id自关联形成两层的树形结构演示递归查询。我一般会在设计文档里附一张ER图并且在图上明确标注每段关系的基数——比如“一份入库单包含1到N个入库单明细一个商品出现在M个入库单明细中”。ER图画清楚之后就可以直接转成SQL建表脚本下面是拆好的建库脚本。CREATE DATABASE IF NOT EXISTS wms_db DEFAULT CHARSET utf8mb4; USE wms_db; CREATE TABLE warehouse ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) NOT NULL UNIQUE COMMENT 仓库编码, name VARCHAR(50) NOT NULL, address VARCHAR(255), manager VARCHAR(20) ) ENGINEInnoDB COMMENT 仓库表; CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(80) NOT NULL, contact VARCHAR(30) COMMENT 联系人, phone VARCHAR(20) ) ENGINEInnoDB COMMENT 供应商表; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, parent_id INT NULL, FOREIGN KEY (parent_id) REFERENCES category(id) ) ENGINEInnoDB COMMENT 商品分类表; CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(30) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, spec VARCHAR(50) COMMENT 规格型号, unit VARCHAR(10) COMMENT 单位, category_id INT NOT NULL, price DECIMAL(10,2) COMMENT 参考单价, FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB COMMENT 商品表; CREATE TABLE stock ( id INT PRIMARY KEY AUTO_INCREMENT, warehouse_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0 COMMENT 账面库存, available_qty INT NOT NULL DEFAULT 0 COMMENT 可用量, locked_qty INT NOT NULL DEFAULT 0 COMMENT 锁定待出库, last_change_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_wh_prod (warehouse_id, product_id), FOREIGN KEY (warehouse_id) REFERENCES warehouse(id), FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINEInnoDB COMMENT 库存表; CREATE TABLE inbound_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) NOT NULL UNIQUE, warehouse_id INT NOT NULL, supplier_id INT NOT NULL, total_quantity INT DEFAULT 0, status TINYINT DEFAULT 0 COMMENT 0待入库 1已完成 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME, FOREIGN KEY (warehouse_id) REFERENCES warehouse(id), FOREIGN KEY (supplier_id) REFERENCES supplier(id) ) ENGINEInnoDB COMMENT 入库单主表; CREATE TABLE inbound_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES inbound_order(id), FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINEInnoDB COMMENT 入库单明细; CREATE TABLE outbound_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) NOT NULL UNIQUE, warehouse_id INT NOT NULL, receiver_name VARCHAR(30), receiver_phone VARCHAR(20), status TINYINT DEFAULT 0 COMMENT 0待出库 1已出库 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME, FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ) ENGINEInnoDB COMMENT 出库单主表; CREATE TABLE outbound_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES outbound_order(id), FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINEInnoDB COMMENT 出库单明细;这段脚本里要重点解释几个设计决策。库存表使用的是InnoDB引擎并加了唯一键uk_wh_prod这个唯一键是防止“同一仓库同一商品出现两行库存记录”的兜底约束应用层即使写错了也不至于把数据写坏。category表的parent_id自引用外键让分类表天然支持无限层级这是课程设计答辩时展示递归查询的基础。入库单和出库单都保留status字段做多状态流转这比“直接删记录”更接近真实系统。所有金额字段都用DECIMAL而不是FLOAT避免浮点误差——这是一个一上来就应该讲清楚的设计细节。2.2 MySQL建表参数选择与自增主键合理性上述脚本中有几个参数容易被课程设计的说明文档忽略但对数据库运维和后续联调影响很大。字符集选用utf8mb4而非utf8原因是utf8在MySQL里只能存3字节的字符而部分特殊符号和生僻字需要4字节编码utf8mb4才是完整的Unicode子集。排序规则如果使用默认的utf8mb4_general_ci在做中文模糊查询时不区分大小写性能略好但如果你需要更精确的语言学排序规则可以换成utf8mb4_unicode_ci。主键用AUTO_INCREMENT自增整数是一个稳妥的选择。UUID做主键在分布式场景有优势但在单机的课程设计里它会让InnoDB的聚集索引产生大量页分裂导致插入性能下降。如果后续想让系统更有工程味道可以加一个order_no作为业务单号并建唯一索引这个单号由应用层生成比如日期加序列IN20240601001。数据库层面的自增主键只做物理标识业务上不暴露给用户看。外键在实际开发中经常被禁用但课程设计建议保留。原因在于MySQL外键会引入额外的锁复杂度——插入明细时会自动对主表记录加共享锁高并发下容易造成间歇性锁等待。但课程设计的核心是展示完整的数据约束能力在删主表的时候让数据库自动拒绝或级联处理是评分的加分项。下面是初始化样例数据的脚本覆盖了供应商、商品、初始库存和一笔状态为“待入库”的入库单。INSERT INTO warehouse (code, name, address, manager) VALUES (WH001, 上海一号仓, 上海市嘉定区博园路, 张伟), (WH002, 苏州二号仓, 苏州市工业园区, 李强); INSERT INTO supplier (code, name, contact, phone) VALUES (SUP001, 华为技术有限公司, 王芳, 13800000001), (SUP002, 联想集团有限公司, 赵磊, 13800000002); INSERT INTO category (name, parent_id) VALUES (电子产品, NULL); INSERT INTO category (name, parent_id) VALUES (手机, 1); INSERT INTO category (name, parent_id) VALUES (笔记本电脑, 1); INSERT INTO category (name, parent_id) VALUES (办公用品, NULL); INSERT INTO product (code, name, spec, unit, category_id, price) VALUES (P001, Mate 60 Pro手机, 12GB512GB, 台, 2, 6999.00), (P002, ThinkPad X1, i7/32GB/1TB, 台, 3, 12999.00), (P003, 中性笔, 0.5mm黑色, 支, 4, 2.50); INSERT INTO stock (warehouse_id, product_id, quantity, available_qty, locked_qty) VALUES (1, 1, 300, 300, 0), (1, 2, 150, 150, 0), (2, 3, 5000, 5000, 0); INSERT INTO inbound_order (order_no, warehouse_id, supplier_id, total_quantity, status) VALUES (IN20240501001, 1, 1, 0, 0);3. 出库流程中的存储过程与事务并发控制3.1 为什么必须在数据库层封装事务而不是在应用层写多条SQL仓库管理最典型的业务是出库扣减库存。一个完整的出库动作在应用层看起来是三条SQL语句插入出库单、插入出库单明细、更新库存表。如果这三条语句分散在Python或Java代码里一旦执行第二条失败第一条就已经提交出库单就会变成脏数据。正确的做法是用存储过程封装事务把三条操作变成一个原子单元要么全部成功要么全部回滚。另一个专业设计是“先锁库存再扣库存”。真实的电商仓配业务里出库分为锁定库存和扣减库存两步。当客户下单时锁定库存优惠券过期或超时未支付时释放锁定库存用户实际支付后系统才做真正的扣减。这套机制保证了“防止超卖”这一核心需求。在数据库层面锁定操作就是UPDATE语句修改locked_qty字段并在条件里加上数量校验。MySQL的InnoDB引擎在UPDATE操作时会自动加行级排他锁所以不用显式写SELECT FOR UPDATE来锁行。但要注意一个细节UPDATE的WHERE条件必须命中唯一索引否则InnoDB会升级为锁多行甚至导致间隙锁死锁。在我们的表结构中库存表的唯一键是(warehouse_id, product_id)所以每次只需要精确指定这两个字段值即可。下面给出锁定库存的存储过程。DELIMITER // CREATE PROCEDURE proc_lock_stock( IN p_warehouse_id INT, IN p_product_id INT, IN p_quantity INT, OUT p_success INT ) proc_label: BEGIN DECLARE v_available INT DEFAULT 0; START TRANSACTION; -- 在事务内锁定目标行并读取当前可用量 SELECT available_qty INTO v_available FROM stock WHERE warehouse_id p_warehouse_id AND product_id p_product_id FOR UPDATE; IF v_available IS NULL THEN SET p_success -1; -- 商品在仓库中不存在 ROLLBACK; LEAVE proc_label; END IF; IF v_available p_quantity THEN SET p_success -2; -- 可用量不足 ROLLBACK; LEAVE proc_label; END IF; UPDATE stock SET available_qty available_qty - p_quantity, locked_qty locked_qty p_quantity WHERE warehouse_id p_warehouse_id AND product_id p_product_id; SET p_success 1; COMMIT; END // DELIMITER ;这个存储过程用了一个特征的写法SELECT语句末尾带上FOR UPDATE它的含义是对命中的行加排他锁锁一直保持到当前事务结束。这样做的目的是让并发场景下两个会话不会同时读到同一个available_qty旧值。加了SELECT之后即使用LOCK IN SHARE MODE或者普通SELECT都不行——普通SELECT不加锁两个事务会同时读到旧值一起通过校验导致超卖。IF v_available IS NULL这个判断解决了“商品在该仓库无库存记录”的情况。很多课程设计在这时直接报错终止但在真实系统里锁库存时可能遇到库存记录尚未初始化的情况返回一个可辨识的错误码比抛出异常更便于应用层做降级处理。调用端可以这样执行CALL proc_lock_stock(1, 1, 50, result); SELECT result;result返回值含义约定如下返回值含义1锁定成功-1库存记录不存在需要先初始化再重试-2可用量不足其他依赖具体业务的扩展错误码3.2 先进先出扣减与出库确认的完整存储过程仓库管理系统在课程设计中要做到比“把数量减掉”更专业可以引入先进先出扣减逻辑。先进先出的意思是同一件商品在不同时间入库单价可能不同出库时应该优先扣最初入库批次的数量。这要求我们再引入一张“批次库存表”核心字段是批次号和剩余数量。出库时先从最早批次开始扣不够再从下一个批次接着扣。批次扣减的逻辑在存储过程里写得清楚。下面是一个基于批次表的扣减示例先遍历出余量不为零且入库时间最早的批次再逐批扣减DELIMITER // CREATE PROCEDURE proc_fifo_deduct( IN p_warehouse_id INT, IN p_product_id INT, IN p_quantity INT, OUT p_result INT ) fifo_label: BEGIN DECLARE v_batch_id INT; DECLARE v_batch_left INT; DECLARE v_need INT DEFAULT p_quantity; DECLARE v_done INT DEFAULT 0; DECLARE cur_batch CURSOR FOR SELECT id, remain_qty FROM stock_batch WHERE warehouse_id p_warehouse_id AND product_id p_product_id AND remain_qty 0 ORDER BY produce_date ASC, id ASC FOR UPDATE; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done 1; START TRANSACTION; OPEN cur_batch; batch_loop: LOOP FETCH cur_batch INTO v_batch_id, v_batch_left; IF v_done 1 THEN LEAVE batch_loop; END IF; IF v_batch_left v_need THEN UPDATE stock_batch SET remain_qty remain_qty - v_need WHERE id v_batch_id; SET v_need 0; ELSE UPDATE stock_batch SET remain_qty 0 WHERE id v_batch_id; SET v_need v_need - v_batch_left; END IF; IF v_need 0 THEN LEAVE batch_loop; END IF; END LOOP; CLOSE cur_batch; IF v_need 0 THEN SET p_result -3; -- 所有批次余量合计不足 ROLLBACK; LEAVE fifo_label; END IF; -- 更新总库存字段 UPDATE stock SET quantity quantity - p_quantity WHERE warehouse_id p_warehouse_id AND product_id p_product_id; COMMIT; SET p_result 1; END // DELIMITER ;这段代码包含一个很容易出错的点游标声明放在了变量声明之后、处理器声明之后也要放在变量声明之后。MySQL存储过程的声明顺序有严格要求DECLARE变量必须在DECLARE游标之前DECLARE CONTINUE HANDLER必须放在所有变量和游标声明之后。把CONTINUE HANDLER放在游标声明前面会导致语法错误。另外这里游标的SELECT语句带FOR UPDATE的意图是防止两个事务同时扣同一批次的余量。这个存储过程同样不会主动判断库存总量是否足够而是在循环结束之后检查v_need是否大于0。这种写法有一个好处如果批次表里一直没有数据虚拟表不会占用存储。注意这里只展示了扣减的主流程出库单状态更新可以在外层事务中再追加两条UPDATE语句把这支存储过程和状态流转包在同一个事务里。3.3 死锁排查一条排查SQL和两个经典规避策略存储过程里大量使用行锁和间隙锁死锁的概率会比增删改查高得多。最具代表性的死锁场景是事务A锁住批次1后想锁批次2事务B锁住批次2后想锁批次1两个事务互相等待InnoDB检测到死锁后会牺牲其中一个事务并回滚它。排查死锁的便捷方法是打开InnoDB的状态输出查看LATEST DETECTED DEADLOCK段落中的持有锁信息和等待锁信息。打开死锁日志的命令如下SHOW ENGINE INNODB STATUS;如果日志被截断可以把显示行数临时调大SET SESSION innodb_print_all_deadlocks ON;规避死锁的方法有两个主流的工程选择。第一种是让所有事务按照固定顺序访问资源比如先锁批次再锁总库存这个顺序全局统一第二种是把事务改成等值更新而非范围更新。我们proc_fifo_deduct里游标的查询条件带了warehouse_id和product_id这个条件组合命中的是唯一索引的等值条件InnoDB只会对匹配的索引项加记录锁间隙锁的影响被大幅缩小。还需要注意的一个细节是批量扣减时ORDER BY子句里如果有非索引字段InnoDB可能不走索引而走全表扫描此时锁的颗粒度可能扩大。建议批量扣减时只用主键或唯一索引字段排序。还有一个容易被忽略的规则事务里加锁的顺序必须与SQL执行顺序一致。比如先更新库存表再更新批次表和另一个事务先更新批次表再更新库存表这就是典型的交叉加锁会让死锁概率成倍上升。建议在存储过程注释里明确写上“固定的加锁顺序先批次表后总库存表”。4. 用Python和MySQL联调实现仓库管理系统的增删改查4.1 最小可用Pymysql连接代码与DBUtils连接池后端联调一般用Python配合pymysql库操作MySQL。为什么不直接用mysql-connector-python因为pymysql体积更小、兼容性好遇到不常见问题更容易查到解决方案。一个最基本的连接是建一个全局单例但课程设计建议直接上DBUtils连接池这样在高并发查询时不会每次重新建立TCP连接。先用pip安装依赖pip install pymysql dbutils连接池初始化如下from dbutils.pooled_db import PooledDB import pymysql POOL PooledDB( creatorpymysql, maxconnections10, mincached2, maxcached5, blockingTrue, host127.0.0.1, port3306, userroot, passwordyour_password, databasewms_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) def get_conn(): return POOL.connection()这里maxconnections设为10高峰期会话数可以到达10个blockingTrue表示连接池耗尽时请求排队等待而不是直接报错。DictCursor让查询结果以字典形式返回通过字段名直接取值可读性更高如果你的项目用于报表打印可以换用默认的元组游标速度更快一点。4.2 入库登记与库存初始化操作函数入库登记的逻辑是插入入库单主表、插入明细然后更新库存。下面的函数把事务控制写在了Python层通过contextlib.closing管理连接资源import contextlib def create_inbound_order(warehouse_id, supplier_id, items): # items: [(product_id, quantity), ...] with contextlib.closing(get_conn()) as conn: with conn.cursor() as cursor: conn.begin() try: order_no generate_order_no(IN) sql INSERT INTO inbound_order (order_no, warehouse_id, supplier_id, total_quantity, status) VALUES (%s, %s, %s, 0, 0) cursor.execute(sql, (order_no, warehouse_id, supplier_id)) total 0 for product_id, qty in items: cursor.execute( INSERT INTO inbound_item (order_id, product_id, quantity) VALUES (LAST_INSERT_ID(), %s, %s), (product_id, qty) ) cursor.execute( INSERT INTO stock (warehouse_id, product_id, quantity, available_qty, locked_qty) VALUES (%s, %s, %s, %s, 0) ON DUPLICATE KEY UPDATE quantity quantity %s, available_qty available_qty %s , (warehouse_id, product_id, qty, qty, qty, qty) ) total qty cursor.execute( UPDATE inbound_order SET total_quantity %s, finish_time NOW(), status 1 WHERE order_no %s, (total, order_no) ) conn.commit() return order_no except Exception as e: conn.rollback() raise e这段函数展示了INSERT与UPDATE的配合。LAST_INSERT_ID()在同一个连接会话内取到刚插入的入库单自增主键可以安全地给明细表做外键关联。INSERT INTO stock配合ON DUPLICATE KEY UPDATE处理了“首次入库和再次入库”两条路径首次入库时插入新行后续入库时则在唯一键冲突时做累加。有一个细节值得记住Python端事务的提交要和pymysql的autocommit配置分开看。我们在创建PooledDB时未显式设置autocommit默认是0所以每次操作结束必须显式commit如果不commit连接池归还连接后数据并未落盘下个请求可能查到旧值。更稳妥的写法是在连接池配置里直接加上autocommitTrue然后显式调用conn.begin()来管理事务边界。4.3 出库确认与可用量校验函数出库确认先调用前面写好的存储过程proc_lock_stock成功后再插入出库单保持锁定与出库状态的一致性。Python侧调用存储过程的写法def confirm_outbound(warehouse_id, items): with contextlib.closing(get_conn()) as conn: with conn.cursor() as cursor: conn.begin() try: store_result [] for product_id, qty in items: cursor.callproc(proc_lock_stock, (warehouse_id, product_id, qty, 0)) cursor.execute(SELECT _proc_lock_stock_3) result cursor.fetchone() code list(result.values())[0] if code ! 1: raise ValueError(f商品 {product_id} 锁定失败错误码{code}) store_result.append((product_id, qty)) order_no generate_order_no(OUT) cursor.execute( INSERT INTO outbound_order (order_no, warehouse_id, receiver_name, receiver_phone, status) VALUES (%s, %s, %s, %s, 1), (order_no, warehouse_id, 测试客户, 13900000000) ) for product_id, qty in store_result: cursor.execute( INSERT INTO outbound_item (order_id, product_id, quantity) VALUES (LAST_INSERT_ID(), %s, %s), (product_id, qty) ) conn.commit() return order_no except Exception: conn.rollback() raise调用callproc后必须执行SELECT _proc_lock_stock_3才能拿返回值其中proc_lock_stock_3是存储过程中第三个IN参数的变量名这里对应OUT参数p_success。该名称遵循“_存储过程名_参数索引”的规则参数索引从0开始p_success是第3个参数所以索引为3。这套设计里有一个被刻意保持的一致性出库单的状态在插入时直接置为1因为锁定库存的前置动作已经完成。如果从流程建模的角度做更细的拆解可以分两步先创建待出库单客户付款后再确认出库。课程设计不必追求这种复杂的业务状态机但建议在答辩文档里写清楚锁定与扣减之间的差异这是面试官容易追问的地方。4.4 综合查询库存汇总、收发存报表与导出数据写进去之后要能统计。一个经典的收发存报表需要把期初库存、本期入库、本期出库、期末库存集成在一张查询里。期初库存可以用子查询先算出时间点之前的累计入减出再把期间的流水聚合起来左连接。MySQL 8.0还支持窗口函数可以用SUM OVER配合ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW得到每日库存快照这在处理动态库存水位分析时比自连接高效得多。一个课程设计能撑住场面的查询是“每个仓库每个商品的月收发存汇总”。下面的SQL用两个独立的子查询分别聚合入库与出库再与库存表做主表关联SELECT w.name AS warehouse_name, p.name AS product_name, COALESCE(init.quantity, 0) AS init_qty, COALESCE(inb.total_in, 0) AS in_qty, COALESCE(oub.total_out, 0) AS out_qty, COALESCE(init.quantity, 0) COALESCE(inb.total_in, 0) - COALESCE(oub.total_out, 0) AS end_qty FROM stock st JOIN warehouse w ON st.warehouse_id w.id JOIN product p ON st.product_id p.id LEFT JOIN ( SELECT warehouse_id, product_id, quantity FROM stock ) init ON init.warehouse_id st.warehouse_id AND init.product_id st.product_id LEFT JOIN ( SELECT oi.product_id, i.warehouse_id, SUM(oi.quantity) AS total_in FROM inbound_item oi JOIN inbound_order i ON oi.order_id i.id WHERE i.status 1 AND i.finish_time 2024-06-01 AND i.finish_time 2024-07-01 GROUP BY oi.product_id, i.warehouse_id ) inb ON inb.product_id st.product_id AND inb.warehouse_id st.warehouse_id LEFT JOIN ( SELECT oi.product_id, o.warehouse_id, SUM(oi.quantity) AS total_out FROM outbound_item oi JOIN outbound_order o ON oi.order_id o.id WHERE o.status 1 AND o.finish_time 2024-06-01 AND o.finish_time 2024-07-01 GROUP BY oi.product_id, o.warehouse_id ) oub ON oub.product_id st.product_id AND oub.warehouse_id st.warehouse_id WHERE st.quantity 0 ORDER BY w.id, p.id;查询结果可以直接用pandas输出Excel报表考虑到这是数据库课程设计推荐使用SQL查询配合Python导出不要在前端用JS做二次计算。如果你使用的是DBeaver写好SQL后可以直接右键导出结果为CSV或Excel作为课程设计报告附录数据来源。5. 数据库同步、备份与课程设计验收的技巧课程设计接近尾声时指导老师最关注的四个点是能否演示数据持久性、能否说明异常恢复方案、是否了解数据库同步软件的实际作用、增删改查是否覆盖了多表关联操作。这里把验收阶段最可能被追问的知识点补齐。数据持久性要讲清楚binlog的角色。MySQL的binlog是二进制日志它记录的是“导致数据变更的逻辑语句”用于主从复制和时间点恢复。开启binlog后如果凌晨三点误删了表DBA可以根据全量备份加上binlog恢复到误删前的秒级时间点。课程设计的演示环境不一定配置主从但至少要在文档里写清楚同步方案。如果你安装了数据库同步软件并通过binlog消费变更事件那么同步工具本身不写业务表它只是binlog的下游消费者这个架构概念可以体现你对数据库生态的完整认知。备份命令建议在报告附录里放上mysqldump -u root -p wms_db wms_db_backup_$(date %Y%m%d).sql恢复的命令mysql -u root -p wms_db wms_db_backup_20240601.sql讲解时注意说明mysqldump备份默认在备份开始与结束之间会加全局读锁因此线上高并发场景建议使用--single-transaction参数配合InnoDB存储引擎实现一致性快照不影响业务的持续写入。例如mysqldump -u root -p --single-transaction --routines --triggers wms_db wms_db.sql--routines和--triggers参数对应导出存储过程、函数和触发器。如果你在报告里写了存储过程这里没导出这两项答辩时导入到另一台机器就会发现业务代码缺失。验收阶段最常见的三个坑提前规避第一外键约束报错导致不能删表要按反向顺序删除或有DISTINCT外键条件第二忘记导入存储过程调用时出现PROCEDURE NOT EXISTS错误第三分页查询没有加索引数据量到十万行时响应变慢需要在ORDER BY的字段上建联合索引。用DBeaver或Navicat做数据可视化对比分析时建议在验收之前跑一遍完整的“入库→锁定→出库→报表”链路把异常流程也演示一遍展示你对事务回滚机制的控制能力。最后留一个可以直接在MySQL命令行执行的验证脚本确认你的库处于健康状态SELECT COUNT(*) FROM stock; SELECT COUNT(*) FROM inbound_order WHERE status 1; SHOW PROCESSLIST;三条SQL分别验证基础数据量、业务状态流转和会话连接数如果入库出库完成后这三个数字都符合预期说明从数据表结构到存储过程再到应用层联调基本闭环可以为课程设计画上一个完整的技术句号。本文还有配套的精品资源点击获取
分享:

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

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