送水系统数据库课设:MySQL表设计与事务实践
简介《某送水公司的送水系统》是一份面向数据库课程设计的完整资料适合高校学生在完成关系数据库课程实践项目时参考。方案覆盖送水公司核心业务工作人员与客户信息管理、矿泉水类别和供应商管理以及入库出库管理同时利用触发器在出入库时自动增减对应矿泉水数量编写存储过程统计指定月份每位送水员工的送水量并查询当月用水量最大的前10位用户且按用水量递减排列还建立了各表之间的参照完整性约束。资源包为rar压缩格式共3个文件Word报告内含清晰设计思路、流程图和E-R图建库建表代码一并收录SQL脚本便于直接执行建库建表bak文件为数据库备份可还原后验证功能。压缩包仅388KB轻量易获取。目前已有1543人学习适合需要完整课设范式、理解触发器与存储过程实际用法并快速动手复现系统的同学。1. 送水系统课设别急着建表先想清楚订单、库存和配送怎么串起来数据库课设拿到“某送水公司的送水系统”这个题目时很多人第一反应是这不就是用户表、订单表、商品表的一套增删改查吗实际动手才发现送水业务里最麻烦的不是建表而是“一桶水从下单到签收”这条数据链路怎么保持一致。订单状态要流转、库存要扣减和回补、配送批次要关联多张订单、空桶还要单独追踪哪一环没想清楚写代码时就会反复改表结构。这个课设题目的价值也正在这里它比图书管理、学生选课多了一层“进销存 配送调度”的业务复杂度但又不至于像电商系统那样需要拆分服务。对正在选课设题目的学生来说它是能同时覆盖业务建模、范式设计、事务、索引、统计查询的完整练手项目对想快速搭建送水业务后台的开发者来说这套表结构和核心 SQL 也可以直接复用。整篇会按照“业务模型 → 建表 → 状态流转和核心 SQL → 避坑 → 验收演示”的顺序推进中间每一张表、每一段事务都给出能直接跑的语句并说明为什么这么做以及评审时容易被问到的地方。2. 先立业务模型再建表送水业务里必须做对的三个设计决策2.1 业务角色与核心流程一桶水的生命周期送水系统不是简单的“用户下单、管理员发单”。把某送水公司的日常运营拆开看至少涉及四类角色订水用户、水站客服或运营人员、配送员、水站管理员或财务人员。用户在小程序或电话里订水客服可能要代替用户录单水站管理员要确认库存足够并派单配送员需要按“配送批次”装车送货送达后还要回收空桶财务则要能把每一笔订单的实收金额和桶押金核对清楚。核心业务流大致是这样用户提交订水需求 → 系统生成订单此时可能是待支付或已支付 → 水站管理员审核并派给配送员 → 配送员按批次配送 → 客户签收、支付尾款或确认收款 → 配送员回收空桶 → 订单完成。这个流程里实际存在三条并行数据流资金流对应支付状态物流对应订单状态和配送批次桶流对应空桶在客户还是在水站。普通外卖系统只需要管前两条送水系统把空桶回收也纳入数据建模后整个系统的复杂度和可讲深度就上来了。我在设计这类系统时一般先把这三条流画在纸上再开始画实体关系图。你不妨也试试把每一条流经历的状态列出来资金流是“待支付 → 已支付 → 已退款”物流是“待派单 → 配送中 → 已完成”桶流是“在库 → 随单出库 → 客户处 → 随单回收”。三条流在“订单”这张表上汇合但不代表要把所有状态都塞进一个字段里硬凑。2.2 ER 图怎么画才不返工实体、关系和基数判断送水系统的核心实体并不少至少包括用户、客户地址、商品、订单、订单明细、送水站、库存、配送批次、配送批次与订单的关联、操作流水。很多人在课设 ER 图阶段就翻车原因不是实体画少了而是关系基数判断错了。最常见的错误是把“客户地址”当作“用户”表的一个普通属性字段结果同一用户给家里和公司分别订水时只能在一行里塞两个地址完全违背第一范式。正确的做法是拆成两张表用户表保存账号信息客户地址表保存送水点信息用户对地址是一对多。订单与客户地址是多对一订单与订单明细是一对多订单明细与商品是多对一商品与送水站通过库存表构成多对多配送批次与订单通过关联表构成一对多。这里特别要说明的是“配送批次对订单”的基数实际业务中配送员经常是一次出车送好几单所以批次和订单是“一个批次包含多张订单、一张订单同一时刻只属于一个未完成批次”。ER 图阶段值得多花时间的还有“空桶”这个实体。商品表里建议区分“桶装水”和“空桶押金”两类或者在订单明细里通过商品类型字段区分。这样做的原因是送水的桶是循环使用的客户退桶要退押金系统里如果不追踪桶的归属财务对账时根本说不清押金到底在谁手里。这个点只要在答辩时讲出来通常能明显看出业务建模的完整度。2.3 范式与冗余的取舍订单明细为什么必须存商品快照课堂上学的是第三范式但实际项目里往往要做有意识的冗余。送水系统里最典型的例子是订单明细表必须保存“下单时的商品名和单价快照”而不是只放一个商品 ID。原因很简单某送水公司过两天调整桶装水价格或者把某个商品改名历史订单如果通过商品 ID 去关联当前商品表查出来的价格和名称就全变了。订单是财务凭证凭证内容不允许被商品的当前状态篡改。所以订单明细表里要有 product_name 和 price_cent 这两个快照字段。同样地订单表里也应该冗余 station_id 送水站标识而不是每次通过配送批次反查。很多同学在范式考试里被扣分扣怕了建表时坚决不肯冗余任何字段等做到统计报表时被迫连五张表查询慢不说逻辑还容易出错。范式要遵守但遵守的是“消除不合理依赖”不是“禁止冗余”。在订单明细、库存流水这类“凭证型”表里快照和冗余是正确设计。另一个容易被忽略的点是金额字段的数据类型不要用 float 或 double要用 int 存“分”。浮点数的精度问题在财务场景属于硬伤这个选择会在后面的建表环节反复出现。3. 把模型落成 MySQL 表送水系统核心建表语句与字段选型3.1 用户与客户地址一个账号多个送水点的设计用户表和客户地址表是整个系统的基础。角色字段用 TINYINT 而不是字符串是因为枚举值用数字存储更省空间也方便前端做字典映射。客户地址表单独拆分并且给 user_id 和 address 建联合唯一索引防止同一用户把同一个地址重复录入进去。CREATE TABLE t_user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, phone VARCHAR(20) NOT NULL, password_hash VARCHAR(64) NOT NULL COMMENT 登录密码哈希, nickname VARCHAR(50), role TINYINT NOT NULL DEFAULT 0 COMMENT 0-普通用户 1-客服 2-配送员 3-水站管理员, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_phone (phone) ) COMMENT送水系统用户表; CREATE TABLE t_customer_site ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, site_name VARCHAR(50) NOT NULL COMMENT 送水点别名家、公司、父母家, contact_person VARCHAR(30) NOT NULL COMMENT 收水联系人, contact_phone VARCHAR(20) NOT NULL COMMENT 收水联系电话, address VARCHAR(200) NOT NULL COMMENT 详细送水地址, is_default TINYINT NOT NULL DEFAULT 0 COMMENT 是否默认送水点, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_address (user_id, address), CONSTRAINT fk_site_user FOREIGN KEY (user_id) REFERENCES t_user (id) ) COMMENT客户送水点地址表;逻辑说明t_user 中的 phone 是用户登录凭证加了唯一索引后注册接口可以先查 phone 是否存在再插入。role 字段的注释很重要课设代码里到处要用没有注释后期全靠猜。t_customer_site 把地址拆成独立表后同一用户可以维护“家”和“公司”两个送水点下单时选择其中一个即可点好之后订单里只需要存 site_id。参数说明phone 定成 VARCHAR(20) 而不是 VARCHAR(11)是给固定电话和区号留余地address 的 200 长度足够容纳带门牌号的详细地址。外键在课设阶段建议保留因为 ER 图评审时需要展示表间关系数据库本身也会帮你拦截一部分非法的明细数据代价是插入时多一次外键检查这个开销在演示数据量下可以忽略。3.2 订单与订单明细主外键和状态字段的约定订单表是整个系统的核心几乎所有的查询最终都会落到它上面。订单号 order_no 要单独用业务号不要直接把自增主键 id 暴露给前端因为自增 id 可被遍历订单号用于客服电话沟通和财务对账。金额字段 total_cent 用 int 单位是“分”转成元给前端显示时再除以 100。CREATE TABLE t_product ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, spec VARCHAR(50) NOT NULL COMMENT 规格描述例如18.9L 桶装水, type TINYINT NOT NULL DEFAULT 0 COMMENT 0-桶装水 1-空桶押金, price_cent INT UNSIGNED NOT NULL COMMENT 单价单位分, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-上架 0-下架 ) COMMENT商品表; CREATE TABLE t_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 对外业务订单号, user_id BIGINT UNSIGNED NOT NULL, site_id BIGINT UNSIGNED NOT NULL COMMENT 送水点地址ID, station_id BIGINT UNSIGNED NOT NULL COMMENT 由哪个送水站供货, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付 1-待派单 2-配送中 3-已完成 4-已取消, total_cent INT UNSIGNED NOT NULL COMMENT 订单总金额单位分, remark VARCHAR(255) COMMENT 客户备注, expect_delivery_time DATETIME COMMENT 期望送达时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME COMMENT 支付完成时间, finished_at DATETIME COMMENT 完成或取消时间, KEY idx_order_user_time (user_id, created_at), KEY idx_order_status_time (status, created_at), CONSTRAINT fk_order_site FOREIGN KEY (site_id) REFERENCES t_customer_site (id) ) COMMENT送水订单主表; CREATE TABLE t_order_item ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL, product_id BIGINT UNSIGNED NOT NULL, product_name VARCHAR(100) NOT NULL COMMENT 商品名快照下单时写入, price_cent INT UNSIGNED NOT NULL COMMENT 单价快照单位分, quantity INT NOT NULL DEFAULT 1 COMMENT 数量, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES t_order (id) ) COMMENT订单明细表;逻辑说明t_order 的 status 用 TINYINT 数字存配合注释比直接存中文字符串更严谨后面做统计时也不需要 CASE WHEN 转枚举。索引设计上重点考虑两类查询一是“查某个用户近 30 天的订单”对应 idx_order_user_time二是“查某个状态下待派单的订单”对应 idx_order_status_time。容易被忽略的是订单明细表里的 product_name 和 price_cent 快照字段这两个字段保证了历史订单不受商品调整影响。参数说明expect_delivery_time 是业务上的“期望时间”不是系统自动生成的创建时间两个时间语义不同。配送中的订单如果需要“超时预警”依赖的是配送批次表的 start_time 和 finish_time而不是订单上的时间字段。另外订单明细表没有对 product_id 建外键原因是商品可能被物理删除或下架明细作为历史凭证不应被外键约束牵连这一点和课程里“外键越多越好”的印象不同。3.3 库存与配送批次送水站仓库和司机配送怎么关联送水公司通常有多个水站每个水站覆盖一片区域所以库存要按“送水站 商品”维度存而不是一个全局总库存。配送批次则解决“一个司机一次送多单”的问题一次出车是一个批次批次里按顺序关联多张订单。CREATE TABLE t_station_stock ( station_id BIGINT UNSIGNED NOT NULL COMMENT 送水站ID, product_id BIGINT UNSIGNED NOT NULL, quantity INT NOT NULL DEFAULT 0 COMMENT 当前可用库存, low_stock_threshold INT NOT NULL DEFAULT 10 COMMENT 低于该值预警, PRIMARY KEY (station_id, product_id), CONSTRAINT chk_stock_quantity CHECK (quantity 0) ) COMMENT送水站商品库存表; CREATE TABLE t_delivery_batch ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, station_id BIGINT UNSIGNED NOT NULL, driver_id BIGINT UNSIGNED NOT NULL COMMENT 配送员对应t_user.id, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待发车 1-配送中 2-已完成, plan_start_time DATETIME COMMENT 计划发车时间, start_time DATETIME COMMENT 实际发车时间, finish_time DATETIME COMMENT 实际完成时间, KEY idx_batch_driver (driver_id, status) ) COMMENT配送批次表; CREATE TABLE t_delivery_batch_order ( batch_id BIGINT UNSIGNED NOT NULL, order_id BIGINT UNSIGNED NOT NULL, seq TINYINT NOT NULL DEFAULT 0 COMMENT 配送顺序越小越先送, PRIMARY KEY (batch_id, order_id), UNIQUE KEY uk_order_batch (order_id, batch_id) ) COMMENT配送批次与订单关联表;逻辑说明库存表的 CHECK 约束 quantity 0 是最后一道防线但并发场景下不能只靠它真正的拦截要在 UPDATE 语句里做“库存充足才扣减”的条件判断这会在第 4 章事务部分详细讲。配送批次和订单用关联表而不是在订单表上加 batch_id 字段是为了支持“一个批次多张订单”的查询需求同时也方便将来一个订单被改派到另一个批次时只改关联行不动订单主表。参数说明low_stock_threshold 是水站管理员看板上的预警阈值具体值要看业务规模某送水公司一般把桶装水阈值设在 10 桶上下小规格瓶装水可以设更高。配送顺序 seq 从 0 开始装车和派单界面都按这个字段排序。这里有一个简化方案如果课设时间紧可以直接在 t_order 上增加 batch_id 字段代替关联表代价是“多对多”语义被压成“多对一”对演示来说基本够用但答辩讲“一个批次多单”时用关联表更站得住。4. 订单状态流转与核心业务 SQL下单扣库存、取消回补要怎么改数据4.1 状态设计为什么用整数状态而不是“状态表 流程图”很多课设喜欢把订单状态做成一张状态字典表理由是“灵活扩展”。但对于送水系统订单状态一共就那几个且流转路径非常清晰单独建状态表只会让每次查询多一次 JOIN得不偿失。这里更推荐的做法是在订单表上用 TINYINT 存状态配合代码里一层简单校验把合法迁移写死。状态定义和交流口径统一如下状态值含义触发动作允许到达的下一个状态0待支付用户下单成功1支付、4超时自动取消1待派单支付完成2派单、4客服取消2配送中配送员发车出库3签收3已完成客户签收且空桶回收无4已取消用户取消或客服取消无为什么不用状态表因为状态迁移不是数据问题而是业务规则问题。你在代码里写“待派单的订单才能派单”是一行 if 判断而用状态表只能查到“存在哪些状态”并不能表达“1 状态可以到 2 不能到 3”的约束最终还得在代码里写规则那建状态表的意义就只剩给评审看属于典型的过度设计。4.2 下单扣库存与取消回补两条必须写成事务的 SQL下单是整个系统里最容易写错的地方。新手写法往往是先 SELECT 一下库存判断数量够然后 INSERT 订单再 UPDATE 库存。这种做法在演示环境单用户操作时看不出问题一旦有两个用户同时下单就会被库存减成负数打脸。正确做法是把库存校验和扣减合并成一条带条件的 UPDATE再插入订单和明细全部放进同一个事务。START TRANSACTION; -- 1. 扣减库存quantity 2 是关键条件同时起到校验和锁行作用 UPDATE t_station_stock SET quantity quantity - 2 WHERE station_id 1 AND product_id 1 AND quantity 2; -- 2. 检查上一步影响行数为 0 说明库存不足直接回滚 -- 在存储过程或应用层判断 ROW_COUNT() 0 时执行 ROLLBACK INSERT INTO t_order (order_no, user_id, site_id, station_id, status, total_cent) VALUES (20250101001, 1001, 2, 1, 1, 3600); INSERT INTO t_order_item (order_id, product_id, product_name, price_cent, quantity) VALUES (LAST_INSERT_ID(), 1, 18.9L 桶装水, 1800, 2); COMMIT;逻辑说明UPDATE 语句里的 quantity 2 不是摆设它让“检查库存”和“扣减库存”在同一个原子操作里完成数据库行锁会保证两个并发事务不会同时通过这个条件。第二步的 ROW_COUNT() 判断是必须的如果库存不足UPDATE 影响 0 行此时还继续插入订单就会产生“没有库存的订单”所以要在应用层或存储过程里立刻回滚。先扣库存再插订单的顺序也让库存不足时不需要回滚已插入的订单处于更安全的中间状态。参数说明这里假设 station_id 1 的送水站有 18.9L 桶装水客户要 2 桶单价 1800 分即 18 元总计 3600 分。实际开发时 station_id 和 product_id 应从下单请求里取quantity 由前端上传但后端必须做上限校验防止一次下单 999 桶把库存扣穿。LAST_INSERT_ID() 是 MySQL 在同一个连接里取上一条 INSERT 自增 ID 的函数用来保证订单主表和明细表的主外键对应。订单取消回补库存同样必须放入事务而且要注意只允许“待支付”和“待派单”状态的订单被取消已完成订单取消属于售后流程要单独走退款不能简单回补库存。START TRANSACTION; -- 1. 把订单置为取消WHERE 里的状态条件防止重复回补 UPDATE t_order SET status 4, finished_at NOW() WHERE id 20001 AND status IN (0, 1); -- 2. 如果上一步影响行数为 0说明订单不存在或状态不允许取消回滚 -- 3. 按订单明细回补送水站库存 UPDATE t_station_stock s JOIN t_order_item i ON i.product_id s.product_id SET s.quantity s.quantity i.quantity WHERE i.order_id 20001 AND s.station_id 1; -- 4. 写一条库存流水方便后续对账 INSERT INTO t_stock_log (station_id, product_id, change_quantity, biz_type, biz_ref_no) VALUES (1, 1, 2, CANCEL_ORDER, 20250101001); COMMIT;逻辑说明回补库存的 UPDATE 用了 JOIN 写法直接把订单明细里的数量和商品带到库存表避免在代码里先查明细再逐行 UPDATE降低事务里出现半截状态的风险。状态条件 status IN (0, 1) 是防止“已取消的订单再次被取消并二次回补库存”这在异常重试和重复提交场景下很常见属于典型的接口幂等保护。4.3 演示常用统计 SQL近 30 天复购率与配送超时清单课设答辩时老师几乎必问“你的系统能统计什么”。与其现场拼 SQL不如提前把两个高频统计写好近 30 天用户复购率和配送超时订单数。复购率的口径要先理清楚分母是近 30 天完成过订单的用户数分子是这些用户里下过 2 单及以上的人数。SELECT COUNT(*) AS active_users, SUM(order_cnt 2) AS repurchase_users, ROUND(SUM(order_cnt 2) * 100.0 / COUNT(*), 2) AS repurchase_rate FROM ( SELECT user_id, COUNT(*) AS order_cnt FROM t_order WHERE created_at NOW() - INTERVAL 30 DAY AND status 3 GROUP BY user_id ) t;逻辑说明内层子查询先过滤出近 30 天已完成的订单并按用户分组统计单量外层再算人数和比例。SUM(order_cnt 2) 这种写法在 MySQL 里可以直接得到满足条件的用户数不用再套一层 CASE WHEN简洁得多。注意口径是“单量大于等于 2”如果想把“同一个用户一天下两单”排除掉就要把内层 GROUP BY 改成 user_id, DATE(created_at)。配送超时清单需要关联配送批次和订单两张表这里的“超时”定义为实际完成时间比实际发车时间晚超过 2 小时。SELECT b.id AS batch_id, b.driver_id, COUNT(bo.order_id) AS order_cnt, TIMESTAMPDIFF(MINUTE, b.start_time, b.finish_time) AS duration_minutes FROM t_delivery_batch b JOIN t_delivery_batch_order bo ON bo.batch_id b.id WHERE b.status 2 AND TIMESTAMPDIFF(MINUTE, b.start_time, b.finish_time) 120 GROUP BY b.id, b.driver_id, b.start_time, b.finish_time;逻辑说明先用 status 2 只筛出已完成的批次再用 TIMESTAMPDIFF 算出分钟级的配送耗时过滤大于 120 分钟的即为超时。这里特意把耗时放在 WHERE 里而不是 SELECT 里目的是减少返回行数。演示时还可以加一个排序条件让超时最久的批次排在前面。5. 送水系统课设避坑指南数据一致性、并发与答辩追问5.1 并发下单超卖库存扣减为什么不能“先查后改”现象演示环境里连续提交两单每单都要 5 桶水库存表中明明只剩 5 桶两单居然都成功了库存变成了负数。群里一问很多人第一反应是 MySQL 出 bug 了实际上这是最经典的并发超卖。原因两个事务几乎同时执行“SELECT quantity FROM t_station_stock WHERE …”时都读到剩余 5 桶都认为库存够然后各自执行 UPDATE 扣减把 5 变成了 3 和 3总量对不上。问题出在“先查后改”中间有一个时间窗口这个窗口里数据被别人改了。解决把判断和扣减合并成一条原子 UPDATEUPDATE t_station_stock SET quantity quantity - 5 WHERE station_id ? AND product_id ? AND quantity 5。数据库的行锁会保证同一时刻只有一个事务能改这行库存第二个事务执行时发现 quantity 已经不满足条件影响行数为 0然后在事务里回滚。这是送水系统课设里最值得写进答辩稿的技术点。5.2 取消订单不回补库存状态改成“已取消”就结束了现象用户下了一单两桶水随后取消。订单详情页状态正确显示“已取消”但水站库存管理系统里这两桶水不见了财务和库存对账怎么都对不上。原因取消订单的代码只执行了UPDATE t_order SET status 4没有在同一个事务里把库存加回去也没有写库存流水。订单状态变了但库存数据没有跟着业务动作一起变化数据一致性被打破。解决把“改订单状态”和“回补库存”放在同一个事务并同时记录一条库存流水。流水表不只为对账也是答辩时展示“数据可追溯”的加分项。具体写法参考第 4 章取消回补的 SQL重点是 WHERE 条件里带上 status IN (0, 1)保证只有待支付和待派单的订单能走取消回补流程。5.3 表设计翻车点一个字段存多个值、金额用浮点、状态用中文现象客户地址表里用“张三138xxxx北京市朝阳区xx小区 3 号楼”这种一个字段塞全部信息的做法订单金额用 float 类型订单状态字段存的是“待支付”“配送中”这样的中文字符串。演示时功能能跑但一被问“你这个设计符合第几范式”“金额为什么不用 decimal”就开始冒汗。原因典型的学生思维是把“能存下数据”当成“设计合理”。一个字段存多个值直接违反第一范式float 存金额会出现 0.1 0.2 ! 0.3 的精度问题状态用中文字符串则让所有统计 SQL 里都要带字符串匹配效率低且容易写错。解决联系人、电话、地址拆成独立字段详见第 3 章 t_customer_site金额统一用 INT 存分查询展示时再转成元状态用 TINYINT 数字并写注释。外键方面不要走向另一个极端——每张表都建外键再加 ON DELETE CASCADE那会在删除商品时把订单明细一起删掉历史凭证说没就没。外键只建在关键关系上明细表的商品引用不建外键这样既能让 ER 图有说服力又不会埋下误删数据的雷。5.4 演示数据不合理被追问订单全在当天时间全是 8 点到 18 点现象演示下单列表页时生成的数据 50 条订单全部是今天创建的时间集中在 08:00 到 18:00配送耗时清一色 30 分钟。老师只看了一眼就追问“你们这个系统的业务高峰是什么时候配送时长分布合理吗”当场答不上来。原因造数脚本用NOW() - INTERVAL FLOOR(RAND()*10) DAY随机生成时间看着随机实际上没按业务规律设计。送水的真实场景是上午 9 点到 11 点、下午 14 点到 17 点是下单高峰周末上午会略高配送耗时通常在 45 分钟到 2 小时之间且偶尔要有超时单不然超时统计功能没有数据可展示。解决写造数脚本时按“工作日高峰时段密集生成、非高峰稀疏生成”的规则分配 created_at配送批次表的 start_time 比订单创建时间晚 3060 分钟finish_time 比 start_time 晚 45120 分钟并故意造 23 单超过 2 小时的超时单。数据造得像真实生意演示时才扛得住追问。这个坑对很多开发者来说是玄学问题其实只是没花十分钟想清楚业务时间分布。6. 用一张操作流水表把验收串起来让每个状态变更都说得清6.1 设计 t_order_log状态变更不再是无痕操作系统上线前建议加一张订单操作流水表无论用触发器还是应用层代码写入都值得做。CREATE TABLE t_order_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL, from_status TINYINT COMMENT 变更前状态, to_status TINYINT COMMENT 变更后状态, operator_id BIGINT UNSIGNED COMMENT 操作人0表示系统自动, remark VARCHAR(255), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_log_order_time (order_id, created_at) ) COMMENT订单状态变更流水表;状态变更时插入一条流水成本极低但答辩时可以按时间轴回放任意订单的完整轨迹“08:01 下单 → 08:05 支付 → 08:20 派单 → 09:10 签收”。有这个表任何“订单为什么会变成这个状态”的质疑都能直接查数据回答不用靠嘴解释。6.2 用一段演示脚本自动跑通验收闭环内容再多不如演示时一条龙跑通。我建议写一个最简 Python 脚本把“下单 → 支付 → 派单 → 发车 → 签收”五步串起来每一步都校验上一个动作的结果。def full_flow(conn, user_id, site_id, product_id, qty): cur conn.cursor() conn.begin() cur.execute(UPDATE t_station_stock SET quantity quantity - %s WHERE station_id 1 AND product_id %s AND quantity %s, (qty, product_id, qty)) assert cur.rowcount 1, 库存不足事务回滚 cur.execute(INSERT INTO t_order (order_no,user_id,site_id,station_id,status,total_cent) VALUES (%s,%s,%s,1,1,%s), (order_no(), user_id, site_id, qty * 1800)) order_id cur.lastrowid conn.commit() # 后续支付、派单、发车、签收都按相同模式写断言这段脚本的价值在于它是可重复执行的验收清单任何一次改动后跑一遍就能确认核心链路没被破坏。当年我答辩最庆幸的投入就是多写了一个闭环脚本老师临时让我“现场下一单”时我直接跑脚本十几秒走完整条链路比在页面上慢慢点可靠得多。如果你的演示环境允许把脚本和页面操作结合展示先脚本批量造数再页面演示一两条手工订单。页面演示看交互脚本演示看数据一致性两者互补。这样整套系统从建表、状态流转、库存回补到统计查询都有完整证据链课设基本就稳了。希望帮到你。本文还有配套的精品资源点击获取