简介:基于Python与MySQL的学生选课管理系统,属于期末大作业级别的高分设计项目,曾获导师指导与评审认可,主要面向计算机相关专业正在完成课程设计、数据库大作业或毕业设计的学生,也适合需要项目实战练习的初级学习者。资源包含完整源码、数据库脚本与课程报告,共8个文件,核心为4个Python程序(覆盖登录窗口、课程管理、教师管理等模块),1个SQL脚本用于建表与初始化数据,另有doc格式的《数据库原理》报告、md说明文档等,压缩包仅1.11MB,目录结构清晰。系统经本地编译与严格调试,可稳定运行,下载后导入数据库即可体验完整选课流程。目前已有88人学习下载,对于需要快速理解选课系统设计思路、参考数据库建表规范、掌握Python与MySQL结合开发技巧或撰写课程报告的学习者具有较高借鉴价值。
1. 基于 Python 和 MySQL 的学生选课管理系统:期末大作业的真实现场
期末周最怕的不是复习,而是突然收到《数据库原理》大作业的补交通知,要求做一个能跑的系统、附建表脚本和报告。这个基于 Python 和 MySQL 的学生选课管理系统,就是我当年在导师指导下从零搭起来、最后拿 98 分的期末大作业,源码、数据库脚本和《数据库原理》报告三件套齐全,本地编译过能直接运行。它解决的痛点很具体:学生登录后能查课、选课、退课,教师端能维护课程信息,管理员能管理学生档案,三张表把选课闭环走通,不用你从零设计表结构、四处找靠谱的 PyMySQL 连接写法。适合正在做课程设计、期末大作业,或者想练一个完整 Python+MySQL 项目的计算机相关专业学生。照着跑一遍,比自己对着零散教程拼代码省下两个通宵。
拿到资源后先别急着写代码,我拆这套系统会按「表结构 → 登录 → 选课 → 教师端 → 排错 → 验收」的顺序讲,坑都踩过了,你照着走就行。
2. 数据库设计与 S 表建表脚本:三张表怎么设才不玄学
2.1 选课系统为什么是三张表:S、course、sc 的职责边界
选课系统能不能撑起来,关键在表结构设计。资源里有一份「根据书上表12.1添加S表信息.sql」,S 表就是教材《数据库原理》里经典的 Student 实体表。选课场景最小闭环需要三张表,各管各的:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| S(student) | 学生档案 | Sno 学号主键、Sname 姓名、Ssex 性别、Sage 年龄、Sdept 院系 |
| course | 课程档案 | Cno 课程号主键、Cname 课程名、Cpno 先修课、Ccredit 学分 |
| sc | 学生选课关系 | Sno、Cno 复合主键、Grade 成绩 |
为什么一定是三张而不是一张大表?拿学生选课来说,一个学生选三门课,如果全塞进一张表,学生姓名、院系就得重复存三遍,改一次院系要同步改三行,更新异常和数据冗余一起冒出来。教科书第三范式讲的就是这个问题:把学生和课程这两个实体拆开,用 sc 表记录它们的多对多关系。答辩时老师最常问「选课关系为什么单独建表」,答案就在这里,能从这个角度答出来,比背范式定义有说服力得多。
2.2 建表脚本:主外键和字段类型怎么定才不会翻车
这套系统的建表脚本直接照搬教材表 12.1 的字段设计,再补上课程表和选课表的约束。我把它整理成标准可执行的版本:
-- 学生表 S(对应教材表 12.1 的字段设计) CREATE TABLE S ( Sno CHAR(9) PRIMARY KEY, -- 学号定长字符,主键唯一 Sname CHAR(20) NOT NULL, -- 姓名必填 Ssex CHAR(2) DEFAULT '男', -- 性别给个默认值,插入时可省略 Sage SMALLINT, -- 年龄用短整型足够 Sdept CHAR(20) -- 所在院系 ); -- 课程表 course CREATE TABLE course ( Cno CHAR(4) PRIMARY KEY, -- 课程号 Cname CHAR(40) NOT NULL, -- 课程名必填 Cpno CHAR(4), -- 先修课程号,可为空 Ccredit SMALLINT -- 学分 ); -- 选课表 sc CREATE TABLE sc ( Sno CHAR(9) NOT NULL, Cno CHAR(4) NOT NULL, Grade DECIMAL(4,1), -- 成绩保留一位小数,没出成绩时为 NULL PRIMARY KEY (Sno, Cno), -- 复合主键,从数据库层拦死重复选课 FOREIGN KEY (Sno) REFERENCES S(Sno), FOREIGN KEY (Cno) REFERENCES course(Cno) );几个参数选择要说清楚。学号用 CHAR(9) 而不是 VARCHAR,因为学号是固定位数,定长字符检索更快,也不会因为尾部空格产生匹配问题。Grade 用 DECIMAL(4,1),最大能存 99.9,覆盖百分制成绩刚刚好,浮点误差不存在。最核心的是 sc 表复合主键 (Sno, Cno),这是防重复选课的第一道防线——代码层判重是第二道,后面会讲。外键约束保证插入选课记录时,学生和课程必须真实存在,数据完整性留给数据库管,Python 代码省掉一半校验逻辑。
注意建表顺序:先建 S 和 course,再建 sc,因为 sc 引用了前两张表的主键。实战中很多人先执行 sc 建表脚本,直接报「无法创建外键约束」的错,不是 SQL 写错了,是顺序问题。
提示:正式环境给 Sage、Grade 这类字段加 CHECK 约束也没问题,期末大作业里 SMALLINT、DECIMAL 加程序层校验就够了。
2.3 初始化数据与连接配置:能跑通的第一步
表建好就该灌数据了。资源里的 SQL 脚本自带了一批示例数据,结构类似这样:
USE xsxk; -- 选课系统库名,按自己环境改 INSERT INTO S (Sno, Sname, Ssex, Sage, Sdept) VALUES ('2018001', '张明', '男', 20, '计算机系'), ('2018002', '李雪', '女', 19, '信息管理系'); INSERT INTO course (Cno, Cname, Cpno, Ccredit) VALUES ('C001', '数据库原理', NULL, 4), ('C002', '数据结构', 'C001', 4); INSERT INTO sc (Sno, Cno) VALUES ('2018001', 'C001');插入顺序跟在建表顺序一样,先学生和课程,再选课关系。sc 表插入时只写 Sno 和 Cno,Grade 等老师录成绩再用 UPDATE 补上,这样设计符合实际业务流程。数据量不用多,每张表三五条就能演示完整闭环。
然后是 Python 连接 MySQL 的配置,这套系统用的驱动是 PyMySQL,轻量而且 pip install pymysql 就能装,不需要像 MySQLdb 那样本地编译一堆依赖:
import pymysql conn = pymysql.connect( host='localhost', # 本机数据库地址 port=3306, # MySQL 默认端口 user='root', password='你的密码', # 改成自己 MySQL 的 root 密码 database='xsxk', # 对应上面 CREATE DATABASE 的库名 charset='utf8mb4' # 注意是 utf8mb4,不是 utf8 )连接参数里最容易被忽略的是 charset。MySQL 的 utf8 实际是 utf8mb3,有些生僻字和 emoji 会存不进去,写入直接报错或变成乱码。utf8mb4 才是完整版 UTF-8,Python 端连 MySQL 我统一用这个,省得后面跟乱码较劲。数据库密码要和你本机安装 MySQL 时设置的一致,README 里一般也会写默认账号,下载后先改这里的 password 再跑程序。
3. 登录模块:login_window.py 里的角色识别与窗口跳转
3.1 单窗口登录:账号密码查询的四步逻辑
登录是整个系统的入口,login_window.py 里做的事可以拆成四步:布局、取值、查库、判断。界面是 PyQt 还是 tkinter 其实不影响核心逻辑,关键在于校验函数怎么写。这套系统的常见做法是把数据库校验独立成函数,界面按钮只负责调用:
def check_login(username, password): try: conn = pymysql.connect(**DB_CONFIG) # 连接参数统一放配置里,多处复用 cur = conn.cursor(pymysql.cursors.DictCursor) sql = "SELECT * FROM user WHERE username=%s AND password=%s" cur.execute(sql, (username, password)) row = cur.fetchone() cur.close() conn.close() return row except pymysql.MySQLError as e: print("数据库错误:", e) return None这里有个细节是参数化查询。SQL 里的 %s 占位符配合元组传参,而不是把 username 直接拼进字符串,这是防 SQL 注入最基本的写法。期末答辩老师看到这个点通常会追问两句,能答上来「参数化查询会让数据库把输入当数据而不是 SQL 代码处理」就稳了。密码在期末项目里一般直接明文存在 user 表,报告里可以明确写这是教学演示的简化处理,生产环境必须用哈希加盐,主动交代比被问出来体面得多。
3.2 角色判定与登录后的窗口跳转
user 表里三类账号怎么区分?这套系统用的是账号前缀方案,不用额外建角色表。学生账号 s 开头,教师 t 开头,管理员 a 开头,判定函数很简单:
def get_role(row): # 账号前缀区分角色:s 前缀学生,t 前缀教师,a 前缀管理员 if row['username'].startswith('a'): return 'admin' elif row['username'].startswith('t'): return 'teacher' else: return 'student'登录按钮的事件处理里,校验通过后再按角色跳转对应主窗口,这是 login_window.py 的收尾动作:
def login_click(self): username = self.entry_user.get().strip() password = self.entry_pwd.get().strip() if not username or not password: messagebox.showwarning("提示", "账号和密码不能为空") return row = check_login(username, password) if row: role = get_role(row) self.destroy() # 登录窗口销毁,释放界面资源 if role == 'student': StudentWindow(row['username']) elif role == 'teacher': TeacherWindow(row['username']) else: AdminWindow(row['username']) else: messagebox.showerror("错误", "账号或密码不正确")跳转用面向对象的方式实例化主窗口类,登录窗口直接 destroy,避免两个窗口叠在一起。传入 row['username'] 是为了让后续窗口知道当前登录人是谁——学生窗口需要学号查选课记录,教师窗口需要工号关联课程,这个参数一定要传。
做扩展时有两点建议:一是空值校验做在查库之前,别拿空字符串去拼 SQL,省一次无意义的数据库往返;二是登录失败不要提示「用户不存在」或「密码错误」这种细粒度信息,统一提示「账号或密码不正确」,防别人遍历账号。
3.3 登录模块的扩展点:记住密码与退出登录
资源交付的版本把登录做到了够用,如果你想加两个小功能撑一下报告篇幅,最划算的是「记住密码」和「退出登录」。记住密码常见做法是写到本地配置文件里,登录成功后把账号写进去,下次启动读出来回填输入框,密码可以用 base64 简单处理,注意这只是演示级方案。退出登录则是在主窗口加一个按钮,动作是销毁当前窗口、重新实例化 LoginWindow,回到登录界面而不是直接关闭程序,这样演示「权限切换」时不用重启程序,视觉效果很加分。
4. 选课核心流程:course_class.py 里的判重、容量与事务
4.1 课程列表:LEFT JOIN 一次性捞出选课状态
进入学生主界面后第一个动作是加载课程列表。course_class.py 的核心查询不是简单 SELECT * FROM course,而是一次性把课程信息和当前学生的选课状态查出来:
def load_courses(self, sno): sql = """ SELECT c.Cno, c.Cname, c.Ccredit, CASE WHEN sc.Sno IS NULL THEN '未选' ELSE '已选' END AS state FROM course c LEFT JOIN sc ON c.Cno = sc.Cno AND sc.Sno = %s ORDER BY c.Cno """ cur.execute(sql, (sno,))为什么用 LEFT JOIN 而不是先查课程再逐条去 sc 表判重?因为一次 JOIN 就能拿到完整数据,不用写循环,代码短,数据库也只跑一次查询。这里的门道在 LEFT JOIN 的 ON 条件里带了AND sc.Sno = %s——这个条件必须放在 ON 后面,如果放到 WHERE 里,JOIN 完再过滤会把没选过的课程全筛掉,结果列表只剩已选课程,这是这个查询最容易踩的坑。CASE WHEN 把是否已选转成可读的「已选 / 未选」状态,表格控件里直接显示,不需要在 Python 里再包一层判断。
4.2 选课落库:判重、容量、事务三步走
选课按钮的点击处理是这套系统的核心逻辑,我把它拆成三步:先判重、再查容量、最后插入并提交事务。代码这样写:
def select_course(self, sno, cno): conn = pymysql.connect(**DB_CONFIG) try: cur = conn.cursor() # 第一步:判重,给用户友好提示 cur.execute("SELECT 1 FROM sc WHERE Sno=%s AND Cno=%s", (sno, cno)) if cur.fetchone(): return "该课程已选,请勿重复选课" # 第二步:检查课程容量(课程表扩展了 capacity 和 selected 字段时使用) cur.execute("SELECT capacity, selected FROM course WHERE Cno=%s", (cno,)) row = cur.fetchone() if row and row[1] >= row[0]: return "课程已满" # 第三步:插入选课记录,同步更新已选人数 cur.execute("INSERT INTO sc (Sno, Cno) VALUES (%s, %s)", (sno, cno)) cur.execute("UPDATE course SET selected = selected + 1 WHERE Cno=%s", (cno,)) conn.commit() # 事务提交,数据真正落库 return "选课成功" except pymysql.MySQLError as e: conn.rollback() # 任何一步出错,回滚所有操作 return f"选课失败:{e}" finally: cur.close() conn.close()这段代码里最容易被忽略的是 commit 和 rollback 这对组合。没有 commit,INSERT 执行后数据只在事务里,程序一关全丢;没有 rollback,INSERT 成功但 UPDATE 失败时,库里会留下一条选课记录却没人数字段没加,脏数据。两个一起用,保证「插选课记录」和「加已选人数」要么全成功要么全失败。
capacity 和 selected 这两个字段原始建表脚本里没有,是给课程表扩展出来的容量控制字段。期末演示时把某门课的 capacity 设成 2,选第三个人就会触发「课程已满」,这个环节在答辩现场效果很好,能证明你考虑过真实选课场景。
注意:先查再插的判重是给用户友好提示用的,sc 表的复合主键才是最终防线。两条请求同时插入时,数据库会拦截重复记录抛异常,被 except 兜住。
4.3 退课与教师端维护:数据一致性怎么保证
退课是选课的逆操作,逻辑对称但有个细节容易漏——退课后要判断影响行数:
def drop_course(self, sno, cno): cur = conn.cursor() cur.execute("DELETE FROM sc WHERE Sno=%s AND Cno=%s", (sno, cno)) if cur.rowcount == 0: return "未找到选课记录" cur.execute("UPDATE course SET selected = selected - 1 WHERE Cno=%s", (cno,)) conn.commit() return "退课成功"cur.rowcount是 PyMySQL 提供的受影响行数,DELETE 删不到记录时它是 0,这时候直接提示用户没选过这门课,而不是继续执行 UPDATE 把已选人数减成负数。这个细节新手很容易翻车——选课记录不存在时,人数照样减,数据就对不上了。
教师端在 teacher_class.py 里,本质是课程的增删改查。新增课程走 INSERT,改课程名和学分走 UPDATE,删除课程前要先检查有没有学生选了这门课——有选课记录的课程直接 DELETE 会触发外键约束报错,常见做法是先 DELETE FROM sc WHERE Cno=%s 清掉关联记录,再删除课程本身,同样用事务包起来。
每个操作完成后都要重新加载课程列表刷新界面,常见做法是操作成功弹一条消息,然后调用一次 load_courses 重新渲染表格,别用「重建整个窗口」这种重办法,数据变了重新查一遍就行。
5. 避坑与排查:五个最容易翻车的点
5.1 MySQL 8.0 认证插件不兼容
现象:pymysql.connect 直接抛异常,报错信息里带Authentication plugin 'caching_sha2_password' cannot be loaded。
原因:MySQL 8.0 默认用 caching_sha2_password 做密码加密,老版本的 PyMySQL 不认这个插件。
解决:两种办法选一个。把 PyMySQL 升到新版,大部分情况下能解决;还不行就在 MySQL 里把 root 账号的认证方式改回 mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;改完重启 MySQL 服务再跑程序。装 MySQL 8.0 的同学大概率会撞上这个,记住这个 SQL 就行。
5.2 登录界面中文全部变成问号
现象:窗口标题、按钮、表格里的中文显示成???或乱码。
原因:三层字符集不一致——MySQL 库或表的默认字符集是 latin1,Python 连接用的是 gbk,终端是 cp936,各说各话。
解决:先查库的真实字符集,再统一改成 utf8mb4:
SHOW CREATE DATABASE xsxk; ALTER DATABASE xsxk CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Python 端连接参数固定写charset='utf8mb4',跟库对齐。改完库字符集后要重跑程序,已经存在的乱码数据删掉重插。光标定位在 Python 源文件里的中文字符串时检查文件本身是不是 UTF-8 编码——用记事本另存为时选 UTF-8,这个最隐蔽。
5.3 服务没启动或端口被占用
现象:报Can't connect to MySQL server on 'localhost:3306',连接被拒绝。
原因:MySQL 服务根本没起来,或者 3306 端口被别的程序占了。
解决:Windows 下按 Win+R 输入 services.msc,找到 MySQL 服务右键启动,顺便把启动类型改成自动。端口被占用时,命令行跑netstat -ano | findstr 3306查占用进程的 PID,到任务管理器结束它;不想动其他程序就把 Python 连接参数的 port 改成 3307,同时改 MySQL 配置文件 my.ini 里的 port 项,两个地方要对齐。
5.4 快速连点选课按钮导致主键冲突
现象:界面连续点选课,某次报Duplicate entry '2018001-C001' for key 'PRIMARY',弹的错误提示很丑。
原因:代码先查后插的判重逻辑在两毫秒内都判断「没选过」,两条 INSERT 都执行了,复合主键在数据库层拦截了第二条。
解决:把 INSERT 改成 INSERT IGNORE,主键冲突时静默跳过并返回影响行数:
cur.execute("INSERT IGNORE INTO sc (Sno, Cno) VALUES (%s, %s)", (sno, cno)) if cur.rowcount == 0: return "该课程已选"先查后插的友好提示保留,INSERT IGNORE 做兜底,界面体验和数据安全两头都占住。
5.5 报告内容与实际运行结果对不上
现象:交上去的报告截图是网上找的,界面风格、按钮文案跟自己程序完全不一样,答辩现场演示时露馅。
原因:赶 deadline 复用了别人的截图,或者改过代码后没重新截图。
解决:跑通程序后自己截全流程,至少覆盖登录、选课、退课、教师端添加课程、管理员查询五张图,贴到报告「系统实现」章节。报告里的建表脚本也要跟 S 表信息.sql 里的字段类型完全一致,老师会翻文件核对。我见过最惨的是报告里写 Sage INT,脚本里是 SMALLINT,当场被指出来。文档和代码保持同一次运行产物,这个习惯能救你无数次。
6. 最后验收与交付:十分钟跑完闭环再截图
6.1 验收清单:演示前过一遍
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 启动登录窗口 | 界面正常无报错 |
| 2 | 空账号登录 | 弹出「账号和密码不能为空」 |
| 3 | 学生账号登录 | 进入学生界面,课程列表加载 |
| 4 | 选一门课 | 提示选课成功,状态变「已选」 |
| 5 | 重复选同一门 | 提示已选,不产生重复记录 |
| 6 | 教师账号登录 | 能添加、修改课程,界面联动刷新 |
| 7 | 管理员登录 | 能看到学生列表和选课统计 |
6.2 用 pymysql 快速核对落库
界面操作完,写一个几行的脚本直连数据库确认数据真的写进去了,防止界面提示成功但库里没数据:
import pymysql conn = pymysql.connect(host='localhost', user='root', password='你的密码', database='xsxk', charset='utf8mb4') cur = conn.cursor() cur.execute("SELECT Sno, Cno FROM sc ORDER BY Sno") for row in cur.fetchall(): print(row) cur.close() conn.close()如果界面显示已选但这个脚本查不到,99% 是漏了 commit。这条检查线我在验收时每次都跑,比肉眼盯界面可靠。
6.3 把演示数据换成自己的学院和专业
交作业前把 S 表里的学生换成自己学校的信息,课程换成自己专业的培养方案课,报告截图会显得更真实。需要同步改三处:S 表的 INSERT、course 的 INSERT、登录账号表,都改成自己熟悉的上下文。改完后重新执行建库脚本,按上面的验收清单完整跑一遍再截图。
我做这个项目时翻过最大的车,是答辩前夜临时改了课程编码规则,忘了同步更新报告里的建表脚本和截图,第二天被老师翻出来对不上。从那以后,每次交课程设计我都强制走一遍「删库重建 → 跑全流程 → 对照报告截图」三连,确认脚本、截图和报告是同一次运行的产物。这份资源下载下来后,建议你也按这个顺序先跑通再改数据,至少能省掉答辩前一半的无谓折腾。希望帮到你。
本文还有配套的精品资源,点击获取