简介:这是一份面向高校计算机及数据库相关专业学生的数据库课程设计参考文档,以花店管理系统为业务场景,完整呈现关系数据库从系统调研、需求分析到实施维护的一般流程。文档根据大连交通大学课程设计任务展开,系统梳理了需求分析、概念结构设计、逻辑结构设计、物理设计以及系统调试维护等关键阶段,重点涉及 IBM DB2 环境下 SQL 语言的应用,以及数据字典与流程图设计、E-R 图向关系模型转换、关系模式优化、表空间与索引建立、触发器设计等内容,目录包含绪论、需求分析、概念结构设计、逻辑结构设计、数据库物理设计和数据库实施等模块,并给出花店商品、采购、店员等信息的库表操作思路,适合作为课程设计报告撰写、实验操作或数据库实践入门的配套参考。资源为单个 doc 文档,大小约 630KB,内容集中、结构紧凑便于直接查阅。该文档已有 261 人学习下载,对正在完成同类课程设计或希望掌握数据库设计方法的学生具有实际参考价值。
1. 花店管理系统设计:一个课程设计题目背后的完整数据库课
数据库课程设计最经典的选题之一就是「花店管理系统设计」。它看起来只是教务系统里常见的作业题目,但实际上把数据库建模、SQL 编写、应用连接、事务并发这些面试必问的点全部串了起来。我见过不少学生花两周写代码,最后却因为表结构设计不合理、外键乱挂、报告里没有 ER 图被老师打回重做。这篇笔记就顺着这个标题,把从需求分析到表结构、再到 JDBC 落地和报告答辩的完整路径讲清楚。正在做课程设计的学生可以照着一路走下来,想练手的开发者也可以直接复用这套库表设计。只要按着步骤把 ER 图、建表 SQL、连接池和事务代码跑通,那份 .doc 报告写起来也就是水到渠成的事。
2. 把花店业务拆进 ER 图:需求分析与数据建模
2.1 先画业务模块,再谈建表:一份可复用的功能拆分
课程设计最忌讳一上来就写 CREATE TABLE。花店业务虽然不大,但线头很全:客户要下单,店员要维护花材库存,供应商要供货,老板要看销售统计。我一般是先画业务模块图,把系统拆成六块:客户管理、花材管理、库存管理、订单管理、供应商管理、员工与权限管理。再补上系统登录和统计报表,功能清单就完整了。下面这份表可以直接铺进文档里的“功能模块设计”一节。
| 功能模块 | 核心业务点 | 主要数据表 | 关键操作 |
|---|---|---|---|
| 客户管理 | 注册、信息维护、等级 | customer | 增删改查、条件查询 |
| 花材管理 | 花材信息、价格、分类 | flower | 增删改查、模糊搜索 |
| 库存管理 | 入库、出库、库存预警 | stock_in_record、flower.stock | 事务更新、联合查询 |
| 订单管理 | 下订单、明细、统计 | orders、order_item | 主表明细表联动 |
| 供应商管理 | 供应商信息、供货记录 | supplier | 增删改查、级联查询 |
| 员工权限 | 登录、角色区分 | employee | 连接查询、权限字段判断 |
这个表直接对应报告里的“功能模块图”那一页。每个模块的数据库操作都落在增删改查上,但课程设计评分拉开差距的往往不是 CRUD 本身,而是表之间的关联怎么设计。花材和订单是典型的多对多关系:一个订单能包含多种花材,一种花材也能出现在多个订单里。所以订单要拆成 orders 主表和 order_item 明细表,主表存订单编号、客户、日期、总价,明细表存花材编号、数量、单价。这个“主表 + 明细表”的做法是所有管理系统的地基,也是老师最愿意细看的地方。
2.2 ER 图转关系模式:三个最容易被扣分的知识点
ER 图画起来不难,难的是往关系模式转。常见的坑有三个:一是多对多关系漏掉中间表,直接把花材编号塞进订单表,导致一个订单只能买一种花;二是外键该建在哪个表没想清楚,比如“入库记录”到底挂在 supplier 上还是 flower 上;三是主键选得不合适,用了花材名称当主键,改个名就全崩。这些其实都是数据库基础知识里的常规问题,但放在真实业务里,一下就能看出谁是真懂、谁在背概念。
我的约定是:customer_id、flower_id、supplier_id、employee_id、order_id 这些自增主键全叫“业务无关主键”;外键统一放在“多”的那一边。订单明细表里用 order_id + flower_id 做联合主键,天然限制同一笔订单里不能重复录入同一种花材。供应商和花材是典型的“一”对“多”,所以 flower 表里放 supplier_id 外键,而不是反过来。客户和订单是一对多,orders 表里放 customer_id 外键。这样画出的 ER 图转成表格结构,基本不用返工。
下面这张表列一版我常用的核心表,字段命名和约束可以直接抄进你的数据字典。这里不追求表很多,七张表足够覆盖一个花店的核心业务,也足够让老师看清你的建模能力。
| 表名 | 字段 | 类型 | 约束与说明 |
|---|---|---|---|
| customer | customer_id | INT | 主键,自增 |
| customer | customer_name | VARCHAR(50) | 非空 |
| customer | phone | VARCHAR(20) | 唯一,非空 |
| customer | address | VARCHAR(200) | 可空 |
| customer | created_at | DATETIME | 默认当前时间 |
| flower | flower_id | INT | 主键,自增 |
| flower | flower_name | VARCHAR(100) | 非空 |
| flower | price | DECIMAL(10,2) | 非空,大于 0 |
| flower | stock | INT | 非空,默认 0 |
| flower | supplier_id | INT | 外键,关联 supplier |
| supplier | supplier_id | INT | 主键,自增 |
| supplier | supplier_name | VARCHAR(100) | 非空 |
| orders | order_id | INT | 主键,自增 |
| orders | customer_id | INT | 外键,关联 customer |
| orders | order_time | DATETIME | 默认当前时间 |
| orders | total_amount | DECIMAL(10,2) | 非空 |
| order_item | order_id | INT | 联合主键,外键 |
| order_item | flower_id | INT | 联合主键,外键 |
| order_item | quantity | INT | 非空,大于 0 |
| order_item | unit_price | DECIMAL(10,2) | 快照单价 |
| employee | employee_id | INT | 主键,自增 |
| employee | username | VARCHAR(50) | 唯一 |
| employee | role | VARCHAR(20) | 如 ADMIN / STAFF |
| stock_in_record | record_id | INT | 主键,自增 |
| stock_in_record | flower_id | INT | 外键 |
| stock_in_record | supplier_id | INT | 外键 |
| stock_in_record | quantity | INT | 非空 |
| stock_in_record | in_time | DATETIME | 默认当前时间 |
这张表就是报告里的“数据字典”。注意 order_item 里保存了 unit_price,这是下单那一刻的价格快照。如果以后花材涨价或降价,订单里存的还是当时的成交价,报表统计才不会跟着现价漂移。这个“快照字段”的细节在答辩时提一句,老师会认为你确实理解了业务,而不是只会照着模板写字段。
写完表结构后,还要顺手做一次范式检查。我的检查顺序是:先看有没有重复组,再看非主属性是否完全依赖主键,最后看有没有传递依赖。花材表和供应商表之间没有冗余字段,orders 和 order_item 分离后,每个非主属性都完全依赖联合主键,这几张表基本满足 3NF。数据库基础知识里最常被抽问的范式,在这里就落到了具体表上,比单纯背定义管用得多。
3. 从 ER 图到能跑的 MySQL 库:建表 SQL 与完整性约束
3.1 一份可以直接执行的建库建表脚本
下面这段 SQL 我在 MySQL 8.0 上跑过,可以直接当成课程设计项目的初始化脚本。字符集用 utf8mb4,避免花束名称里的特殊符号变成问号;存储引擎统一用 InnoDB,外键和事务才能生效。我习惯把建库和建表拆成两段,先执行建库,再执行后面的表结构。
-- 建库:字符集和排序规则一次性设好 CREATE DATABASE IF NOT EXISTS flower_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE flower_shop; -- 客户表:phone 加唯一约束,避免重复注册 CREATE TABLE customer ( customer_id INT AUTO_INCREMENT PRIMARY KEY, customer_name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL UNIQUE, address VARCHAR(200) NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT '1-正常 0-停用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;customer_id 设为自增主键,因为业务里没有适合做主键的自然属性。phone 加唯一约束,一是防止同一个手机号被重复录入,二是条件查询时可以直接命中这个唯一索引。status 用 TINYINT 而不是字符串,想停用某个客户时只需要 UPDATE 一行,不删除历史数据,这就是软删除的思路。好处是在订单关联查询里不会因为删了客户而丢失订单。
-- 供应商表:先于 flower 创建 CREATE TABLE supplier ( supplier_id INT AUTO_INCREMENT PRIMARY KEY, supplier_name VARCHAR(100) NOT NULL, contact_phone VARCHAR(20) NULL, address VARCHAR(200) NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 花材表:价格用 DECIMAL,库存用 INT,外键连供应商 CREATE TABLE flower ( flower_id INT AUTO_INCREMENT PRIMARY KEY, flower_name VARCHAR(100) NOT NULL, category VARCHAR(50) NULL, price DECIMAL(10,2) NOT NULL CHECK (price > 0), stock INT NOT NULL DEFAULT 0, supplier_id INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_flower_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ON UPDATE CASCADE ON DELETE RESTRICT, INDEX idx_flower_name (flower_name), INDEX idx_flower_supplier (supplier_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段 SQL 里先写 supplier 是因为 flower 的外键引用它,执行顺序反了会报“表不存在”。价格用 DECIMAL(10,2) 而不是 FLOAT,因为浮点数在累计求和时会产生 0.1 + 0.2 这种对账差。CHECK (price > 0) 在 MySQL 8.0.16 之后才真正强制生效,8.0.16 之前只是语法接受、运行时不检查,这一点要在报告里写明你用的 MySQL 版本。外键用 ON DELETE RESTRICT,目的就是不让订单明细引用的花材被随意删掉。
-- 订单表:主表只放客户和时间,总金额在应用层计算后写入 CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, customer_id INT NOT NULL, order_time DATETIME DEFAULT CURRENT_TIMESTAMP, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表:联合主键保证同一订单不重复录入同一花材 CREATE TABLE order_item ( order_id INT NOT NULL, flower_id INT NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), unit_price DECIMAL(10,2) NOT NULL, PRIMARY KEY (order_id, flower_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(order_id) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_item_flower FOREIGN KEY (flower_id) REFERENCES flower(flower_id) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 员工表:单独拆出来,避免把所有字段堆在 customer 里 CREATE TABLE employee ( employee_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT '存加盐哈希,不存明文', role VARCHAR(20) NOT NULL DEFAULT 'STAFF' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 入库记录表:每次补货记一条流水 CREATE TABLE stock_in_record ( record_id INT AUTO_INCREMENT PRIMARY KEY, flower_id INT NOT NULL, supplier_id INT NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), in_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_stock_flower FOREIGN KEY (flower_id) REFERENCES flower(flower_id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_stock_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段代码里最值得注意的设计有两个:order_item 的 unit_price 是快照,不从 flower 反查,避免后续改价影响历史订单;stock_in_record 独立成表,库存变化有迹可循,老师问“库存怎么溯源”时可以直接展示这张表。员工表和客户表分开,是因为登录权限和客户信息的字段、安全要求完全不同,硬塞在同一张表里会违反字段语义单一原则。
3.2 外键、索引与约束:哪些地方能省,哪些地方不能省
外键删起来很麻烦,很多生产项目为了灵活性会禁用外键,靠应用层保证一致性。但课程设计不推荐这么做,评分标准里通常明确写了“完整性约束设计”,而且在数据库基础知识的面试题里,外键机制是高频考点。不过外键也不是乱加:我见过有人把 order_time 也建了普通索引,纯属浪费。
真正该建索引的位置就三类:主键和外键列、频繁出现在 WHERE 条件里的字段、ORDER BY 或 GROUP BY 用到的列。建表脚本里 idx_flower_name 就是给花材名做模糊搜索准备的,idx_flower_supplier 是外键列的辅助索引,虽然 InnoDB 会自动给外键列建索引,但手动标注可以在报告里把“数据库优化”这个点讲得更清楚。索引不是越多越好,每一份索引都占用磁盘空间、拖慢写入速度。
外键的 ON DELETE 策略要按业务逐一选。orders 和 order_item 之间用 CASCADE,因为主表订单删了,明细没有存在意义;flower 和 supplier 之间用 RESTRICT,因为供应商一旦有花材关联就不能直接删除,只能停用;customer 和 orders 之间也用 RESTRICT,防止删客户连带删掉历史订单。如果全用 CASCADE,删一个供应商就会连带删除一批花材,这是课程设计里典型的“看起来很痛快、实际很危险”的写法。把每个外键的级联策略写进数据字典,答辩时被问到的概率极高。
4. 用 Java + MySQL 把系统跑起来:连接池与增删改查的事务边界
4.1 数据源配置:别再手动 new Connection 了
到了编码阶段,我最常看到的写法是每个 DAO 方法里都写一遍 Class.forName,再 DriverManager.getConnection。功能测试勉强能过,但只要系统一开销量,数据库连接就不够用了。正确的做法是用连接池。这里以 HikariCP 为例,因为它配置最少,Spring Boot 默认也用它。即使你的课程设计要求手写 JDBC,也可以用 DataSource 接口无缝替换。
<!-- pom.xml 里的关键依赖 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>版本号可以按你本机环境调整,但注意 MySQL Connector/J 8.x 的驱动类名是 com.mysql.cj.jdbc.Driver,不是老版本那个 com.mysql.jdbc.Driver。用错了会直接报“找不到驱动类”。连接池参数里最影响课程设计的是三个:maximumPoolSize 建议设为 10,连接一多就自动排队,避免把 MySQL 打趴;minimumIdle 保持默认;connectionTimeout 设成 30000 毫秒,连接获取超过 30 秒就报超时而不是无限等。
// HikariCP 配置示例:把连接信息集中在一个类里 private static DataSource createDataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/flower_shop?useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("your_password"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); config.setPoolName("FlowerShopPool"); return new HikariDataSource(config); }JdbcUrl 里如果漏掉 serverTimezone,MySQL 8 会拿默认时区去转换 DATETIME,时间差 8 个小时,属于玄学级别的报错,排查半天才发现是时区问题。连接池创建完成后,DAO 里从 DataSource 拿 Connection,用完调用 close 归还而不是真正关闭。这个“从池里借用、用后归还”的思维,能避免一大半连接泄漏问题。配合 try-with-resources,连接归还的代码会自动执行。
4.2 订单落库:事务、行锁与库存扣减
订单的完整操作是典型的事务:要往 orders 插主表,再往 order_item 插明细,同时还要把 flower.stock 减掉。这三步任何一步失败都要回滚,否则就会出现订单明细写了一半、库存却已扣减的脏数据。下面这段代码是一个核心的购买流程,直接体现事务边界。
public boolean createOrder(int customerId, List<OrderItemInput> items) throws SQLException { String insertOrder = "INSERT INTO orders(customer_id, total_amount) VALUES (?, ?)"; String insertItem = "INSERT INTO order_item(order_id, flower_id, quantity, unit_price) VALUES (?, ?, ?, ?)"; String deductStock = "UPDATE flower SET stock = stock - ? WHERE flower_id = ? AND stock >= ?"; Connection conn = dataSource.getConnection(); boolean autoCommit = conn.getAutoCommit(); try { conn.setAutoCommit(false); int orderId = 0; try (PreparedStatement psOrder = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS)) { psOrder.setInt(1, customerId); psOrder.setBigDecimal(2, computeTotal(items)); psOrder.executeUpdate(); try (ResultSet generatedKeys = psOrder.getGeneratedKeys()) { if (generatedKeys.next()) { orderId = generatedKeys.getInt(1); } } } for (OrderItemInput item : items) { // 关键点:带条件更新,影响行数为 0 说明库存不足 try (PreparedStatement psStock = conn.prepareStatement(deductStock)) { psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getFlowerId()); psStock.setInt(3, item.getQuantity()); if (psStock.executeUpdate() == 0) { throw new SQLException("库存不足,花材ID:" + item.getFlowerId()); } } try (PreparedStatement psItem = conn.prepareStatement(insertItem)) { psItem.setInt(1, orderId); psItem.setInt(2, item.getFlowerId()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getUnitPrice()); psItem.executeUpdate(); } } conn.commit(); return true; } catch (SQLException ex) { conn.rollback(); throw ex; } finally { conn.setAutoCommit(autoCommit); conn.close(); } }这段代码里有几个地方需要看仔细。第一条是 PreparedStatement 全部参数化,所有的值都用 setXxx 传入,不拼接字符串,这是最基本也最有效的防 SQL 注入手段。第二条是 RETURN_GENERATED_KEYS 用来拿订单自增主键,拿到之后才能在明细表里写 order_id。第三条是库存扣减的 SQL 里带AND stock >= ?,这相当于是数据库并发锁的落地:执行 UPDATE 时,InnoDB 会对 flower 表的这一行加排他锁,两个并发事务同时扣同一朵花的库存,后一个事务会阻塞,直到前一个提交或回滚,所以不会出现超卖。
如果不用这个写法,先 SELECT 查库存,再 UPDATE 扣库存,两个事务都读到 stock=10,都试图扣 3,最后库存可能变成 7 而不是 4,这就是典型的并发超卖。课程设计的演示环境可能测不出来,但老师一旦问“两个订单同时买最后三枝玫瑰怎么办”,这条带条件的 UPDATE 就是你最好的回答。注意这里没有显式 SELECT FOR UPDATE,因为条件更新本身就带了行锁。
4.3 登录查询与统计报表:参数绑定 + 聚合 SQL
登录和报表同样不要用 Statement 拼字符串。登录查询的常见写法是“SELECT * FROM employee WHERE username = ? AND password = ?”,参数绑定交给 PreparedStatement。重点在于 password 字段不能在数据库里存明文,课程设计也应至少做个 SHA-256 加盐哈希,否则报告里的“安全管理”一栏会显得很空。统计报表通常用聚合函数,比如按月份看销售额。
-- 近 30 天按花材汇总销量 SELECT f.flower_name, SUM(oi.quantity) AS sold_count, SUM(oi.quantity * oi.unit_price) AS sales_amount FROM order_item oi JOIN flower f ON oi.flower_id = f.flower_id JOIN orders o ON oi.order_id = o.order_id WHERE o.order_time >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 30 DAY) GROUP BY f.flower_name ORDER BY sold_count DESC LIMIT 10;这条查询里用了 JOIN 把三张表接起来,用了 SUM 做求和,用了 DATE_SUB 做时间偏移,是答辩时最好用的一段 SQL。GROUP BY 之后 ORDER BY 的是聚合列 sold_count,不是原始字段,这是一个容易写错的地方。如果数据量一大发现报表查询慢,优先看 order_item 的 order_id 和 flower_id 上的索引,索引覆盖到了 JOIN 条件,查询性能基本够用。
5. 花店系统落地时的五个翻车现场:从外键删不掉到库存超卖
5.1 花材删不掉:外键约束与删除顺序的恩怨
现象:在后台点删除一个花材,系统报错,说“不能在子表存在关联记录时删除父表”,甚至直接把程序卡在那一行。
原因:order_item 和 stock_in_record 都外键引用了 flower,ON DELETE 设置的是 RESTRICT 或 CASCADE 的情况各不相同。如果订单明细里已经存在这个花材,RESTRICT 会拒绝删除,这是数据库在保护数据完整性,不是程序 bug。
解决:不要在业务里做物理删除。给 flower 表加一个 status 或 is_deleted 字段,删除动作改成 UPDATE flower SET status = 0。查询列表时默认过滤 status = 1,历史订单仍然能够正常关联。这样既保住了外键约束,又保持了数据可追溯,课程设计的“删除”功能也更好演示。
5.2 库存扣成负数:SELECT 后 UPDATE 的并发超卖
现象:测试时一个订单只买 3 枝玫瑰,库存显示 10,下单成功后库存却变成了 4,而不是 7;多开几个窗口同时下单时尤其明显。
原因:代码里先执行 SELECT stock FROM flower WHERE flower_id = ?,拿到数量后在 Java 里判断够不够,再执行 UPDATE flower SET stock = stock - ?。两个事务同时读到旧值,判断时都够,然后都去扣,最后一次扣减直接把库存覆盖成负数。
解决:把判断和扣减合并成一条带条件的 UPDATE,也就是上一章写的UPDATE flower SET stock = stock - ? WHERE flower_id = ? AND stock >= ?。这条语句在数据库层面用行锁保证同一时刻只有一个事务在改这一行,第二个事务会等第一个提交后才继续,看到库存不足就返回 0 行,事务回滚。这个方案实际也是在回答“数据库并发锁”的面试题,值得好好写进报告。
5.3 FLOAT 金额对账不平:钱别用浮点数
现象:演示订单列表时,总金额和明细里每一项加起来差了 0.01 元甚至 0.02 元;统计报表一多,偏差越积越大。
原因:price 字段建表时用了 FLOAT 或 DOUBLE。浮点数用二进制表示十进制小数时本身有误差,单条记录看起来没问题,SUM 一大就暴露了。这是计算机组成和数据库基础知识的经典坑。
解决:所有金额字段统一用 DECIMAL(10,2)。Java 侧对应 BigDecimal,不要用 double 去接收。写入时用 psBigDecimal,读取时 getBigDecimal。课程设计报告里如果能写一句“金额采用定点数 DECIMAL 存储以避免浮点误差”,会比大段废话有用得多。
5.4 连接池连接耗尽:Connection 没有归还的乌龙
现象:系统运行十几分钟后报“Connection is not available, request timed out”,重启程序又好了,过一会儿又报错。
原因:某段代码里手动 new 了 Connection,用完后没有 close。连接池里的 10 个连接全被借走,而且一直不归还,新的请求只能排队到超时。最常见的就是 DAO 里 catch 到异常直接 return,把 finally 里的 close 跳过去了。
解决:全面检查取连接的地方,把 Connection、Statement、ResultSet 全部放进 try-with-resources,或者确保 finally 里 close。如果用了 Spring 的 JdbcTemplate,它自己会管理连接,但也要在配置里把 maximumPoolSize 调到合适值。排查时可以执行 SHOW PROCESSLIST,看到大量 Sleep 状态的连接,基本就是泄漏了。
5.5 死锁日志一堆:事务更新顺序不统一
现象:两个线程同时操作时,数据库偶尔报 Deadlock found when trying to get lock,然后整个事务回滚,页面弹错。
原因:事务 A 先更新订单明细再扣库存,事务 B 先扣库存再更新订单明细,两边互相持有对方要的锁,形成循环等待。InnoDB 会检测到死锁并强制回滚其中一个事务,对用户来说就是偶发的失败。
解决:所有事务里涉及多张表的更新操作,必须按同一个顺序加锁。以第二个版本代码为例,先扣 flower 的库存,再写 order_item 明细,最后更新 orders 主表。这个顺序写成团队约定,全项目统一。如果用的是 Spring 事务注解,还可以额外设置一个重试,捕获 DeadlockLoserDataAccessException 后重新执行一次,但对课程设计来说,统一顺序已经足够稳定。
6. 用验证 SQL 和演示数据把报告做到能打:答辩前的检查习惯
答辩前我习惯准备三组演示数据,分别覆盖正常下单、库存不足报错、供应商缺货停用三个场景。库存不足的演示最容易翻车:如果库存字段随便填了一个很大的数,老师看不到你的事务回滚逻辑,会觉得你只是写了个增删改查。正确做法是提前把某一朵花材的库存改成 1,再演示一次买 2 枝,让系统当场报错,这一步能直观展示事务和业务校验的价值。
我还会准备两条验证 SQL,一条统计近 30 天销量,一条查客户消费排行,作为“系统能跑、数据能看”的证据。具体做法是先把演示数据插进去,再执行下面的查询,把结果截图放进报告。
-- 客户消费排行:谁才是花店的大客户 SELECT c.customer_name, COUNT(DISTINCT o.order_id) AS order_count, SUM(oi.quantity * oi.unit_price) AS total_spent FROM customer c LEFT JOIN orders o ON c.customer_id = o.customer_id LEFT JOIN order_item oi ON o.order_id = oi.order_id GROUP BY c.customer_id, c.customer_name ORDER BY total_spent DESC;注意这里的 LEFT JOIN 保留了没有买过东西的会员,避免被 COUNT 和 SUM 天然过滤掉。GROUP BY 中同时写了主键和姓名,符合“select 非聚合列必须包含在 group by 中”的规范,MySQL 8 默认 sql_mode 下不这样做会直接报错。
最后一条是我自己的习惯:写报告前把每张表的“为什么这样设计”在文档里注释一遍,比如联合主键为什么选这两个字段、单位价格为什么单独存。这样做不是为了给别人看,而是防止答辩时老师追问一句“这个字段为什么不放那一边”就卡壳。花店管理系统这个题目能挖的点其实很多,数据库面试题里那些范式、锁、索引、事务,都能在这套表上找到对应案例。把每个设计决策讲出原因,这份课程设计就不仅仅是能跑,而是真正值回票价。希望这些踩过的坑能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取