简介:一份数据库课程设计实验报告,以火车票售票管理系统为完整案例,面向软件工程、计算机等相关专业学生,系统展示从需求分析、数据库逻辑与物理设计到应用程序实现和测试的全流程。报告基于Eclipse与MySQL开发,覆盖车次管理、车票管理、售票、退票、查询和异常处理等核心业务,并包含ER图、数据字典、关系表、索引、安全机制、功能模块、界面设计、事务设计、测试与总结等关键内容,可作为同类课程设计或毕业设计的写作模板与实现参考。资源为1个doc文档,共741KB,目录清晰、结构完整,便于直接阅读或按需修改。已有1330人学习下载,适合正在完成数据库课程设计或设计售票类信息管理系统的学生参考借鉴。
1. 数据库课程设计选它,闭着眼都能过?先看看火车票售票管理系统到底在练什么
你在教务系统选题列表里翻到「火车票售票管理系统」,或者在网盘里看到这个带序号(1).doc的文档时,第一反应大概率是:又是一个从网上抄来、改个表名就能交差的课设题。如果你真这么想,就浪费了这个题目里最值钱的部分。火车票售票管理系统把数据库课程的核心考点全串在了一起:ER 建模、建表、增删改查、事务、并发锁、索引、视图、存储过程甚至触发器,它既没有图书管理系统那么平淡,又比电商系统少很多业务噪音,是数据库课程设计里适合做深、也适合答辩的选题之一。这篇笔记适合已经上完数据库理论课、准备动手做课设的在校生,也适合想拿真实数据库项目练手、补一补 SQL 实操和事务经验的从业者。我会按自己当年踩过的坑,把「建模 -> 建表 -> 事务 -> 调优 -> 避坑 -> 写报告」整条路走一遍。
2. 火车票售票系统的数据库建模:从 ER 图到关系模式,别急着敲 create table
很多人的课设翻车是从「打开 Navicat 直接建表」开始的。数据库设计的第一步永远不是写 SQL,而是先把实体、属性、联系在纸上画明白。这一步没做对,后面所有增删改查都是在错误的地基上盖楼。
2.1 实体与联系:用户、车次、订单、余票四张核心表怎么拆出来
火车票售票管理系统的业务需求通常包含三部分:用户注册登录、查车次下单、退票与管理维护。按实体划分,核心对象有四个:用户、车次、订单、余票。用户好理解,一个用户有多条订单,这是典型的1:N;车次和订单也是1:N,一个车次可以被多个订单关联;难点在余票——它不能简单挂靠在车次下,因为同一个车次每天都有不同的库存,必须由「车次号 + 出行日期」共同决定,所以余票表的主键是复合主键。
经常有人把订单表做成「车次号 + 出发站 + 到达站 + 发车时间 + 价格」全冗余进去,理由是查询快。这确实能减少一次 join,但也埋了隐患:车次表的价格或时间调整后,历史订单的数据会跟着业务变成一团乱麻。正确做法是订单只存 train_id 外键,出发站、到达站、价格等业务信息需要展示时再去 join train 表。冗余只留给金额,因为金额是下单那一刻的成交结果,之后车次价格改了也不应该影响历史订单。
还有一个经常被忽略的多对多联系:车次和经停站。一个车次会经过多个站点,一个站点又有多个车次经过,必须拆出中间表 train_station,把「第几站、到达时间、离开时间」这些属于「车次在某站」的属性放进去。不做这一步,站点信息只能拼字符串塞在车次表里,遇到中途站改点你就知道什么叫后悔药都没处买。
2.2 建库建表:把关系模式落成 MySQL 的 create table 脚本
关系模式确定后,才能动手建表。下面这组脚本是我在课设里实际用过的结构,以 MySQL 8.0 为准,引擎统一 InnoDB,字符集 utf8mb4。建库和建表语句可以直接抄,但你最好逐行读一下注释再决定要不要改字段名。
create database if not exists travel_db default charset utf8mb4; use travel_db; -- 用户表:pwd_hash 存哈希后的密码,不要存明文 create table users ( uid bigint primary key auto_increment, uname varchar(32) not null unique, pwd_hash char(64) not null comment 'SHA2-256哈希结果', phone varchar(20) default null, create_time datetime default current_timestamp ) engine=InnoDB default charset=utf8mb4; -- 车次表:depart/arrive 存站名,列车时刻用 time 类型 create table train ( train_id bigint primary key auto_increment, train_no varchar(12) not null unique comment '如 G102', train_name varchar(50) not null comment '如 北京南-上海虹桥', depart_station varchar(32) not null, arrive_station varchar(32) not null, depart_time time not null, arrive_time time not null, price decimal(8,2) not null, status tinyint not null default 1 comment '1运营 0停运' ) engine=InnoDB default charset=utf8mb4; -- 订单表:只存外键,业务字段通过 join 获取 create table orders ( order_id bigint primary key auto_increment, uid bigint not null, train_id bigint not null, travel_date date not null comment '出行日期', ticket_count int not null default 1, amount decimal(10,2) not null comment '成交金额,下单时快照', order_status tinyint not null default 0 comment '0已支付 1已退票 2已失效', create_time datetime default current_timestamp, refund_time datetime default null, constraint fk_orders_uid foreign key (uid) references users(uid), constraint fk_orders_train foreign key (train_id) references train(train_id) ) engine=InnoDB default charset=utf8mb4; -- 余票表:复合主键由车次和日期共同决定 create table ticket_stock ( train_id bigint not null, travel_date date not null, stock int not null default 0 comment '当前剩余票数', primary key (train_id, travel_date), constraint fk_stock_train foreign key (train_id) references train(train_id) ) engine=InnoDB default charset=utf8mb4;这段脚本的要点有三个。第一,订单表的 amount 字段是故意做的冗余,因为下单后车次价格可能调整,历史订单必须保留下单时的价格快照;第二,订单表不存「出发站、到达站、发车时间」,这些字段每次查询时 join train 表拿,避免车次信息变更后订单数据不一致;第三,ticket_stock 的主键是 (train_id, travel_date),这样同一个车次在不同日期的余票互不干扰,查「某天某车次还有几张票」走的就是主键索引。字符集用 utf8mb4 是为了让中文站名和 emoji 备注都不乱码,这是我在课设里被「车站名显示为??」坑过之后养成的习惯。
2.3 范式与冗余取舍:为什么余票不能靠 count 订单统计出来
答辩时老师最喜欢追问的问题之一就是:余票为什么不直接算「总票数 - 已售订单数」?答案分三层。第一层,订单会退票,退票后余票要加回来,如果你用 count 统计,退票逻辑还得额外维护一张「退票记录表」来校正,业务复杂度凭空翻倍。第二层,历史订单是只增不减的,跑了三个月后 orders 表可能几十万行,每次查询余票都要全表聚合,性能扛不住。第三层,也是最关键的并发问题:两个用户同时买最后一张票时,count 统计极容易出现「都查到还剩1张,都下单成功」的超卖,即使后续用锁,也会让事务迟迟无法提交,下一节我会专门讲事务,这里先记住结论——余票必须作为独立状态存在,用字段记录当前剩余量。
这个设计也顺便满足了三范式。users、train 各自只存自己实体的属性,没有重复存储;orders 表里 uid、train_id 都是外键,通过引用而不是复制来表达关系;ticket_stock 的主键不依赖任何非主属性。唯一刻意违反范式的地方是 orders.amount 冗余,而这是为了保留快照、避免历史记录被车次调价污染,属于合理冗余。如果老师追问「你违反第三范式了吗」,正面回答「金额是有意保留的下单快照,保证历史订单可审计」比支支吾吾好得多。
3. 用建好的库把增删改查跑通:订票、退票事务与并发控制
表结构建完,接下来是让系统真正能干活。火车票售票管理系统的核心业务就两个:订票和退票。这两个操作都不是单条 SQL 能完成的,必须依赖事务把「查余票、写订单、扣库存」绑在一起。这一章是整篇笔记里最值得抄作业的部分,我会直接给出可运行的 SQL 和 Python 代码。
3.1 查询余票的正确姿势:普通 select 和 for update 有什么区别
先看第一版最容易写出的代码——查余票再决定能不能下单,它长这样:
select stock from ticket_stock where train_id = 1 and travel_date = '2025-06-10'; -- 程序判断 stock >= 1,然后执行 insert 订单、update 余票这段 SQL 在单用户手动测试时没有任何问题,但只要两个会话同时执行,就等着翻车。事务 A 查到 stock=1,事务 B 也查到 stock=1,两个都想买最后一张票,各自 insert 订单、update 余票,最终 stock 变成 -1。超卖的根本原因不是 update 语句错了,而是「查」和「扣」之间隔着一段可被插队的窗口。解决方案是把查询语句加上 for update,让查到的行在事务提交前被锁住:
begin; select stock from ticket_stock where train_id = 1 and travel_date = '2025-06-10' for update; -- 其他事务再执行同样的 select for update 会等待,直到本事务 commit 或 rollbackfor update 是当前读,它读到的是已提交的最新值,并给命中的记录加行级排他锁。其它事务对同一行做 for update、update、delete 都会被阻塞,直到锁释放。注意它只能锁住查询条件命中且实际存在的行,如果行不存在,是锁不住「空隙」的——这就是为什么余票表在设计时必须保证「车次+日期」一定会先初始化一行 stock 记录,不能等用户下单时才 insert。
3.2 订票事务:把查库存、写订单、扣余票放进同一个事务
我把订票流程用 Python pymysql 写完整,每一步都有注释,这个脚本可以直接在你的本地数据库跑通。核心思路是:先关闭自动提交,再在一个事务里完成全部三步,最后统一提交或回滚。
import pymysql conn = pymysql.connect( host='127.0.0.1', user='root', password='your_password', database='travel_db', autocommit=False, # 关键参数:关闭自动提交,事务由代码显式控制 charset='utf8mb4' ) try: with conn.cursor() as cur: # 第一步:锁行查余票,for update 保证并发安全 cur.execute( "select stock from ticket_stock " "where train_id=%s and travel_date=%s for update", (1, '2025-06-10')) row = cur.fetchone() if row is None or row[0] < 1: raise RuntimeError('无余票') # 第二步:写订单,金额这里直接硬编码,正常应 join 车次表查 price cur.execute( "insert into orders(uid, train_id, travel_date, ticket_count, amount) " "values (%s, %s, %s, 1, %s)", (1001, 1, '2025-06-10', 553.00)) # 第三步:扣减余票,用 stock = stock - 1 而不是直接赋绝对值 cur.execute( "update ticket_stock set stock = stock - 1 " "where train_id=%s and travel_date=%s", (1, '2025-06-10')) conn.commit() print('订票成功,事务已提交') except Exception as e: conn.rollback() print('订票失败,事务已回滚:', e) finally: conn.close()代码里的参数化查询用了 %s 占位符,这是防止 SQL 注入的标准做法,课设里如果直接把用户输入拼进 SQL,答辩时被老师问一句就能把你问住。autocommit=False 是整个事务生效的前提,如果开着自动提交,每一条 SQL 都会立刻落盘,for update 的锁也在第一条语句执行完就释放,超卖照样出现。提交成功后再关闭连接,确保连接不会因为异常泄漏。
Java 侧的写法思路完全一致,只是 API 不同:connection.setAutoCommit(false),然后 preparedStatement.executeQuery() 执行 for update,最后 connection.commit(),发生 SQLException 时 connection.rollback()。一个容易漏的细节是 JDBC 连接串要加 serverTimezone 参数,否则日期字段传参时经常报时区错误;连接 MySQL 8 还要带 useSSL=false 避免本地测试时 SSL 告警刷屏。
3.3 退票流程:状态推进与余票回补要按同一个事务来写
退票是订票的逆操作,但比订票容易写错。我见过不少人退票只改订单状态,忘记回补余票,或者先回补余票再更新订单,导致中间状态不一致。退票事务要做的事严格来说是两件:把订单状态从「已支付」改成「已退票」,同时把对应车次当天的 stock 加一。这两个动作之间不允许插入任何其它操作,所以必须放同一事务。
begin; -- 先锁订单行,防止两个会话同时退同一张票 select order_status from orders where order_id = 2025061001 and uid = 1001 for update; -- 业务代码判断 order_status = 0 才允许退票,此处省略条件判断分支 update orders set order_status = 1, refund_time = now() where order_id = 2025061001 and uid = 1001; -- 回补余票:加一而不是赋值,避免覆盖并发下的其它变更 update ticket_stock set stock = stock + 1 where train_id = 1 and travel_date = '2025-06-10'; commit;退票这里有两个坑。第一个是 update 回补余票时要用stock = stock + 1,不要写成stock = 5这种绝对值,因为同一时间可能有好几个退票事务在跑,后提交的会把先提交的覆盖掉。第二个坑是订单状态判断。有些同学图省事只 update 不查询状态,结果一张已退票的订单被退了两次款,余票加了两张。正确做法是先 for update 锁订单行,再检查状态是否为「可退」,整个判断和更新同步完成,其它会话即使同时发起退票也会在抢锁时排队等结果。
3.4 隔离级别与事务超时:这些参数不改,并发问题会让你怀疑人生
MySQL 8 默认的隔离级别是 REPEATABLE READ,这个级别下,普通的 select 是快照读,如果你先 select 查了余票,再在同一个事务里执行 select for update,看到的可能是两套不同的数据。听起来很绕,但实际操作中有一个简单准则:凡是「查了还要在同一个事务里更新」的数据,一律用 for update 当前读;隔离级别如果不是因为业务有特殊要求,课设场景保持默认即可。
事务超时参数同样影响成败。innodb_lock_wait_timeout 的默认值是 50 秒,也就是说一个事务拿不到锁时会傻等 50 秒才报错。本地调试时,一旦忘了 commit,下一个事务就会被卡到超时,你甚至不知道是死锁还是锁等待。排查方法是在 MySQL 命令行执行show variables like 'innodb_lock_wait_timeout';,看看当前值;需要临时调整可以set session innodb_lock_wait_timeout = 5;把等待时间调短,让测试阶段的报错尽快暴露。还有一个容易被忽略的参数是事务隔离级别,如果你确实想改成 READ COMMITTED,在 JDBC 连接串后加transactionIsolation=TRANSACTION_READ_COMMITTED,或通过 Python 客户端执行set session transaction isolation level read committed;。课设场景里我基本不推荐动它,REPEATABLE READ 的默认行为配合 for update 已经足够稳。
4. 课设加分项三件套:索引、视图、存储过程与触发器
如果你的目标是「能跑、能过、能答辩」,第三章已经够了。但数据库课程设计的评分标准里通常有一档是「设计合理、有优化意识、体现数据库高级特性」,这一章就是为那一档准备的。索引、视图、存储过程、触发器,四样东西全部围绕火车票系统的真实查询场景来写,不是硬凑。
4.1 索引到底建在哪:给订单表加复合索引,用 explain 验证走没走
火车票系统最频繁的查询有两个:按用户查自己的历史订单、按车次和日期查余票。订单表在没有任何索引时,select * from orders where uid=1001 and order_status=0会全表扫描,数据量到十万行时响应时间肉眼可见变慢。正确的做法是在 uid 和 order_status 上建复合索引:
alter table orders add index idx_uid_status (uid, order_status); alter table ticket_stock add index idx_stock_date (travel_date); -- 验证索引是否生效 explain select * from orders where uid = 1001 and order_status = 0;执行 explain 后,重点看 type 列和 rows 列。type 如果从 ALL 变成 ref,说明索引生效了;rows 如果从全表行数变成个位数,说明扫描范围被大幅削减。复合索引要遵守最左前缀原则:idx_uid_status(uid, order_status) 能命中「只按 uid 查」和「按 uid+status 查」两种场景,但如果单独按 order_status 查,这个索引就帮不上忙。课设里没必要给所有字段都建索引,普通字段的写放大比查询收益更明显,尤其是订单这种高频 insert 的表。
4.2 视图:把复杂的多表 join 包装成一张假表
视图在课设里是个漂亮的展示点:把「查余票要 join 车次表」的逻辑封装成一张只读的假表,业务代码里查视图就像查单表一样简单。我通常把余票视图和用户订单视图一起建。
-- 余票查询视图:把车次基本信息与余票表关联 create view v_ticket_left as select t.train_no, t.depart_station, t.arrive_station, t.depart_time, t.arrive_time, t.price, s.travel_date, s.stock from train t join ticket_stock s on s.train_id = t.train_id where t.status = 1; -- 用户订单视图:把订单与车次信息拼好,前端直接查 create view v_order_info as select o.order_id, u.uname, t.train_no, t.depart_station, t.arrive_station, o.travel_date, o.ticket_count, o.amount, o.order_status, o.create_time from orders o join users u on u.uid = o.uid join train t on t.train_id = o.train_id; -- 用法:查 2025-06-10 所有有余票的车次,不再需要手写 join select * from v_ticket_left where travel_date = '2025-06-10' and stock > 0 order by depart_time;视图本质是一段被命名的 select 语句,它不占额外存储,每次查询时动态执行底层 SQL,所以不要指望用视图能提升性能,它的价值全在可读性和复用性上。有两个边界要记住:多表视图默认不能 update;如果底层表结构改了,视图不会自动更新旧字段,需要 create or replace view 重建。课设报告里展示视图时,建议附上一张「不写视图版」的 join SQL 做对比,评委一看就知道你懂视图解决的是什么问题。
4.3 存储过程:把订票业务封装成一个 CALL 就能调的黑匣子
存储过程适合做课时长度的加分项,把之前用 Python 写的订票逻辑搬到数据库内部,业务调用方只需要一条 CALL 语句。这样做的真正优势不是性能,而是把事务边界钉死在数据库层——不管谁来调用,都绕不开过程内部的事务控制。
delimiter $$ create procedure p_book_ticket( in p_uid bigint, in p_train_id bigint, in p_travel_date date, in p_count int, out p_result int ) begin declare v_stock int default 0; declare v_price decimal(8,2); start transaction; -- 锁行查余票 select stock into v_stock from ticket_stock where train_id = p_train_id and travel_date = p_travel_date for update; if v_stock < p_count then set p_result = -1; rollback; else select price into v_price from train where train_id = p_train_id; insert into orders(uid, train_id, travel_date, ticket_count, amount) values (p_uid, p_train_id, p_travel_date, p_count, v_price * p_count); update ticket_stock set stock = stock - p_count where train_id = p_train_id and travel_date = p_travel_date; set p_result = 1; commit; end if; end$$ delimiter ; -- 调用方式 call p_book_ticket(1001, 1, '2025-06-10', 1, @result); select @result;过程代码有几个容易写错的点。declare 必须写在过程体的最前面,不能和业务语句穿插,否则直接语法报错;select ... into 变量时,如果查询没有返回行会抛 1329 错误,所以余票查询前要确保 ticket_stock 里已经有初始化行;事务里一旦 rollback,之前的锁全部释放,但连接会话还在,可以继续执行其它语句。out 参数在调用时用 @result 接收,这是 MySQL 客户端变量的写法。如果你的课设选了 Java 后端,CallableStatement 可以调这个存储过程,代码量比 PreparedStatement 拼事务小很多,答辩时还能顺口说出「我把并发控制收敛到了数据库层,应用侧不用关心锁的细节」。
4.4 触发器:把「余票不能为负」变成数据库的最后一道防线
触发器是加分项里最容易展示「我理解了数据库约束」的东西。这里的业务规则很简单:余票字段任何时刻都不能小于 0。虽然事务里已经判断过库存,但逻辑上多个入口都可能改 ticket_stock——管理员手工补数据、以后新增的退票功能、测试脚本直接 update,都可以绕过业务判断。触发器就成了兜底防线。
delimiter $$ create trigger trg_stock_not_negative before update on ticket_stock for each row begin if new.stock < 0 then signal sqlstate '45000' set message_text = 'stock cannot be negative'; end if; end$$ delimiter ;这个触发器的作用是在每次 update 之前检查新的 stock 值,一旦为负就主动抛错,MySQL 会把本次语句所在的整个事务标记为失败,后续的 commit 会变 rollback。trigger 本身有三个容易踩的坑:一是 for each row 的语义在 MySQL 里默认就是行级触发,写多行更新时会逐行检查;二是 signal 的 sqlstate 45000 代表自定义错误,message_text 会显示在异常信息里,方便定位;三是触发器有额外开销,高频写入场景下不建议堆太多逻辑进去。课设场景里加这一个就够了,它能压住超卖的最后一条路,也让报告里「触发器保证数据一致性」这句话有实际代码支撑。
5. 常见问题避坑:超卖、死锁、锁挂起、外键删除失败怎么排查
这一章是我最想让读者直接跳到的地方。前面讲的是正确写法,但真实课设是一边写一边踩坑的过程。我把带过的学生里最高频的五个问题按「现象 -> 原因 -> 解决」列出来,每个都对应着一类真实的数据库事故。
5.1 超卖:余票扣成了负数,订单却生成成功了
现象:两个测试账号同时买同一车次最后一张票,两个订单都插入成功,ticket_stock 的 stock 变成了 -1。
原因:代码先执行普通 select 查到还有 1 张,再执行 insert 写订单、update 扣库存。这两步之间的空隙里,另一个事务也完成了同样的查询,两台「收银机」同时看到同一张余票,最后各自扣减,库存就负了。
解决:查询改成select ... for update,让第一个事务锁住余票行;更激进的做法是直接把扣减语句写成update ticket_stock set stock = stock - 1 where train_id=? and travel_date=? and stock > 0,然后判断受影响行数,rowcount 为 0 就说明没抢到票。两种方案可以叠加用,for update 锁行是先到先得的公平排队,带条件 update 是原子操作兜底。
5.2 死锁:两个订票事务同时卡住,报 MySQL 1213
现象:并发压测跑到第几十轮,控制台突然报 Deadlock found when trying to get lock; try restarting transaction。
原因:事务 A 先锁了 orders 表某一行,再去锁 ticket_stock;事务 B 反着来,先锁 ticket_stock 再写 orders。两边各自持有对方要的资源,互相等待,死锁形成。在火车票系统里,这种交叉等待最常见于「订票」和「退票」同时操作同一车次时。
解决:全局统一加锁顺序,所有事务都先锁 ticket_stock 再写 orders,永远不要反过来。另一个习惯是让事务尽可能短,锁越早释放死锁概率越低。排查死锁可以执行show engine innodb status\G,拉到 LATEST DETECTED DEADLOCK 段落,里面会打印两个事务分别持有哪些锁、等待哪些锁,对照加锁顺序改代码即可。MySQL 检测到死锁会自动回滚被牺牲的那个事务,所以应用层对报错要做重试机制,捕到 1213 就重新调用一次订票过程。
5.3 锁挂起:明明没写错 SQL,一条 update 卡了几分钟不动
现象:在 Navicat 或者 IDEA 的数据库控制台执行一条简单的 update 语句,一直转圈不返回,也没有报错。
原因:另一个会话开启事务后执行了 for update 或 insert,然后既没有 commit 也没有 rollback,连接还开着,行锁一直被持有。最常见的是测试时手动执行了带 for update 的 SQL,查完结果直接关掉控制台窗口,没提交也没回滚,锁就留在那里了。
解决:执行下面的 SQL 查当前所有未提交的事务,找到卡住锁的源头:
select trx_id, trx_state, trx_mysql_thread_id, trx_query from information_schema.innodb_trx;trx_mysql_thread_id 是持有锁的会话线程号,确认它已经卡死或无人处理之后,执行kill 线程号;强制结束。注意 kill 之前先用select * from information_schema.innodb_trx\G看看 trx_query 里是不是真有业务在跑长事务,万一是正常的大批量更新,kill 会导致数据回滚,影响面比卡顿更大。
5.4 外键约束导致车次删不掉,一删就报错
现象:管理员想删除一个已停运的车次,执行delete from train where train_id = 1;直接报外键约束错误:Cannot delete or update a parent row。
原因:orders 表和 ticket_stock 表都通过外键引用 train 表的 train_id,MySQL 默认的约束策略是 RESTRICT,父表有子表引用时禁止删除。实际业务里车次根本不应该物理删除,它关联了海量历史订单,删掉等于丢审计数据。
解决:给 train 表加 status 字段做软删除,已经设计好了;停运只需要update train set status = 0 where train_id = 1;。查询可售车次时始终带where status = 1过滤,历史订单 join 车次信息仍然能查到。如果老师要求你演示物理删除,可以在检查完没有任何未完成订单后先删子表数据再删父表,但课设里不建议展示这种危险操作。
5.5 用户密码明文入库,中文站名显示成问号
现象:打开 users 表能看到一串明文密码;车次表的「北京南」显示成「??」。
原因:前者是代码里没做哈希直接存原密码,后者是建库时字符集用了默认的 latin1 或者连接串没指定 utf8mb4。明文密码是课设里最容易被扣分的点,老师一看就知道你没接触过真实系统的安全规范。
解决:密码入库前用sha2(明文, 256)计算哈希,查询登录时比对哈希值;SQL 里其实不建议写哈希函数,一般由后端语言算好再传字符串。字符集问题分两层处理:建库语句加default charset utf8mb4,连接串再加charset=utf8mb4,两侧一致才能保证中文不乱码。已经导错的旧表可以用alter table train convert to character set utf8mb4;转换,但转换前最好先备份,批量转字符集时数据长度变化偶尔会报错。
6. 把它收进实验报告:验证数据、并发压测与导出备份的小技巧
课程设计最终交付物里,代码只是其中一部分,实验报告才是决定分数的东西。很多人写完代码不知道往报告里放什么,我的做法是:用验证过程反推报告内容,把每个关键结论都留证据。
报告里至少要有三组证据。第一组是功能验证,插入几个车次、给每个车次初始化库存、模拟用户下单和退票,把每一步的前后数据变化截图。第二组是并发验证,写一个 Python 脚本用 20 个线程同时抢同一车次的 5 张票,最终断言成功订单数等于 5、余票不小于 0,这组数据能直接证明你的事务和锁是有效的。第三组是备份恢复,用mysqldump -uroot -p travel_db > backup.sql导出整库,然后删掉部分数据再用mysql -uroot -p travel_db < backup.sql恢复,截图放文档里说明你做了数据库维护。这三组截图比贴几十页代码管用得多。
最后一个我想强调的教训是:写事务代码一定要先写异常处理再写业务逻辑。我课设时有一次调试订票存储过程,忘记在异常分支里 rollback,导致一批测试订单和余票数据拧在一起,最后只能删库重建,白白浪费一个晚上。从那以后,凡是我经手的事务代码,第一件事就是写 try-finally 或 except-rollback,确保任何分支退出都释放锁和连接。火车票售票管理系统这个题目做到这里,数据库层面的知识和真实项目的差距已经没有多大了,把事务和并发这两关过了,以后面试时聊项目也更有底气。希望这篇笔记能帮你少走几个弯路,顺利把课设交上去。
本文还有配套的精品资源,点击获取