ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Flask的学生信息管理系统:B/S架构、ORM与部署实战

基于Flask的学生信息管理系统:B/S架构、ORM与部署实战 简介这是一份基于Python Flask Web框架的B/S结构学生信息管理系统毕业设计项目源码主要面向计算机相关专业的学生可直接用于毕业设计、课程设计或期末大作业。整套项目已通过导师指导并获得97分高分包含完整可运行的源代码与项目说明文档下载后无需修改即可部署使用。压缩包约3.82MB共593个文件以508个Python源码文件为核心同时包含前端HTML/CSS/JS页面、数据库文件、配置文件以及虚拟环境相关组件目录结构清晰便于按模块查阅和二次开发。系统覆盖学生信息管理中的常见业务场景可作为学习Flask框架、理解B/S架构与数据库交互的实战范例。目前已有864人浏览/学习适合需要快速完成高质量毕设项目或希望提升Web开发能力的开发者参考。1. 毕设做学生信息管理选 Flask 到底图什么每年毕业季都能看到大量「学生信息管理系统」选题表面看是课程设计的常见客实际上它把 B/S 架构最核心的几条链路全走了一遍浏览器发起请求、后端处理业务逻辑、数据库做持久化、再把结果渲染回页面。选 Python 和 Flask 来落地这个题目不是因为 Flask 功能最全而是它足够轻、扩展点足够清晰一个上午就能把骨架搭起来剩下的时间全部留给业务代码和项目文档这对毕设节奏非常友好。Flask 属于微框架自带的东西只有路由和模板引擎ORM、表单校验、登录会话这些都要自己集成。听起来麻烦但恰恰因为「自己动手」你在答辩时才有东西可讲——你清楚每一层是怎么接起来的而不是拖了一个全家桶框架然后什么都说不出来。B/S 架构在这里也天然成立老师通过浏览器访问系统不用装客户端Flask 内置的开发服务器在局域网里就能直接演示部署到公网也只是多一步 Nginx 反向代理的事。这篇文章按「架构 → 数据 → 业务 → 部署」的顺序把从空目录到一个能跑、能查、能写、能答辩的学生信息管理系统完整过一遍代码可以直接抄但更重要的是理解每一步为什么这么写。新手建议从头跟到尾有经验的可以直接跳到第 4 章看查询和会话处理的边界情况。2. 先立架构Flask 项目骨架与 B/S 请求链路2.1 为什么 B/S 架构是毕设演示的最优解C/S 架构需要装客户端评分老师电脑上不一定有对应运行时演示五分钟有一半时间在处理环境问题。B/S 架构只需要浏览器笔记本开个 Flask 服务同一局域网内任何设备都能访问这在实际答辩场景里是非常实在的优势。从技术层面看B/S 的请求链路也更适合教学展示HTTP 请求从浏览器发出Flask 通过路由分发到视图函数视图函数操作数据库最后把数据塞进 Jinja2 模板返回 HTML。这个链路里Flask 承担的是「路由 视图 模板渲染」三层职责数据库操作交给 SQLAlchemy登录状态交给 Session。各司其职出了问题能直接定位到某一层这也是我给学生信息管理系统选 Flask 而不是直接用 Django 的原因——Django 把一切都封装好了ORM 迁移、Admin 后台、认证系统全都内置反而少了「理解原理」的过程。2.2 从零开始建项目目录结构与最小 Flask 应用先建虚拟环境避免污染全局 Python。Windows 和 Linux 命令略有差异但思路一致都是创建独立解释器环境然后安装依赖。mkdir student_system cd student_system python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate pip install flask flask-sqlalchemy装完依赖后项目目录结构建议按下述方式组织这能让蓝图和模型各归其位后续加功能不用推倒重来student_system/ ├── app.py # 应用入口 ├── models.py # 数据库模型 ├── views/ # 蓝图目录 │ ├── __init__.py │ ├── student.py # 学生信息相关路由 │ └── auth.py # 登录登出路由 ├── templates/ # Jinja2 模板 ├── static/ # CSS/JS └── requirements.txt # 依赖清单最小可运行的入口文件做成「应用工厂」形式create_app返回一个 Flask 实例这样测试环境和生产环境可以传入不同配置。代码如下# app.py from flask import Flask def create_app(): app Flask(__name__) app.config[SECRET_KEY] dev-key-please-change app.config[SQLALCHEMY_DATABASE_URI] sqlite:///student.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False # 注册蓝图 from views.student import student_bp app.register_blueprint(student_bp) return app if __name__ __main__: create_app().run(host0.0.0.0, port5000, debugTrue)2.3 蓝图拆分让路由不再堆成一个文件学生信息管理系统的路由至少有登录、登出、学生列表、新增、编辑、删除、搜索这几类全部写在app.py里会导致单文件超过三百行排错时上下翻找很痛苦。蓝图Blueprint就是干这个的它把一组相关路由打包成模块再统一注册到应用上。学生相关的视图独立成views/student.py# views/student.py from flask import Blueprint, render_template, request, redirect, url_for student_bp Blueprint(student, __name__) student_bp.route(/) def index(): return render_template(index.html) student_bp.route(/student/list) def student_list(): # 暂时返回空模板后续章节具体实现 return render_template(student_list.html)登录取名为auth_bp挂在/auth前缀下# views/auth.py from flask import Blueprint, render_template, request, session, redirect, url_for auth_bp Blueprint(auth, __name__, url_prefix/auth) auth_bp.route(/login, methods[GET, POST]) def login(): if request.method POST: # 登录逻辑在下一章实现 pass return render_template(login.html)参数说明Blueprint第一个参数是蓝图名url_prefix给该蓝图下所有路由加统一前缀。这样登录页面地址是/auth/login学生列表是/student/list访问路径一目了然。注册蓝图时注意from views.student import student_bp必须放在create_app内部因为导入语句依赖应用上下文已经初始化。提示SQLALCHEMY_TRACK_MODIFICATIONS设置成False可以关闭 SQLAlchemy 的对象修改追踪省一点内存不是必须但建议养成习惯。3. 数据模型与登录会话ORM 设计和权限控制3.1 SQLAlchemy 模型学生表和用户表怎么建学生信息管理系统最核心的表有两张student表存学号、姓名、性别、年龄、班级、入学年份、联系方式user表存系统登录账号和密码哈希。用 Flask-SQLAlchemy 定义模型本质是写 Python 类让 ORM 自动转换成建表语句。# models.py from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime db SQLAlchemy() class Student(db.Model): __tablename__ student id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) student_no db.Column(db.String(20), uniqueTrue, nullableFalse, indexTrue) name db.Column(db.String(50), nullableFalse) gender db.Column(db.String(10), nullableFalse) age db.Column(db.Integer) class_name db.Column(db.String(50), nullableFalse) # 班级名称 enroll_year db.Column(db.String(4), nullableFalse) # 入学年份如 2023 phone db.Column(db.String(20)) created_at db.Column(db.DateTime, defaultdatetime.now) def to_dict(self): return { id: self.id, student_no: self.student_no, name: self.name, gender: self.gender, age: self.age, class_name: self.class_name, enroll_year: self.enroll_year, phone: self.phone, } class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse) password_hash db.Column(db.String(200), nullableFalse) role db.Column(db.String(10), defaultadmin) # admin / teacher def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)参数设计有几个容易漏的坑学号必须加uniqueTrue这是业务上的自然主键但为了后续关联外键方便仍然保留自增id作为主键indexTrue加在学号上列表页按学号搜索时走索引虽然 SQLite 数据量小感觉不出来但这是数据库设计的正确习惯created_at设置默认值为当前时间新增记录时不用手动赋值。3.2 密码为什么要哈希存储这是答辩高频问题。明文密码一旦数据库泄露所有账号直接暴露。哈希是单向不可逆的即使数据库被拖走攻击者也拿不到原始密码。Werkzeug 的generate_password_hash默认使用 pbkdf2 算法并自动加盐每次生成的哈希值都不同同一密码存储结果也不一样。校验时调用check_password_hash它会从存储的哈希里取出盐和算法参数重新计算比对。# 创建管理员账号的脚本直接在 Python 交互环境执行 from models import db, User from app import create_app app create_app() with app.app_context(): ... db.create_all() ... admin User(usernameadmin) ... admin.set_password(your-password) ... db.session.add(admin) ... db.session.commit()再补充一个对比表说明不同存储方式的差异这个在写项目文档时也用得上存储方式安全性实现复杂度推荐场景明文存储极差最低不存在任何时候都别用MD5 直接散列差可查彩虹表低早期遗留系统MD5 固定盐中等盐泄露后可预计算较低不推荐新项目使用pbkdf2 / bcrypt 加盐哈希高自带随机盐低Werkzeug 内置本项目使用3.3 Session 登录与会话保持Flask 的 Session 基于 cookies 实现服务端把会话数据签名后发给浏览器浏览器下次请求时带上这个 cookieFlask 用SECRET_KEY验签防止数据被篡改。登录成功后只需要往session字典里写入用户标识后续请求就能通过session.get(user_id)判断登录状态。# views/auth.py from flask import Blueprint, render_template, request, session, redirect, url_for, flash from models import db, User auth_bp Blueprint(auth, __name__, url_prefix/auth) auth_bp.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form.get(username, ).strip() password request.form.get(password, ) user User.query.filter_by(usernameusername).first() if user and user.check_password(password): session[user_id] user.id session[username] user.username session[role] user.role return redirect(url_for(student.student_list)) else: flash(用户名或密码错误) return render_template(login.html) auth_bp.route(/logout) def logout(): session.clear() return redirect(url_for(auth.login))关键点说明query.filter_by(usernameusername).first()是精确匹配查询如果用户不存在则返回None所以后面用if user and user.check_password(password)先判断用户存在再校验密码避免NoneType报错。写登录逻辑时很多人习惯先查用户再查密码分两步其实合并到一个 if 里更简洁。登出用session.clear()清空全部会话数据比逐项 delete 更彻底。3.4 用装饰器统一做权限校验每个需要登录才能访问的路由都写一遍if user_id not in session: return redirect(...)太啰嗦。写一个装饰器Python 的语法糖机制让它可以在视图函数执行前拦截请求。# utils/decorators.py from functools import wraps from flask import session, redirect, url_for def login_required(view_func): wraps(view_func) def wrapped(*args, **kwargs): if user_id not in session: return redirect(url_for(auth.login)) return view_func(*args, **kwargs) return wrapped def role_required(role): 带参数的装饰器用于区分管理员和普通教师 def decorator(view_func): wraps(view_func) def wrapped(*args, **kwargs): if user_id not in session: return redirect(url_for(auth.login)) if session.get(role) ! role: return 无权限访问, 403 return view_func(*args, **kwargs) return wrapped return decorator使用时在路由函数上方加上login_required即可。role_required是带参数的装饰器写两层函数是因为装饰器本身要先接收角色参数再返回真正的装饰器函数。4. 核心业务实现学生信息增删改查与分页搜索4.1 路由设计一个列表页串起全部分支学生信息管理的核心操作都围绕「列表 → 详情 → 新增 → 编辑 → 删除」这条线展开。先定义好所有路由再逐个实现不容易漏功能。方法URL功能对应视图函数GET/student/list分页展示学生列表student_listGET/student/add跳转新增页面student_add_pagePOST/student/add提交新增表单student_addGET/student/edit/int:student_id跳转编辑页面student_edit_pagePOST/student/edit/int:student_id提交编辑表单student_editPOST/student/delete/int:student_id删除学生student_delete注意删除请求用 POST 而不是 GET。用 GET 做删除会被搜索引擎爬虫误触发也会诱导用户点击恶意链接直接删数据。页面上删除按钮放在form里method 设为 post这是一个必须养成的 Web 安全习惯。4.2 列表页分页、搜索、排序一气呵成列表页是最常被考察的功能要求至少支持分页和按关键字搜索。SQLAlchemy 提供了paginate方法直接返回分页对象不需要手动写LIMIT OFFSET。# views/student.py from flask import Blueprint, render_template, request, redirect, url_for, flash from models import db, Student from utils.decorators import login_required student_bp Blueprint(student, __name__) student_bp.route(/student/list) login_required def student_list(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) keyword request.args.get(keyword, , typestr).strip() gender request.args.get(gender, , typestr) # 先构造基础查询对象 query Student.query # 模糊搜索姓名或学号 if keyword: like_pattern f%{keyword}% query query.filter( db.or_( Student.name.like(like_pattern), Student.student_no.like(like_pattern) ) ) # 按性别筛选空字符串表示未选择 if gender in (男, 女): query query.filter(Student.gender gender) # 按入学年份倒序新生排在前面 pagination query.order_by(Student.enroll_year.desc()).paginate( pagepage, per_pageper_page, error_outFalse ) return render_template( student_list.html, studentspagination.items, paginationpagination, keywordkeyword, gendergender )参数说明写一下request.args.get(page, 1, typeint)中的typeint是 Flask 提供的参数类型转换自动把字符串2转成整数2如果传入非数字则使用默认值 1。error_outFalse表示当请求的页码超出范围时不要报 404而是渲染一个空列表——这种处理更友好页面底部显示「共 0 页」。4.3 新增和编辑表单校验与数据回填新增和编辑的表单字段相同区别在于编辑时要先查出已有记录并把数据填回页面。写两个路由但可以共用一个模板student_form.html用student变量判断是新增还是编辑student_bp.route(/student/add, methods[GET, POST]) login_required def student_add(): if request.method POST: # 从表单收集数据 student Student( student_norequest.form.get(student_no, ).strip(), namerequest.form.get(name, ).strip(), genderrequest.form.get(gender, ), agerequest.form.get(age, 0, typeint), class_namerequest.form.get(class_name, ).strip(), enroll_yearrequest.form.get(enroll_year, ).strip(), phonerequest.form.get(phone, ).strip(), ) # 学号唯一性校验 exists Student.query.filter_by(student_nostudent.student_no).first() if exists: flash(学号已存在请检查后重新填写) return render_template(student_form.html, studentNone) db.session.add(student) db.session.commit() flash(新增成功) return redirect(url_for(student.student_list)) return render_template(student_form.html, studentNone) student_bp.route(/student/edit/int:student_id, methods[GET, POST]) login_required def student_edit(student_id): student db.session.get(Student, student_id) if not student: flash(记录不存在或已删除) return redirect(url_for(student.student_list)) if request.method POST: student.student_no request.form.get(student_no, ).strip() student.name request.form.get(name, ).strip() student.gender request.form.get(gender, ) student.age request.form.get(age, 0, typeint) student.class_name request.form.get(class_name, ).strip() student.enroll_year request.form.get(enroll_year, ).strip() student.phone request.form.get(phone, ).strip() db.session.commit() flash(修改成功) return redirect(url_for(student.student_list)) return render_template(student_form.html, studentstudent)编辑路由中一个隐藏细节db.session.get(Student, student_id)是 SQLAlchemy 2.0 推荐的按主键查询方法替代了旧版的Student.query.get()。如果记录不存在返回None必须先判断再继续否则下面的student.name ...会抛AttributeError。4.4 模板中的表单处理与 CSRF 预防模板里用一个隐藏变量区分新增和编辑模式同时给表单加 CSRF 令牌。Flask-WTF 是完整的表单处理方案但毕设项目里手动实现更轻量# utils/csrf.py import secrets from flask import session def generate_csrf_token(): if _csrf_token not in session: session[_csrf_token] secrets.token_hex(16) return session[_csrf_token] def validate_csrf_token(token): stored session.get(_csrf_token, ) return token and secrets.compare_digest(stored, token)在表单中加上input typehidden namecsrf_token value{{ csrf_token() }}视图函数里在 POST 分支开头检查令牌。CSRF 攻击原理是攻击者诱导已登录用户向你的站点提交恶意表单由于浏览器会自动携带目标站点的 cookie服务端无法区分请求是否是用户本意。加了随机令牌后攻击者无法预知令牌值请求直接被拦截。5. 部署上线与答辩检查点让项目从「能跑」到「能讲」5.1 用 gunicorn 代替 Flask 开发服务器Flask 自带的开发服务器是单进程、单线程的只适合调试。真正部署时要换用 gunicornLinux/macOS 环境Windows 可用 waitress 替代它本身就是 WSGI 服务器能处理多进程并发配合 Nginx 做反向代理和静态文件服务。# 安装依赖 pip install gunicorn # 启动命令4 个 worker 进程绑定 8000 端口 gunicorn -w 4 -b 0.0.0.0:8000 app:create_app()命令含义-w 4表示开启 4 个 worker 进程每个进程都是独立的 Python 解释器能利用多核 CPU-b 0.0.0.0:8000绑定所有网卡的 8000 端口app:create_app()指在 app 模块中调用create_app()工厂函数得到应用实例。如果遇到旧版 Flask 不能直接用工厂函数的场景也可以先from app import app直接导入实例。生产环境下必须关闭调试模式debugTrue会带来两个问题一是调试器允许在浏览器端执行任意 Python 代码这是灾难级漏洞二是调试模式下 Flask 会额外加载调试器组件拖慢响应速度。同时SECRET_KEY一定不要写在代码里建议通过环境变量传入import os app.config[SECRET_KEY] os.environ.get(SECRET_KEY) or fallback-dev-key5.2 Nginx 反向代理静态文件交给 Nginx启动 gunicorn 后还缺一块拼图。gunicorn 对静态文件CSS、JS、图片的处理能力较弱因为它要经过 WSGI 层逐字节转发。正确做法是让 Nginx 直接服务/static/路径的文件其他动态请求反向代理到 gunicorn。server { listen 80; server_name your-domain.com; # 静态文件直接由 Nginx 处理 location /static/ { alias /path/to/student_system/static/; expires 7d; } # 动态请求转发给 gunicorn location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }proxy_set_header X-Real-IP $remote_addr这行不能省。不设置的话Flask 里request.remote_addr拿到的都是 Nginx 所在机器的 IP 地址而不是真实访问者的 IP。项目文档里写「支持记录操作日志」时如果没做这个配置日志里的 IP 全一样答辩时被问到会很尴尬。5.3 requirements.txt 锁版本换机器能直接复现毕设答辩经常换机器演示老师可能让你在他电脑上跑一遍。如果项目依赖的包版本没固定现场装出来版本不一致代码跑挂的概率极高。生成锁定版本清单pip freeze requirements.txtrequirements.txt内容类似于Flask2.3.3 Flask-SQLAlchemy3.0.5 gunicorn21.2.0 Werkzeug2.3.7换机器后执行pip install -r requirements.txt即可复现环境。注意只是pip freeze会把当前环境所有包都写进去建议在虚拟环境里操作避免无关包混入。如果项目里用了 Flask 2.3 和 Werkzeug 2.3 这对版本组合特别留意 Werkzeug 的版本Flask 2.3 要求 Werkzeug2.3.0装错版本会出现ImportError。5.4 答辩前必查的三个隐藏考点第一个考点是「请求生命周期」。面试官或老师会问一次完整的请求经过了哪些处理标准答案是浏览器发 HTTP 请求 → Nginx 接收并转发 → gunicorn 启动 WSGI 服务 → Flask 根据路由找到视图函数 → 视图函数操作 ORM → SQLAlchemy 执行 SQL → 返回结果 → Jinja2 渲染模板 → Nginx 返回 HTML。这条链路里每一环都能展开说是你项目里最「深」的理解。第二个考点是「数据库设计为什么这么建」。学号字段即使设置了uniqueTrue也没有把它设为主键原因是后续如果要做选课、成绩等关联表外键引用自增主键比引用学号更稳定。学号是业务标识主键是物理标识两者分离是规范做法。项目说明文档里若能写出这句话和只会复制代码的同学立刻拉开差距。第三个考点是「登录会话过期处理」。Session 的默认过期时间是浏览器关闭就失效如果要求更长的保持时间要在PERMANENT_SESSION_LIFETIME配置项里设置。手动实现一个「记住我」功能登录时如果勾选把 session 设置为永久会话有效期设为 7 天。可以用session.permanent True配合 Flask 的默认配置或者自定义before_request钩子检查会话时间戳。最后一个实操技巧是给上线后的系统加一个健康检查接口/health返回{status: ok}。配置 Nginx 定时轮询这个接口服务挂了自动重启或告警。这个小细节加在项目说明文档的「后期优化」小节里会让整个项目的完整度明显上一个档次。本文还有配套的精品资源点击获取
返回列表