首页 / 资讯中心 / 文章详情

Python学校教务系统源码拆解:登录鉴权、选课并发与成绩管理

Python学校教务系统源码拆解:登录鉴权、选课并发与成绩管理 ★ FEATURED ARTICLE
简介这套Python学校教务系统项目面向课程设计、毕业设计及Python Web入门学习者围绕学生信息、课程安排、成绩录入与查询等教务场景提供可运行的源码、数据库脚本与设计文档帮助理解从需求分析到功能实现的完整开发链路。压缩包共9个文件约467KB其中7个SQL脚本用于建库建表与初始数据1个Python主程序承载后端业务逻辑1份Word文档记录系统设计与实现过程结构紧凑便于按模块查阅。已有594人学习说明其在教学管理类项目中具有一定参考价值。读者可结合源码与脚本梳理用户、课程、教师、成绩等数据表关系学习Python Web框架下的路由、视图与数据库CRUD操作也可从前端页面交互、系统模块划分和测试思路中提炼课程项目写法文档则补足架构设计、技术选型与性能测试等表述适合需要快速搭出教务系统原型并完成报告整理的学习者。1. 一套能跑通的教务系统为什么值得拆开看每年选课季教务系统都是被骂得最惨的那类软件学生挤不进选课页老师排课排到崩溃管理员在后台一遍遍导 Excel。很多人第一次接触「python学校教务系统」这类项目都是被逼的要么课程设计要交要么接了个小私活要么单纯想弄明白一套多角色后台系统到底怎么搭起来。这套 Python 数据库实现的教学管理 Demo恰好把学生、教师、管理员三种角色的登录、课程、成绩、选课流程串成了一条完整链路麻雀虽小但五脏俱全。它适合谁适合已经会写点 Python、能看懂 SQL但没独立做过「权限 业务 数据」三件套联动的开发者照着跑一遍就能摸清教务类系统的基本骨架。2. 项目结构与环境搭建先把架子立起来拿到压缩包别急着双击运行先弄清楚它长什么样。这类教务系统通常是一个轻量 Web 应用或者桌面应用前者多用 Flask 或 Django后者多用 Tkinter 或 PyQt。从「python 数据库」这个组合判断大概率走的是 Web 路线Python 负责路由和业务逻辑数据库负责持久化模板负责渲染页面。本章先讲目录结构再讲依赖安装和建库最后把启动命令跑通。2.1 目录结构三层划分看懂了就不慌这种项目的目录组织有一套常见套路来看一个典型布局school_edu_system/ ├── app.py # 应用入口注册路由、启动服务 ├── config.py # 数据库连接、密钥等配置 ├── requirements.txt # 依赖清单 ├── models/ # 数据模型层 │ ├── __init__.py │ ├── db.py # 数据库连接与初始化 │ ├── student.py # 学生表操作 │ ├── teacher.py # 教师表操作 │ └── course.py # 课程与选课表操作 ├── views/ # 视图层路由 业务逻辑 │ ├── auth.py # 登录鉴权 │ ├── admin.py # 管理员功能 │ ├── teacher_view.py # 教师功能 │ └── student_view.py # 学生功能 ├── templates/ # 页面模板 │ ├── login.html │ ├── admin/ │ └── student/ ├── static/ # CSS、JS、图片 └── init_db.py # 建表 初始化账号脚本这个结构把「数据」和「视图」分开是小型教务系统的标准做法。models 里的每个文件对应一组表操作views 里的每个文件对应一类角色的页面逻辑templates 按角色分子目录存放避免页面互相污染。新手最容易犯的错是拿到项目直接在根目录python app.py跑结果报错说找不到模块——那是因为某些脚本用了相对导入必须在正确的父目录下执行或者先在根目录生成一个__init__.py让 Python 把它当包。有些版本会把所有逻辑塞进一个main.py几百行从头写到尾。遇到这种也不用慌顺着路由函数往下读就行只是维护性差一些。判断一个教务项目质量好坏看 models 和 views 是否分离、看有没有init_db.py这种初始化脚本基本就能下结论。2.2 依赖与环境别用最新版 Python 硬刚老项目教务系统的依赖清单一般不长核心就那么几个。以 Flask 版本为例# 创建虚拟环境别污染全局 Python python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 安装依赖 pip install -r requirements.txtrequirements.txt里通常包含 Flask、Flask-SQLAlchemy 或 pymysql、Werkzeug、Jinja2 等。这里有个血泪经验老项目的依赖版本往往卡得很死用 Python 3.12 硬装可能直接报Werkzeug或SQLAlchemy兼容错误。稳妥做法是先看 requirements 里有没有版本号有就照装没有就按报错提示锁一个旧版本比如 Flask 2.0.x 搭配 SQLAlchemy 1.4.x 是那个年代的经典组合。如果项目用的是 SQLite那基本零配置数据库就是一个.db文件随项目走。如果用 MySQL就得先装好服务、建好库-- 建库字符集用 utf8mb4 避免中文乱码 CREATE DATABASE school_edu DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 建一个专用账号别直接用 root 跑业务 CREATE USER edu_userlocalhost IDENTIFIED BY your_password; GRANT ALL PRIVILEGES ON school_edu.* TO edu_userlocalhost; FLUSH PRIVILEGES;注意字符集一定要用utf8mb4教务系统里学生姓名、课程名称全是中文用latin1或utf8存 emoji 和生僻字会出问题。2.3 建表和初始账号init_db.py 是第一个要跑的文件很多新手跳过init_db.py直接启动应用然后登录页一直提示「用户名或密码错误」。原因很简单表是空的初始账号根本没插进去。这类脚本通常做了三件事建表、插入管理员账号、插入测试学生和教师账号。# init_db.py —— 建表并写入初始数据 from app import db from models.student import Student from models.teacher import Teacher import hashlib def init(): db.create_all() # 根据模型定义建表 # 初始管理员密码做一次 SHA256别明文存 admin_pwd hashlib.sha256(admin123.encode()).hexdigest() db.session.add(Teacher( teacher_idadmin, name系统管理员, passwordadmin_pwd, roleadmin )) # 测试学生账号方便验证选课流程 db.session.add(Student( student_id2023001, name测试学生, passwordhashlib.sha256(123456.encode()).hexdigest(), major计算机 )) db.session.commit() print(初始化完成) if __name__ __main__: init()这段代码逻辑很直白db.create_all()根据模型类生成表结构然后往表里塞初始账号。参数上要注意teacher_id和student_id是主键或唯一键重复执行脚本会因为主键冲突报错所以跑过一次后要么先清表要么在脚本里加if not exists判断。提示初始脚本里的密码是示例值跑通之后第一件事就是改成自己设定的强密码否则部署到内网就是裸奔。最后启动应用python app.py # 默认跑在 http://127.0.0.1:5000看到控制台输出Running on http://127.0.0.1:5000浏览器能打开登录页这一步就算成了。如果端口被占用改app.run(port5001)即可。3. 核心模块拆解登录、选课、成绩三件事怎么落地架子立起来之后真正值得读的代码就三块登录鉴权怎么区分角色选课怎么防超选成绩录入怎么保证数据一致。这三个模块吃透了教务系统的业务逻辑就掌握了八成。3.1 登录鉴权一张表还是两张表决定了你踩多少坑多角色系统的登录设计有两条路所有角色共用一张 user 表用 role 字段区分或者学生、教师各一张表分别查。这两种写法在这个项目里都可能出现读代码时先看models/目录下的表定义。共用一张表的好处是登录逻辑只写一遍坏处是字段冗杂——教师不需要学号学生不需要职称全塞一张表里一堆 NULL。分表的写法更贴近教务系统的实际语义但登录时要先判断角色再查对应表或者用两个不同的登录入口。一个典型的登录视图长这样# views/auth.py —— 登录逻辑 from flask import request, session, redirect, url_for, render_template from models.student import Student from models.teacher import Teacher import hashlib app.route(/login, methods[GET, POST]) def login(): if request.method GET: return render_template(login.html) # 取表单参数role 决定查哪张表 username request.form.get(username, ).strip() password request.form.get(password, ).strip() role request.form.get(role) # student / teacher / admin if not username or not password: return render_template(login.html, msg账号密码不能为空) pwd_hash hashlib.sha256(password.encode()).hexdigest() if role student: user Student.query.filter_by(student_idusername).first() else: user Teacher.query.filter_by(teacher_idusername).first() # 先判断用户存在再比密码避免对 None 取属性的报错 if user is None or user.password ! pwd_hash: return render_template(login.html, msg账号或密码错误) # 登录成功后写 session后续页面靠它鉴权 session[user_id] username session[role] role session[name] user.name # 按角色跳不同首页 return redirect(url_for(f{role}_index))逐段说明。第一段处理 GET 请求直接返回登录页模板。第二段从表单取值strip()用来清掉用户手滑输入的空格这个细节看着不起眼但实际使用中相当一部分「密码明明对却登不上」都是首尾空格导致的。第三段做密码哈希比对项目里用的是 SHA256安全强度比不上 bcrypt但要改的话得先看建库脚本里存的格式两边必须一致不然全员登录失败。第四段是鉴权核心登录成功后把user_id和role写进 session后面每个受保护的页面开头都会检查session.get(role)是否匹配不匹配就直接踢回登录页。这种基于 session 的鉴权在 Flask 里是最省事的写法缺点是不适合多机部署——session 存在单机内存里换台机器就失效。生产环境常见做法是换成 Redis 存 session或者用 JWT。读这段代码时要特别注意一个易错点user is None or user.password ! pwd_hash这个条件顺序不能反。如果写成user.password ! pwd_hash or user is None用户不存在时会先对 None 取.password直接抛 AttributeError接口 500。这种短路求值的顺序问题是新手读源码时最常忽略的细节。3.2 选课模块并发下的超选问题怎么处理选课是整个系统里唯一的高并发场景也是最容易翻车的地方。课容量 50 人结果选进去 52 个这种事故在真实教务系统里并不罕见。项目里如果只是简单的「查余量 → 人数加一 → 插入记录」单机测试毫无问题一旦多人同时点就会超选。先看典型的错误写法# 错误示范先查后插中间存在窗口期 course Course.query.get(course_id) if course.selected course.capacity: return 课程已满 course.selected 1 db.session.add(Enrollment(student_idsid, course_idcourse_id)) db.session.commit()问题出在「查余量」和「写入」之间有时间窗口两个请求同时查到selected49都认为还有名额于是都执行加一最终变成 51。解决思路有三种按复杂度递增第一种是用数据库行锁在查询时加with_for_update()把这条课程记录锁住其他事务只能等# 行锁方案查询时锁定该行事务提交前别的请求改不了 course Course.query.with_for_update().get(course_id) if course.selected course.capacity: db.session.rollback() return 课程已满 course.selected 1 db.session.add(Enrollment(student_idsid, course_idcourse_id)) db.session.commit()第二种是不在代码里判断直接靠数据库唯一约束选课表上建(student_id, course_id)联合唯一索引防止同一学生重复选同一门课容量控制则用一条带条件的 UPDATE-- 只有没满的时候才加一影响行数为 0 说明已满 UPDATE course SET selected selected 1 WHERE course_id :cid AND selected capacity;然后根据这条 SQL 的影响行数判断是否选课成功。这种写法把判断和更新合并成原子操作不依赖应用层的锁是更推荐的方式。第三种是分布式锁用 Redis 给每门课加锁。学生项目一般用不上知道有这条路就行。读源码时如果发现选课逻辑既没有行锁也没有唯一约束那基本可以判定这是个「能跑但不抗压」的版本二次开发时得补上。另外注意选课成功后的提示语很多项目只弹一句「选课成功」没告诉学生当前已选多少人、还剩多少名额这个体验细节改进起来很容易加一行查询就行。3.3 成绩模块录入、修改、查询的完整链路成绩模块的业务逻辑比选课简单但数据一致性要求更高。教师录入成绩、学生查询成绩、管理员导出成绩三个入口共用一张成绩表。关键点在于成绩只能由教这门课的教师录入学生只能查自己的管理员能看全部但不能随便改。教师录入的视图通常长这样# views/teacher_view.py —— 成绩录入 app.route(/teacher/grade, methods[POST]) def input_grade(): # 鉴权必须是教师角色 if session.get(role) not in (teacher, admin): return redirect(url_for(login)) teacher_id session[user_id] course_id request.form.get(course_id) student_id request.form.get(student_id) score request.form.get(score) # 参数校验分数必须是 0-100 的数字 try: score float(score) assert 0 score 100 except (ValueError, AssertionError): return 分数格式不合法, 400 # 权限校验这门课必须是该教师开的 course Course.query.get(course_id) if course is None or course.teacher_id ! teacher_id: return 无权操作该课程, 403 # 先查是否已有成绩记录有则更新无则插入 record Grade.query.filter_by( student_idstudent_id, course_idcourse_id ).first() if record: record.score score else: db.session.add(Grade( student_idstudent_id, course_idcourse_id, scorescore, teacher_idteacher_id )) db.session.commit() return redirect(url_for(grade_list, course_idcourse_id))这段代码有几个值得学的点。第一是三层校验角色校验、格式校验、权限校验缺一不可。很多人只做了角色校验就以为安全了结果教师 A 能改教师 B 课程的成绩这是权限越界的典型漏洞。第二是「有则更新、无则插入」的写法避免了重复录入产生多条成绩记录比先删后插更稳。第三是分数用float存允许 89.5 这种小数如果学校要求整数改成int并在校验里加取整逻辑即可。学生查询成绩的视图则要收紧范围SQL 里必须带学生 ID 条件# 学生只能查自己的成绩条件里必须带 student_id grades Grade.query.filter_by(student_idsession[user_id]).all()如果发现查询语句里没有学生 ID 过滤直接Grade.query.all()返回全表那就是个严重的数据泄露问题二次开发时必须修掉。管理员导出成绩一般用csv模块或者pandas生成表格注意导出时中文文件名要编码否则浏览器下载下来是乱码。4. 常见问题排查登录、乱码、外键报错的实战记录项目跑不起来的原因翻来覆去就那么几类下面按「现象 → 原因 → 解决」整理了几条高频踩坑记录都是我在本地反复折腾过的。4.1 现象登录页一直提示账号密码错误原因通常有三种。一是没跑init_db.py表里没数据二是密码哈希算法不一致建库脚本用 MD5、登录逻辑用 SHA256两边对不上三是表单字段名写错前端nameuser而后端取request.form.get(username)取到空字符串。排查顺序先进数据库SELECT * FROM student LIMIT 5;看有没有记录有记录就把库里的密码字段拿去和登录代码里的哈希函数对一遍手动算一次比对最后打开浏览器开发者工具看表单提交的字段名和后端取值名逐个对。4.2 现象页面中文全部变成问号或乱码原因出在编码链路的某一环没统一。常见的是数据库字符集不是utf8mb4、连接字符串没加charsetutf8mb4、模板文件本身不是 UTF-8 保存。三层里任何一层掉链子都会乱码。解决建库时指定字符集连接串写成mysqlpymysql://user:pwdlocalhost/school_edu?charsetutf8mb4用编辑器把 HTML 文件另存为 UTF-8 无 BOM 格式响应头里确认Content-Type: text/html; charsetutf-8。全部对齐之后重启服务乱码基本消失。4.3 现象删课程时报外键约束错误原因是有学生已经选了这门课选课表里有指向该课程的外键记录直接删主表记录会被数据库拒绝。解决有两个方向要么先删选课表里的关联记录再删课程要么在建表时给外键加ON DELETE CASCADE让数据库自动级联删除。前者更安全适合需要保留选课历史的场景后者省事但删课会连带删掉学生的选课记录得确认业务上能接受。改外键约束需要重建表SQLite 下还得先关掉PRAGMA foreign_keys操作前记得备份数据库文件。4.4 现象改了模板但页面没变化原因是 Flask 在生产模式下会缓存模板或者浏览器缓存了静态资源。开发时把app.run(debugTrue)打开模板改动会自动重载浏览器那边按 CtrlF5 强制刷新或者在 CSS 链接后加个版本号参数。如果开了 debug 还是不变检查是不是改错了模板文件——templates 目录下按角色分了子目录学生首页和教师首页可能是两个同名文件。4.5 现象选课成功但人数没变原因是更新课程人数和插入选课记录不在同一个事务里某一步失败后另一部分提交了。也可能是前端展示的人数来自缓存或者查询条件写错。解决确认选课逻辑里course.selected 1和db.session.add(...)之间没有提前 return 或异常吞掉两者必须在同一次commit()里提交失败就整体rollback()。排查时可以临时在关键位置打日志把每次选课前后的人数打出来看是哪一步没执行。5. 二次开发改造从能跑到好用我通常动这几刀这套系统的骨架够用但离好用还有距离。拿到源码之后我一般不会大刀阔斧重构而是按「先补安全、再补体验、最后扩功能」的顺序小步改每改一处就跑一遍验证避免越改越乱。第一刀砍在密码存储上。项目里用 SHA256 裸哈希没有加盐彩虹表一撞一个准。改成werkzeug.security的generate_password_hash和check_password_hash两行代码的事但要注意存量数据得做一次迁移写个脚本把老密码逐个读出来、重新哈希、写回去。迁移时先备份数据库跑完抽三五个账号验证登录别一次性全量刷。第二刀补权限校验。前面提到教师能改别人课程成绩的漏洞修法是每个涉及课程操作的接口都加一句course.teacher_id ! session[user_id]的判断。更系统的做法是抽一个装饰器from functools import wraps from flask import session, abort def role_required(*roles): 角色校验装饰器用法role_required(teacher, admin) def decorator(fn): wraps(fn) def wrapper(*args, **kwargs): if session.get(role) not in roles: abort(403) return fn(*args, **kwargs) return wrapper return decorator挂到路由上之后新增接口只要加一行装饰器就自动受保护比每个函数开头手写 if 干净得多。改完用不同角色账号各点一遍学生访问教师接口应该返回 403这个验证不能省。第三刀是给选课加防重和限流。防重靠数据库联合唯一索引限流可以在选课接口上加一个简单的计数同一学号 1 秒内超过 3 次请求就拒绝。学生项目不用上 Redis内存字典加时间戳就够用。第四刀是体验层面登录失败提示区分「账号不存在」和「密码错误」还是统一提示取决于安全要求选课页显示实时余量成绩列表加分页。这些改动不涉及架构但用户感知最明显。验证方法很简单每改完一处用管理员、教师、学生三个账号各走一遍主流程——登录、查课、选课、录成绩、查成绩。改密码存储后重点验证老账号能登、新账号能注册改权限后重点验证越权访问被拦改选课并发后开两个浏览器窗口同时点选课看人数是否准确。这套流程我每次都强制走一遍因为教务系统的坑往往不在代码本身而在「改了一处、忘了另一处还依赖它」——从那以后我每次动数据库结构都先把建库脚本和模型定义对照一遍确认两边字段完全一致再启动服务省下的调试时间比写脚本的时间多得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站