学生选课管理系统:Python+MySQL 事务与并发控制实现 📅 发布时间:2026/9/16 9:26:08 👁 浏览次数: 简介基于Python和MySQL的学生选课管理系统期末项目资料包面向计算机相关专业正在完成期末大作业、课程设计或需要数据库项目实战练习的学习者。资源包含学生选课系统的完整源码、MySQL数据库脚本与《数据库原理》课程报告可用于快速理解登录验证、课程分类、教师管理等典型模块的实现逻辑并在此基础上进行二次开发或答辩讲解。整个压缩包共包含8个文件包括4个Python源文件登录窗口、课程类、教师类及测试脚本、1个SQL数据库脚本、1份Word版课程报告、1个说明文档及附属配置文件压缩包整体仅1.11MB结构清晰便于对照学习和直接部署运行。源码经过本地编译与严格调试确保可正常运行评审分为98分属于导师认可的高分设计项目。目前已有86人学习下载非常适合需要同时参考代码实现、数据库设计与报告撰写的初学者作为期末大作业的完整参考资料。1. 学生选课管理系统的定位与选型判断期末大作业选“学生选课管理系统”的人每年都不少但真正能过答辩、扛得住老师追问的版本并不算多。原因在于这个题目看起来只是两张表加几条增删改查实际做下去会遇到三个坎选课余量怎么防超卖、同一个学生重复选同一门课怎么约束、事务回滚后列表数据能不能保持一致。用 Python 写业务层、MySQL 做存储层正好能把这三个点全部讲透也是数据库课程设计里最常见、最稳妥的技术组合。本文面向已经会基本 Python 语法、想直接交出可演示项目的读者按“建表、连库、选课事务、报告答辩”四个环节展开每一步都能照着复现。2. MySQL 端数据库设计与建表 SQL2.1 三张表解决“选课关系”的建模一个选课系统至少要表达三类实体学生、课程以及学生和课程之间的选课关系。不少作业只建一个选课记录表把所有字段塞进去虽然也能跑通但老师一看就知道没掌握关系型数据库的基本设计思路。更规范的课程设计是使用三张表表名作用关键字段students学生基础信息sid、name、major、passwordcourses课程基础信息cid、cname、teacher、capacity、selected_countenrollments选课关系表id、sid、cid、enroll_timestudents 表用学号做主键比自增整数更贴近真实校园场景courses 表把容量 capacity 和已选人数 selected_count 拆开存是为了在后面做余量判断时直接比对两个数值而不是反复COUNT选课表。enrollments 表比较关键它保存两个外键和选课时间属于典型的多对多关系拆分students 和 courses 在概念上是多对多中间表把它们拆成两个一对多。期末报告里要画的 ER 图画的就是这三张表以及这两组一对多关系。2.2 建表 SQL 与 utf8mb4 字符集设定下面是一份可以直接拿去用的建库建表 SQL保存为course_select.sqlCREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_select; CREATE TABLE students ( sid CHAR(10) NOT NULL, name VARCHAR(50) NOT NULL, major VARCHAR(50) DEFAULT NULL, grade INT DEFAULT NULL, password CHAR(64) NOT NULL, PRIMARY KEY (sid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE courses ( cid CHAR(6) NOT NULL, cname VARCHAR(50) NOT NULL, teacher VARCHAR(50) DEFAULT NULL, credit DECIMAL(2,1) DEFAULT NULL, capacity INT NOT NULL, selected_count INT NOT NULL DEFAULT 0, PRIMARY KEY (cid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE enrollments ( id INT NOT NULL AUTO_INCREMENT, sid CHAR(10) NOT NULL, cid CHAR(6) NOT NULL, enroll_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_sid_cid (sid, cid), CONSTRAINT fk_enroll_sid FOREIGN KEY (sid) REFERENCES students (sid) ON DELETE CASCADE, CONSTRAINT fk_enroll_cid FOREIGN KEY (cid) REFERENCES courses (cid) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建库时显式声明utf8mb4是为了避免中文乱码。MySQL 5.7 及更早版本默认字符集不是 utf8mb4不设置时插入“数据结构”“操作系统”这类中文课程名很容易报 1366 错误。students.password用CHAR(64)是为了存 SHA-256 哈希值不要明文存密码这点写进报告“安全设计”一节是明显加分项。外键约束都加了ON DELETE CASCADE删除学生或课程时选课记录会自动清理避免产生悬空引用。2.3 外键、唯一索引与初始化数据enrollments表上有两个约束需要重点理解。UNIQUE KEY uk_sid_cid (sid, cid)用来阻止同一个学生重复选同一门课没有这个索引时应用层必须先SELECT再INSERT两步之间存在并发窗口可能插入重复记录。有了唯一索引第二次插入直接报 1062 错误Python 侧捕获异常即可。导入数据库时在 MySQL 命令行执行一句即可mysql -uroot -p course_select.sql这里不要用图形工具导入命令行反馈更直接。导入成功后可以用一段初始化数据验证表结构是否合理常见做法是插入 5 名学生、6 门课程其中一两门课的selected_count预置为接近容量的值方便演示“课程已满”的分支。还可以用下面这条 SQL 检查选课记录和课程人数是否对应得上SELECT cid, COUNT(*) AS actual_count FROM enrollments GROUP BY cid;拿查询结果去比对courses.selected_count这也是数据库课程设计报告里“数据一致性测试”的常用素材。3. Python 端的连接层与 CRUD 封装3.1 选 PyMySQL 还是 mysql-connector-pythonPython 连 MySQL 最常见的两个驱动是 PyMySQL 和官方 mysql-connector-python。做期末大作业时建议用 PyMySQL理由是它纯 Python 实现pip install pymysql后直接可用不需要编译 C 扩展遇到本机缺编译器的概率低很多。mysql-connector-python 本身也不错但有些 Linux 发行版上依赖处理略显繁琐并不是课程设计场景里最顺手的选项。连接参数里有两个细节容易被忽略一是charsetutf8mb4保证中文读写不乱码二是autocommitFalse确保选课操作在一个显式事务里提交这正是答辩时老师会追问的点。装好 Python 3.10 和 MySQL 8.0 后在虚拟环境里安装依赖即可pip install pymysqlPyMySQL 只是客户端驱动不包含数据库内核装完不需要重启 MySQL也无需修改任何服务端配置。3.2 一个能在课程设计中直接使用的数据库工具类如果在每个函数里重复写连接、游标、关闭代码会非常啰嗦还容易在异常时漏掉回滚。常见做法是封装一个DB类把连接配置、查询、事务提交和回滚集中管理。import pymysql from pymysql.cursors import DictCursor class DB: def __init__(self, hostlocalhost, port3306, userroot, password123456, databasecourse_select): self.conn pymysql.connect( hosthost, portport, useruser, passwordpassword, databasedatabase, charsetutf8mb4, cursorclassDictCursor, autocommitFalse ) def query(self, sql, argsNone): with self.conn.cursor() as cursor: cursor.execute(sql, args) return cursor.fetchall() def execute(self, sql, argsNone): with self.conn.cursor() as cursor: rows cursor.execute(sql, args) return rows def commit(self): self.conn.commit() def rollback(self): self.conn.rollback() def close(self): self.conn.close()这个工具类做了三件重要的事连接参数集中到__init__多人协作时只需要改一处密码query返回字典列表取字段用row[sid]而不是row[0]可读性提升明显autocommitFalse保证所有写操作必须显式调用commit()给后续事务控制留出空间。参数说明上cursorclassDictCursor是最常用的配置它让每条记录变成字典不设置时默认返回嵌套元组调试时很难一眼看出字段对应关系。port默认 3306如果本机 MySQL 改过端口构造DB时传入即可。charset参数必须和建库时的字符集保持一致否则中文可能出现编码错误。3.3 参数化查询与 SQL 注入预防Python 侧执行增删改查时要使用 pymysql 的参数化写法不要在字符串里直接拼接用户输入。登录功能是典型的对比场景# 错误写法字符串拼接输入 1 OR 11 会被当成 SQL 语句的一部分 # sql SELECT * FROM students WHERE sid%s AND password%s % (sid, pwd) # 正确写法参数由 pymysql 转义后传给服务端 def login(db, sid, pwd): rows db.query( SELECT sid, name FROM students WHERE sid%s AND password%s, (sid, pwd) ) return rows[0] if rows else None参数化查询的价值在报告里值得专门写一小节用户输入只作为数据处理不参与 SQL 语句结构拼接 OR 11这类注入字符串会被当作普通文本。课程设计中单是这一条安全设计评价就能从“没有”变成“有基本防护”。需要记住 pymysql 的占位符是%s不是?与 sqlite3 不同多参数时传入元组顺序对应占位符位置。4. 选课主流程的事务与并发处理4.1 选课时的检查顺序选课是典型的“读改写”操作先查课程是否存在再判断余量随后插入选课记录最后把课程已选人数加一。这个顺序不能乱。先把学生和课程的存在性校验放在前面可以拦截大量无效请求减少后续持有锁的时间。下面这个函数可以直接嵌进命令行菜单或 Flask 路由中使用import pymysql def select_course(db, sid, cid): try: student db.query(SELECT sid FROM students WHERE sid%s, (sid,)) if not student: return False, 学生不存在 course db.query( SELECT cid, capacity, selected_count FROM courses WHERE cid%s FOR UPDATE, (cid,) ) if not course: return False, 课程不存在 c course[0] if c[selected_count] c[capacity]: return False, 课程已选满 db.execute( INSERT INTO enrollments (sid, cid) VALUES (%s, %s), (sid, cid) ) db.execute( UPDATE courses SET selected_count selected_count 1 WHERE cid%s, (cid,) ) db.commit() return True, 选课成功 except pymysql.err.IntegrityError as e: db.rollback() if e.args[0] 1062: return False, 你已经选过这门课程 return False, 选课失败数据约束冲突 except Exception: db.rollback() return False, 系统异常已回滚SELECT ... FOR UPDATE是这段代码的核心它把课程表里对应行锁住直到当前事务提交或回滚。两个学生同时选同一门课时后一个会话会阻塞在锁上等前一个事务结束后再读取最新数据因此capacity和selected_count不会超卖。enrollments表的唯一索引在这里是第二道保险即使并发绕过了余量判断重复插入也会触发 1062 异常。4.2 为什么必须用 UPDATE 而不是 COUNT一个很常见的误用是用SELECT COUNT(*) FROM enrollments WHERE cid%s来判断余量。单用户演示时没有问题但老师如果追问“两个学生同时选最后 1 个名额怎么办”COUNT 方案答不出来。因为COUNT之后再INSERT之间有间隔两个事务可能同时读到同样的剩余名额然后各自插入记录选课人数最终超过容量。对比三种余量控制方案方案并发安全性实现成本COUNT 后 INSERT不安全最低SELECT ... FOR UPDATE安全简单直接唯一索引兜底 重试靠约束拦截业务层要处理异常稍复杂推荐第二种方案它在courses表维护selected_count核心是把“判断余量”和“修改余量”合并到同一个被锁保护的操作单元中。比较selected_count和capacity不用 COUNT更新用自增表达式也不会读到旧值不会有中间态。4.3 退课删除记录并释放容量退课是选课的逆操作同样需要锁。如果不先锁课程行另一个事务可能同时在选课并更新同一个selected_count最终字段值和enrollments表的记录数对不上。def drop_course(db, sid, cid): try: db.execute(SELECT cid FROM courses WHERE cid%s FOR UPDATE, (cid,)) row db.execute( DELETE FROM enrollments WHERE sid%s AND cid%s, (sid, cid) ) if row 0: db.rollback() return False, 没有找到选课记录 db.execute( UPDATE courses SET selected_count selected_count - 1 WHERE cid%s, (cid,) ) db.commit() return True, 退课成功 except Exception: db.rollback() return False, 退课失败这里三个操作在同一个事务里锁定课程行、删除选课记录、减少已选人数。先锁再删是为了和选课事务形成串行化两个事务操作同一门课时必须排队执行。如果业务上要求退课后马上释放名额给其他学生这个事务结束的同时锁也释放不需要额外刷新缓存。5. 期末报告撰写与答辩演示要点5.1 报告里放什么才能拿高分课程设计报告通常包含需求分析、数据库设计、系统实现、测试和总结五个部分。数据库设计部分必须放一张清晰的 ER 图同时附带完整的数据字典表字段名、类型、约束、说明四项不能缺。系统实现里贴代码不要整篇复制选三处有辨析度的内容参数化查询代码、SELECT ... FOR UPDATE的加锁片段、异常回滚逻辑。老师现场最爱追的两个问题几乎固定一是“两个学生同时点选课会怎样”对应第 4 章的事务与锁二是“密码为什么用哈希存”对应第 2 章的CHAR(64)设计。这两段要提前组织成两分钟以内的口述版本讲清楚现象和解决方案即可。5.2 演示剧本固定顺序演示时按固定顺序操作更容易出效果先登录一名学生查看全部课程列表选一门容量未满的课成功后立刻重复选一次展示“已经选过”的业务拦截再换另一个学生账号把同一门课余量选到 0第三次尝试时展示“课程已满”最后退课验证列表和余量同步更新。演示到“退课后余量恢复”这一步时可以从 MySQL 命令行执行一条对比 SQLSELECT selected_count FROM courses WHERE cidCS101; SELECT COUNT(*) FROM enrollments WHERE cidCS101;两个结果一致数据一致性当场可见比口头解释有说服力得多。5.3 交付目录与初始化脚本完整的课程设计源码包建议按这个结构整理根目录放app.py入口和db.py连接与工具类sql/目录放course_select.sqldocs/目录放报告文档和 ER 图。数据库在验收前重新执行一次导入脚本保证所有表结构和数据都处于可复现状态。这份随源码附带的 SQL 文件不要是“当时建库的临时脚本”要保证课程设计打分老师在任何一台装了 MySQL 的机器上执行一遍都能恢复出同样结构这也是报告里“运行环境与部署步骤”这一节真正要写清楚的内容。本文还有配套的精品资源点击获取