尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Python和MySQL的选课系统:tkinter与pymysql实践

基于Python和MySQL的选课系统:tkinter与pymysql实践 简介这是一个基于Python和MySQL的学生选课管理系统期末大作业项目面向计算机相关专业正在完成课程设计、期末大作业或毕业设计的学生也适合需要项目实战练习的入门学习者。项目经导师指导并认可评审分98分源码均本地编译调试通过可稳定运行能直接用于学习或二次开发。资源包共8个文件包含4个Python源码文件如登录窗口、课程管理、教师管理等模块、1个SQL数据库脚本、1份数据库原理报告Word文档、1个README说明文件及1个gitattributes配置压缩包仅1.11MB结构紧凑便于快速下载和查阅。其中SQL脚本按教材表12.1添加学生信息配套报告内容完整可帮助理解选课系统的数据库设计与Python实现思路。目前已有86人学习下载适合作为课设模板或项目参考性价比高。1. 为什么期末大作业选课系统要自己从零搭一遍期末大作业里十个人有八个交的是「图书管理系统」剩下两个里有一个是「学生选课」。这套基于 Python 和 MySQL 的选课管理系统少见的地方在于它没有用 Django 或 Flask 这种重框架而是用原生 tkinter 写的桌面 GUI 程序配合 pymysql 直连数据库。也就是说你看到的每一个按钮、每一条 SQL 都是裸写出来的没有任何框架替你兜底遮丑导师查重、问原理的时候每一个点都能答得上这是它最终拿到高分的关键原因。整个项目围绕三个核心类展开login_window.py负责登录鉴权和窗口跳转course_class.py封装选课业务的增删改查teacher_class.py处理教师端的成绩录入。数据库端按《数据库原理》教材的表 12.1 建 S 表学生表再扩展出课程表、选课表和教师表一共 4 张表构成完整的第三范式结构。难度上比 CRUD 四张表的入门项目高半档比动不动上 Redis 和微服务的伪需求低得多恰好卡在课程设计要求的「有业务逻辑、有约束控制、有界面交互」这条线上。这套项目适合两类人一是正在做数据库课程设计、需要一份能跑通且能讲明白参考实现的在校生二是想快速掌握 tkinter pymysql 这套「最朴素数据库应用栈」的 Python 入门者。文件包里附带完整的《数据库原理》报告文档、建表 SQL 和已调试过的可运行源码本地装好 MySQL 后改一下连接参数就能跑起来。2. 数据库端设计从教材表 12.1 到四张业务表2.1 为什么选 MySQL 而不是 SQLite 或 Access课程设计的评审老师关注的不是功能炫不炫而是你能否把「关系完整性、事务、视图、存储过程」这些数据库原理课的核心概念落到实现里。SQLite 是嵌入式文件数据库装完即用但触发器、事务隔离级别、用户权限这些概念不好展开写Access 更偏向桌面文件型数据库跑在 Windows 上和 Python 的连接方式老旧。MySQL 是目前生产环境使用率最高的开源关系型数据库网上排错资料多5 年以上经验的人也都绕不开它。从课程设计的角度MySQL 8.0 支持窗口函数、CTE公共表表达式在写报告时可以多写一章「基于窗口函数的学生选课排名查询」这是一个很自然的加分点不需要额外引入其他技术栈。2.2 实体关系梳理与范式校验根据业务需求系统至少要管理四类实体学生、教师、课程、选课记录。学生和课程之间是多对多关系必须通过选课表解耦教师和课程是一对多关系一门课只有一个主讲教师一个教师可以带多门课所以教师的主键直接作为课程表的外键即可。学生(学号, 姓名, 性别, 年龄, 系别) 教师(工号, 姓名, 职称, 系别) 课程(课程号, 课程名, 学分, 工号) 选课(学号, 课程号, 成绩)选课(学号, 课程号)作为联合主键满足第二范式不存在部分函数依赖和第三范式不存在传递函数依赖。成绩字段允许为空表示已选课但尚未录入成绩这是实际业务所必需的。2.3 建表 SQL 与关键约束说明文件包里的根据书上表12.1添加S表信息.sql对应教材的 S 表学生表我在实际复现时在其基础上补充了另外三张表。以下是最小可运行的完整建表脚本-- 学生表S对应教材表12.1 CREATE TABLE IF NOT EXISTS S ( sno CHAR(9) PRIMARY KEY, sname VARCHAR(20) NOT NULL, ssex CHAR(2) DEFAULT 男 CHECK (ssex IN (男, 女)), sage TINYINT, sdept VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 教师表T CREATE TABLE IF NOT EXISTS T ( tno CHAR(6) PRIMARY KEY, tname VARCHAR(20) NOT NULL, title VARCHAR(10), tdept VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表C CREATE TABLE IF NOT EXISTS C ( cno CHAR(6) PRIMARY KEY, cname VARCHAR(30) NOT NULL, credit DECIMAL(3,1), tno CHAR(6), FOREIGN KEY (tno) REFERENCES T(tno) ON DELETE SET NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课表SC CREATE TABLE IF NOT EXISTS SC ( sno CHAR(9), cno CHAR(6), grade DECIMAL(5,1), PRIMARY KEY (sno, cno), FOREIGN KEY (sno) REFERENCES S(sno) ON DELETE CASCADE, FOREIGN KEY (cno) REFERENCES C(cno) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个关键设计决策值得解释。CHAR定长字符串用于学号、工号等固定长度字段MySQL 对CHAR的检索速度优于VARCHAR且学号前导零不会被截断sage用TINYINT而不是INT节省存储空间范围 0-255 足够成绩字段用DECIMAL(5,1)而不是FLOAT因为FLOAT的二进制浮点表示会导致 89.5 这类小数出现精度误差而DECIMAL是字符串存储的定点数计算成绩总分和平均分时不会出错。外键约束上课程表的tno外键设ON DELETE SET NULL意思是教师离职后被删除课程信息保留但主讲教师为空选课表的两个外键都设ON DELETE CASCADE即学生退学、课程取消时对应的选课记录自动连带删除避免出现「选了课但课没了」的脏数据。2.4 初始化数据的正确姿势根据书上表12.1添加S表信息.sql文件里包含了教材上的示例数据也就是《数据库原理》教材 12.1 节用到的那批学生记录比如学号201215121的李勇、201215122的刘晨等经典样例数据。实际运行时要先执行建表脚本再执行数据插入脚本。插入数据时注意顺序先插 S、T 表再插 C 表因为 C 表依赖 T 表的主键最后插 SC 表否则会违反外键约束直接报Cannot add or update a child row错误。3. Python 端模块划分与 tkinter pymysql 核心实现3.1 为什么用 tkinter 而非 PyQt 或 Web 前端选择 tkinter 不是因为它强大而是因为它足够「裸」。tkinter 是 Python 标准库自带的 GUI 工具包不需要额外安装这在期末大作业答辩时有两点实际意义第一演示环境往往不是你自己配好的机器可能是老师办公室的电脑或者机房机器tkinter 省去了 PyQt5 那几百 MB 的依赖安装第二tkinter 的事件循环、控件变量绑定StringVar/IntVar机制能直观体现「事件驱动」的 GUI 编程思想这在课程报告里可以展开写半页。PyQt5 虽然界面更现代、控件更丰富但对选课系统这种表单密集型应用来说是杀鸡用牛刀。你在课程设计的场景里只需要窗口、标签、输入框、按钮、表格Treeview、下拉框这几个控件tkinter 足够覆盖。3.2 数据库连接的正确封装方式整个系统最容易被扣分的地方就是数据库连接。很多大作业的代码会在每个按钮事件里重复写pymysql.connect(...)连接用完不关最后报Too many connections。我拆这个项目时重构了连接逻辑用一个独立的数据库工具类管理连接# db_util.py import pymysql from dbutils.pooled_db import PooledDB POOL PooledDB( creatorpymysql, maxconnections10, mincached2, maxcached5, blockingTrue, hostlocalhost, port3306, userroot, password123456, databasecourse_select, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) def query(sql, argsNone): conn POOL.connection() try: with conn.cursor() as cursor: cursor.execute(sql, args) return cursor.fetchall() finally: conn.close() def execute(sql, argsNone): conn POOL.connection() try: with conn.cursor() as cursor: rows cursor.execute(sql, args) conn.commit() return rows except Exception: conn.rollback() raise finally: conn.close()这段代码的逻辑重点在PooledDB连接池maxconnections10限制最大连接数mincached2表示启动时预先创建 2 个空闲连接blockingTrue表示连接池用尽后请求方阻塞等待而不是直接抛异常。查询操作走with语法确保游标释放写操作INSERT/UPDATE/DELETE需要commit()提交事务异常时rollback()回滚。cursorclasspymysql.cursors.DictCursor这个参数值得特别说明。默认的游标返回元组取数据时要用row[0]按下标访问代码可读性差且容易越界改成DictCursor后每行是字典可以通过row[sname]访问字段代码维护成本降低一个量级。如果连接池不是自己搭的而是逐条connect()那每个连接都需要手动conn.close()一旦在业务逻辑里提前return就很容易漏关连接。3.3 course_class.py 选课核心业务类选课业务逻辑封装在course_class.py中它对外暴露学生选课、退课、查看已选课程、查看可选课程四个方法。核心选课方法要考虑三重约束课程容量、重复选课、选课时间窗口。# course_class.py import pymysql from db_util import execute, query class CourseManager: # 学生选课 def select_course(self, sno, cno): # 1. 检查是否已选过该课程 selected query(SELECT 1 FROM SC WHERE sno%s AND cno%s, (sno, cno)) if selected: raise ValueError(不能重复选课) # 2. 检查课程容量是否已满假设 C 表有 capacity 字段 row query( SELECT capacity, (SELECT COUNT(*) FROM SC WHERE cno%s) AS selected_num FROM C WHERE cno%s, (cno, cno) ) if not row: raise ValueError(课程不存在) if row[0][selected_num] row[0][capacity]: raise ValueError(课程容量已满) # 3. 执行插入利用联合主键兜底并发重复 return execute( INSERT INTO SC(sno, cno) VALUES(%s, %s) ON DUPLICATE KEY UPDATE cnocno, (sno, cno) ) # 学生退课 def drop_course(self, sno, cno): return execute(DELETE FROM SC WHERE sno%s AND cno%s, (sno, cno)) # 查已选课程及成绩 def get_selected_courses(self, sno): sql SELECT C.cno, C.cname, C.credit, SC.grade FROM SC JOIN C ON SC.cno C.cno WHERE SC.sno %s return query(sql, (sno,))选课的三个步骤按业务优先级排序先查重复再查容量最后落库。SQL 全部使用%s参数占位符而不是字符串拼接这是防止 SQL 注入的第一道防线。ON DUPLICATE KEY UPDATE cnocno这行是并发兜底——如果两个窗口同时提交同一学号同一课程号的选课请求联合主键冲突会被这个语句吞掉而不是抛异常实际效果等同于忽略重复插入。SELECT 1而不是SELECT *是一个性能习惯只需要知道「是否存在」不需要取出整行数据MySQL 碰到SELECT 1且命中主键索引时可以直接走索引覆盖扫描不做回表查询在大数据量下性能差距显著。4. 登录窗口到业务流转从 login_window.py 跑通全流程4.1 登录窗口的三态设计学生 / 教师 / 管理员login_window.py是系统的入口文件它的设计决定了用户对系统的第一印象。课程设计里最常见的错误是登录窗口只有一个「用户名密码」输入框登录进去后所有功能平铺展开不管你是学生还是老师权限完全一样。这套系统把登录角色做成下拉框选择共三种态学生、教师、管理员登录后跳转到不同的主窗口。角色权限拆分是数据库课程设计答辩时高频被问的点设计上按「最小权限原则」来学生只能查自己的成绩和选课教师只能看自己教的课程并录入成绩管理员拥有全部权限。这一点在数据库层面还要配合视图来实现——后面第 5 章会讲。登录窗口的按钮事件绑定和回车提交是 tkinter 里最容易出问题的两个点# login_window.py import tkinter as tk from tkinter import messagebox, ttk from db_util import query LOGIN_SQL { student: SELECT sno, sname FROM S WHERE sno%s AND sname%s, teacher: SELECT tno, tname FROM T WHERE tno%s AND tname%s, admin: SELECT 1 FROM admin WHERE username%s AND password%s } def do_login(role_var, user_var, pwd_var, win): role role_var.get() username user_var.get().strip() password pwd_var.get().strip() if not username or not password: messagebox.showwarning(输入错误, 用户名和密码不能为空) return sql LOGIN_SQL[role] result query(sql, (username, password)) if result: win.destroy() if role student: open_student_window(result[0][sno]) elif role teacher: open_teacher_window(result[0][tno]) else: open_admin_window() else: messagebox.showerror(登录失败, 账号或密码错误请重试) # 绑定回车键提交 def bind_enter(entry, callback): entry.bind(Return, lambda event: callback())不同角色查不同的表登录 SQL 直接写在映射字典里维护后续扩展角色比如加一个助教角色只需要增加一行配置和一个窗口跳转分支。.strip()去掉输入框首尾空格是个小细节但经常被忽略——用户在输入框里复制账号时如果带了换行或空格不 strip 就会「明明账号正确却登录失败」。open_student_window(result[0][sno])登录成功后只传递学号不在登录窗口对象里持有整个用户字典避免后续子窗口把密码之类的敏感字段带到内存的其他角落。窗口销毁用win.destroy()而不是win.withdraw()前者释放内存后者只是隐藏窗口。4.2 学生主窗口选课流程的完整界面闭环学生登录后进入选课主窗口界面布局一般用tk.Frame分成两个区域左侧是「已选课程」表格右侧是「全部可选课程」表格底部是「选课」「退课」「刷新」三个按钮。用ttk.Treeview做表格展示时需要为每一列指定column和heading属性# student_window.py import tkinter as tk from tkinter import ttk from course_class import CourseManager from db_util import query class StudentWindow: def __init__(self, root, sno): self.manager CourseManager() self.sno sno # 已选课程表 self.selected_tree ttk.Treeview(root, columns(cno, cname, credit, grade), showheadings, height10) for col, title in [(cno, 课程号), (cname, 课程名), (credit, 学分), (grade, 成绩)]: self.selected_tree.heading(col, texttitle) self.selected_tree.column(col, width100, anchorcenter) self.selected_tree.pack(sidetk.LEFT, filltk.BOTH, expandTrue) # 给表格绑定双击事件双击已选课程行 触发退课 self.selected_tree.bind(Double-1, self._on_double_click_drop) def _on_double_click_drop(self, event): item self.selected_tree.selection()[0] cno self.selected_tree.item(item, values)[0] self.manager.drop_course(self.sno, cno) self.refresh()showheadings参数让表格只显示列头不显示最左侧的树形列anchorcenter让每列内容居中视觉效果比默认的左对齐好很多。双击退课是一个容易被忽略的交互细节很多课程设计只做了按钮退课用户必须先用鼠标选中一行再去点退课按钮加上Double-1事件绑定后双击行直接退课交互流畅度提升明显。4.3 教师端成绩录入的批处理优化教师端功能相对简单核心是查看「自己教的课」有哪些学生选然后录入成绩。通常用一个下拉框选择课程下面表格列出选课学生。成绩录入如果一次只能录一个学生几十个学生的课操作起来非常痛苦我一般会在表格里加一列「成绩输入框」用tk.Entry放进每个单元格教师填完后点「批量保存」一次性提交所有成绩-- 批量更新成绩使用 CASE WHEN 语法一次更新多条记录 UPDATE SC SET grade CASE WHEN sno 201215121 AND cno CS101 THEN 95.5 WHEN sno 201215122 AND cno CS101 THEN 88.0 WHEN sno 201215123 AND cno CS101 THEN 76.5 ELSE grade END WHERE cno CS101 AND sno IN (201215121, 201215122, 201215123);逐条UPDATE需要发起 N 次网络往返批量CASE WHEN语法一次搞定。MySQL 的CASE表达式在SET子句中按行匹配匹配到对应WHEN分支就更新成指定值ELSE grade保证不在列表内的行保持不变。会话层面开启事务可以保证这批更新的原子性要么全部提交、要么全部回滚。5. 报告与答辩视图、存储过程加索引的实战验证5.1 用视图收敛权限答辩问不倒课程设计报告里最值得写也最容易加分的部分是数据库安全性设计。直接用表的系统在答辩时会暴露一个问题学生端的 Python 代码用的是一个高权限数据库账号理论上学生可以绕过界面直接改自己的成绩。通过 MySQL 视图把学生可见的数据范围收敛是一个能从「开发能力」上升到「设计能力」的亮点。-- 学生成绩查询视图学生只能看到自己本人在选课表里的记录 CREATE OR REPLACE VIEW v_student_grade AS SELECT S.sno, S.sname, C.cname, C.credit, SC.grade FROM S JOIN SC ON S.sno SC.sno JOIN C ON SC.cno C.cno WITH CHECK OPTION; -- 教师授课视图教师只能看到与自己工号匹配的课程和选课学生 CREATE OR REPLACE VIEW v_teacher_teach AS SELECT T.tno, T.tname, C.cno, C.cname, SC.sno, S.sname FROM T JOIN C ON T.tno C.tno JOIN SC ON C.cno SC.cno JOIN S ON SC.sno S.sno WHERE T.tno SUBSTRING_INDEX(CURRENT_USER(), , 1);WITH CHECK OPTION表示通过视图插入或更新的数据必须满足视图定义中的WHERE条件这是视图安全性的关键。第二个视图的WHERE T.tno SUBSTRING_INDEX(CURRENT_USER(), , 1)是视图自动按当前登录数据库用户即tno过滤数据的写法这样教师执行SELECT * FROM v_teacher_teach时就天然只能看到自己的数据无需在 SQL 里手动传教师工号。5.2 存储过程跑通选课事务选课操作的原子性可以通过存储过程在数据库端兜底。学生选课是一个「先查后插」的组合操作Python 端先发起 SELECT 再发起 INSERT两次交互之间如果恰好有并发请求就会出现「两个人都查到课还没选满同时插入都成功」的超出容量问题。存储过程把这两步放到数据库服务端执行配合FOR UPDATE行锁在事务内锁定课程记录DELIMITER // CREATE PROCEDURE sp_select_course(IN p_sno CHAR(9), IN p_cno CHAR(6), OUT p_msg VARCHAR(50)) BEGIN DECLARE v_capacity INT; DECLARE v_selected INT; START TRANSACTION; -- 锁定课程行防止并发选课超员 SELECT capacity INTO v_capacity FROM C WHERE cno p_cno FOR UPDATE; IF v_capacity IS NULL THEN SET p_msg 课程不存在; ROLLBACK; ELSE SELECT COUNT(*) INTO v_selected FROM SC WHERE cno p_cno; IF v_selected v_capacity THEN SET p_msg 课程容量已满; ROLLBACK; ELSE IF EXISTS (SELECT 1 FROM SC WHERE sno p_sno AND cno p_cno) THEN SET p_msg 不可重复选课; ROLLBACK; ELSE INSERT INTO SC(sno, cno) VALUES (p_sno, p_cno); SET p_msg 选课成功; COMMIT; END IF; END IF; END IF; END// DELIMITER ;SELECT ... FOR UPDATE是 InnoDB 引擎的行级排他锁锁住课程行后其他事务对该行的SELECT ... FOR UPDATE操作会阻塞等待直到本事务提交或回滚释放锁。这是典型的「悲观锁」思路在并发量不高课程设计的场景时比乐观锁版本号重试实现简单且可靠。OUT p_msg参数把执行结果以字符串返回给 Python 端Python 端调用后直接弹窗提示不需要再查一次数据库确认结果。Python 端调用存储过程的方式很简单。# 调用存储过程选课 import pymysql conn pymysql.connect(hostlocalhost, userroot, password123456, databasecourse_select) with conn.cursor() as cursor: cursor.callproc(sp_select_course, (201215121, CS101, )) results cursor.fetchone() # 获取 OUT 参数 cursor.execute(SELECT _sp_select_course_2) msg cursor.fetchone()[0] conn.close() print(msg)cursor.callproc(sp_select_course, ...)调用完成后第三个 OUT 参数被存储到_sp_select_course_2前缀固定是_存储过程名_第几个参数从 0 开始计数需要再执行一条 SELECT 才能读出来。这一步是很多人在调用存储过程时卡住的地方——fetchone()拿到的是存储过程内部 SELECT 的最后一个结果集而不是 OUT 参数值。5.3 索引验证用 EXPLAIN 证明设计合理性报告里写「我建了索引」不算数要给出验证过程。MySQL 的EXPLAIN命令可以查看 SQL 的执行计划重点看type和rows两个字段。type从好到差依次是system const eq_ref ref range index ALLALL全表扫描是要避免的。-- 查看选课查询的执行计划 EXPLAIN SELECT S.sname, C.cname, SC.grade FROM SC JOIN S ON SC.sno S.sno JOIN C ON SC.cno C.cno WHERE SC.sno 201215121;由于sno和cno在 SC 表中是联合主键MySQL 会直接走主键索引找到对应选课记录type显示为constrows为 1说明查询只扫描了一行——这是最优情况。如果WHERE条件用的是S.sdept这种非索引列type会退化为ALLrows会显示全表行数这就是明显的性能隐患。把这个 EXPLAIN 结果截图放进报告比写三页理论分析都有说服力。5.4 运行前必查的三个环境问题MySQL 8.0 的认证插件默认是caching_sha2_password而 pymysql 旧版本只支持mysql_native_password。如果代码连接时报Authentication plugin caching_sha2_password cannot be loaded需要执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;另外两个高频坑一个是连接 URL 里databasecourse_select必须与建表脚本中的库名完全一致MySQL 对大小写敏感度取决于操作系统配置另一个是建表时ENGINEInnoDB不能省略MyISAM 引擎不支持外键约束和外键级联删除如果用的是默认 MyISAM建表脚本里那三条 FOREIGN KEY 会直接报错跳过。跑通之前SELECT VERSION()确认 MySQL 客户端与服务端版本一致能省掉很多莫名其妙的兼容性报错。本文还有配套的精品资源点击获取
返回列表