Python+MySQL实现学生选课系统:数据库设计与事务避坑指南
简介基于Python和MySQL的学生选课管理系统源码包专为计算机相关专业期末大作业及课程设计场景打造项目经导师指导并审核评审得分98分难度适中适合正在完成课设、毕设或需要项目实战练习的学习者。压缩包共8个文件容量1.11MB其中包含4个Python源码文件分别实现登录窗口、课程管理、教师端等核心模块1个MySQL数据库脚本按教材表12.1添加学生表信息并含建表语句与示例数据1份《数据库原理》报告文档以及README说明文件。所有源码均已本地调试通过确保可直接运行。目前已有88人学习浏览。借助本套资源可掌握Python与MySQL联动下的选课业务逻辑、数据库表设计与界面交互实现覆盖从建库到功能测试的完整流程既是期末大作业的高分模板也是数据库课程设计的实战练习项目具有较高的参考价值。1. 学生选课管理系统期末大作业为什么都选 Python MySQL又是期末季学生选课管理系统几乎是计算机专业数据库课程里最经典的期末大作业业务边界清楚——学生、课程、选课、退课、成绩技术栈又足够大众——Python 写逻辑、MySQL 存数据源码、数据库脚本加一份实验报告就能凑齐一份完整交付。它真正要练的是三件事ER 图到表结构的映射、增删改查背后的 SQL 功底、以及事务与约束的意识。这篇笔记面向正要动手、或者已经开始翻车的同学按“设计→建表→写代码→排错→写报告”的顺序把一套能直接跑通的最小可行系统拆开讲清楚参数怎么配、哪些坑不值得重踩也一并说明。2. 系统设计与数据库建模先把表结构画对后面少改十次2.1 功能模块拆解与角色权限怎么分学生选课管理系统的功能理论上可以拆出学生、教师、管理员三种角色但期末大作业我不推荐一上来就做三角色。教师端的本质是“课程维护”完全能被管理员端覆盖。多一张教师表意味着登录逻辑、权限判断、课程归属关系都要跟着多一套分支报告里却不会因此多得几分。常见做法是保留学生和管理员两类账号学生能浏览课程、选课、退课、查成绩管理员维护课程信息、维护学生账号、统计选课人数。权限控制也不用做成复杂的 RBAC 表。把用户类型当做一个字段在业务入口做一次角色判断就够。这样选型的理由是作业规模只有几张表真正要展示的能力在“选课事务”和“约束设计”上不在权限框架。等答辩老师问“权限怎么控制的”你能说清“登录后按角色分支进入不同菜单”比背一套 RBAC 术语更显扎实。还有一点容易被忽略很多模板会把登录后显示的学生姓名、专业、年级也归为“个人信息模块”这其实只是登录查询时顺带返回的字段不需要单独建扩展表。功能边界越小越能把每个功能的正确性做扎实。2.2 学生选课系统的核心四张表与 ER 关系核心实体就是四个学生students、课程courses、选课记录selections、管理员admins。有些网上的模板把教师单拆一张表再做课程与教师的开课关联表这适合真实教务系统但对期末作业来说复杂度超标。我一般把 teacher_name 直接放在 courses 表里因为教师维度在这个作业里只有“展示”价值没有独立业务操作。选课是典型的多对多关系一个学生可以选多门课一门课可以被多个学生选。MySQL 里不能直接表达多对多所以要拆出中间表 selections只保留 student_id、course_id、选课时间、成绩。这样形成的关联是students 1:N selectionscourses 1:N selections学生和课程通过 selections 构成 M:N。这里有一个关键设计把课程的已选人数 selected_count 冗余在 courses 表里。每次选课成功加一退课减一查询直接读这个字段不要用 count(*) 实时统计。原因有两点一是选课高峰 count 延迟会随记录数增长二是容量判断必须在事务里和“插入选课记录”放在同一个原子操作中靠应用层去数数根本挡不住并发。画 ER 图时常见错误是直接在学生和课程之间画一条 M:N 线就完事没有标出关系属性。选课时间、成绩、状态属于关系属性只能挂在 selections 表上挂在学生或课程上都会造成数据冗余。把这一点写进报告评分老师会觉得你的数据库设计是动过脑子的。2.3 建库建表 SQL主键、外键与选课唯一约束下面这份 SQL 是我做同类系统常用的最小结构兼顾演示效果和后期并发验证。CREATE DATABASE IF NOT EXISTS course_select_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_select_db; CREATE TABLE students ( student_id CHAR(10) PRIMARY KEY COMMENT 学号, name VARCHAR(32) NOT NULL COMMENT 姓名, major VARCHAR(64) NOT NULL COMMENT 专业, grade VARCHAR(10) NOT NULL COMMENT 年级, password VARCHAR(64) NOT NULL COMMENT 密码, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE courses ( course_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 课程编号, course_name VARCHAR(64) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT 学分, teacher_name VARCHAR(32) NOT NULL COMMENT 授课教师, capacity INT NOT NULL DEFAULT 50 COMMENT 容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE selections ( id INT PRIMARY KEY AUTO_INCREMENT, student_id CHAR(10) NOT NULL, course_id INT NOT NULL, select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5,2) DEFAULT NULL, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE, CONSTRAINT fk_sel_course FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admins ( admin_id VARCHAR(32) PRIMARY KEY, password VARCHAR(64) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明students 和 admins 是账号入口courses 是业务主数据selections 是关联表也是整个系统事务和并发验证的核心。所有表都用 InnoDB因为 InnoDB 支持外键与行级锁而 MyISAM 不支持事务选课容量判断会失去可靠依据。外键的 ON DELETE CASCADE 保证管理员删除学生或课程时关联的选课记录自动清理应用层少写一段维护代码。参数说明CHAR(10) 的学号是定长字符串等值查询效率比 VARCHAR 更好DECIMAL 用于学分和成绩避免浮点误差DEFAULT CURRENT_TIMESTAMP 让创建时间自动填充。capacity 和 selected_count 没有加 CHECK 约束是因为 MySQL 5.7 根本不执行 CHECK8.0 才真正生效我更倾向于在事务里做容量校验行为一致且演示效果好。主键选型上学号天然唯一直接 CHAR(10) 做主键没有问题课程用自增主键因为课程编号在现实系统里可能由编码规则生成作业里用自增最省事。selections 自己也设一个自增 id再把两个业务键做唯一约束这是中间表最常见的建模方式。建表后插入演示数据INSERT INTO students (student_id, name, major, grade, password) VALUES (20240001, 张三, 计算机科学与技术, 2024, MD5(123456)), (20240002, 李四, 软件工程, 2024, MD5(123456)); INSERT INTO courses (course_name, credit, teacher_name, capacity) VALUES (数据库原理, 3.0, 王老师, 2), (Python程序设计, 2.5, 刘老师, 30); INSERT INTO admins (admin_id, password) VALUES (admin, MD5(admin123));逻辑说明这里特意把数据库原理的容量设置为 2方便后面演示“课程满员”和“并发抢课”。密码用 MD5 只是为了作业演示报告里补一句“正式系统应使用 bcrypt”老师反而会觉得你有安全意识。参数说明MD5 函数由 MySQL 内置提供不需要在 Python 端引入额外依赖MySQL 5.7 和 8.0 都可用。如果你在 8.0 里遇到密码策略问题那是连接账号的事和这里的数据插入无关。3. 用 Python 实现后台逻辑从连接池到选课事务3.1 PyMySQL 与连接池为什么不用裸连接Python 连接 MySQL 的常见选择有 PyMySQL、mysqlclient、SQLAlchemy。期末大作业我建议用 PyMySQL原因很直接它是纯 Python 实现pip 安装不需要编译Windows 和 Linux 行为一致mysqlclient 在 Windows 下经常要处理 MSVC 编译环境时间容易耗在环境上SQLAlchemy 虽然写起来更优雅但把 SQL 藏在 ORM 后面答辩时老师问“这条更新语句到底怎么执行的”你容易卡壳。安装命令只有一行pip install pymysql DBUtilsDBUtils 提供连接池。为什么不每次 new 一个连接MySQL 连接建立要经过 TCP 握手、认证、权限校验一次几十毫秒单次执行可能感知不明显但演示时要连续跑选课、退课、查询而且本地 MySQL 默认最大连接数只有 151只要有一步忘了 close后面就会开始报 Too many connections。连接池的核心价值是复用连接把“借出/归还”变成一对固定操作。from dbutils.pooled_db import PooledDB import pymysql POOL PooledDB( creatorpymysql, maxconnections10, mincached2, maxcached5, blockingTrue, ping1, host127.0.0.1, port3306, userroot, password123456, databasecourse_select_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, ) def get_conn(): return POOL.connection()逻辑说明模块加载时创建全局连接池get_conn 每次从池子里借一条连接。业务代码统一走 get_conn用完必须 close 归还连接池自己维护连接状态。参数说明maxconnections 是池子上限10 对期末作业绰绰有余mincached 是启动时保持的最小空闲连接数设 2 可以让第一次查询不经历完整握手blockingTrue 表示连接不够用时请求排队等待而不是直接抛异常演示现场不会莫名崩掉ping1 会在连接归还时做一次心跳检查防止 MySQL 的 wait_timeout 把空闲连接断掉后池子还把它当做好连接发给你——这类报错长得很像 MySQL server has gone away排查很费时间。有一点要注意DBUtils 2.x 的导入路径是from dbutils.pooled_db import PooledDB1.x 是from DBUtils.PooledDB import PooledDB。网上老教程经常混着写装新库配老代码一启动就 ImportError这个细节很坑。3.2 学生选课的核心流程事务与行锁怎么落选课是学生选课管理系统最值得展示的一段代码也是拿高分的地方。它的核心要求是同一门课不能重复选不能超容量选并发抢最后一个名额时只能有一个人成功。import pymysql from db_helper import get_conn def select_course(student_id: str, course_id: int): conn get_conn() try: conn.begin() with conn.cursor() as cursor: cursor.execute( SELECT course_name, capacity, selected_count FROM courses WHERE course_id %s FOR UPDATE, (course_id,) ) row cursor.fetchone() if row is None: raise ValueError(课程不存在) if row[selected_count] row[capacity]: raise ValueError(课程已满) try: cursor.execute( INSERT INTO selections (student_id, course_id) VALUES (%s, %s), (student_id, course_id) ) except pymysql.err.IntegrityError: raise ValueError(你已经选过这门课了) cursor.execute( UPDATE courses SET selected_count selected_count 1 WHERE course_id %s, (course_id,) ) conn.commit() return True except Exception: conn.rollback() raise finally: conn.close()逻辑说明conn.begin() 开启事务。SELECT ... FOR UPDATE 先把这门课的行锁住锁定期间其他事务对该行执行 FOR UPDATE 会等待直到当前事务提交或回滚。锁住后读真实的容量再插入选课记录插入如果撞上唯一约束IntegrityError 被捕获并翻译成友好提示。最后更新已选人数。整个流程要么全部提交要么一个异常全部回滚。参数说明%s 是 PyMySQL 的占位符参数必须通过元组传入不要自己拼接字符串这是防 SQL 注入的关键也能让中文值不被错误转义。FOR UPDATE 是 InnoDB 行级锁WHERE 用的是主键 course_id所以是精确锁一行不会锁整表。这段代码最容易被忽视的点是顺序必须先锁行再查容量然后插入。如果先查询再锁两个事务会同时读到 selected_count 容量 - 1然后一起通过检查最终超选。FOR UPDATE 把“判断容量”和“更新人数”放进了同一个临界区。3.3 命令行界面还是 Web 界面期末大作业的选型建议交付形态直接影响剩下几天的开发量。如果老师没有明确要求 Web我强烈建议先做命令行界面。CLI 版本把登录、选课、退课、查询全部走通核心业务逻辑和数据库设计已经验证完毕报告里贴运行结果也够用稳。如果老师要求“系统”两个字常见做法是用 Flask 套一个小网页不要上 Django。Flask 两个文件能跑起来Django 要初始化项目目录、settings、urls、orm对期末作业比例失衡还容易被答辩追问细节。简单示例from flask import Flask, request, session app Flask(__name__) app.secret_key demo-secret app.route(/select, methods[POST]) def select(): if session.get(role) ! student: return 请先登录, 403 try: select_course(session[student_id], int(request.form[course_id])) return 选课成功, 200 except ValueError as e: return str(e), 400 if __name__ __main__: app.run(debugTrue, port5000)逻辑说明Web 层只做参数接收、会话判断和调用业务函数真正的选课事务仍然留在 select_course 里。CLI 和 Web 共用同一套核心代码报告里可以同时讲业务逻辑和 Web 接口。参数说明session[role] 来自登录时写入的用户角色用它拦截管理员操作request.form[course_id] 从前端表单读课程编号int 转换失败会直接抛异常答辩时可以说“Web 层做了类型校验避免脏数据进入事务”。页面就用模板字符串渲染表格和表单不要引入前端框架除非你额外想写写组件化——那是另一个项目的事了。4. 避坑学生选课系统开发中最常踩的 5 个坑4.1 中文乱码从建库到连接串一路排查现象INSERT 执行成功SELECT 出来中文全是 ??或者终端里显示乱码。原因字符集不统一。最常见是建库时没带 DEFAULT CHARACTER SETMySQL 5.7 默认可能是 latin1也可能是连接串没写 charset还有一种情况是 Windows 终端用 GBK 显示 UTF-8 数据数据库和连接其实都没问题。解决先看当前字符集。SHOW VARIABLES LIKE character_set%;如果库或表是 latin1执行ALTER DATABASE course_select_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE students CONVERT TO CHARACTER SET utf8mb4;同时把 Python 连接串里的 charset 固定为 utf8mb4不要依赖 MySQL 默认值。Windows 命令行里再执行chcp 65001把代码页切到 UTF-8。这个坑排查起来很有意思有时候三层都改了还是乱最后发现是终端字体或代码页问题。4.2 选课超选没加唯一约束导致的重复选课现象同一学生同一门课程出现两条记录管理员统计人数翻倍或者演示时连点两次选课都成功。原因只在应用层做了“查询有没有已选记录”的判断。这个判断在并发请求时可能同时读到“未选”然后都插入。单机演示时确实不好复现但期末作业的并发测试环节很容易露馅。解决第一道防线是 selections 表的 UNIQUE KEY (student_id, course_id)第二道防线才是应用层捕获 IntegrityError。前面选课事务里已经是这个结构。有些同学想实现“退课再选”做成先 DELETE 旧记录再 INSERT 新记录如果放在同一个事务里勉强能跑但一旦插入失败而回滚没写完整旧记录也跟着没了。更合理的做法是把退课和选课拆成两个独立操作各自提交不要混在一个事务里。4.3 事务没提交数据“丢了”的真相现象程序运行时能查到刚插入的数据但重启服务或第二天再打开数据没了于是怀疑数据库坏了。原因没有 commit。PyMySQL 默认 autocommit 是 Falseexecute 之后数据只存在于当前事务的视角里连接关闭时未提交的事务会被回滚。更隐蔽的是用了连接池以后有人以为 close 就是“保存”其实 close 只是把连接归还池子数据是否落盘完全取决于有没有 commit。解决统一用 try-except-commit-rollback-finally-close 的结构每个写操作都装进去。不要 execute 后面裸写一句 print 就完事。还有一个知识盲区DDL 语句比如 CREATE TABLE、ALTER TABLE 在 MySQL 里隐式提交事务回滚对它们无效所以别指望用事务去撤销表结构变更。4.4 连接数耗尽pool 回收与关闭顺序现象系统跑十几分钟突然报 Too many connections或者卡在等待连接池的连接上不动。原因get_conn 之后没有 finally close或者连接池 maxconnections 配得过大超过了本地 MySQL 的 max_connectionsmincached 配得过大也会白白占住一批连接。解决业务代码严格保证每借一次就还一次。conn get_conn() try: with conn.cursor() as cursor: cursor.execute(SELECT 1) finally: conn.close()排查时在 MySQL 里执行 SHOW PROCESSLIST看到大量 Sleep 状态的连接堆积在同一 IP 下基本就是代码忘了关。参数建议 maxconnections 给 10 以内、mincached 给 2已经够演示和测试。连接池不是越大越好它同时受 MySQL 连接数上限约束。4.5 MySQL 版本差异认证插件与密码策略现象MySQL 8.0 装好后PyMySQL 连接时报 Authentication plugin caching_sha2_password cannot be loaded。原因MySQL 8.0 把默认认证插件换成了 caching_sha2_password老版本 PyMySQL 不认识。解决先升级 PyMySQLpip install -U pymysql。如果因为环境原因不能升级就为应用单独建一个兼容认证账号CREATE USER course_userlocalhost IDENTIFIED WITH mysql_native_password BY 123456; GRANT ALL PRIVILEGES ON course_select_db.* TO course_userlocalhost;另一个版本差异在安装阶段就会遇到MySQL 8.0 的 validate_password 组件强制密码复杂度你设 123456 会被拒5.7 弱很多。期末演示没必要在密码策略上较劲先按官方要求设一个复杂密码装完再调低策略。SHOW VARIABLES LIKE validate_password%; SET GLOBAL validate_password.policy LOW; SET GLOBAL validate_password.length 4;注意8.0 的变量名带点前缀5.7 是 validate_password_policy两种写法不一样网上教程混着看很容易翻车。5. 验收自查把系统演示成能交作业的样子5.1 演示脚本与检查清单系统写完先别急着交按这条顺序自己走一遍启动前执行 reset 脚本清空选课记录并重置课程数据保证演示状态可重复。学生登录选一门容量充足的课到数据库里查 selections 和 courses 的 selected_count确认两个值都变了。重复选同一门课界面必须提示“已选过”。把某门课容量改为 1开两个终端同时抢课验证只有一个人成功。管理员登录统计每门课选课人数再退课一次确认统计值减一。这套顺序对应报告里的“功能测试”和“并发测试”两章。答辩时老师让你现场演示你不会手忙脚乱。5.2 报告结构与答辩要点报告按“需求分析→ER 图→表结构→系统实现→测试用例→遇到的问题与解决”写。“遇到的问题”一定要写真实踩坑比如中文乱码和连接数耗尽评委看的是排查能力不是完美。答辩高频问题无非三个为什么用连接池、为什么选课要开事务、并发抢课怎么防止超选。回答直接顺着第 4 章的思路讲连接池复用减少握手开销事务加行锁保证容量判断和更新原子唯一约束兜底防重复。说清楚这三件事作业基本就稳了。5.3 我的教训我第一次交类似作业时代码全跑通报告写了三十页结果演示时连点两次选课按钮数据库里出现了重复记录当场被老师追问“你的系统怎么保证不重复选课”。当时选课逻辑只有一句“先查询再插入”没有唯一约束没有事务。后来重做补上连接池、事务和唯一约束三件套这套组合在之后的课程设计和面试项目里一直沿用。做完功能只是第一步把并发和边界条件想清楚才算真正做完。希望帮到你。本文还有配套的精品资源点击获取