
简介这是一套基于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激活虚拟环境Windowsvenv\Scripts\activatemacOS / Linuxsource venv/bin/activate然后安装 Flaskpip install flask装完可以验证一下python -c import flask; print(flask.__version__)能输出版本号就说明环境没问题。项目里我一般还会把依赖导出到一个 requirements.txt 文件里方便换机器复现环境pip freeze requirements.txt2.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.htmltemplates 目录存 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, studentsrows) 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, studentsstudents, coursescourses)这里我做了两层校验前端 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 namestudent_id required option value请选择学生/option {% for student in students %} option value{{ student.id }}{{ student.student_no }} - {{ student.name }}/option {% endfor %} /select同时给 form 加上required和min/max只是前端体验后端校验才是安全底线。我建议成绩录入用input typenumber namescore min0 max100 step0.5 required这样用户大多数误操作能被浏览器直接拦掉。3.3 Jinja2 模板与 CSS 美化我用了模板继承来统一页面风格。先写一个 base.html把导航栏、提示消息区域和内容块都定义好!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title学生成绩管理系统/title link relstylesheet href{{ url_for(static, filenamestyle.css) }} /head body nav classnavbar 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 classcontainer {% with messages get_flashed_messages() %} {% if messages %} div classflash {% 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(debugTrue)在虚拟环境中运行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, statsrows)这里 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(port5001)。或者先用命令找到占用端口的进程结束掉再重新启动。最简单粗暴的方案就是 debug 模式下 CtrlC 两次强制退出然后再启动。5.2 表单提交后 400 或 500 错误我在写成绩录入时遇到过一次提交后直接 500。打开终端看报错提示某个字段是 None。排查后发现模板里两个下拉框 name 属性写重了课程下拉框也写了namestudent_id导致后端request.form.get(course_id)拿到的始终是 None。所以遇到 500 不要慌把 debugTrue 打开Flask 会直接在浏览器和终端里打印堆栈信息大多数错误都能立刻定位。如果看到 400 Bad Request多半是表单位文类型问题检查 form 和 input 的 name 是否齐全以及请求方法是否和路由里定义的一致。5.3 中文乱码问题Flask 页面中文乱码一般不是 Python 的问题而是 HTML 编码声明不一致。我在 base.html 里写了meta charsetutf-8并且所有模板文件都用 UTF-8 编码保存中文就能正常显示。如果是从数据库读出的中文乱码检查数据库连接是不是 default 编码SQLite 本身对 UTF-8 支持还行极少出现乱码。真要遇到就在 get_db() 里加一句conn.text_factory str强制以文本方式读取。5.4 debug 模式与重复初始化开发阶段 debugTrue 确实方便但线上部署一定要关掉并且把 secret_key 换成一个复杂随机字符串。我见过不少人把所有代码写完secret_key 还留着默认的dev-secret-key这在真实部署环境里很危险。课程设计演示没有太大问题但如果后续加登录功能这个 key 会被用来签名 session泄露给用户很可能导致会话伪造。5.5 依赖迁移如果要从一台机器搬到另一台机器不要手动一个一个装包。用之前生成的 requirements.txtpip 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 刷新就能看到效果不需要手动重启服务这能让调试效率提高不少。本文还有配套的精品资源点击获取