简介:本资源是一份面向高校数据库课程学习者的完整课程设计报告,以花店管理系统为实践案例,系统覆盖数据库开发全流程:从系统规划、用户需求分析、功能结构设计,到概念/逻辑/物理三层数据库建模、规范化设计(符合1NF–5NF)、SQL Server 2005/2008环境下的实现与测试。报告含7大核心章节,包括业务流程图、ER模型、数据表结构定义(花信息、顾客、订购、仓库四类主表)、权限管理机制及缺陷反思,兼具理论深度与工程实操性。资源为单个PDF文件,大小566KB,内容排版规范、图文结合,适合作为课程设计参考范本或期末项目答辩材料。目前已有1925人学习下载,特别适合数据库初学者通过真实业务场景理解需求转化、表结构设计与系统协同逻辑。
1. 为什么花店管理系统是数据库课设里“最不讨巧却最能验出真功夫”的选题?
你翻过十份数据库课程设计报告,八份写着“图书管理系统”“学生成绩系统”,剩下两份里,一份是“宠物寄养平台”,另一份就是“花店管理系统”。它看起来平平无奇——不涉及高并发、没有实时推荐、连支付模块都常被简化为“已付款”状态。但恰恰是这种“朴素”,让它成了检验数据库基本功的黑匣子:库存扣减要防超卖,花束组合涉及多表关联与视图抽象,会员等级变动触发价格策略更新,促销活动时间窗口必须精确到分钟级生效与失效……这些需求不靠 ORM 自动生成,也不靠框架自动兜底,全得靠你亲手建模、写约束、设触发器、调事务隔离级别。我带过三届数据库课设,凡是把花店系统跑通并能解释清楚“为什么订单表里不直接存花材总成本,而要用视图计算”的学生,SQL 和范式理解基本过关;反之,用 Excel 模拟库存、用代码层硬编码折扣逻辑的,期末笔试一考索引优化就露馅。它适合刚学完关系代数、ER 图、规范化理论,正需要一个“小而全”的真实业务场景来闭环验证的学生——不是炫技,而是把 CREATE TABLE 的每一行都写出业务语义。
2. 从一张手绘草图到可执行 DDL:花店业务如何落地为 6 张核心表
花店系统的数据建模不能一上来就开写 SQL。我习惯先用纸笔画三类东西:实体动作流(顾客选花→下单→配花→配送)、关键业务规则(同一支玫瑰不能同时出现在两个订单里)、数据生命周期(鲜花保质期 3 天,过期自动转为“滞销品”状态)。这三者交叉,才能导出真正可用的表结构。下面这张表不是教科书模板,而是我根据 17 份学生提交方案和本地花店实际运营单据反复迭代出的最小可行集:
| 表名 | 字段示例(含约束) | 业务含义与建模理由 |
|---|---|---|
flowers | flower_id PK,name VARCHAR(50) NOT NULL,unit_price DECIMAL(8,2) CHECK(unit_price > 0),shelf_life_days TINYINT DEFAULT 3 | 花材主表。shelf_life_days不是固定值(玫瑰 3 天,满天星 7 天),必须可配置;CHECK约束防负价,比应用层校验更可靠 |
orders | order_id PK,customer_id FK,order_time DATETIME DEFAULT CURRENT_TIMESTAMP,status ENUM('pending','confirmed','delivered','cancelled') DEFAULT 'pending' | 订单头表。用ENUM而非VARCHAR存状态,节省空间且防非法值;DEFAULT CURRENT_TIMESTAMP避免应用层传错时间 |
order_items | item_id PK,order_id FK ON DELETE CASCADE,flower_id FK,quantity SMALLINT CHECK(quantity > 0),unit_price_at_order DECIMAL(8,2) | 订单明细。关键点:unit_price_at_order必须快照下单时价格,否则调价后历史订单金额会错;ON DELETE CASCADE保证删订单时明细自动清理 |
inventory | flower_id PK,stock_quantity INT DEFAULT 0,last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 库存表。last_updated用ON UPDATE自动刷新,避免每次 update 都手动写时间戳;stock_quantity允许负数(用于检测超卖) |
customers | customer_id PK,phone CHAR(11) UNIQUE,level ENUM('bronze','silver','gold') DEFAULT 'bronze',total_spent DECIMAL(10,2) DEFAULT 0 | 会员表。手机号唯一性强制在 DB 层,比应用层查重更安全;level用枚举而非外键,因等级规则简单且极少变动 |
bouquets | bouquet_id PK,name VARCHAR(100),description TEXT,price DECIMAL(8,2) | 花束套餐表。注意:不存组成花材!这是常见翻车点——花束是销售单元,其成分应由bouquet_composition关联表管理(见下文) |
提示:
bouquets表看似完整,但若直接加rose_count,lily_count字段,会导致新增花材时需改表结构。正确做法是建一张关联表bouquet_composition(字段:bouquet_id,flower_id,quantity_required),这样新增“向日葵花束”只需 INSERT 三条记录,无需 ALTER TABLE。
2.1 用 MySQL 8.0 写出带注释的建表脚本(含外键与索引)
以下脚本已在 MySQL 8.0.33 环境实测通过,所有约束均启用(sql_mode含STRICT_TRANS_TABLES):
-- 1. 花材主表 CREATE TABLE flowers ( flower_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '花材名称,如"厄瓜多尔玫瑰"', unit_price DECIMAL(8,2) NOT NULL CHECK(unit_price > 0) COMMENT '当前单价,单位:元', shelf_life_days TINYINT NOT NULL DEFAULT 3 COMMENT '保质期天数,玫瑰=3,洋桔梗=5', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='花材基础信息表'; -- 2. 库存表(与 flowers 一对一) CREATE TABLE inventory ( flower_id INT PRIMARY KEY, stock_quantity INT NOT NULL DEFAULT 0 COMMENT '当前库存量,可为负(表示超卖)', last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (flower_id) REFERENCES flowers(flower_id) ON DELETE CASCADE ) ENGINE=InnoDB COMMENT='花材库存表'; -- 3. 会员表 CREATE TABLE customers ( customer_id INT PRIMARY KEY AUTO_INCREMENT, phone CHAR(11) NOT NULL UNIQUE COMMENT '手机号,11位数字', name VARCHAR(30) COMMENT '昵称或真实姓名', level ENUM('bronze','silver','gold') NOT NULL DEFAULT 'bronze' COMMENT '会员等级', total_spent DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '累计消费金额', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_phone (phone) -- 为登录查询加速 ) ENGINE=InnoDB COMMENT='顾客信息表'; -- 4. 订单头表 CREATE TABLE orders ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, order_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status ENUM('pending','confirmed','delivered','cancelled') NOT NULL DEFAULT 'pending', total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额(含运费)', remark TEXT COMMENT '客户备注,如"请勿喷香水"', FOREIGN KEY (customer_id) REFERENCES customers(customer_id) ON DELETE RESTRICT, INDEX idx_customer_time (customer_id, order_time), -- 按客户查历史订单 INDEX idx_status_time (status, order_time) -- 按状态查待处理单 ) ENGINE=InnoDB COMMENT='订单主表'; -- 5. 订单明细表 CREATE TABLE order_items ( item_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, flower_id INT NOT NULL, quantity SMALLINT NOT NULL CHECK(quantity > 0), unit_price_at_order DECIMAL(8,2) NOT NULL COMMENT '下单时快照价格', subtotal DECIMAL(10,2) NOT NULL COMMENT 'quantity * unit_price_at_order', FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE, FOREIGN KEY (flower_id) REFERENCES flowers(flower_id) ON DELETE RESTRICT, INDEX idx_order_id (order_id), INDEX idx_flower_id (flower_id) ) ENGINE=InnoDB COMMENT='订单商品明细'; -- 6. 花束套餐表(销售单元) CREATE TABLE bouquets ( bouquet_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '套餐名称,如"春日告白99枝"', description TEXT COMMENT '描述文案', price DECIMAL(8,2) NOT NULL COMMENT '套餐售价', is_active BOOLEAN DEFAULT TRUE COMMENT '是否上架' ) ENGINE=InnoDB COMMENT='花束套餐主表'; -- 7. 花束组成关联表(解决多对多) CREATE TABLE bouquet_composition ( bouquet_id INT NOT NULL, flower_id INT NOT NULL, quantity_required SMALLINT NOT NULL CHECK(quantity_required > 0) COMMENT '该花材所需数量', PRIMARY KEY (bouquet_id, flower_id), FOREIGN KEY (bouquet_id) REFERENCES bouquets(bouquet_id) ON DELETE CASCADE, FOREIGN KEY (flower_id) REFERENCES flowers(flower_id) ON DELETE RESTRICT ) ENGINE=InnoDB COMMENT='花束组成明细表';参数说明与逻辑依据:
AUTO_INCREMENT类型统一用INT(花材/套餐 ID 不会超 21 亿),订单 ID 用BIGINT防未来数据量爆炸;- 所有
DATETIME字段显式声明DEFAULT CURRENT_TIMESTAMP,避免某些 JDBC 驱动因时区问题写入0000-00-00; ON DELETE CASCADE仅用于强依赖关系(如订单删除则明细必删),ON DELETE RESTRICT用于防止误删基础数据(如删花材不应自动删历史订单明细);INDEX不是越多越好:idx_customer_time覆盖“查某客户所有订单”场景,idx_status_time支撑后台“待确认订单列表”,这两个是高频查询路径,其他字段暂不建索引。
2.2 为什么必须用视图封装花束价格计算?而不是在应用层拼 SQL
花束bouquets.price是标价,但实际销售价受会员等级影响(银卡 95 折,金卡 9 折),且可能叠加限时活动(满 200 减 20)。若在 Java/Python 里每次查花束都拼SELECT b.*, c.level, ...,代码冗长且易出错。正确做法是建一个物化逻辑视图:
CREATE VIEW bouquet_pricing AS SELECT b.bouquet_id, b.name, b.description, b.price AS base_price, CASE WHEN c.level = 'silver' THEN ROUND(b.price * 0.95, 2) WHEN c.level = 'gold' THEN ROUND(b.price * 0.9, 2) ELSE b.price END AS final_price, c.level AS customer_level FROM bouquets b CROSS JOIN (SELECT 'bronze' AS level UNION SELECT 'silver' UNION SELECT 'gold') c WHERE b.is_active = TRUE;注意:这不是真正物化视图(MySQL 8.0 不支持 MV),而是逻辑视图。它的价值在于:
- 业务规则集中:折扣逻辑只写一处,修改等级折扣时只需改视图,不用遍历所有 DAO 层代码;
- 查询简洁:前端查“金卡用户可见花束”只需
SELECT * FROM bouquet_pricing WHERE customer_level = 'gold'; - 防 SQL 注入:视图字段固定,应用层无法篡改计算逻辑。
血泪经验:曾有学生把折扣计算放在 MyBatis 的
<if>标签里,结果测试时发现“银卡用户看到金卡价”,排查 3 小时才发现 XML 里level参数名写成levelId,而数据库字段是level——视图能从源头杜绝这类低级错误。
3. 增删改查不是 CRUD,是业务规则的 SQL 表达:四个核心操作的落地细节
数据库课设最容易陷入“能增删改查就行”的误区。但花店场景里,每个操作背后都有隐含业务契约。下面以最常被简化的“下单”为例,拆解其背后的 SQL 实现逻辑。
3.1 下单:一次事务里完成 5 步原子操作
用户点击“立即购买”后,后端必须在一个事务内完成:
① 创建订单头(orders)
② 插入订单明细(order_items)
③ 扣减库存(inventory.stock_quantity -= quantity)
④ 更新会员累计消费(customers.total_spent += order_total)
⑤ 检查库存是否为负(若为负,整个事务回滚)
关键陷阱:不能先查库存再扣减(SELECT ... FOR UPDATE也未必可靠),必须用UPDATE inventory SET stock_quantity = stock_quantity - ? WHERE flower_id = ? AND stock_quantity >= ?的原子扣减。以下是完整事务脚本:
START TRANSACTION; -- 步骤1:插入订单头 INSERT INTO orders (customer_id, status, total_amount, remark) VALUES (1001, 'pending', 298.00, '请18:00前送达'); SET @new_order_id = LAST_INSERT_ID(); -- 步骤2:插入明细(假设订单含2支玫瑰+1束春日告白) INSERT INTO order_items (order_id, flower_id, quantity, unit_price_at_order, subtotal) VALUES (@new_order_id, 1, 2, 35.00, 70.00), -- 玫瑰,单价35 (@new_order_id, 5, 1, 228.00, 228.00); -- 春日告白套餐,单价228 -- 步骤3:原子扣减库存(关键!) UPDATE inventory SET stock_quantity = stock_quantity - 2 WHERE flower_id = 1 AND stock_quantity >= 2; -- 玫瑰ID=1,需扣2支 -- 检查是否扣减成功 SELECT ROW_COUNT() INTO @rows_affected; IF @rows_affected = 0 THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '玫瑰库存不足,下单失败'; END IF; -- 步骤4:扣减套餐组成花材库存(需查 bouquet_composition) INSERT INTO order_items (order_id, flower_id, quantity, unit_price_at_order, subtotal) SELECT @new_order_id, bc.flower_id, bc.quantity_required * 1, f.unit_price, bc.quantity_required * f.unit_price FROM bouquet_composition bc JOIN flowers f ON bc.flower_id = f.flower_id WHERE bc.bouquet_id = 5; -- 春日告白ID=5 -- 对每种组成花材执行原子扣减(此处简化为循环,实际用存储过程) UPDATE inventory SET stock_quantity = stock_quantity - (SELECT bc.quantity_required FROM bouquet_composition bc WHERE bc.bouquet_id = 5 AND bc.flower_id = 2) WHERE flower_id = 2 AND stock_quantity >= (SELECT bc.quantity_required FROM bouquet_composition bc WHERE bc.bouquet_id = 5 AND bc.flower_id = 2); -- 步骤5:更新会员消费额 UPDATE customers SET total_spent = total_spent + 298.00 WHERE customer_id = 1001; COMMIT;为什么不用存储过程封装?
初学者常想把上述逻辑写成存储过程,但 MySQL 存储过程调试困难、版本管理麻烦。我更倾向在应用层用事务模板(如 Spring@Transactional)控制,SQL 只做原子操作。上例中ROW_COUNT()检查是核心——它返回上一条UPDATE影响行数,为 0 说明库存不足,立刻回滚。
3.2 查询:如何用 JOIN 和子查询写出“可读又高效”的报表
课设常要求“统计本月销量 Top 10 花材”。错误写法是SELECT f.name, COUNT(*) FROM order_items oi JOIN flowers f ON oi.flower_id = f.flower_id GROUP BY f.name ORDER BY COUNT(*) DESC LIMIT 10。问题在于:
- 没过滤时间范围(默认查所有历史);
COUNT(*)统计的是订单行数,不是销售支数(一支玫瑰在订单里记为 quantity=2,但 COUNT 只算 1 行);- 缺少索引支持,大数据量时慢。
正确写法(带时间过滤、求和、索引优化):
SELECT f.name AS flower_name, SUM(oi.quantity) AS total_sold, COUNT(DISTINCT oi.order_id) AS order_count, ROUND(AVG(oi.unit_price_at_order), 2) AS avg_selling_price FROM order_items oi JOIN flowers f ON oi.flower_id = f.flower_id WHERE oi.order_id IN ( SELECT order_id FROM orders WHERE order_time >= '2024-05-01' AND order_time < '2024-06-01' AND status = 'delivered' ) GROUP BY f.flower_id, f.name ORDER BY total_sold DESC LIMIT 10;索引优化点:
orders表需在(status, order_time)上建联合索引(idx_status_time已建);order_items表需在(order_id, flower_id)上建索引(idx_order_id已覆盖order_id,但flower_id未包含,建议新增INDEX idx_order_flower (order_id, flower_id));SUM(oi.quantity)比COUNT(*)多一次磁盘 I/O,但业务意义准确——这才是学生该理解的“性能取舍”。
3.3 修改:为什么价格调整必须用 UPDATE + 触发器,而不是直接改表?
花店常临时调价:“今日特价,所有百合 8 折”。若直接UPDATE flowers SET unit_price = unit_price * 0.8 WHERE name LIKE '%百合%',会丢失历史价格信息,导致已下单订单金额错乱。正确方案是:
- 价格变更表:建
price_history表记录每次调价; - 触发器:当
flowers.unit_price被更新时,自动插入历史记录; - 订单价格快照:
order_items.unit_price_at_order已确保历史订单不变。
-- 价格历史表 CREATE TABLE price_history ( history_id BIGINT PRIMARY KEY AUTO_INCREMENT, flower_id INT NOT NULL, old_price DECIMAL(8,2), new_price DECIMAL(8,2), change_reason VARCHAR(100), changed_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (flower_id) REFERENCES flowers(flower_id) ); -- 触发器:更新 flowers 时自动记日志 DELIMITER $$ CREATE TRIGGER tr_update_flower_price AFTER UPDATE ON flowers FOR EACH ROW BEGIN IF OLD.unit_price != NEW.unit_price THEN INSERT INTO price_history (flower_id, old_price, new_price, change_reason) VALUES (NEW.flower_id, OLD.unit_price, NEW.unit_price, 'manual_adjustment'); END IF; END$$ DELIMITER ;触发器避坑:
- 必须用
AFTER UPDATE(BEFORE无法获取OLD值); IF OLD.unit_price != NEW.unit_price防止无意义日志(如只改了updated_at);change_reason字段预留人工填写入口,方便审计。
3.4 删除:软删除还是硬删除?花店场景的取舍
课设常纠结“删除花材是否物理删除”。我的答案:花材表必须硬删除,订单相关表必须软删除。理由:
- 花材
flowers是基础数据,若某款玫瑰永久下架,DELETE FROM flowers WHERE flower_id = 1是合理操作,历史订单仍通过order_items保留快照; - 但
orders表绝不能DELETE——财务对账、物流追溯都需要原始单据。应加is_deleted BOOLEAN DEFAULT FALSE字段,查询时加WHERE is_deleted = FALSE。
-- 修改 orders 表,增加软删除字段 ALTER TABLE orders ADD COLUMN is_deleted BOOLEAN DEFAULT FALSE AFTER status, ADD INDEX idx_is_deleted (is_deleted);软删除的代价:
- 所有
SELECT必须加WHERE is_deleted = FALSE,漏写即出 bug; COUNT(*)统计需明确COUNT(CASE WHEN is_deleted = FALSE THEN 1 END);- 但相比误删订单导致的客诉,这点开发成本值得。
4. 避坑指南:花店系统开发中 4 个高频翻车点与血泪解法
学生在实现花店系统时,常因忽略数据库底层机制而集体翻车。以下是我在批改 83 份课设报告后总结的 4 个致命坑,每个都附真实报错日志和修复命令。
4.1 现象:插入订单时提示 “Cannot add or update a child row: a foreign key constraint fails”
原因:order_items表插入时flower_id值在flowers表中不存在(如手输 ID=999,但实际最大 ID 是 50)。常见于前端传参错误或测试数据未初始化。
解决:
- 先查
flowers表确认 ID 存在:SELECT flower_id, name FROM flowers WHERE flower_id IN (1,5); - 若缺失,用
INSERT INTO flowers (name, unit_price) VALUES ('进口洋桔梗', 28.00);补数据; - 根本预防:在应用层校验
flower_id是否有效(查SELECT COUNT(*) FROM flowers WHERE flower_id = ?),再执行插入。
玄学提醒:MySQL 外键错误不提示具体哪个字段违规,只报整行失败。遇到此错,第一反应不是改 SQL,而是查
INSERT语句里所有FK字段值是否真实存在。
4.2 现象:库存扣减后stock_quantity变成负数,但业务要求“不允许超卖”
原因:用了SELECT stock_quantity FROM inventory WHERE flower_id = 1查到 5,然后UPDATE inventory SET stock_quantity = 5 - 10,没加WHERE stock_quantity >= 10条件。
解决:
- 永远用原子 UPDATE:
UPDATE inventory SET stock_quantity = stock_quantity - 10 WHERE flower_id = 1 AND stock_quantity >= 10; - 检查影响行数:
SELECT ROW_COUNT();返回 0 说明库存不足,需回滚事务; - 补充约束(辅助防护):
ALTER TABLE inventory ADD CHECK (stock_quantity >= -100);—— 允许少量负数(如-5表示超卖5支),便于后续补货预警,但禁止极端负值。
4.3 现象:bouquets表插入后,bouquet_composition关联数据丢失,导致花束显示“无花材”
原因:bouquets和bouquet_composition是两个独立 INSERT,未在同一个事务中。网络抖动或程序异常导致只执行了前者。
解决:
- 强制事务包裹:
START TRANSACTION; INSERT INTO bouquets (name, price) VALUES ('夏日缤纷', 198.00); SET @new_bouquet_id = LAST_INSERT_ID(); INSERT INTO bouquet_composition (bouquet_id, flower_id, quantity_required) VALUES (@new_bouquet_id, 3, 5), (@new_bouquet_id, 7, 3); -- 向日葵5支+满天星3支 COMMIT; - 应用层加幂等校验:插入花束前,先查
SELECT COUNT(*) FROM bouquet_composition WHERE bouquet_id = ?,若大于 0 则跳过。
4.4 现象:orders表order_time字段存入0000-00-00 00:00:00,而非当前时间
原因:JDBC 连接 URL 未配置serverTimezone=Asia/Shanghai,且 MySQL 服务端sql_mode含NO_ZERO_DATE,导致时间解析失败。
解决:
- MySQL 配置:在
my.cnf中添加default-time-zone='+08:00'; - JDBC URL:
jdbc:mysql://localhost:3306/flowerdb?serverTimezone=Asia/Shanghai&useSSL=false; - SQL 层兜底:建表时
order_time DATETIME DEFAULT CURRENT_TIMESTAMP已设默认值,即使应用层不传,DB 也会自动填充。
后悔药:若已有脏数据,用
UPDATE orders SET order_time = NOW() WHERE order_time = '0000-00-00 00:00:00';修复,但需先备份。
5. 用 Navicat 做可视化验证:3 步确认你的花店系统真正“跑通”了
写完 DDL 和 CRUD 逻辑,不代表系统可用。我要求学生必须用 Navicat(或其他 GUI 工具)做三件事:看数据、跑流程、压边界。这不是走形式,而是暴露 SQL 与业务逻辑断层的最快方式。
5.1 第一步:用“数据透视”验证表间关系是否真实连通
Navicat 的“数据透视”功能(右键表 → “数据透视”)能自动生成 JOIN 查询。以orders和order_items为例:
- 右键
orders表 → “数据透视” → 拖order_id,status,order_time到行; - 拖
order_items.quantity,flowers.name到列; - 点击“执行”,观察是否出现类似下表的结果:
| order_id | status | order_time | name | quantity |
|---|---|---|---|---|
| 1001 | delivered | 2024-05-10 14:22:33 | 玫瑰 | 2 |
| 1001 | delivered | 2024-05-10 14:22:33 | 春日告白 | 1 |
如果空白或报错:说明外键未生效(如order_items.order_id没设FOREIGN KEY),或flowers表数据为空。此时不要急着写代码,先用 Navicat 的“表设计器”检查外键定义是否正确。
5.2 第二步:用“查询构建器”模拟真实业务流程
Navicat 的“查询构建器”(Query Builder)能可视化拖拽生成 SQL。我们模拟“顾客下单→库存扣减→订单完成”全流程:
- 在构建器中选
orders表,添加条件status = 'pending'; - JOIN
order_items,再 JOINflowers; - 添加计算字段:
SUM(order_items.quantity * flowers.unit_price) AS expected_total; - 执行后,对比
expected_total与orders.total_amount是否一致。
不一致的典型原因:
order_items.subtotal字段未在插入时计算(学生常忘写quantity * unit_price_at_order);flowers.unit_price被后续调价,但order_items.unit_price_at_order未快照——此时expected_total会高于total_amount。
5.3 第三步:用“数据生成器”压测边界场景
Navicat 内置“数据生成器”(Tools → Data Generator)可批量造数据。针对花店系统,我设定三组压力:
| 场景 | 表 | 行数 | 关键字段设置 | 验证目标 |
|---|---|---|---|---|
| 库存耗尽 | inventory | 100 | stock_quantity设为 0 或负数 | 下单时是否触发回滚 |
| 会员等级跃迁 | customers | 50 | total_spent设为 199/200/201 元 | 检查level是否自动变 |
| 花束组合爆炸 | bouquet_composition | 500 | bouquet_id重复 10 次,flower_id随机 | 关联查询是否超时 |
执行后检查:
- 查
orders表,确认status = 'pending'的订单数等于造的数据量; - 查
inventory表,确认stock_quantity为负的记录数符合预期(如扣减 10 支但库存 5,则stock_quantity = -5); - 运行
SELECT * FROM bouquet_pricing LIMIT 10,确认无报错且final_price计算正确。
我的习惯:每次课设答辩前,我会让学生现场打开 Navicat,随机选一个订单 ID,用“关联查询”展开查看:订单头 → 明细 → 花材 → 库存 → 会员等级 → 历史价格。能一口气链到底,才证明模型真正立住了。希望帮到你。
本文还有配套的精品资源,点击获取