简介:基于Python和MySQL实现的学生选课管理系统,是一份面向计算机相关专业期末大作业或毕业设计的完整项目包。项目由导师指导并通过高分评审,源码均经过本地编译调试,可稳定运行,适合需要快速构建课程设计或提升实战能力的学习者。压缩包共八个文件,包含四个Python源码文件(覆盖登录窗口、课程管理、教师管理等核心模块)、一份数据库SQL脚本(含表结构及示例数据)、一份《数据库原理》课程报告文档、一个说明文件及配套配置文件,整体大小约1.11MB,结构清晰便于查阅。目前已有八十八人学习浏览,具备一定参考价值。资源不仅提供了可直接运行的选课系统实现,还附带了数据库设计思路与报告文档,可帮助理解Python与MySQL的整合应用、界面与业务逻辑分离的编码方式,以及常见调试排错经验,是完成课程作业或入门管理信息系统开发的有力参考。
1. 期末大作业的隐藏分水岭:同样是“学生选课管理系统”,为什么有人答辩轻松有人翻车
期末答辩现场最怕的不是功能没写完,而是本机跑得好好的,演示时却连不上 MySQL。基于 python 和 mysql 的学生选课管理系统,是绝大多数高校 Python 与数据库课程的期末大作业标配,看起来就是学生、课程、选课三张表做增删改查,但真正拉开分数的点很反直觉:需求拆得是否克制、表结构是否诚实、选课流程里的数据一致性有没有兜住,而不是功能贴了多长的菜单。
这篇文章顺着这个标题,把一个期末大作业该有的源码骨架、数据库脚本、报告写作思路和演示避坑一次讲透。适合正在赶大作业、准备期末答辩的新手,也适合想把这个项目改造成毕设起点的老手。前几章讲怎么设计和实现,后两章集中解决你答辩前一晚最容易遇到的几个玄学问题。
2. 先把业务拆明白:期末评分视角下的需求边界与选型理由
2.1 大作业评分视角下的功能需求:哪些功能必须出现,哪些功能是给自己挖坑
老师评阅这类项目,翻代码时通常会先确认三件事:有没有区分学生和管理员角色,有没有完整的选课、退课闭环,MySQL 是不是真的在业务里起作用,而不是只拿来做了一个登录验证。
按这个逻辑,我一般把功能清单控制在六项:登录(按学号加密码)、浏览课程列表、选课、退课、查看已选课程、按课程统计选课人数。这个清单已经能覆盖「增删改查 + 联表查询 + 事务」全部考点。不要在期末阶段往上加购物车、排课冲突检测、教师端排课管理这类功能。做得面面俱到,看起来工作量很大,但答辩时老师针对每个模块追问下去,你很难全部讲透,反而暴露实现粗糙的地方。
这里有一个很多人忽略的点:期末大作业的报告和源码同等重要。报告里对需求的描述,决定了老师对你系统的第一印象。需求描述不要写「系统支持选课管理」这种空话,写成「系统提供课程列表浏览,学生可选选课人数未满的课程,同一学生不可重复选择同一课程,退课后课程名额自动回补」这样的可验证条目。每条需求对应一条测试路径,老师照着走一遍,整个项目就立住了。
2.2 为什么是 Python + MySQL:与 SQLite、JSON 文件存储的实际差距
很多同学问我:期末作业用 SQLite 不也一样能跑吗?确实能跑,但评分观感完全不同。SQLite 是一个文件型数据库,不需要安装服务端,对学习数据库原理几乎没有帮助,老师在报告里看不到你设计表结构、处理外键约束、排查连接问题的过程。MySQL 是教学和招聘要求里的常客,你简历上写「熟练使用 MySQL」,总得有一个项目是真的连过 MySQL 服务的。
Python 这边的理由更直接。pymysql 库足够轻量,不需要像 SQLAlchemy 那样引入 ORM 概念,代码里写的就是原生 SQL,正好和数据库课程的知识点一一对应。相比之下,用纯 JSON 文件存数据,虽然也能实现选课逻辑,但并发场景下两个学生同时选同一门最后一门课,文件读写根本没有事务和行锁的概念,数据会静默出错,这种系统拿去做大作业属于给自己埋雷。
从踩坑风险看,Python 加 MySQL 的主要成本在环境安装:MySQL 服务端要装、密码要记、字符集要配。这个成本恰恰也是评分点,因为报告里可以专门写一节「环境配置与问题排查」,记录你从安装 MySQL 到跑通第一个查询的全过程。老师看重的往往不是你一次装成功,而是你遇到问题后查资料、定位原因、解决掉的过程。
2.3 数据模型设计:学生、课程、选课三张表的字段取舍
这个项目的核心数据模型就是三张表:学生表 student、课程表 course、选课记录表 elective。不要在期末阶段强行拆出教师表、专业表、班级表,关系拉长之后,插入数据、写联表查询、画 ER 图的成本会翻倍,而且容易出错。
学生表我建议直接用学号做主键,不要再用自增 id。学号本身就是业务上的唯一标识,用它做主键,报告里的 ER 图一画就懂,查询 where 条件也直观。字段可以这样设计:student_id 定长字符串,name 姓名,gender 性别,major 专业,class_name 班级,password 密码字段。专业和班级直接冗余在学生表里,期末项目里拆出单独的班级表没有实际收益。
课程表的核心是 course_id、name、teacher、credit、capacity、selected_count。capacity 是课程容量,selected_count 是已选人数。这里有一个设计取舍:selected_count 是冗余字段,因为理论上可以用「统计 elective 表里该课程的记录数」来得到已选人数,不必单独存。但期末项目里我强烈建议保留这个字段,理由有二:一是选课时判断是否满员,直接比较 selected_count 和 capacity,一条条件更新语句就能完成,不用先做子查询再更新;二是报告里可以写「用冗余字段避免频繁聚合统计」,老师会认为你考虑了性能。当然你要在报告里写清楚:冗余字段需要在选课事务里同步自增、退课事务里同步自减,这是用空间换一致性维护成本。
选课记录表是业务核心,字段要包含自增主键 id、student_id、course_id、select_time 选课时间,以及一个可选的 score 成绩字段。选课记录表必须建唯一索引,约束同一学生不能重复选同一门课,这个约束要放在数据库层,不能只靠 Python 代码先查询再判断。数据库层的约束是最终防线,程序逻辑在并发场景下不可靠。自增主键 id 必须有,因为如果拿 student_id 加 course_id 做联合主键,后续想支持一个学生多次选修同一门课(比如重修)时,表结构就锁死了。
3. 建库到初始化:MySQL 表结构 SQL 与测试数据的设计
3.1 建库建表脚本:完整 DDL 与字符集处理
新建数据库和建表的脚本,我习惯写成可重复执行的格式,用 IF NOT EXISTS,这样答辩前即使数据库被搞乱了,重跑一遍 init.sql 就能救回来。完整的建库建表语句如下:
-- 创建数据库,指定 utf8mb4 字符集,避免中文乱码 CREATE DATABASE IF NOT EXISTS course_select_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE course_select_db; -- 学生表:学号是天然主键 CREATE TABLE IF NOT EXISTS student ( student_id CHAR(10) NOT NULL COMMENT '学号', name VARCHAR(20) NOT NULL COMMENT '姓名', gender VARCHAR(2) DEFAULT '男' COMMENT '性别', major VARCHAR(50) COMMENT '专业', class_name VARCHAR(50) COMMENT '班级', password VARCHAR(64) DEFAULT '123456' COMMENT '登录密码,演示用默认值', PRIMARY KEY (student_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; -- 课程表:selected_count 为冗余的已选人数 CREATE TABLE IF NOT EXISTS course ( course_id CHAR(8) NOT NULL COMMENT '课程编号', name VARCHAR(50) NOT NULL COMMENT '课程名称', teacher VARCHAR(20) COMMENT '授课教师', credit DECIMAL(2,1) DEFAULT 2.0 COMMENT '学分', capacity INT NOT NULL COMMENT '课程容量', selected_count INT DEFAULT 0 COMMENT '已选人数', PRIMARY KEY (course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; -- 选课记录表:唯一索引防止重复选课 CREATE TABLE IF NOT EXISTS elective ( id INT AUTO_INCREMENT COMMENT '自增主键', student_id CHAR(10) NOT NULL COMMENT '学号', course_id CHAR(8) NOT NULL COMMENT '课程编号', select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', score DECIMAL(4,1) DEFAULT NULL COMMENT '成绩,可选', PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course_id (course_id), CONSTRAINT fk_elective_student FOREIGN KEY (student_id) REFERENCES student (student_id) ON DELETE CASCADE, CONSTRAINT fk_elective_course FOREIGN KEY (course_id) REFERENCES course (course_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课记录表';字符集这里要一次配全,才能彻底避免中文乱码。数据库层面用 utf8mb4,它是 utf8 的超集,能存表情符号,更重要的是和 MySQL 8.0 默认字符集对齐。排序规则用 utf8mb4_unicode_ci,ci 表示大小写不敏感,这样查询时不用纠结 'python' 和 'Python' 的拼写差异。Python 连接串后面也要配 charset='utf8mb4',两层一致才靠谱。
外键的级联策略我在脚本里做了一个区分:学生删除了,他的选课记录跟着删(ON DELETE CASCADE);课程被选过之后不允许直接删(ON DELETE RESTRICT),这样能保住历史选课数据,报告的完整性也好写。课程表里 capacity 和 selected_count 都用了 INT,credit 用 DECIMAL(2,1),可以存 2.5 这种半分制学分。 password 字段期末项目直接用明文即可,但报告里要补一句「生产环境应使用哈希存储」,这是专业的体现。
3.2 外键约束与唯一索引:让数据库替你把关,而不是靠程序 if 判断
很多人写选课逻辑时习惯先查一遍有没有选过,if 不存在就执行插入。这种做法在期末答辩演示时看不出问题,但只要有两个窗口同时提交,程序就会放行两次查询,插进去两条重复记录。数据库层的唯一索引才是真正的兜底。
上面的 DDL 里,UNIQUE KEY uk_student_course 建立在 (student_id, course_id) 联合列上,它的语义就是「同一个学生对同一门课最多只能有一条选课记录」。程序层面哪怕出了问题,INSERT 语句一执行就会触发 1062 Duplicate entry 报错,事务回滚,数据不会脏。报告里写「利用唯一索引约束保证选课数据一致性」,比写「程序里做了判断」高一个档次。
还有一点容易被忽视:MySQL 要求外键列上必须有索引。elective 表里我加了 KEY idx_course_id,就是因为 course_id 作为外键虽然包含在联合唯一索引里,但联合索引的最左前缀是 student_id,单独拿 course_id 作为条件时走不了这个索引,所以单独建一个索引给外键使用。这个细节在报告里写清楚,很能体现数据库功底。
课程删除用 RESTRICT 的问题也要在报告里讲一下:课程表里有课程被选过后,DELETE FROM course WHERE course_id='...' 会直接报外键约束错误,老师看了会认为你理解了数据完整性。不用怕报错,报错本身就是设计的一部分,你要在报告里写明:这是故意限制的,目的是保留选课历史。
3.3 初始化测试数据:数据少而精,把每个演示场景都铺好
测试数据的质量直接决定演示效果,我一般是「学生 6 到 8 人、课程 10 门左右」。课程数据要覆盖四种状态:一门课容量 2 并且已选满,用于演示选课失败;一门课容量 3 已选 1 人,用于演示选课成功;一门课容量 30 但没人选,用于演示排在末尾;一门课容量 1,用于演示最后一个名额被抢走的边界。没有这些数据,你演示时想展示报错和异常分支都拿不出素材。
-- 学生数据,密码统一为 123456,省得登录时记不住 INSERT INTO student (student_id, name, gender, major, class_name) VALUES ('2024001', '张伟', '男', '计算机科学与技术', '计科2401'), ('2024002', '李娜', '女', '软件工程', '软工2401'), ('2024003', '王强', '男', '数据科学', '数据2401'); -- 课程数据:C001 已满,C002 余量充足,C003 无人选 INSERT INTO course (course_id, name, teacher, credit, capacity, selected_count) VALUES ('C001', 'Python程序设计', '刘老师', 3.0, 2, 2), ('C002', 'MySQL数据库', '陈老师', 2.5, 3, 1), ('C003', '操作系统', '赵老师', 4.0, 30, 0); -- 选课记录:张伟选了 C001 和 C002 INSERT INTO elective (student_id, course_id) VALUES ('2024001', 'C001'), ('2024001', 'C002'), ('2024002', 'C001');INSERT 出来的数据要和上面的 DDL 对得上号:C001 的 selected_count 是 2,elective 表里正好有 2 条 C001 记录,这个一致性在答辩时可以用一条联表查询当场验证,老师会觉得你的数据不是随手造的。你可以用一条 SQL 检查冗余字段是否和数据一致:
SELECT c.course_id, c.name, c.selected_count, COUNT(e.id) AS real_count FROM course c LEFT JOIN elective e ON c.course_id = e.course_id GROUP BY c.course_id, c.name, c.selected_count HAVING c.selected_count <> real_count;这条查询跑出来如果是空结果,说明所有课程的已选人数冗余字段都和实际记录数一致。把它写进报告的测试章节,是一个很漂亮的加分动作:它证明你意识到了冗余字段可能导致的数据不一致,并且设计了校验手段。LEFT JOIN 加 GROUP BY 加 HAVING,三个考点一次用齐,比单独写一条 SELECT 展示「我会用 JOIN」高级得多。
4. 用 Python 把选课流程串起来:pymysql 封装、事务与统计
4.1 数据库连接封装:把连接参数集中起来,避免代码里到处写密码
一个很常见的坏习惯是把数据库连接写在每个函数里,host、user、password 散落七八处,到最后改一个密码要全文替换。我一般会做一个 db.py 模块,把连接配置集中管理,核心代码如下:
# db.py import pymysql from pymysql.cursors import DictCursor DB_CONFIG = { "host": "localhost", # MySQL 服务地址,本机演示用 localhost "port": 3306, # MySQL 默认端口 "user": "stu", # 建议用专用账号,不要用 root "password": "stu_pass", # 专用账号密码 "database": "course_select_db", "charset": "utf8mb4", # 与建库字符集保持一致 "cursorclass": DictCursor, # 查询结果返回字典,按字段名取值 } def get_conn(): """获取一个数据库连接,autocommit 默认关闭,事务由调用方控制""" return pymysql.connect( autocommit=False, **DB_CONFIG )这段代码里有三个细节值得说。cursorclass 用 DictCursor,查询结果每一行是字典,比如 row["name"] 直接按字段名取,比元组下标 row[1] 可读性强很多,视图代码里不用记字段顺序。autocommit=False 是必须的,选课和退课都是多语句事务,如果每次 execute 都自动提交,中途出错就没有回滚的机会。DB_CONFIG 字典用 ** 展开传入 connect,后续要改连接参数只动这一处。
password 字段建议用专用账号而不是 root,原因后面第 5 章会专门说。这里先埋一个伏笔:老师打开你的 db.py,看到 root 密码裸写在代码里,第一印象就打了折扣;看到一个名为 stu 的专用账号,权限只授给这个库,他会认为你懂最小权限原则。
4.2 选课核心逻辑:先扣名额再插记录,整个操作放一个事务里
选课是这个系统的核心,也是答辩的重点。最容易翻车的写法是先 SELECT 查剩余名额,if 剩余名额大于 0 再 UPDATE 扣减,再 INSERT 记录。这个流程在单用户操作时没有任何问题,但两个学生同时提交时,两个 SELECT 都读到剩余 1 个名额,两个都执行 UPDATE,最后把容量撑爆。
正确做法是把「检查名额」和「扣减名额」合并成一条条件更新语句,让数据库在行锁层面做判断:
# course_service.py import pymysql from db import get_conn def select_course(student_id: str, course_id: str) -> str: conn = get_conn() try: with conn.cursor() as cursor: # 条件更新:仅当当前已选人数小于容量时才更新成功 sql = """ UPDATE course SET selected_count = selected_count + 1 WHERE course_id = %s AND selected_count < capacity """ affected = cursor.execute(sql, (course_id,)) if affected == 0: return "选课失败:课程不存在或容量已满" # 名额扣减成功后,写入选课记录 insert_sql = """ INSERT INTO elective (student_id, course_id) VALUES (%s, %s) """ cursor.execute(insert_sql, (student_id, course_id)) conn.commit() return "选课成功" except pymysql.IntegrityError: # 唯一索引冲突:同一学生重复选同一门课 conn.rollback() return "选课失败:你已选过这门课程" except Exception: conn.rollback() return "选课失败:数据库异常,操作已回滚" finally: conn.close()逻辑拆解一下:UPDATE course SET selected_count = selected_count + 1 WHERE course_id = %s AND selected_count < capacity 这条语句是关键,它把「查容量、判容量、扣名额」三个动作合并成一个原子操作。affected == 0 说明课程不存在或者已经满员,不需要再查一次。这里不抛异常,用受影响行数判断,是因为 UPDATE 没匹配到行是正常业务分支,不是错误。
affected 是 cursor.execute 的返回结果,表示受影响行数,这是 pymysql 等 DB-API 的标准行为。选课记录插入放在名额更新之后,如果 INSERT 因为唯一索引冲突失败,整个事务回滚,前面 UPDATE 加上的名额也会被撤销,数据回到一致状态。IntegrityError 是 pymysql 对 MySQL 1062 重复键错误的封装,捕获它单独返回用户友好提示,而不是把原始异常堆栈抛给界面。
这里有一个容易忽略的边界条件:万一某条课程记录的历史数据里 selected_count 本来就是错的,比如比实际记录数大,条件更新就永远更新不成功。所以初始化数据时做一次「对账」很重要,第 3.3 末尾那条 HAVING 查询就是干这个的。数据有脏值,业务逻辑再对也会出现查不出原因的现象。
4.3 退课与统计查询:事务回补名额,联表统计交给 SQL 而不是 Python
退课是选课的逆操作,逻辑是删除 elective 里的记录,再把 course 表的 selected_count 减一。这两个操作必须放在同一个事务里,不然就会出现「课退了,名额没回来」的经典翻车现场:
def drop_course(student_id: str, course_id: str) -> str: conn = get_conn() try: with conn.cursor() as cursor: # 先删除选课记录 delete_sql = """ DELETE FROM elective WHERE student_id = %s AND course_id = %s """ affected = cursor.execute(delete_sql, (student_id, course_id)) if affected == 0: conn.rollback() return "退课失败:没有找到选课记录" # 名额回补,加一个大于 0 的保护避免误回补成负数 update_sql = """ UPDATE course SET selected_count = selected_count - 1 WHERE course_id = %s AND selected_count > 0 """ cursor.execute(update_sql, (course_id,)) conn.commit() return "退课成功" except Exception: conn.rollback() return "退课失败:数据库异常,操作已回滚" finally: conn.close()DELETE 的 affected 为 0 表示学生压根没选过这门课,这个分支直接 rollback 然后返回提示。名额回补的 UPDATE 带上 selected_count > 0 条件,是一种防御性写法,理论上如果数据一致,删除记录后名额至少是 1,这个条件永远不会失败;但万一出现脏数据导致 selected_count 已经不正确,这个条件能防止名额被减成负数。
统计查询是期末报告里必须出现的一页。我建议直接用一条 JOIN 加 GROUP BY 的 SQL,在数据库端完成统计,不要在 Python 里循环计数。Python 做二次聚合既慢又容易出错,而且体现不出你会 SQL:
def list_course_stats(): conn = get_conn() try: with conn.cursor() as cursor: sql = """ SELECT c.course_id, c.name, c.teacher, c.capacity, c.selected_count, COUNT(e.id) AS real_count FROM course c LEFT JOIN elective e ON c.course_id = e.course_id GROUP BY c.course_id, c.name, c.teacher, c.capacity, c.selected_count ORDER BY real_count DESC """ cursor.execute(sql) rows = cursor.fetchall() for row in rows: print(f"{row['course_id']} {row['name']} " f"已选 {row['selected_count']}/{row['capacity']} " f"实际记录 {row['real_count']}") return rows finally: conn.close()ORDER BY real_count DESC 就是热搜里常说的 mysql 排序在业务里的典型用法。COURSE 表用 LEFT JOIN 关联选课记录,保证没有学生选的课程也会出现在结果里,real_count 为 0,这就不会丢数据。输出里同时打印 selected_count 和 real_count,演示时你当场说一句「冗余的已选人数和实际记录数完全一致」,就能证明初始化数据的正确性。
MySQL 8.0 默认开启 ONLY_FULL_GROUP_BY,所以 SELECT 里出现的非聚合列必须全部写进 GROUP BY。上面 SQL 里 c.course_id、c.name、c.teacher、c.capacity、c.selected_count 都进了 GROUP BY,如果你漏掉任何一个,执行会直接报错。这是初学者写 GROUP BY 最高频的报错,放到报告排错记录里也是一条素材。
5. 期末答辩避坑检查单:演示现场最容易翻车的 6 个问题
5.1 中文乱码:从建库到连接,三层字符集设置缺一层就乱
现象:MySQL 客户端里插入的中文正常,Python 查出来后变成问号;或者 PyCharm 控制台输出中文正常,写进数据库再查出来就是乱码。
原因:字符集配置只做了一部分。建库时指定了 utf8mb4,但 Python 连接串没带 charset;或者建表语句没指定字符集,继承的还是库级别默认值;还有一种情况是连接串写了 utf8,而数据库是 utf8mb4,两者不一致。
解决:把三层全检查一遍。第一层,建库建表时用 DEFAULT CHARACTER SET utf8mb4,表结构创建后可以用 SHOW CREATE TABLE course 确认;第二层,pymysql 连接参数里写 charset="utf8mb4";第三层,Python 源码文件本身要保证是 UTF-8 编码,PyCharm 右下角或文件头检查一下编码声明。三个字符集问题排查完还乱码,就别再改代码了,直接查 MySQL 系统变量,SELECT @@character_set_database 看库的当前值。
5.2 MySQL 8.0 认证插件导致的连接失败:pymysql 报错与处理
现象:MySQL 8.0 环境里,Python 程序一执行 connect 就抛异常,报错信息类似 Authentication plugin 'caching_sha2_password' cannot be loaded。
原因:MySQL 8.0 默认认证插件是 caching_sha2_password,而某些版本或编译方式的 pymysql 对它支持不完整。这个报错几乎只在 MySQL 8.0 上出现,老版本 MySQL 用的是 mysql_native_password,不存在这个问题。
解决:不要尝试去升级或更换驱动,最可靠的做法是创建一个使用旧认证插件的专用账号。执行 SQL:CREATE USER 'stu'@'localhost' IDENTIFIED WITH mysql_native_password BY 'stu_pass'; 然后 GRANT ALL PRIVILEGES ON course_select_db.* TO 'stu'@'localhost'; 程序里改用这个账号连接。这个操作既解决了认证问题,又顺便把 root 账号从代码里替换掉了,一举两得。注意老师机器上可能是 MySQL 5.7 也可能是 8.0,用 mysql_native_password 的账号在两种版本下都能连,兼容性最好。
5.3 退课后名额没回补:典型的事务边界错误
现象:退课操作提示成功,选课记录也没了,但课程列表里已选人数没减少,另一门容量很小的课依然显示满员。
原因:DELETE 和 UPDATE 放在了两个事务里。很多人写退课时先执行 DELETE,commit,再执行 UPDATE 回补名额,第二个操作失败时第一个已经提交了,名额就永久少了。还有一种情况是两个操作在同一个事务,但代码里漏了 commit,两个都没生效。
解决:退课必须是一个事务,DELETE 和 UPDATE 之间不执行 commit;先删记录再回补名额,两步都成功才统一 commit,任一步抛异常就 rollback。代码结构和上面 4.3 给出的函数完全一致。排查时看程序逻辑,更快的办法是手动在 MySQL 里对这个同学重新执行一遍 DELETE 加 UPDATE,观察 affected 行数,确认表结构和数据本身没问题,问题就锁定在代码事务边界上。
5.4 报告里的 ER 图与代码不一致:答辩时被追问最多的点
现象:报告里的 ER 图画的是四个表,代码里只有三个;报告里写了 elective 表用联合主键,代码里却是自增 id;报告里字段叫 stu_name,数据库里列名是 name。老师对照着报告看代码,前后对不上,整个项目的可信度立刻崩塌。
原因:报告先写完,代码后改动,改完代码没有回头更新报告。这是期末项目最常见的问题,没有之一。
解决:交付前最后一天,对着数据库导出的实际结构改报告。用 SHOW CREATE TABLE elective 拿到真实 DDL,照着画 ER 图、列字段;用 SELECT * FROM elective LIMIT 5 核对测试数据。如果文档和代码不一致,宁可花半小时改报告,也绝不要临时改代码去迁就文档,因为改代码会引入新的运行风险,离答辩只剩一晚时不值得冒险。
5.5 换电脑跑不起来:路径、依赖和 MySQL 服务的三连坑
现象:在自己电脑上好好的,到演示机器上双击运行直接抛 ModuleNotFoundError 或者连接拒绝。学生借了老师的投影电脑,插上 U 盘开始演示,开场就卡住。
原因:三种情况叠加。一是依赖没导出,对方机器没装 pymysql;二是连接配置写死了绝对路径或者特殊密码;三是 MySQL 服务没启动,或者对方机器 MySQL 的 root 密码跟你代码里写的不一样。
解决:项目根目录放一个 requirements.txt,内容写 pymysql;代码里连接参数全部收敛到 db.py 的 DB_CONFIG,演示前先问清楚对方机器 MySQL 的版本和密码,不行就在答辩现场用自己电脑演示。更专业的做法是带一条初始化命令,在演示机器上先执行 mysql 客户端导入 init.sql,再跑程序,整个过程控制在两分钟内。血泪经验:答辩前把整个流程在另一台电脑上完整走一遍,装依赖、启服务、导数据、跑功能,这一遍能暴露 80% 的现场问题。
5.6 root 密码裸奔与最小权限意识:老师一眼就能看出的不专业
现象:db.py 里直接用 root 连数据库,密码明文写着 123456,甚至 root 没有密码。代码能跑,但老师翻到这一页时眉头一皱。
原因:很多初学者只求能连通,没想过数据库账号的权限边界。root 是超级管理员,它不应该出现在业务程序的代码里,这是数据库安全的基本常识。期末项目虽然不涉及生产环境,但老师会拿行业标准来评价你的代码习惯。
解决:创建一个专用账号,只授予当前项目的库权限。SQL 如下:先用 root 登录 MySQL 执行 CREATE USER 'stu'@'localhost' IDENTIFIED BY 'stu_pass'; 再执行 GRANT ALL PRIVILEGES ON course_select_db.* TO 'stu'@'localhost'; 最后 FLUSH PRIVILEGES。程序连接串里把 user 和 password 换成这个专用账号。这不会花超过五分钟,但报告里能写一条「数据库连接使用最小权限账号,避免业务连接使用管理员账户」,专业性一下就凸显了。
6. 答辩演示的验证路径:从一条 SQL 到一键启动脚本,再到一份拿得出手的测试记录
答辩前一天的验证,不要从头到尾点一遍菜单就算完,那样记不住结果,老师一问数据就说不上来。我习惯把验证做成一串可重复执行的清单,每一条都有预期结果:正常选课返回成功;重复选同一门课返回「已选过」;选满员课程返回「容量已满」;退课后再选同一门课能成功;统计列表的已选人数和 elective 实际记录数一致。五条跑完,核心业务闭环就验证完了。
为了节省演示时间,我会写一个一键启动脚本,它的职责不是启动 GUI,而是做环境自检:检查 pymysql 是否安装、连接 MySQL 是否成功、三张表是否存在、测试数据是否齐全。如果表不存在就自动执行 init.sql 重建,并且初始化脚本要做成幂等的,重复运行不会产生重复数据。原理是用 DROP TABLE IF EXISTS 重建表结构,再重新插入测试数据。演示现场万一数据库出了状况,重跑一遍就能恢复到干净状态,这手救场设计能保证你的演示不会在中途卡死。
还有一个加分技巧是 EXPLAIN 验证索引。演示时在 MySQL 里跑一句 EXPLAIN SELECT * FROM elective WHERE student_id='2024001'; 展示结果是走了 uk_student_course 这个联合索引,type 是 ref。解释一句「联合索引的最左前缀匹配规则,所以按学号查选课记录能走索引」,老师通常会对这个细节刮目相看。这不是让你临时背概念,而是把你自己设计的表结构真正说清楚。
最后说一个我的教训:毕业设计答辩前两天,我改了 elective 表结构,忘了更新报告里的 ER 图,答辩时老师三分钟就指出了不一致,那一段对话成为整场答辩最尴尬的环节。从那之后我养成了习惯,任何项目交付前,用 SHOW CREATE TABLE 导出真实表结构,再对着它写文档、画图、准备讲稿。代码诚实,文档才敢诚实。这个习惯帮我省下了很多事后补救的麻烦,希望帮到你。
本文还有配套的精品资源,点击获取