ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Python和Flask的学生教务管理系统:从数据库设计到全栈实现

2026/9/14 9:29:24 拓冰建站 浏览量
基于Python和Flask的学生教务管理系统:从数据库设计到全栈实现 简介这是一份基于Python和Flask开发的学生教务管理系统完整源码与数据库面向计算机、通信、人工智能、自动化等专业的学生或从业者适用于期末课程设计、课程大作业、毕业设计等场景也可作为Flask入门到进阶的参考项目。资源包共61个文件压缩包约3.14MB主要包含Python后端主程序、SQL数据库脚本、HTML页面、CSS样式、JavaScript脚本以及多张界面和效果图片结构清晰便于按模块查阅修改。目前已有80人浏览学习代码经过调试测试答辩评审分达98分核心功能均可直接运行包含登录验证、学生与教师管理、密码修改等典型模块完整度较高。在此基础上开发者可快速替换前端模板或扩展业务逻辑也能通过阅读源码和数据库表结构理解Flask项目的分模块设计思路具有较高的学习借鉴价值。1. 基于 Python 和 Flask 的学生教务管理系统为什么它是数据库课程设计里的经典选题基于 Python 和 Flask 构建的学生教务管理系统是数据库课程设计和 Web 开发实训里出现频率最高的项目选题之一。它要解决的业务很直白维护学生、教师、课程、选课、成绩这几类核心数据再提供登录、选课、退课、成绩录入与查询这些操作。正是这种不复杂让它能完整覆盖从建库、建模、写接口到交付的全部环节评审老师能顺着页面看到功能闭环答辩时也能在每一层问出深度。对正在做课程设计的同学、想系统走一遍 Flask MySQL 全栈的 Python 开发者以及要快速搭内部管理系统的工程师来说把这个项目的骨架吃透比东拼西凑一堆源码片段有用得多。下文按课程设计项目的交付顺序从数据库设计讲到答辩防坑每一层都给出取舍理由和可直接复制的代码。2. 学生教务管理系统的数据库设计五张表的实体关系与可执行建表 SQL2.1 先理清实体关系为什么是五张表而不是四张评审老师看项目时往往先翻数据库设计说明再打开 Navicat 看表结构。教务系统最常见的角色划分是三端管理员、教师、学生。管理员维护教师和课程信息教师录入成绩学生选课并查看成绩。由此得到五张核心表admin管理员、teacher教师、student学生、course课程和 selection选课/成绩。关系层面教师与课程是 1 对 N一个教师可以开多门课course 表里放 teacher_id 外键学生与课程是 N 对 M必须用中间表 selection 拆成两个 1 对 N。常见的错误设计是给 student 表加一个已选课程字段把多个课程 id 拼成逗号串存进去或者干脆建一张冗余的学生课程关联表却不加唯一约束。这类设计在答辩第一阶段就会被否掉因为后续要回答某学生选了几门课某课程选课人数是否满员这类问题时字符串拆分和全表扫描都很难看。另一个高频决策是成绩字段放哪。常见做法是直接把 score 放进 selection 表一门课一次选课对应一条记录记录里带上最终成绩查询时连 join 都省了。只有当业务需要支撑平时成绩、期末成绩、补考标志、重修记录这类扩展字段时才值得单独拆一张 score 表。两个方案的边界可以这样判断设计选择适用场景成绩查询写法选课与成绩合并一张表每门课每次选课只有一个最终成绩selection.score直接读取无需二次 join成绩单独一张表需要平时分、期末分、补考标记等扩展字段关联 score 表一门课可对应多行成绩记录课程设计项目选前者即可并在设计说明里补一句当前业务下选课与成绩是一对一关系故合并存储这句话就能表明你权衡过扩展性而不是没想到。2.2 一套可直接执行的 MySQL 建库建表脚本下面这套 SQL 在 MySQL 5.7 和 8.0 下都能直接跑。用 Navicat 或 mysql 命令行客户端登录后按顺序执行CREATE DATABASE IF NOT EXISTS edu_admin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE edu_admin; -- 管理员表 CREATE TABLE admin ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE COMMENT 登录名, password_hash VARCHAR(128) NOT NULL COMMENT 密码哈希, real_name VARCHAR(50) COMMENT 姓名 ) ENGINEInnoDB; -- 学生表 CREATE TABLE student ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, password_hash VARCHAR(128) NOT NULL COMMENT 密码哈希, name VARCHAR(50) NOT NULL COMMENT 姓名, gender ENUM(男,女) DEFAULT 男, major VARCHAR(50) COMMENT 专业, class_name VARCHAR(50) COMMENT 行政班级, enrollment_year YEAR COMMENT 入学年份 ) ENGINEInnoDB; -- 教师表 CREATE TABLE teacher ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, teacher_no VARCHAR(20) NOT NULL UNIQUE COMMENT 工号, password_hash VARCHAR(128) NOT NULL COMMENT 密码哈希, name VARCHAR(50) NOT NULL COMMENT 姓名, title VARCHAR(30) COMMENT 职称, department VARCHAR(50) COMMENT 所属院系 ) ENGINEInnoDB; -- 课程表 CREATE TABLE course ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, name VARCHAR(100) NOT NULL COMMENT 课程名, credit DECIMAL(3,1) DEFAULT 0 COMMENT 学分, capacity INT UNSIGNED DEFAULT 50 COMMENT 选课容量, semester VARCHAR(20) COMMENT 开课学期如 2025-2026-1, teacher_id INT UNSIGNED NOT NULL, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINEInnoDB; -- 选课表携带成绩字段 CREATE TABLE selection ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_id INT UNSIGNED NOT NULL, course_id INT UNSIGNED NOT NULL, score DECIMAL(5,2) DEFAULT NULL COMMENT 百分制成绩未录入为 NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id), CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sel_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB;这段脚本里有几个参数值得单独说明。字符集统一用 utf8mb4 而不是 utf8因为它能完整存下中文和 emoji避免库里能查到、页面上显示问号的乱码类问题所有表显式指定 ENGINEInnoDB外键约束和事务才生效后面写选课事务时会依赖这一点。selection 表上的联合唯一索引 uk_stu_course 是数据库层面的兜底即使应用层代码有 bug重复选课在入库那一刻也会被数据库拒绝这是答辩时的高光点。score 用 DECIMAL(5,2) 而不是 FLOAT浮点类型在比较和展示时会出现 59.999999 这类脏数据定点数不会。2.3 建表时最容易让评审皱眉的三个细节第一处是学号和工号的数据类型。常见错误是设计成 BIGINT但学号本质是编码而不是数字很多学校学号以 0 开头年份变化还会导致位数变化用整数类型会丢掉前导零之后所有关联查询都要补位处理。统一用 VARCHAR 保存查询走等值匹配性能不受影响。第二处是密码字段。课程设计阶段很多人直接存明文答辩老师一句数据库泄露了怎么办就顶不住。高分的做法是列名写成 password_hashPython 侧用 Werkzeug 的 generate_password_hash 做单向哈希登录时用 check_password_hash 比对。默认密码 123456 在数据库里显示为一串随机字符观感上比明文更有说服力。第三处是外键和索引的可见性。评审老师会在 Navicat 里点开表结构确认外键存在所以不要只把外键关系写在 ORM 模型里而数据库里没有。上面脚本里所有外键和联合唯一索引都在建表时显式声明模型层再按同样结构映射。这个阶段做完后Navicat 里应该能看到五张表、三个外键和一个联合唯一索引第一步就算过关了。3. Flask 项目骨架与 SQLAlchemy 模型层让 Python 代码真正对接 MySQL3.1 环境准备python 版本选择与依赖安装动手前先把环境拉齐。课程设计场景推荐 Python 3.8 到 3.11不要裸装在系统 Python 里先建虚拟环境 venv。在项目根目录执行mkdir edu_admin cd edu_admin python -m venv venv # Windows 下激活 venv\Scripts\activate # Linux / macOS 下激活 # source venv/bin/activate pip install --upgrade pip pip install flask flask-sqlalchemy pymysqlvenv 的作用是把依赖隔离在项目目录内避免和机器上其它 Python 项目互相干扰pymysql 是纯 Python 实现的 MySQL 驱动Windows 上不需要编译安装完就能用。如果用 PyCharm在 Settings → Project → Python Interpreter 里选择 venv 路径编辑器就不会对 Flask 相关的 import 标红pycharm 配置 flask 环境这一步是最常见的启动障碍。安装完执行pip list应该能看到 Flask 3.x、Flask-SQLAlchemy 3.x、SQLAlchemy 2.x 和 PyMySQL 这几个包下面的代码对这几个大版本都兼容。3.2 项目目录与 config.py 的四个关键参数拆目录不是必须的但答辩时单文件跑通和分层清晰的印象分差距明显。我一般这样组织edu_admin/ ├── app.py # 应用入口注册蓝图 ├── config.py # 配置 ├── models.py # SQLAlchemy 模型 ├── views/ │ ├── __init__.py │ ├── auth.py # 登录/登出 │ ├── admin.py # 管理员管理教师与课程 │ ├── teacher.py # 教师成绩录入 │ └── student.py # 学生选课与成绩查询 ├── templates/ # Jinja2 模板 └── static/ # css/jsconfig.py 里四个参数要能讲清楚作用import os BASE_DIR os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY change-me-to-a-random-string SQLALCHEMY_DATABASE_URI ( mysqlpymysql://root:your_password127.0.0.1:3306/ edu_admin?charsetutf8mb4 ) SQLALCHEMY_TRACK_MODIFICATIONS False SQLALCHEMY_ENGINE_OPTIONS { pool_size: 5, pool_recycle: 280, }配置项作用演示时建议值SECRET_KEY签名 Flask session 的密钥泄露等于 session 可伪造随机长字符串SQLALCHEMY_DATABASE_URI数据库连接串pymysql 驱动charset 必须与建库一致mysqlpymysql://user:pwdhost:3306/edu_admin?charsetutf8mb4pool_recycle空闲连接回收周期秒280小于 MySQL wait_timeoutSQLALCHEMY_TRACK_MODIFICATIONS是否追踪对象修改False节省内存pool_recycle 设为 280 秒是为了避开 MySQL 默认 8 小时断开空闲连接的问题。这个坑在演示现场很常见前一天晚上连上数据库第二天第一次点按钮直接报 MySQL server has gone away重启 Flask 开发服务器又好了观感很差。开发调试时可以在 SQLALCHEMY_ENGINE_OPTIONS 里临时加echo: True控制台会打印每条 SQL方便对着表结构查问题演示前记得去掉。app.py 入口按工厂方式写注册各角色蓝图from flask import Flask from config import Config from models import db def create_app(): app Flask(__name__) app.config.from_object(Config) db.init_app(app) from views.auth import auth_bp from views.admin import admin_bp from views.teacher import teacher_bp from views.student import student_bp app.register_blueprint(auth_bp) app.register_blueprint(admin_bp, url_prefix/admin) app.register_blueprint(teacher_bp, url_prefix/teacher) app.register_blueprint(student_bp, url_prefix/student) return app app create_app() if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)host0.0.0.0 表示监听所有网卡演示时同一局域网的电脑能用 IP 访问debugTrue 在开发阶段会输出完整堆栈但正式演示前要改成 False否则页面报错时现场会刷出代码详情。3.3 模型层把第二张章节的表一一映射成 Python 对象models.py 是核心。数据库里每一张表对应一个模型类字段名与列名保持一致from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db SQLAlchemy() class Student(db.Model): __tablename__ student id db.Column(db.Integer, primary_keyTrue) student_no db.Column(db.String(20), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) name db.Column(db.String(50), nullableFalse) gender db.Column(db.Enum(男, 女), default男) major db.Column(db.String(50)) class_name db.Column(db.String(50)) def set_password(self, raw): self.password_hash generate_password_hash(raw) def check_password(self, raw): return check_password_hash(self.password_hash, raw) class Course(db.Model): __tablename__ course id db.Column(db.Integer, primary_keyTrue) course_no db.Column(db.String(20), uniqueTrue, nullableFalse) name db.Column(db.String(100), nullableFalse) credit db.Column(db.Numeric(3, 1)) capacity db.Column(db.Integer, default50) semester db.Column(db.String(20)) teacher_id db.Column(db.Integer, db.ForeignKey(teacher.id), nullableFalse) teacher db.relationship(Teacher, backrefdb.backref(courses)) class Selection(db.Model): __tablename__ selection id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(student.id), nullableFalse) course_id db.Column(db.Integer, db.ForeignKey(course.id), nullableFalse) score db.Column(db.Numeric(5, 2)) created_at db.Column(db.DateTime) student db.relationship(Student, backrefdb.backref(selections)) course db.relationship(Course, backrefdb.backref(selections))set_password 和 check_password 把哈希逻辑封装进模型业务层永远见不到明文密码也见不到哈希算法细节。relationship 声明的是 ORM 级关联teacher.courses能直接拿到这个教师开的所有课程selection.student能拿到这条选课记录的学生对象比手动 join 后按下标取值可读性强很多。提示db.create_all() 只创建不存在的表不会修改已存在的表结构。表结构统一由 SQL 脚本维护模型只做映射不要在模型里加列后指望数据库自动同步否则运行时会报 Unknown column。3.4 模型写完后先做一次查询自检代码能不能连上库不需要先写页面用一段小脚本验证。新建 test_connection.pyfrom app import app from models import db, Student with app.app_context(): print(Student.query.count()) for s in Student.query.limit(5).all(): print(s.student_no, s.name, s.class_name)能打印出测试数据行数说明连接串、账号权限、字符集和模型映射全部正常。这里有个常见报错必须知道模型操作要包在 app.app_context() 里因为 SQLAlchemy 需要知道当前属于哪个 Flask 应用脱离上下文直接调用会抛 RuntimeError: Working outside of application context。答题时说出上下文是把配置与模型绑定到当前应用实例这道基础题就过了。4. 基于 Flask 的教务核心接口实现登录鉴权、选课事务与成绩统计4.1 三角色登录与 session 鉴权系统分管理员、教师、学生三种身份登录时先选角色再去对应表验证。角色判断做成装饰器比在每个视图函数里重复 if 判断要整洁也方便后续扩展到更多角色from functools import wraps from flask import (Blueprint, render_template, request, redirect, url_for, session, flash) from models import db, Student, Teacher, Admin auth_bp Blueprint(auth, __name__) def login_required(roleNone): def decorator(f): wraps(f) def wrapper(*args, **kwargs): if user_id not in session: flash(请先登录) return redirect(url_for(auth.login)) if role and session.get(role) ! role: flash(当前角色无权访问) return redirect(url_for(auth.login)) return f(*args, **kwargs) return wrapper return decorator auth_bp.route(/login, methods[GET, POST]) def login(): if request.method POST: account request.form.get(account, ).strip() password request.form.get(password, ) role request.form.get(role, ) if role student: user Student.query.filter_by(student_noaccount).first() elif role teacher: user Teacher.query.filter_by(teacher_noaccount).first() else: user Admin.query.filter_by(usernameaccount).first() if user and user.check_password(password): session[user_id] user.id session[role] role return redirect(url_for(role .dashboard)) flash(账号或密码错误) return render_template(login.html)这里有一个 filter 和 filter_by 的差异代码评审和答辩都会问到API等值条件写法多条件filterStudent.student_no accountfilter(Student.x 1, Student.y 2)filter_bystudent_noaccountfilter_by(x1, y2)filter 接收的是 SQLAlchemy 表达式对象filter_by 接收的是关键字参数两者不能混用。session 是 Flask 对 Cookie 的签名封装服务端只持有签名密钥所以 SECRET_KEY 不能泄露。三个角色的首页都命名为 dashboard登录后通过role .dashboard动态拼接跳转目标新增角色时登录逻辑不用改。4.2 选课与退课事务边界要画清楚选课是教务系统里唯一涉及并发语义的操作。两个学生同时选同一门只剩一个名额的课时如果代码只做先查询数量再插入记录两个请求可能都查到名额然后都插入成功超额一节。常见做法是给课程行加锁把容量检查和插入放进同一个事务from sqlalchemy import func from sqlalchemy.exc import IntegrityError def do_select_course(course_id): student_id session[user_id] # 应用层先查一次拦截正常路径下的重复选课 if Selection.query.filter_by( student_idstudent_id, course_idcourse_id).first(): return 你已经选过这门课 # 行级锁同一时刻只有一个事务能读到并修改这行 course Course.query.with_for_update().filter_by(idcourse_id).first() if course is None: return 课程不存在 selected db.session.query(func.count(Selection.id)).filter_by( course_idcourse_id).scalar() or 0 if selected course.capacity: return 选课人数已满 try: db.session.add(Selection(student_idstudent_id, course_idcourse_id)) db.session.commit() return 选课成功 except IntegrityError: db.session.rollback() return 选课失败重复选课或数据冲突with_for_update 对应数据库的SELECT ... FOR UPDATE行级排他锁锁住 course 这一行后另一个选课请求要等当前事务提交才能读到这行把查容量、插选课变成一个原子区间。如果觉得这层复杂可以退一步至少保证数据库唯一索引兜底捕获 IntegrityError 后回滚并返回友好提示。选课失败的三层防御可以这样组织答案应用层预先查重负责正常路径行锁负责并发竞争唯一索引负责代码 bug。每层对应一种失败场景答辩时按这个顺序讲非常清晰。退课是选课的逆操作按 student_id 和 course_id 删除记录后 commit。注意删除失败时要在 except 里 rollback否则抛错后连接池里的连接会带着未提交事务返回池子影响下一次复用。4.3 成绩录入与班级统计func 聚合的正确打开方式教师端成绩录入本质是单条更新真正有价值的是成绩统计接口。用 func 模块做聚合配合 join 与 group_by一次查询拿到按班级分组的选课人数和平均分rows (db.session.query( Student.class_name, func.count(Selection.id).label(total), func.avg(Selection.score).label(avg_score), func.max(Selection.score).label(max_score)) .join(Selection, Selection.student_id Student.id) .filter(Selection.course_id course_id) .group_by(Student.class_name) .order_by(func.avg(Selection.score).desc()) .all())这段查询与原生 SQL 的对应关系很直接join 指定关联列filter 是 wheregroup_by 按班级分组order_by 按平均分倒序。返回的 rows 是 Row 对象可以直接用 row.total、row.avg_score 取值比裸元组按下标取值清晰。func.avg 在没有任何成绩时返回 NULL模板里展示前要判断否则页面上会出现 None。成绩录入页还有一个隐性需求教师只能看到自己开的课所以课程列表查询要带 teacher_id 条件。这种业务权限下沉到查询条件的写法比在模板里靠 if 隐藏按钮干净也更容易被评审认可。成绩分布统计也是加分点用 case 表达式做分段聚合from sqlalchemy import case dist (db.session.query( case((Selection.score 90, 优秀), (Selection.score 80, 良好), (Selection.score 60, 及格), else_不及格).label(level), func.count(Selection.id)) .filter(Selection.course_id course_id) .group_by(level) .all())这个结果可以直接喂给模板画一个简单的柱状分布不需要前端框架一段内联样式就能出图。它展示的不是功能复杂度而是对 SQL 聚合能力的理解评审判分时这类细节比页面数量更管用。5. 学生教务管理系统收尾阶段的高分实操CSV 导出、故障排查与答辩防坑5.1 给成绩查询加一个 CSV 导出接口导出功能是成本最低的加分点。给教师端加一个 CSV 导出接口把某一门课的成绩下载成文件import csv, io from flask import make_response def export_course_scores(course_id): rows (db.session.query(Student.student_no, Student.name, Selection.score) .join(Selection, Selection.student_id Student.id) .filter(Selection.course_id course_id) .all()) buf io.StringIO() writer csv.writer(buf) writer.writerow([学号, 姓名, 成绩]) writer.writerows(rows) resp make_response(buf.getvalue()) resp.headers[Content-Type] text/csv; charsetutf-8-sig resp.headers[Content-Disposition] attachment; filenamescores.csv return resputf-8-sig 是让导出文件在 Excel 里双击打开不乱码的编码普通 utf-8 会被 Excel 识别成系统默认字符集中文行直接变乱码。这个接口复用 4.1 的 login_required(teacher) 装饰器接到教师端成绩页的一个下载链接上功能闭环就完整了。5.2 演示现场的三个高频故障与对应检查手段故障现象根因检查方式页面中文显示问号表字符集或连接串不是 utf8mb4执行SHOW CREATE TABLE student;看 CHARSET连接串补?charsetutf8mb4隔夜后首个请求报 MySQL server has gone away连接池中的连接被 MySQL 空闲断开设置 pool_recycle 小于 wait_timeout或请求前 ping 重连首次运行报 Access denied / Unknown database账号权限或库名错误先用 Navicat 同一账号测试连接排除配置问题排查顺序永远是先确认数据库能连再翻 Flask 终端日志最后看模板渲染。演示现场把这三条记牢比临时翻报错要稳。5.3 答辩必问的三个问题与回答口径第一问为什么用 ORM 而不是原生 SQL答三点参数绑定天然防注入、模型类即表结构文档、聚合与分页 API 可复用。第二问怎么防止重复选课按 4.2 的三层逻辑答应用层先查、行锁保并发、唯一索引兜底。第三问密码能不能被还原说明用的是单向哈希而非加密数据库泄露也逆推不出明文。回答时不要背术语用先查、加锁、索引、哈希这几个动作词组织语言简洁且符合工程直觉。答辩前一晚按这个顺序自查一遍Navicat 里能打开五张表和外键、三个角色分别能登录、教师导出 CSV 在 Excel 不乱码、数据库里 password_hash 列看不到明文。四步都过这份代码加文档的完整交付就可以进场了。本文还有配套的精品资源点击获取