简介:基于Python与Flask框架搭建的学生信息管理系统完整源码包,面向高校教务管理人员、教育信息化实施者以及需要完成Web课程设计的开发者。系统内置管理员、教师、学生三种角色,分别支持账户管理、课程设置、成绩录入、选课审批和个人成绩查询,权限划分清晰,覆盖学生信息与课程信息的新增、删除、修改、检索全套数据操作。压缩包共80个文件,约1.7MB,主要由Python后端逻辑、HTML页面模板、CSS样式表与JavaScript脚本构成,并配有PNG效果示意图、项目依赖清单和项目说明文档,整体内容完整、模块划分便于按需检索。目录中模板、静态资源、后端模型与表单模块分层清楚,可帮助理解Flask框架的请求路由、数据库模型和页面渲染协作方式;包内保留数据库配置和初始化脚本,可快速还原本地运行环境,便于二次开发与功能扩展。已有60人下载学习,适合用于学生信息管理系统课程设计、期末实训或学校信息化管理教学演示。
1. 学生信息管理系统:先想清楚再动手的三件事
去年帮一个系里做信息化改造,新来的教务老师把几千条学生记录散在十几个Excel工作簿里,选课记录靠微信群接龙,期末成绩录入要手动复制三次。这种局面不是个案——绝大多数中小规模的学校或培训机构,缺的不是管理意愿,而是一套低成本、能改代码、能自己维护的系统。Flask搭建的学生信息管理系统源代码,解决的正是这件事:三个角色(管理员、老师、学生)共用一套系统,覆盖选课、成绩录入、信息维护这三项日常高频事务,完整实现数据增删改查。它适合两类人:一类是刚学完Python基础、想找真实项目练手的开发者,另一类是学校老师或教务人员,想在自己的服务器上搭一套不依赖第三方SaaS的本地管理系统。从技术上说,Flask轻量、上手快,一个项目就能把数据库设计、表单验证、模板渲染、权限控制这几块核心技能串起来,这也是它至今仍是Python开发者起步项目的最大理由。
2. Flask项目骨架:从目录结构到数据库建模
2.1 目录结构与各文件的职责划分
拿到项目源代码后,先不要急着python app.py跑起来,第一件事是看清目录结构。这套系统遵循Flask应用最常见的单包结构,典型布局是这样的:
student_management/ ├── app.py # 应用入口,路由注册与启动 ├── models.py # 数据库ORM模型定义 ├── forms.py # 表单类定义与校验规则 ├── admin_views.py # 管理员角色相关路由 ├── teacher_views.py # 老师角色相关路由 ├── student_views.py # 学生角色相关路由 ├── auth.py # 登录认证与权限装饰器 ├── extensions.py # 独立db对象,防止循环导入 ├── requirements.txt # 依赖清单 ├── config.py # 配置文件,数据库连接参数 └── templates/ # Jinja2模板 ├── base.html # 基础布局模板 ├── login.html ├── admin/ # 管理员页面模板 ├── teacher/ # 老师页面模板 └── student/ # 学生页面模板先说代码逻辑:extensions.py单独放db = SQLAlchemy()实例,这是Flask开发里很关键的一步。如果直接在app.py里创建db对象再导入models,很容易踩循环导入的坑——因为models.py里用db定义模型,而app.py又需要导入models里的类来建表。把db独立出来,谁要用就导入谁,这个顺序就理顺了。
auth.py里的登录处理我建议优先读。它通常包含两样东西:一个session写入逻辑,一个装饰器函数。装饰器是权限控制的核心手段,后面会细讲。config.py里的SQLALCHEMY_DATABASE_URI是数据库连不上的第一排查点,下文也会单独说。
2.2 数据表设计:五张表如何支撑三种角色
这套系统的数据模型一般由五张表构成,理解它们的关联关系是看懂项目的钥匙:
| 数据表 | 关键字段 | 作用 |
|---|---|---|
| user | id, username, password_hash, role, name | 统一登录账号,role区分管理员/老师/学生 |
| student | id, user_id, student_no, class_name, major | 学生扩展信息,与user一对一 |
| teacher | id, user_id, teacher_no, title | 老师扩展信息 |
| course | id, course_name, credit, teacher_id | 课程基本信息 |
| course_student | id, course_id, student_id, score | 选课关系表,成绩字段放这里 |
user表用role字段区分身份,而不是拆三张登录表,这是这类管理系统的常见做法。因为三个角色的登录逻辑是一模一样的——用户名加密码,查表验密,区别只在于登录成功后能访问哪些路由。
course_student是典型的多对多中间表,学生和课程之间的关系由它维系,同时比分表多存一个score字段。值得留意的是,score被允许为空(nullable=True),因为选课和成绩录入通常是两个独立动作——先选课,以后再录成绩。这个表的设计直接影响整个业务流程:管理员能看所有选课记录,老师只能改自己任课班级的成绩,学生只能看自己的选课和成绩。
2.3 建库与初始化的完整操作
在config.py里确认数据库连接字符串后,执行初始化和建表:
# 首次运行前,安装依赖 pip install -r requirements.txt # 进入Python交互式环境,创建数据表 python >>> from app import app >>> from extensions import db >>> with app.app_context(): ... db.create_all()建表一定要放在app.app_context()里执行,这是Flask-SQLAlchemy最容易忽略的细节。不在应用上下文中操作数据库,会报RuntimeError: Working outside of application context。如果项目自带初始化脚本(比如init_db.py),直接运行那个文件也是一样的效果。
建表完成后,建议顺手做一次验证:
# 用SQLite场景下可以直接查表 sqlite3 student.db .tables看到user、course、course_student这五张表都创建成功,再继续跑python app.py进登录页。这一步快进不得,很多项目跑不起来的根源就是表没建对——尤其是中间表缺失或字段漏配,后患无穷。
3. 角色与权限:三种身份的边界怎么控制
3.1 权限控制的实现原理
权限控制是这个项目最值得读的部分,它决定了一个普通学生账号能不能偷偷进入管理员的删除页面。实现方式不复杂,依靠的是两个机制:登录状态写入session,视图函数加装饰器。
登录逻辑的核心代码如下:
from flask import session, redirect, url_for, flash from functools import wraps from models import User def login_required(func): """强制要求登录才能访问的装饰器""" @wraps(func) def wrapper(*args, **kwargs): if 'user_id' not in session: flash('请先登录', 'warning') return redirect(url_for('auth.login')) return func(*args, **kwargs) return wrapper def role_required(required_role): """限定角色访问的装饰器工厂""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('auth.login')) user = User.query.get(session['user_id']) if user.role != required_role: flash('权限不足,无法访问该页面', 'danger') return redirect(url_for('main.index')) return func(*args, **kwargs) return wrapper return decorator代码逻辑说明:login_required检查session里有没有user_id,没有就跳到登录页;role_required是闭包写法,接收一个角色字符串参数(如'admin'或'teacher'),先查库拿到当前用户对象,再比对role字段,不一致就拦截并提示权限不足。
@wraps(func)这行务必保留,它会把原函数的元信息复制给包装函数,否则Flask路由映射可能出现同名函数冲突的怪问题。实际使用时,在视图函数上方叠加装饰器:
@app.route('/admin/course/delete/<int:course_id>') @role_required('admin') def admin_delete_course(course_id): # 只有管理员能进入的删除逻辑 ...装饰器从下往上执行,所以role_required加在路由装饰器下面一行。有人会把两个装饰器顺序写反,导致权限检查永远不触发——这是非常隐蔽的坑,翻车过的人才知道。
3.2 密码的哈希存储方式
项目中user表存的是password_hash而不是明文密码,底层使用werkzeug.security的generate_password_hash和check_password_hash完成写入与校验:
from werkzeug.security import generate_password_hash, check_password_hash # 创建用户时,只存哈希值 new_user = User( username='2023001', password_hash=generate_password_hash('初始密码123'), role='student', name='张三' ) db.session.add(new_user) db.session.commit() # 登录校验时,比对哈希 user = User.query.filter_by(username=form.username.data).first() if user and check_password_hash(user.password_hash, form.password.data): session['user_id'] = user.id session['user_role'] = user.rolegenerate_password_hash默认使用pbkdf2算法加随机盐,每次生成的结果都不一样,这是刻意的——就算两个用户密码相同,数据库里的哈希值也不同,防止彩虹表反推。新手常犯的错是把check_password_hash写成generate_password_hash(user.password_hash) == form.password.data,这永远是False,因为每次生成的盐都不一样。
3.3 老师与学生的操作边界
三个角色对应的操作边界要心里有数:
- 管理员:维护所有用户账号的增删改查、维护课程信息、重置任何用户的密码、查看全站选课与成绩报表。
- 老师:查看自己的任课列表,查看选了自己课的学生名单,录入或修改学生成绩——但只能改自己任课课程里的成绩行。
- 学生:修改个人部分信息(如联系方式),查看可选课程列表,选课和退选,查看已选课程与最终成绩。
老师的成绩更新逻辑用course_student表过滤:CourseStudent.query.join(Course).filter(Course.teacher_id == current_teacher_id)保证只能动自己带的课。代码里必须做这层关联过滤,只按course_id过滤是不够的——万一有老师知道别人的课程号直接改URL传参,就能改到他课程范围之外的成绩,权限边界就形同虚设。
4. 核心功能落地:选课、成绩录入与增删改查细节
4.1 选课流程的完整实现
学生选课是这个系统里业务逻辑最完整的模块,它涉及三张表的联动:课程状态判断、选课记录写入、容量限制检查。典型的实现如下:
from models import Course, CourseStudent, db from flask import flash, redirect, url_for @app.route('/student/select_course/<int:course_id>', methods=['POST']) @login_required def select_course(course_id): student = current_user.student_profile # 当前登录学生的扩展表记录 course = Course.query.get_or_404(course_id) if not course: flash('课程不存在', 'danger') return redirect(url_for('student.course_list')) # 重复选课检查 existing = CourseStudent.query.filter_by( course_id=course_id, student_id=student.id ).first() if existing: flash('你已经选过这门课了', 'warning') return redirect(url_for('student.my_courses')) # 选课人数上限检查 selected_count = CourseStudent.query.filter_by(course_id=course_id).count() if selected_count >= course.capacity: flash('该课程选课人数已满', 'danger') return redirect(url_for('student.course_list')) # 写入选课记录,score留空 enrollment = CourseStudent( course_id=course.id, student_id=student.id, score=None ) db.session.add(enrollment) db.session.commit() flash('选课成功', 'success') return redirect(url_for('student.my_courses'))这里的顺序很讲究:先查课程存在,再查重复,再查容量,最后才写记录。这个顺序一旦颠倒,可能出现你给A课程选课失败,却把B课程记录写进去的错乱操作。容量检查放在写库之前是必须的,如果先写入再检查,就会允许超员记录产生。另一个细节是score=None明确传了空值,告诉数据库这条记录还没录入成绩,后面成绩模块才好区分「未录」和「不及格」。
4.2 成绩录入与更新:条件更新的常见写法
老师端成绩录入,涉及批量更新多条记录。常见实现是接收一个字典,key是选课记录ID,value是成绩数值:
from models import CourseStudent @app.route('/teacher/grade/<int:course_id>', methods=['POST']) @role_required('teacher') def submit_grades(course_id): # 确认这门课确实属于当前老师 course = Course.query.filter_by( id=course_id, teacher_id=current_user.teacher_profile.id ).first_or_404() # 表单中grade_map的格式: {"12": "85", "13": "92"} grade_map = request.form.get('grade_map', type=dict) for enrollment_id, score in grade_map.items(): score = float(score) if score < 0 or score > 100: flash('成绩必须在0-100之间', 'danger') return redirect(url_for('teacher.grades', course_id=course_id)) enrollment = CourseStudent.query.filter_by( id=enrollment_id, course_id=course_id ).first() if enrollment: enrollment.score = score db.session.commit() flash('成绩已保存', 'success') return redirect(url_for('teacher.course_detail', course_id=course_id))filter_by(id=enrollment_id, course_id=course_id)是刻意加的双重条件——即使前端传来的ID有异常,也保证只能更新当前课程下的选课记录。如果去掉课程过滤,一个懂HTML开发的老师完全可以直接构造POST请求修改别的课程的成绩。
float(score)有个坑:如果表单传了空字符串,这里会抛ValueError。所以前端要么用HTML的required属性限制空值,要么后端先判断score == ''再转换,推荐后端校验兜底。
4.3 增删改查的通用模式与分页写法
整个系统的增删改查遵循一个统一的四步模板:校验表单 → 操作数据库 → 提交事务 → 重定向加提示。学生的增删改查典型代码如下:
# 删除学生(同时删除关联的user账号) @app.route('/admin/student/delete/<int:student_id>', methods=['POST']) @role_required('admin') def admin_delete_student(student_id): student = Student.query.get_or_404(student_id) user = User.query.get(student.user_id) # 先删除选课记录,再删学生,最后删账号 CourseStudent.query.filter_by(student_id=student.id).delete() db.session.delete(student) db.session.delete(user) db.session.commit() flash(f'学生 {student.student_no} 已删除', 'success') return redirect(url_for('admin.student_list'))删除顺序有讲究。选课记录表通过外键关联学生,如果不先清理,即使数据库设了外键级联删除,ORM层面的操作也可能因顺序问题报错。先删中间表的记录,再删学生和用户,这个顺序能避免外键约束冲突。
列表页建议在查询时直接加paginate:
from flask import request @app.route('/admin/student_list') @role_required('admin') def admin_student_list(): page = request.args.get('page', 1, type=int) keyword = request.args.get('keyword', '') query = Student.query if keyword: query = query.filter( (Student.student_no.contains(keyword)) | (Student.name.contains(keyword)) ) pagination = query.paginate(page=page, per_page=20, error_out=False) students = pagination.items return render_template('admin/student_list.html', pagination=pagination, students=students, keyword=keyword)列表页千万别把所有记录一次查出渲染,数据量一上来页面就卡死。用paginate配合模板里的分页控件,每页20条,对这类管理系统的查询体验来说足够了。error_out=False的意思是当请求页码超出范围时返回空列表而不是报404,体验更好。
5. 常见问题排查:五个高频坑的定位与修复
5.1 数据库连不上、表建不出来
现象:运行db.create_all()不报错但表建不出来,或者运行app.py后提示sqlalchemy.exc.OperationalError。
原因:三个来源——数据库连接串写错、models.py里的模型没有被app.py导入、SQLAlchemy实例和app对象初始化顺序不对。
解决:先打开config.py核对SQLALCHEMY_DATABASE_URI,SQLite场景下路径要正确,比如sqlite:///student.db是相对路径,会创建在Flask应用的根目录。然后在app.py中检查是否有from models import *或显式导入所有模型类。最后确认初始化语句的位置:
db.init_app(app)必须在app = Flask(__name__)之后调用。记住一个判断标准:如果app.py里有from models import User,那这一行必须放在db.init_app(app)之后,否则模型里注册的表不会被SQLAlchemy追踪到。
5.2 登录后跳转变成死循环
现象:输入正确账号密码后一直跳回登录页,或者浏览器提示重定向次数过多。
原因:login_required装饰器检查的是session,但登录成功的视图函数里没有写入session['user_id'],或者session写入时键名对不上——登录时存的是session['user_id'],装饰器检查的却是session['uid']。
解决:检查auth.py登录视图里写入session的字段名,和装饰器检查的字段名必须完全一致。推荐的做法是把登录成功的写session语句放在db.session.commit()之后,确认账号真的存在再写入session。另外检查Flask的SECRET_KEY是否设置,如果没设置,session无法正常工作:
app.config['SECRET_KEY'] = 'r2D6s8uHhP9vC4lN'SECRET_KEY长度没有硬性规定,但要足够随机, 至少建议32个字符 。每次重启服务后session全部失效是正常的——因为session签名依赖这个密钥,改了就全部作废。
5.3 模板继承失效、页面样式乱掉
现象:子页面能渲染但样式丢失,或者出现TemplateNotFound报错。
原因:render_template里写的模板路径不对,或者base.html中{% block %}的块名和子模板里填的不一致。最常见的是一个写成{% block content %},另一个继承时写{% block body %},块名不匹配导致整个模板内容被忽略。
解决:所有继承base.html的子模板,顶部统一用{% extends "base.html" %},块名全部统一为content。调用render_template时注意模板文件路径是相对于 templates 目录的,比如render_template('admin/student_list.html'),这里的子目录必须真实存在。如果css加载不出来,看base.html里用的链接是/static/css/style.css还是相对路径,相对路径在子页面渲染时会指向错误的URL。
5.4 修改密码后旧session仍生效
现象:用户改了密码,但之前登录的session不变,别人拿了旧session还能继续操作。
原因:session里没有记录密码的哈希值或修改时间,Flask默认session只在浏览器关闭时过期。
解决:在session里额外存一个密码哈希的短签名,每次请求时比对。常见做法是装饰器里检查session中的password_hash与数据库当前值一致——是简洁有效的思路:
def login_required(func): @wraps(func) def wrapper(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('auth.login')) user = User.query.get(session['user_id']) if not user: session.clear() return redirect(url_for('auth.login')) if session.get('pwd_hash') != user.password_hash: session.clear() return redirect(url_for('auth.login')) return func(*args, **kwargs) return wrapper这样用户改密码后,旧session里的哈希值和数据库不一致,自动被强制下线。隐患是查库次数多了,但在这个规模的管理系统里完全值得,安全性优先。
5.5 CSRFToken验证失败导致表单无法提交
现象:表单提交时提示CSRF token missing or incorrect,或者浏览器控制台没有报错但后端返回400。
原因:Flask-WTF默认开启CSRF保护,而表单类里没有传csrf_token,或者使用原生HTML表单时忘了加隐藏字段。还有一个隐蔽场景:多个表单模板共用时忘记在页面里加表单对应的隐藏域。
解决:在表单类中引入CSRF依赖,并在模板的form标签内加入隐藏字段:
from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField class LoginForm(FlaskForm): username = StringField('用户名') password = PasswordField('密码') submit = SubmitField('登录')<form method="post"> {{ form.hidden_tag() }} {{ form.username.label }} {{ form.username() }} {{ form.password.label }} {{ form.password() }} {{ form.submit() }} </form>form.hidden_tag()会自动渲染CSRF隐藏字段,这是最省事的做法。如果是纯HTML表单不经过FlaskForm渲染,需要手动加上<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">。
6. 项目再进阶:分页搜索与Excel批量导入导出
系统跑通之后,有三件事能让它从「能用」变成「好用」:搜索增强、数据导入导出、以及部署上线。前两件事只涉及视图函数和模板的小改动,第三件事需要想清楚部署方式。
搜索增强的核心是把contains换成优先级更高的模糊匹配:
from sqlalchemy import or_ # 学生列表页搜索同时匹配学号和姓名 query = Student.query if keyword: query = query.filter(or_( Student.student_no.contains(keyword), Student.name.contains(keyword), Student.major.contains(keyword) ))or_是SQLAlchemy的条件组合函数,contains对应SQL中的LIKE '%keyword%',在SQLite和MySQL下都可用。注意如果数据库是MySQL,中文contains可能因为字符集排序规则导致大小写或全半角匹配不准,建议在连接串里加上charset=utf8mb4。
Excel导入导出是教务场景里最高频的增量需求——老师手里几乎清一色是Excel版的学生名单或成绩单。用pandas处理Excel文件是常规做法:
import pandas as pd from io import BytesIO from flask import send_file @app.route('/admin/student/export') @role_required('admin') def export_students(): students = Student.query.all() data = [{ '学号': s.student_no, '姓名': s.name, '专业': s.major, '班级': s.class_name } for s in students] df = pd.DataFrame(data) output = BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, index=False, sheet_name='学生名单') output.seek(0) return send_file(output, download_name='students.xlsx', as_attachment=True)pandas会把student_no这类长数字转为科学计数法,导出后学号显示成1.00202e+08是经典翻车现场。解决方法是导出时把学号列转成字符串再写入:
data = [{ '学号': str(s.student_no), # 强制转字符串,防止Excel科学计数法 '姓名': s.name, '专业': s.major, '班级': s.class_name } for s in students]导入则反过来,读取Excel后要额外做类型清洗,比如str(row['学号']).strip()去掉Excel残留的空格和幕后字符。
部署上线这块,我的建议是:Flask自带的服务器只能用于开发调试,对外服务至少换成waitress或gunicorn(Linux环境)。
# 安装生产级WSGI服务器 pip install waitress # Windows下启动,监听8000端口 waitress-serve --host=0.0.0.0 --port=8000 app:appwaitress-serve是Windows环境最省心的生产服务器,不用配Nginx也能撑起中小规模的使用量。跨平台场景用gunicorn更主流,但它在Windows原生支持有缺陷,需要WSL环境。上线前记得用下面的命令检查配置:
# 开启debug模式时绝对不可用于生产 export FLASK_ENV=production python app.py如果把调试模式部署到公网,用户报错时页面会直接吐出完整堆栈和局部变量内容——数据库连接信息、源码路径、甚至SECRET_KEY都可能暴露。这类翻车事故不在少数,不要问我怎么知道的。
最后说一个我自己的习惯:任何增删改查类的管理项目,在交付或自用前都强制跑一遍「最深处路径」——用一个学生账号实际走完选课、退课、看成绩,用一个老师账号走完录分、改分,再用管理员账号走完重置密码和删除课程。三条链路全部通过,才算这个系统的冒烟测试及格。从那以后,每次改动涉及session或数据库结构的代码,我都会强制走一遍三个角色的全流程,能早发现问题就不留给部署后的用户去踩。希望这些经验能帮你在自己的学生信息管理系统上少走弯路。
本文还有配套的精品资源,点击获取