Python Flask学生成绩管理系统实战:从数据库设计到页面渲染
2026/9/20 12:08:59 网站建设 项目流程

简介:这是一套基于Python Flask + HTML + CSS的学生成绩管理录入系统源码,面向Web开发初学者与课程设计人群,演示了前后端结合及MySQL增删改查的完整流程。系统登录时通过正则校验输入,可有效防止SQL注入;功能覆盖管理员注册/修改/删除与展示、学生信息与选课记录维护、成绩录入及按姓名/学号/课程号/所属班级的多条件查询,并支持退出系统,适合作为实训项目或毕设参考。资源包共1624个文件,以Python脚本(.py)为主,另含编译缓存(.pyc)、可执行文件(.exe)、依赖库(.whl)、HTML/CSS页面及少量说明文档,压缩后大小7.1MB,便于下载部署。目前已有307人学习,目录中包含虚拟环境与配置文件,有助于读者理解Flask项目结构、MySQL操作细节及前后端联动方式,可直接运行体验或基于此二次扩展。 学生成绩管理录入系统这个题目,几乎每年都能在课程设计和毕业设计里看到。用 Python 写后端、Flask 做 Web 框架、HTML 和 CSS 画页面,听起来基础,但真正动手做的时候,隐藏问题一点不少。我这次就用这套技术栈完整做了一遍,从数据库设计到前端页面全部走通,顺便把思路、代码和踩坑记录都整理出来,希望能给正在做同样项目的同学一点实际参考。

这个项目适合谁?一是刚学完 Flask 基础、想通过完整项目练手的人;二是需要交课程设计、毕业设计的学生;三是想了解一个 Web 项目从零到能运行该怎么组织代码的入门者。核心功能不复杂:能维护学生信息、课程信息,能录入成绩、查询成绩,最后还能按课程算平均分和及格率。

1. 项目结构与整体设计

1.1 需求拆解与功能规划

做项目之前先别急着写代码,把功能列清楚,后面会省很多事。我给自己定的核心闭环是:添加学生 → 添加课程 → 录入成绩 → 查看成绩列表 → 查看课程统计。

再往下拆,就是几个页面和路由:

  • 首页:简单介绍系统入口。
  • 学生管理:学生列表、添加学生。
  • 课程管理:课程列表、添加课程。
  • 成绩录入:通过下拉框选择学生和课程,输入分数后保存。
  • 成绩列表:按录入时间倒序展示所有成绩。
  • 统计页面:按课程展示选课人数、平均分、及格率。

不需要一上来就搞登录权限、分页、导入导出,先把一个最小可用的闭环跑通,再慢慢加东西。否则很容易陷入“功能还没做完,代码已经乱成一团”的局面。

1.2 技术选型:为什么是 Flask + SQLite

很多初学者纠结该用 Django 还是 Flask。如果你只是想快速实现一个成绩管理系统,Flask 是更轻的选择。它路由简洁,配合 Jinja2 模板就能直接渲染 HTML,不需要额外引入前端框架。Django 当然强大,但对这个场景来说有点重,而且学习曲线更陡。

数据库我选了 SQLite。原因是零配置、单文件、Python 内置 sqlite3 模块就能操作,非常适合本地开发和课程设计。等以后需要多人并发访问,再换 MySQL 也行,Flask 的数据库操作层可以平滑迁移。前端的 HTML + CSS 我坚持用原生写法,不引 Bootstrap,也不上 Vue,先把页面结构、表单交互、样式基础弄明白,后面用框架才有意义。

2. 环境搭建与数据库设计

2.1 本地 Python 环境准备

如果你还没装 Python,先去官网下载 Python 3.10 或更高版本,安装时一定记得勾选Add Python to PATH,不然命令行里敲python会提示找不到命令。装完以后打开终端,创建一个独立的虚拟环境,避免依赖污染系统环境:

python -m venv venv

激活虚拟环境:

  • Windows:venv\Scripts\activate
  • macOS / Linux:source venv/bin/activate

然后安装 Flask:

pip install flask

装完可以验证一下:

python -c "import flask; print(flask.__version__)"

能输出版本号就说明环境没问题。项目里我一般还会把依赖导出到一个 requirements.txt 文件里,方便换机器复现环境:

pip freeze > requirements.txt

2.2 数据库表设计与初始化

成绩管理系统的数据关系其实很清晰:一个学生可以选多门课,一门课可以被多个学生选,成绩就是学生和课程之间的关系记录。所以我设计了三张表:student 表存学生,course 表存课程,score 表存成绩。score 表通过 student_id 和 course_id 作为外键,关联另外两张表,同时加了一个唯一约束UNIQUE(student_id, course_id),防止同一学生对同一门课程重复录成绩。

我单独写了一个 db.py 文件,把数据库连接和初始化逻辑放在里面:

import sqlite3 DB_NAME = 'school.db' def get_db(): conn = sqlite3.connect(DB_NAME) conn.row_factory = sqlite3.Row return conn def init_db(): conn = get_db() conn.executescript(''' CREATE TABLE IF NOT EXISTS student ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, class_name TEXT ); CREATE TABLE IF NOT EXISTS course ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ); CREATE TABLE IF NOT EXISTS score ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, score REAL NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id), UNIQUE(student_id, course_id) ); ''') conn.commit() conn.close()

这里有个细节:conn.row_factory = sqlite3.Row会让查询结果像字典一样支持字段名访问,比如row['name'],在模板里写起来非常方便。初始化函数是幂等的,重复执行不会报错,这点在 Flask 调试模式下很重要,后面我会再提。

2.3 项目目录结构

我习惯把项目按清晰职责分层:

score_system/ ├── app.py ├── db.py ├── school.db ├── requirements.txt ├── static/ │ └── style.css └── templates/ ├── base.html ├── index.html ├── students.html ├── add_student.html ├── courses.html ├── add_course.html ├── add_score.html └── scores.html

templates 目录存 Jinja2 模板,static 目录存 CSS、JS、图片等静态文件。Flask 默认会自动识别这两个目录,所以不需要额外配置。保持了前后端文件分离,看代码时逻辑更清楚。

3. 核心功能实现与页面渲染

3.1 Flask 路由与视图函数

Flask 的核心是路由。我打开 app.py,把所有 URL 和视图函数的对应关系写好。先看最简单的首页和学生添加功能:

from flask import Flask, render_template, request, redirect, url_for, flash import sqlite3 from db import get_db, init_db app = Flask(__name__) app.secret_key = 'dev-secret-key' @app.route('/') def index(): return render_template('index.html') @app.route('/students') def students(): db = get_db() rows = db.execute('SELECT * FROM student ORDER BY id DESC').fetchall() return render_template('students.html', students=rows) @app.route('/students/add', methods=['GET', 'POST']) def add_student(): if request.method == 'POST': student_no = request.form.get('student_no') name = request.form.get('name') class_name = request.form.get('class_name') if not student_no or not name: flash('学号和姓名不能为空') return redirect(url_for('add_student')) try: db = get_db() db.execute( 'INSERT INTO student (student_no, name, class_name) VALUES (?, ?, ?)', (student_no, name, class_name) ) db.commit() flash('学生添加成功') return redirect(url_for('students')) except sqlite3.IntegrityError: flash('学号已存在') return redirect(url_for('add_student')) return render_template('add_student.html')

注意这里插入数据时用了?占位符,而不是把变量直接拼进 SQL 字符串。这是防止 SQL 注入的基本操作,千万别省。

成绩录入的视图函数稍微复杂一点,因为需要先加载全部学生和课程,用来渲染两个下拉框:

@app.route('/score/add', methods=['GET', 'POST']) def add_score(): db = get_db() students = db.execute('SELECT id, name, student_no FROM student ORDER BY id DESC').fetchall() courses = db.execute('SELECT id, name FROM course ORDER BY id DESC').fetchall() if request.method == 'POST': student_id = request.form.get('student_id') course_id = request.form.get('course_id') score = request.form.get('score') error = None if not student_id or not course_id or not score: error = '请完整选择学生、课程并填写成绩' else: try: score = float(score) if score < 0 or score > 100: error = '成绩必须在0到100之间' except ValueError: error = '成绩必须是数字' if error: flash(error) return redirect(url_for('add_score')) try: db.execute( 'INSERT INTO score (student_id, course_id, score) VALUES (?, ?, ?)', (student_id, course_id, score) ) db.commit() flash('成绩录入成功') return redirect(url_for('scores')) except sqlite3.IntegrityError: flash('该学生此课程已经录过成绩,请勿重复录入') return render_template('add_score.html', students=students, courses=courses)

这里我做了两层校验:前端 HTML 负责提示,后端视图函数负责真正拦截。特别是成绩范围,服务端一定要再做一次,因为浏览器上的校验可以被绕过。

3.2 表单处理与数据校验

成绩录入页面在 templates/add_score.html 里,核心是一个 POST 表单。页面上两个下拉框分别用request.form.get('student_id')request.form.get('course_id')来接收,名字必须和模板里 select 标签的 name 属性一致,否则后台收不到值。

一个很常见的坑是:只给 input 写了 name,忘记给 select 写 name,结果提交后 area 拿到的全是 None。所以在表单里,学生和课程下拉框一定要写:

<select name="student_id" required> <option value="">请选择学生</option> {% for student in students %} <option value="{{ student.id }}">{{ student.student_no }} - {{ student.name }}</option> {% endfor %} </select>

同时给 form 加上requiredmin/max只是前端体验,后端校验才是安全底线。我建议成绩录入用<input type="number" name="score" min="0" max="100" step="0.5" required>,这样用户大多数误操作能被浏览器直接拦掉。

3.3 Jinja2 模板与 CSS 美化

我用了模板继承来统一页面风格。先写一个 base.html,把导航栏、提示消息区域和内容块都定义好:

<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>学生成绩管理系统</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <nav class="navbar"> <a href="{{ url_for('index') }}">首页</a> <a href="{{ url_for('students') }}">学生管理</a> <a href="{{ url_for('courses') }}">课程管理</a> <a href="{{ url_for('add_score') }}">成绩录入</a> <a href="{{ url_for('scores') }}">成绩查询</a> <a href="{{ url_for('stats') }}">统计</a> </nav> <main class="container"> {% with messages = get_flashed_messages() %} {% if messages %} <div class="flash"> {% for message in messages %} <p>{{ message }}</p> {% endfor %} </div> {% endif %} {% endwith %} {% block content %}{% endblock %} </main> </body> </html>

然后每个子页面只需要重写 content 块。CSS 方面不用写太多,把握几个关键点就行:导航栏用 flex 布局横向排开,内容区用 max-width 居中,表格做条纹背景,按钮给 hover 效果。我贴几个核心样式片段:

* { box-sizing: border-box; } body { margin: 0; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; background: #f5f7fa; color: #333; } .navbar { display: flex; gap: 20px; background: #2c3e50; padding: 14px 30px; } .navbar a { color: #fff; text-decoration: none; padding: 6px 12px; border-radius: 4px; } .navbar a:hover { background: #34495e; } .container { max-width: 960px; margin: 30px auto; background: #fff; padding: 24px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } table { width: 100%; border-collapse: collapse; margin-top: 16px; } th, td { padding: 10px 12px; border-bottom: 1px solid #e8e8e8; text-align: left; } tr:nth-child(even) { background: #fafbfc; } .btn { display: inline-block; background: #3498db; color: #fff; padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; text-decoration: none; } .btn:hover { background: #2980b9; }

其实整套页面并不需要多惊艳,能清晰、整齐地把信息展示出来就够了。对于课程设计来说,这种干净的风格反而更受欢迎。

4. 实操过程与完整流程

4.1 初始化数据库与启动服务

我在 app.py 最后加上启动代码:

if __name__ == '__main__': init_db() app.run(debug=True)

在虚拟环境中运行:

python app.py

终端会显示:

* Running on http://127.0.0.1:5000

说明服务已经启动。打开浏览器访问 http://127.0.0.1:5000 ,就能看到系统首页。第一次打开时,因为还没有初始化数据,学生、课程、成绩列表都是空的,这很正常。

这里要提醒一下:如果你把 init_db() 放在 app.run() 前面,debug 模式下 Flask 的自动重载会让 init_db() 执行两次。由于我的建表语句都用了IF NOT EXISTS,所以不会报错。但如果你在初始化里插入测试数据,就可能会看到重复数据。解决方法是给插入语句加一个“先查再插”的判断,或者只在手动执行初始化脚本时插入测试数据。

4.2 录入一名学生的完整操作

我实际走一遍流程来验证功能。先打开“学生管理”页面,点击“添加学生”,填写:

  • 学号:2024001
  • 姓名:张三
  • 班级:计科2401

提交后页面自动跳到学生列表,可以看到这一条记录已经出现。然后进入“课程管理”,添加一门“Python程序设计”。最后进入“成绩录入”,在学生下拉框里选择“2024001 - 张三”,课程下拉框选择“Python程序设计”,成绩填 92.5,点击提交。

如果一切正常,flash 提示会显示“成绩录入成功”,随后跳转到成绩列表页,表格中会出现张三、Python程序设计、92.5 这条记录。

我还故意做了一次重复提交,页面提示“该学生此课程已经录过成绩,请勿重复录入”,说明 UNIQUE 约束和 IntegrityError 处理都在正常工作。

4.3 成绩统计功能如何做

统计页面我放在了 stats 路由里,核心逻辑是一条 SQL 语句,按课程分组计算人数、平均分和及格率:

@app.route('/stats') def stats(): db = get_db() rows = db.execute(''' SELECT c.name AS course_name, COUNT(s.id) AS student_count, ROUND(AVG(sc.score), 2) AS avg_score, ROUND( SUM(CASE WHEN sc.score >= 60 THEN 1 ELSE 0 END) * 1.0 / COUNT(s.id) * 100, 1 ) AS pass_rate FROM score sc JOIN course c ON sc.course_id = c.id JOIN student s ON sc.student_id = s.id GROUP BY c.name ORDER BY c.name ''').fetchall() return render_template('stats.html', stats=rows)

这里 SQL 里的CASE WHEN用来统计及格人数,乘以 1.0 是为了让除法结果是小数而不是整数,最后ROUND控制小数位数。这个统计口径很实用,也能体现对 SQL 聚合函数的理解。

5. 常见问题与排查技巧

5.1 端口被占用怎么办

有几次我启动 Flask 时终端报错:

OSError: [Errno 98] Address already in use

这是因为上一次运行没有正常关闭,5000 端口还被占着。解决办法是换一个端口启动:

python app.py --port 5001?

准确说,Flask 命令行不支持直接这样传端口,要改成 app.run(port=5001)。或者先用命令找到占用端口的进程,结束掉再重新启动。最简单粗暴的方案就是 debug 模式下 Ctrl+C 两次强制退出,然后再启动。

5.2 表单提交后 400 或 500 错误

我在写成绩录入时遇到过一次提交后直接 500。打开终端看报错,提示某个字段是 None。排查后发现模板里两个下拉框 name 属性写重了,课程下拉框也写了name="student_id",导致后端request.form.get('course_id')拿到的始终是 None。

所以遇到 500 不要慌,把 debug=True 打开,Flask 会直接在浏览器和终端里打印堆栈信息,大多数错误都能立刻定位。如果看到 400 Bad Request,多半是表单位文类型问题,检查 form 和 input 的 name 是否齐全,以及请求方法是否和路由里定义的一致。

5.3 中文乱码问题

Flask 页面中文乱码一般不是 Python 的问题,而是 HTML 编码声明不一致。我在 base.html 里写了<meta charset="utf-8">,并且所有模板文件都用 UTF-8 编码保存,中文就能正常显示。如果是从数据库读出的中文乱码,检查数据库连接是不是 default 编码,SQLite 本身对 UTF-8 支持还行,极少出现乱码。真要遇到,就在 get_db() 里加一句conn.text_factory = str,强制以文本方式读取。

5.4 debug 模式与重复初始化

开发阶段 debug=True 确实方便,但线上部署一定要关掉,并且把 secret_key 换成一个复杂随机字符串。我见过不少人把所有代码写完,secret_key 还留着默认的'dev-secret-key',这在真实部署环境里很危险。课程设计演示没有太大问题,但如果后续加登录功能,这个 key 会被用来签名 session,泄露给用户很可能导致会话伪造。

5.5 依赖迁移

如果要从一台机器搬到另一台机器,不要手动一个一个装包。用之前生成的 requirements.txt:

pip install -r requirements.txt

这样包括 Flask 在内的所有依赖版本都能一键装好,学校机房部署也不用怕环境不一致。

6. 后续扩展与个人体会

6.1 可以怎么继续扩展

这个项目跑通之后,再往上加功能就顺理成章了。建议优先做这几项:

  • 登录功能:用 Flask 的 session 实现管理员登录,普通用户只能查成绩,管理员才能录入修改。
  • 分页和搜索:学生和成绩列表一多,就需要按姓名或学号搜索,列表加分页。
  • Excel 导入导出:用 openpyxl 把成绩表导出成 Excel,或者批量导入学生名单,教室管理场景特别刚需。
  • 图表展示:在统计页面接一个 ECharts,把平均分、及格率用柱状图和饼图画出来,视觉冲击立刻提升一个档次。

这些功能不会改变原有核心结构,只是在现有路由和模板上做增量开发。这也是我为什么一开始强调不要把前端框架塞进来的原因,原生结构在做这些扩展时反而更清爽。

6.2 我在实操中的几点真实体会

整套做下来,我最大的感触是:Web 项目不要停留在看教程,一定要自己敲一遍。Flask 的代码量不大,真正的门槛在“路由-模板-数据库”这三者之间的数据流动上。我一开始总搞不清request.form拿到的值该往哪里传,后来把每个视图函数的输入输出都打印一遍,思路就清楚了。

另外,CSS 千万别一上来就追求炫酷。先把布局对齐、间距统一、颜色克制这三件事做好,页面就不会丑。最后再分享一个小技巧:修改模板后,只要 app.py 还在 debug 模式,浏览器按 F5 刷新就能看到效果,不需要手动重启服务,这能让调试效率提高不少。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询