尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

学生选课成绩分析系统实战:Flask+Vue+ECharts前后端分离开发

学生选课成绩分析系统实战:Flask+Vue+ECharts前后端分离开发 做学生选课成绩分析这个系统我踩了不少坑也攒了不少可以直接抄作业的方案。这个项目从标题看涉及 Flask、Vue、Django、PyCharm 一整套东西但本质上就是一个典型的前后端分离 Web 应用核心是做学生选课、成绩管理再把成绩数据做成可视化图表。今天我把这套系统的完整思路、关键代码、踩坑记录全部整理出来希望能帮你少走弯路。这套东西适合谁看正在做课程设计的学生、刚接触 Flask 想练手完整项目的开发者、以及想给学校或培训机构做内部成绩分析系统的人。项目不算特别复杂但麻雀虽小五脏俱全足够把 Python Web 开发、前端框架、数据可视化、数据库设计这些点全部串起来。1. 项目整体设计与技术选型思路1.1 技术栈为什么这么选两个 Python 框架怎么分工标题里同时出现了 Flask 和 Django很多人会疑惑到底用哪个。我的建议是核心业务逻辑用 Flask别把 Django 硬塞进来。Django 自带 Admin 后台、ORM、认证体系开发效率确实高但正因为内置东西太多学习成本反而高而且对学生项目来说有点“重”。Flask 轻量、灵活路由自己写ORM 用 SQLAlchemy 自己配代码结构完全可控更适合做这种需要展示完整开发过程的项目。那 Vue 和 Django 出现在标题里是什么情况我的理解是Vue 是前端框架负责页面交互和数据可视化展示Django 可以作为“备选方案”或者你在对比调研时涉及过。实际开发中我不建议一个项目里同时用 Flask 和 Django服务端架构反而变乱。1.2 整体架构按前后端分离的标准来搭这套系统的整体结构是这样的后端Flask 提供 RESTful API负责学生管理、课程管理、选课逻辑、成绩录入和成绩分析计算前端Vue 2 Element UI 搭建管理界面ECharts 渲染可视化图表数据库MySQL 存储业务数据SQLAlchemy 做 ORM开发工具PyCharm 写 Python 后端VSCode 或 WebStorm 写 Vue 前端前后端分离的好处是职责清晰后端只返回 JSON 数据前端只管页面渲染和交互。考试系统、选课系统这类项目评审老师或答辩评委往往更看重架构的规范性这算是加分项。1.3 功能模块拆解一个成绩系统要拆成几块把需求拆成模块学生模块学生信息的增删改查、导入导出课程模块课程信息管理、授课教师信息选课模块学生选课、退课、选课名单查看成绩模块成绩录入、修改、成绩单查询分析模块班级平均分、课程通过率、成绩分布、分数段统计可视化模块ECharts 柱状图、饼图、折线图展示分析结果这种拆分方式是所有管理系统的基础套路做完这个项目换任何业务场景都能套用这套结构。2. 数据模型设计与核心接口实现2.1 数据库表设计三张核心表和一张关联表数据库我用 MySQL建库语句CREATE DATABASE student_course CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;核心表设计如下学生表(student)字段类型说明idINT 主键自增学生IDstudent_noVARCHAR(20) 唯一学号nameVARCHAR(50)姓名genderVARCHAR(10)性别class_nameVARCHAR(50)班级enrollment_yearINT入学年份课程表(course)字段类型说明idINT 主键自增课程IDcourse_noVARCHAR(20) 唯一课程编号course_nameVARCHAR(100)课程名称creditFLOAT学分teacherVARCHAR(50)授课教师成绩表(score)字段类型说明idINT 主键自增成绩IDstudent_idINT 外键关联学生course_idINT 外键关联课程scoreFLOAT成绩semesterVARCHAR(20)学期这里有个关键点成绩表的设计决定后续分析的灵活度。我见过有人把成绩字段直接冗余到选课表里这样也能实现但后期做统计分析时逻辑会绕很多。单独建 score 表每个学生每门课一条记录统计分析时 group by 课程、group by 班级都很方便。2.2 Flask-SQLAlchemy 模型定义新建 models.py核心代码from flask_sqlalchemy import SQLAlchemy 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) name db.Column(db.String(50), nullableFalse) gender db.Column(db.String(10)) class_name db.Column(db.String(50)) enrollment_year db.Column(db.Integer) created_at db.Column(db.DateTime, defaultdatetime.now) scores db.relationship(Score, backrefstudent, lazydynamic) def to_dict(self): return { id: self.id, student_no: self.student_no, name: self.name, gender: self.gender, class_name: self.class_name, enrollment_year: self.enrollment_year } class Course(db.Model): __tablename__ course id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) course_no db.Column(db.String(20), uniqueTrue, nullableFalse) course_name db.Column(db.String(100), nullableFalse) credit db.Column(db.Float, default0) teacher db.Column(db.String(50)) scores db.relationship(Score, backrefcourse, lazydynamic) def to_dict(self): return { id: self.id, course_no: self.course_no, course_name: self.course_name, credit: self.credit, teacher: self.teacher } class Score(db.Model): __tablename__ score id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) 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.Float, nullableFalse) semester db.Column(db.String(20)) created_at db.Column(db.DateTime, defaultdatetime.now) def to_dict(self): return { id: self.id, student_id: self.student_id, student_name: self.student.name if self.student else , course_id: self.course_id, course_name: self.course.course_name if self.course else , score: self.score, semester: self.semester }to_dict 方法非常重要因为 Flask 的 jsonify 不能直接序列化 SQLAlchemy 的模型对象。每次写接口都要转 dict这个方法是 Python 后端开发的基础功。2.3 核心 API 接口设计在 app.py 中路由定义如下from flask import Flask, request, jsonify from models import db, Student, Course, Score app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:passwordlocalhost/student_course app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db.init_app(app) app.route(/api/students, methods[GET]) def get_students(): 获取学生列表支持按姓名和班级搜索 keyword request.args.get(keyword, ) query Student.query if keyword: query query.filter(db.or_( Student.name.like(f%{keyword}%), Student.student_no.like(f%{keyword}%) )) students query.all() return jsonify({code: 0, data: [s.to_dict() for s in students]}) app.route(/api/students, methods[POST]) def add_student(): 新增学生 data request.get_json() student Student( student_nodata.get(student_no), namedata.get(name), genderdata.get(gender, ), class_namedata.get(class_name, ), enrollment_yeardata.get(enrollment_year, 2024) ) db.session.add(student) db.session.commit() return jsonify({code: 0, data: student.to_dict()}) app.route(/api/courses, methods[GET]) def get_courses(): courses Course.query.all() return jsonify({code: 0, data: [c.to_dict() for c in courses]}) app.route(/api/scores, methods[POST]) def add_score(): 录入成绩注意检查该学生是否已选该课程 data request.get_json() student_id data.get(student_id) course_id data.get(course_id) score_value data.get(score) # 检查学生是否选了这门课 if not is_enrolled(student_id, course_id): return jsonify({code: 1, msg: 该学生未选择此课程无法录入成绩}), 400 score Score( student_idstudent_id, course_idcourse_id, scorescore_value, semesterdata.get(semester, 2024-2025-1) ) db.session.add(score) db.session.commit() return jsonify({code: 0, data: score.to_dict()})这里录入成绩时检查是否选课这个逻辑很多人会漏掉。不检查的话就会出现没选课却有成绩的数据导致后续分析结果不准。这是从业务逻辑层面就要卡住的点。3. 成绩分析算法与可视化实现3.1 成绩分析的核心维度可视化不是把成绩表搬到网页上而是通过图表直接看出问题。我做了四个核心分析维度维度一班级平均分对比。按班级分组计算每门课程的平均分用柱状图对比。这能直观看出哪个班级在某门课程上明显落后背后可能是教学问题或生源差异。维度二课程成绩分布。将一门课的成绩按优秀90-100、良好80-89、中等70-79、及格60-69、不及格0-59五个区间统计人数用饼图展示。这个分布能快速判断试卷难度是否合理优秀率和不及格率的比例是否符合预期。维度三成绩趋势分析。同一个学生或同一个班级的成绩是上升还是下降通过折线图呈现。比如某学生大一到大三的加权平均分变化可以辅助辅导员做学业预警。维度四课程通过率排名。统计每门课程的通过率用横向柱状图排序。通过率最低的课程需要重点关注可能是教学难度设置有问题。3.2 后端统计分析接口的实现以成绩分布为例Flask 端代码app.route(/api/analysis/distribution/int:course_id, methods[GET]) def get_score_distribution(course_id): 统计指定课程的成绩分布 scores Score.query.filter_by(course_idcourse_id).all() distribution { excellent: 0, # 90-100 good: 0, # 80-89 medium: 0, # 70-79 pass: 0, # 60-69 fail: 0 # 0-59 } for s in scores: if s.score 90: distribution[excellent] 1 elif s.score 80: distribution[good] 1 elif s.score 70: distribution[medium] 1 elif s.score 60: distribution[pass] 1 else: distribution[fail] 1 total len(scores) if total 0: for key in distribution: distribution[key] round(distribution[key] / total * 100, 2) course Course.query.get(course_id) return jsonify({ code: 0, data: { course_name: course.course_name if course else , distribution: distribution } })这个接口返回的是百分比不是人数。前端拿百分比直接画饼图不用重复计算前后端的职责更清晰。计算完成后要学会用 SQL 验证开发阶段我经常在 Navicat 里手动跑一遍 SQL 对比接口数据确保代码逻辑没写错SELECT SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS excellent_pct, SUM(CASE WHEN score 80 AND score 90 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS good_pct, SUM(CASE WHEN score 70 AND score 80 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS medium_pct, SUM(CASE WHEN score 60 AND score 70 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS pass_pct, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS fail_pct FROM score WHERE course_id 1;3.3 前端 Vue ECharts 可视化展示前端我用 Vue 2 Element UI ECharts。安装依赖npm install vue2 npm install element-ui npm install echartsvue.config.js 里配置开发环境代理解决跨域问题module.exports { devServer: { proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } } }在 ScoreAnalysis.vue 组件中成绩分布饼图这样实现template div classanalysis-container el-card div slotheader span课程成绩分布分析/span /div el-select v-modelselectedCourse placeholder请选择课程 changeloadDistribution el-option v-forcourse in courses :keycourse.id :labelcourse.course_name :valuecourse.id /el-option /el-select div refdistributionChart classchart/div /el-card /div /template script import * as echarts from echarts import request from /utils/request export default { name: ScoreAnalysis, data() { return { selectedCourse: null, courses: [], distributionChart: null } }, mounted() { this.loadCourses() }, methods: { loadCourses() { request.get(/api/courses).then(res { this.courses res.data }) }, loadDistribution(courseId) { request.get(/api/analysis/distribution/${courseId}).then(res { const data res.data this.renderPieChart(data.distribution) }) }, renderPieChart(distribution) { if (this.distributionChart) { this.distributionChart.dispose() } this.distributionChart echarts.init(this.$refs.distributionChart) const labels [优秀(90-100), 良好(80-89), 中等(70-79), 及格(60-69), 不及格(60)] const values [ distribution.excellent, distribution.good, distribution.medium, distribution.pass, distribution.fail ] const option { title: { text: 成绩分布饼图百分比, left: center }, tooltip: { trigger: item, formatter: {b}: {c}% }, legend: { orient: vertical, left: left }, series: [{ name: 成绩分布, type: pie, radius: 55%, center: [50%, 60%], data: labels.map((label, index) ({ name: label, value: values[index] })), emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: rgba(0, 0, 0, 0.5) } } }] } this.distributionChart.setOption(option) } }, beforeDestroy() { if (this.distributionChart) { this.distributionChart.dispose() } } } /script style scoped .chart { width: 100%; height: 400px; margin-top: 20px; } /style注意 beforeDestroy 里一定要 dispose 图表实例否则路由切换后 ECharts 实例不会自动销毁内存泄漏不说还会导致图表重绘报错警告。4. 开发环境与工具链配置4.1 PyCharm 环境搭建PyCharm 是 Python 后端最重要的 IDE安装后第一件事是配置解释器和创建虚拟环境。打开 PyCharmSettings → Project → Python Interpreter → 设置齿轮 → Add → Virtualenv Environment → New environment。虚拟环境一定要建在项目目录下这样换电脑项目迁移时能直接重建。禁用系统 Python 环境是个重要习惯。如果把第三方库装进系统 Python不同项目之间权限完全混乱A 项目要 Django 3.2B 项目要 Django 4.2就会相互冲突。虚拟环境隔离了每个项目的依赖。然后安装后端依赖pip install flask pip install flask-sqlalchemy pip install flask-cors pip install pymysql pip install pandas pip install flask-restfulflask-cors 是必须装的否则前端跨域请求会被浏览器拦截。也可以在 Flask 端这样配from flask_cors import CORS CORS(app)4.2 Vue 前端环境配置前端侧Node.js 环境我建议 LTS 版本使用 nvm 管理 Node 版本切换版本比手动安装方便得多。# 全局安装 Vue CLI npm install -g vue/cli # 创建前端项目 vue create student-analysis-web # 进入项目目录 cd student-analysis-web # 安装依赖 npm install在 PyCharm 里也可以直接打开 vue.config.js 改写端口配置后端 Flask 跑在 5000 端口前端 Vue 跑在 8080 端口通过 devServer proxy 请求后端接口去掉跨域限制。4.3 启动前后端服务的顺序整个开发环境启动流程启动 MySQL 服务确认 3306 端口正常监听用 PyCharm 打开后端项目运行 app.pyFlask 开发服务器跑在 5000 端口用终端进入前端项目目录执行 npm run serveVue 跑在 8080 端口浏览器访问 http://localhost:8080遇到启动失败不要慌多半是端口被占用。用命令行查端口netstat -ano | findstr :5000把占用进程杀掉或者改 Flask 的启动端口if __name__ __main__: app.run(debugTrue, port5001)5. 数据初始化与完整项目联调5.1 测试数据怎么造手动录不如脚本插真实开发中不可能靠页面一条条录数据我写了个 Python 脚本批量生成模拟数据这样测试可视化效果才有足够的数据样本import random from app import app from models import db, Student, Course, Score def init_data(): with app.app_context(): # 先清空旧数据 db.session.query(Score).delete() db.session.query(Student).delete() db.session.query(Course).delete() # 生成学生 classes [计算机2101, 计算机2102, 软件2101] students [] for i in range(90): s Student( student_nof2021{1000i}, namef学生{i1}, genderrandom.choice([男, 女]), class_namerandom.choice(classes), enrollment_year2021 ) db.session.add(s) students.append(s) db.session.commit() # 生成课程 courses_data [ (C001, Python程序设计, 3, 张老师), (C002, 数据结构, 4, 李老师), (C003, 数据库原理, 3.5, 王老师), (C004, 操作系统, 4, 赵老师), (C005, 计算机网络, 3, 孙老师) ] courses [] for no, name, credit, teacher in courses_data: c Course(course_nono, course_namename, creditcredit, teacherteacher) db.session.add(c) courses.append(c) db.session.commit() # 随机生成选课和成绩 for student in students: # 每个学生选修 3-5 门课 selected_courses random.sample(courses, random.randint(3, 5)) for course in selected_courses: score_value random.randint(45, 100) sc Score( student_idstudent.id, course_idcourse.id, scorescore_value, semester2023-2024-1 ) db.session.add(sc) db.session.commit() print(初始化完成) if __name__ __main__: init_data()这段脚本我故意让成绩分布在 45 到 100 之间就会有少量不及格记录饼图渲染时各个分数段都有数据展示效果好。全部 90 分以上会显得假全是高分在真实教学场景中几乎不可能。5.2 整体联调的关键检查点联调阶段我整理了一个检查清单学生列表能否正常按班级筛选姓名搜索是否准确匹配新增学生后页面列表能否立即刷新录入成绩时未选课的学生是否会被正确拦截修改成绩后分析页面的图表数据是否同步更新删除学生时该学生的选课和成绩记录是否级联处理干净最后一个删除学生的级联问题很隐蔽。我一开始删学生时报外键约束错误原因是 score 表里还有该学生的成绩记录。解决办法有两种一是在删除接口中手动先删关联成绩二是在数据库外键上设置 ON DELETE CASCADE。新手建议用第一种逻辑透明可控。删除接口改成这样app.route(/api/students/int:student_id, methods[DELETE]) def delete_student(student_id): student Student.query.get(student_id) if not student: return jsonify({code: 1, msg: 学生不存在}), 404 # 先删除关联成绩 Score.query.filter_by(student_idstudent_id).delete() db.session.delete(student) db.session.commit() return jsonify({code: 0, data: 删除成功})5.3 部署时的线程与高并发问题开发环境里 Flask 自带的开发服务器是单进程的性能有限。到了需要部署给多人用的阶段不要直接python app.py上线。生产环境推荐用 gunicornLinux 下或 waitressWindows 下作为 WSGI 服务器pip install waitress waitress-serve --host0.0.0.0 --port5000 app:app这时候就容易碰到 SQLAlchemy 的线程问题。开发模式单线程没事多线程并发时会出现“SQLite objects created in a thread can only be used in that same thread”之类的报错解决方法是给数据库连接池加上合适的配置或者确保 session 按请求创建和销毁。Flask-SQLAlchemy 本身做了请求上下文管理但多线程部署时仍建议检查app.config[SQLALCHEMY_ENGINE_OPTIONS] { pool_size: 10, pool_recycle: 3600, pool_pre_ping: True }6. 常见问题与排查技巧实录6.1 跨域请求被拦截浏览器 Console 报错怎么办现象前端页面能打开但请求后端接口时报 CORS error或者 Network 面板里请求状态是 failed。排查思路首先是确认后端有没有开启 CORS 支持。在 Flask 里最简单的做法from flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})其次是检查前端代理是否配置正确。开发环境推荐用 devServer proxy而不是在后端开全局 CORS这样生产环境前端的 API 路径不用改。6.2 中文乱码页面显示问号或华文乱码现象MySQL 里的中文显示正常但页面显示问号或者页面正常数据库里全是问号。排查思路这是编码问题分三段查。第一数据库连接串要加 charsetapp.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:passwordlocalhost/student_course?charsetutf8mb4第二MySQL 建库时指定 charset这一步我在前面建库语句里就写好了。第三Python 文件本身要保存为 UTF-8 编码PyCharm 默认是 UTF-8但如果你用记事本改过文件头建议加# -*- coding: utf-8 -*-兜底。6.3 ECharts 图表不显示页面空白但数据有返回现象接口返回的数据正常但图表区域空白控制台没有报错。排查思路这是初始化时机问题。ECharts 初始化时必须保证 DOM 已经渲染完成如果在数据加载之前图表 DIV 还没计算好宽高init 时拿到的是 0 或 undefined图表就画不出来。解决办法有两种一是在 mounted 里用this.$nextTick等待 DOM 渲染完成this.$nextTick(() { this.distributionChart echarts.init(this.$refs.distributionChart) })二是 window 的 resize 事件触发后手动调用 chart.resize() 让图表重新计算宽高window.addEventListener(resize, () { if (this.distributionChart) { this.distributionChart.resize() } })6.4 Flask debug 模式自动重启导致的数据库连接池耗尽现象开着 debugTrue修改代码后服务自动重启跑一段时间后 MySQL 报 Too many connections。排查思路debug 模式的 reloader 会启动两个进程父进程和子进程都持有数据库连接。开发环境一般不设连接池上限但生产环境必须关掉 debug并配置 pool_size 和 pool_recycle。我在生产部署时是彻底关 debugif __name__ __main__: app.run(debugFalse, host0.0.0.0, port5000)6.5 PyCharm 终端执行 pip install 报错现象PyCharm 终端执行 pip install flask 时报 Microsoft Visual C 14.0 is required或者 pip 找不到。这个报错多见于 Windows 环境某些 Python 包需要 C 编译环境比如 mysqlclient、bcrypt 这类依赖。解决办法有两个一是换成纯 Python 实现的替代包比如 PyMySQL 替代 MySQLdb二是安装 Microsoft C Build Tools。对课程设计项目直接用 PyMySQL 是最省事的驱动类型在连接串里指定 pymysql 就能避开编译问题。6.6 前端调用接口一直是 404现象后端接口用 Postman 测没问题前端页面上请求就变 404。排查思路检查 vue.config.js 的 proxy 是否配对了 prefix。比如后端接口是/api/students前端请求如果写成了/students代理就匹配不上会直接把请求发给 Vue dev server 自己自然 404。解决方法是保持前后端路径一致统一从/api开头。7. 项目扩展方向从课程设计到简历亮点这个系统做完如果你想让它在面试或答辩中更有亮点我建议往几个方向扩展方向一引入数据分析深化场景。现在只是最基础的统计汇总可以用 pandas 和 scikit-learn 做更深入的学业预警分析比如根据历次成绩预测学生期末是否可能挂科、哪些因素对成绩影响最大。这一步能把项目从“管理系统”拔高到“分析系统”。方向二权限体系升级。目前前端只是简单页面展示可以接入 Flask-Login 做登录认证把学生、教师、管理员三个角色分开。学生只能看自己的成绩和选课情况教师能录入和查看所授课程成绩管理员有全部权限。这个工作量和面试加分都很可观。方向三导出报表功能。用 Flask 生成 Excel 报表是比较高频的需求可以前端下载 xlsx 文件或者后端生成 CSV 供批量导入导出。我的实践是用 pandas 的 to_excel 生成 xlsx和 openpyxl 一起配合。方向四性能与部署优化。用 Nginx 反向代理前端静态文件后端用 gunicorn 或 waitress 多 worker 跑再把 Flask 的 debug 彻底关掉配上 HTTPS。这套部署方案很多实际工作的初级开发都不一定全程做过。做这个项目过程中我最大的体会是一个看起来“只是增删改查”的管理系统真正把它拆开揉碎去实现涉及到的知识点远比表面上多。数据库设计要提前考虑统计分析的需求接口粒度要兼顾前端展示可视化不是画个饼图那么简单得结合业务解释数据意义。这套系统的所有源码和表结构我都已经跑通验证过可以直接作为课程设计或毕业设计的参考也可以在此基础上加功能、换技术栈扩展成更适合你自己业务的版本。最后分享一个实操小技巧在开发阶段把 Flask 的 JSON 返回格式统一成{code: 0, data: ..., msg: ...}前端封一个 request 工具类所有接口统一处理这个格式后续加功能、排错都会很爽。这个习惯我一直沿用到现在比每个接口各写各的返回结构省心太多了。
返回列表