ARTICLE DETAIL

资讯详情

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

Flask + Vue3 实战:从零开发学生成绩管理系统

Flask + Vue3 实战:从零开发学生成绩管理系统 最近帮朋友做了一个内部练手用的学生成绩管理系统技术栈选的是大家比较熟的 Python Flask Vue3。这个组合在中小型管理系统里其实很能打Flask 轻量、上手快、周边资料多Vue3 配合 Vite 开发体验也很顺畅拿来当学生成绩管理这种典型 CRUD 项目再合适不过。这篇文章我不会去复述一遍官方文档就把实际开发过程中踩过的坑、做过的关键决策、以及如何把整个系统从零搭起来并能本地跑通的过程完整写一遍。如果你正想用 Flask Vue3 做一套后台管理系统或者拿“学生成绩管理”当毕设、练习项目那这篇文章值得你耐心看完。先说清楚这套系统解决的几个核心问题学生信息维护、课程管理、成绩录入与查询以及基于成绩数据的统计分析。细拆下来它就是一个典型的前后端分离项目后端暴露 RESTful API前端通过 Vue3 组件化开发实现页面交互。难点其实不在技术本身而在于数据模型怎么设计、权限怎么划分、成绩录入体验怎么做顺手、以及前后端联调时那些隐蔽的坑怎么绕过去。下面我按实际开发顺序展开。1. 整体设计与技术选型为什么最终选了 Flask Vue31.1 技术栈选型背后的权衡有不少人纠结学生成绩管理系统到底选 Django 还是 Flask选 Vue2 还是 Vue3我这里把自己的理由讲清楚。Flask 的优势是“轻”和“透”。一个学生成绩管理系统本质上就是对几张表的增删改查加统计用 Django 自带 Admin、ORM、Middleware 这些重家伙当然也行但很多功能我们用不上反而增加了框架本身的学习成本。Flask 的核心只有一个 WSGI 应用路由、请求、响应都好理解你写几行代码就能跑起一个接口特别适合想把原理吃透的人。再说前端。Vue3 在 Vue2 基础上最大的变化是组合式 APIComposition API。成绩管理系统这种页面逻辑往往分散在多个组件里比如成绩录入页涉及学生下拉选择、成绩表单、课程筛选如果用 Vue2 的 Options API逻辑会分散在 data、methods、watch 里改起来很累。而组合式 API 可以把同一业务逻辑集中到一个 composable 函数里用在成绩管理这种场景非常顺手。此外Vue3 配套的 Vite 开发服务器启动速度极快几个大依赖一上热更新几乎是秒级这在调样式、调接口联调时体验提升非常明显。1.2 项目整体功能拆解在动笔写代码前我习惯先把系统模块画一遍不是画架构图给领导看而是自己梳理清楚再动手。这套学生成绩管理系统我拆成了这几个模块用户认证模块登录、登出、会话保持。教师和学生角色分开教师可以录入和管理成绩学生只能查看自己的成绩。学生管理模块学生的基本信息学号、姓名、班级、性别、入学年份支持新增、编辑、删除、按学号/姓名关键词搜索。课程管理模块课程名称、课程代码、任课教师、学分。课程是成绩表的关联主体之一。成绩管理模块选中一个学生、选择一门课程录入成绩。支持批量修改、按课程或班级筛选。统计与分析模块单门课程的最高分、最低分、平均分、及格率以及班级成绩分布的柱状图/饼图。前端路由与权限控制不同角色登录后看到不同的菜单和页面。这个功能划分并不新鲜但难在细节。比如成绩录入时怎么避免重复录入同一学生同一门课的成绩删除学生时成绩记录要不要级联删除统计接口是一次性返回全部数据还是按课程分页查询。这些都会直接影响后面的数据库设计和接口定义。2. 后端设计Flask API 与数据模型的核心细节2.1 数据库模型设计几张表之间怎么关联数据库我选了 SQLite。这不是为了演示而是这套系统定位在本地可部署、零配置即用SQLite 不需要额外装服务文件即库对练手和教育场景太友好了。后期想换 MySQL 或 PostgreSQLSQLAlchemy 的模型层改动也很小。下面是核心模型设计。from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) role db.Column(db.String(10), nullableFalse, defaultstudent) # teacher / student student_id db.Column(db.Integer, db.ForeignKey(students.id), nullableTrue) created_at db.Column(db.DateTime, defaultdatetime.now) class Student(db.Model): __tablename__ students id db.Column(db.Integer, primary_keyTrue) student_no db.Column(db.String(20), uniqueTrue, nullableFalse) name db.Column(db.String(50), nullableFalse) gender db.Column(db.String(4)) grade db.Column(db.String(20)) # 例如 2023级 class_name db.Column(db.String(50)) enroll_year db.Column(db.String(10)) scores db.relationship(Score, backrefstudent, lazyTrue, cascadeall, delete-orphan) class Course(db.Model): __tablename__ courses id db.Column(db.Integer, primary_keyTrue) course_code db.Column(db.String(20), uniqueTrue, nullableFalse) course_name db.Column(db.String(100), nullableFalse) teacher db.Column(db.String(50)) credit db.Column(db.Float, default2.0) class Score(db.Model): __tablename__ scores id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(students.id), nullableFalse) course_id db.Column(db.Integer, db.ForeignKey(courses.id), nullableFalse) score db.Column(db.Float, nullableFalse) exam_date db.Column(db.Date) is_pass db.Column(db.Boolean, defaultTrue) __table_args__ (db.UniqueConstraint(student_id, course_id, nameuniq_student_course),)这里有几个容易踩坑的细节我单独说。第一scores关系里的cascadeall, delete-orphan是必须的否则删除学生时成绩表会残留脏数据。但要注意如果你后面加了“还原删除”功能这种物理删除就要改成逻辑删除加一个is_deleted字段级联关系也要换。第二UniqueConstraint(student_id, course_id)非常重要。没有这个约束同一个学生同一门课就可能录入多条成绩后面前端统计平均分的时候会出现数据翻倍的问题。我一开始没加测试时不小心录了两遍统计出来平均分低得离谱排查了半天才发现是数据重复。第三用户表里的role字段我建议用字符串而不是布尔值后面加角色比如管理员不用改表结构。password_hash存的是哈希值千万不要明文存密码。登录接口用werkzeug.security的generate_password_hash和check_password_hash就行。2.2 接口设计RESTful 风格怎么落到成绩系统上后端接口我按 RESTful 风格来。先说路径设计这地方很有文章。POST /api/auth/login 登录 GET /api/students 学生列表支持 name、class_name 模糊查询与分页 POST /api/students 新增学生 PUT /api/students/id 编辑学生 DELETE /api/students/id 删除学生 GET /api/courses 课程列表 POST /api/courses 新增课程 PUT /api/courses/id 编辑课程 DELETE /api/courses/id 删除课程 GET /api/scores 成绩列表支持 student_id、course_id、min_score 等筛选 POST /api/scores 录入成绩 PUT /api/scores/id 修改成绩 DELETE /api/scores/id 删除成绩 GET /api/stats/course/course_id 课程统计 GET /api/stats/class/class_name 班级统计这里我特别强调一下成绩列表接口它是整个系统的核心查询接口。最好别一股脑返回全部数据再让前端过滤而应该把筛选参数放在查询字符串里后端通过 SQLAlchemy 动态组装查询条件。from flask import request from sqlalchemy import or_, func app.get(/api/scores) def list_scores(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) course_id request.args.get(course_id, typeint) student_name request.args.get(student_name, ).strip() query Score.query.join(Student, Score.student_id Student.id) if course_id: query query.filter(Score.course_id course_id) if student_name: query query.filter(Student.name.like(f%{student_name}%)) if request.args.get(min_score): query query.filter(Score.score float(request.args[min_score])) pagination query.order_by(Score.score.desc()).paginate(pagepage, per_pageper_page, error_outFalse) return { items: [serialize_score(s) for s in pagination.items], total: pagination.total, page: page, per_page: per_page, }用动态查询而不是写死条件的原因很实际前端筛选条件是变化的学生可能选择只看某门课、只看不及格的学生、只看某个姓名的学生。如果每个组合写一个接口那接口数量会爆炸维护成本也高。动态组装查询条件一个接口就全搞定了。2.3 权限控制教师和学生看到的页面为什么不同权限控制我用了最简单也最稳妥的方案登录时把用户角色信息返回给前端前端根据角色控制路由和按钮显隐。但这只是“体验层面”的权限控制后端接口依然要做校验。from functools import wraps from flask import request, jsonify import jwt from datetime import datetime, timedelta SECRET_KEY your-secret-key def create_token(user): payload { user_id: user.id, role: user.role, exp: datetime.utcnow() timedelta(hours24) } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def login_required(f): wraps(f) def wrapper(*args, **kwargs): auth request.headers.get(Authorization, ) if not auth.startswith(Bearer ): return jsonify({msg: 未登录或登录已过期}), 401 try: payload jwt.decode(auth[7:], SECRET_KEY, algorithms[HS256]) request.user payload except jwt.ExpiredSignatureError: return jsonify({msg: 登录已过期}), 401 except jwt.InvalidTokenError: return jsonify({msg: 无效令牌}), 401 return f(*args, **kwargs) return wrapper def teacher_required(f): wraps(f) login_required def wrapper(*args, **kwargs): if request.user.get(role) ! teacher: return jsonify({msg: 无权限}), 403 return f(*args, **kwargs) return wrapper这里用 JWTJSON Web Token做无状态会话不需要服务端额外存储 session对 Flask 这种轻量后端挺合适。要注意一点前端保存 token 时不要存进 localStorage 然后允许 XSS 读取到——理想做法是放内存里配合刷新令牌机制。但练手项目如果嫌复杂把 token 存 localStorage 也凑合不过要留意这个隐患。关键校验逻辑有三处新增/修改/删除成绩的接口必须teacher_required学生查询的接口login_required即可而学生查看成绩时后端要做数据隔离。app.get(/api/my-scores) login_required def my_scores(): if request.user.get(role) student: user User.query.get(request.user[user_id]) if not user or not user.student_id: return jsonify({msg: 未绑定学生信息}), 400 student_id user.student_id else: student_id request.args.get(student_id, typeint) # 查询并返回成绩这个隔离非常重要。学生登录后如果调用/api/scores且只传student_id就能看到别人的成绩那就是严重的数据安全问题。实测中我发现很多新手容易漏掉这一点前端看不显示不等于接口没泄漏。3. 前端工程化Vue3 Vite Pinia 的组合实践3.1 从 Vite 脚手架开始目录结构怎么规划前端我用 Vite 初始化了一个 Vue3 项目命令很简单但有几个坑值得提前说。npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 pinia axios element-plus这里有两个坑。第一个npm create vite如果你少了--template vue它会进入交互式询问手一抖可能就选了 React不常操作的人容易在这里卡住。第二个Element Plus 按需引入和全量引入的选择。练手项目我建议全量引入省去配置unplugin-auto-import和unplugin-vue-components的时间。如果你要追求性能优化再改成按需引入也不迟。目录结构我用了实战中验证过不少次的结构src/ api/ auth.js student.js course.js score.js stats.js assets/ components/ ScoreTable.vue StudentForm.vue StatChart.vue router/ index.js stores/ user.js views/ LoginView.vue StudentManage.vue CourseManage.vue ScoreManage.vue StatsView.vue ProfileView.vue utils/ request.js App.vue main.js这个结构不要嫌啰嗦。api/目录单独放请求函数是为了页面组件里不直接出现axios.get这种裸调用。你想想如果项目里有 20 个页面都在直接发请求后面后端接口路径一变你得全局搜索替换而集中管理的话只需改一处。stores/管理登录用户状态router/做路由和权限守卫utils/request.js封装 axios 实例和拦截器这个套路适合绝大多数中小型后台系统。3.2 axios 封装统一处理 token 和错误提示axios 封装是整个前端项目里最重要的基础设施之一。我封装时做了四件事基础路径配置、请求头自动带 token、统一处理 HTTP 错误、响应数据直接解包。// utils/request.js import axios from axios; import { ElMessage } from element-plus; import router from /router; const request axios.create({ baseURL: /api, timeout: 10000, }); request.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); request.interceptors.response.use( (response) { return response.data; }, (error) { if (error.response) { const status error.response.status; if (status 401) { ElMessage.error(登录已过期请重新登录); localStorage.removeItem(token); router.push(/login); } else if (status 403) { ElMessage.error(没有权限执行此操作); } else { ElMessage.error(error.response.data?.msg || 请求失败); } } else { ElMessage.error(网络异常请检查后端服务是否启动); } return Promise.reject(error); } ); export default request;我特别想说一下第 401 处理那段。401 表示 token 失效此时前端应该清掉本地 token 并跳转登录页而不是让用户看到一堆报错。实际项目中我见过很多同学把 401 和 500 混在一起弹窗结果用户登录过期后疯狂点还在想“为啥我的页面一直出错误弹窗”。一道拦截器就把这件事统一解决掉了。而且我把baseURL设为/api而不是完整的http://localhost:5000/api配合 Vite 的 proxy 做代理转发这样在开发环境完全没有跨域烦恼。// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:5000, changeOrigin: true, }, }, }, });采用 proxy 而不是在 axios 里写死http://localhost:5000最大的好处是开发环境和生产环境的前端代码不用改。只要后端接口都挂在/api下生产环境里 Nginx 直接把/api转发给 Flask 就行。3.3 路由权限守卫角色不同看到的菜单不同路由配置我用vue-router4写的时候需要注意模式选择然后配合一个全局前置守卫做登录校验。// router/index.js import { createRouter, createWebHistory } from vue-router; const routes [ { path: /login, component: () import(/views/LoginView.vue) }, { path: /, component: () import(/views/HomeView.vue), redirect: /scores, children: [ { path: students, component: () import(/views/StudentManage.vue), meta: { role: teacher } }, { path: courses, component: () import(/views/CourseManage.vue), meta: { role: teacher } }, { path: scores, component: () import(/views/ScoreManage.vue) }, { path: stats, component: () import(/views/StatsView.vue) }, { path: profile, component: () import(/views/ProfileView.vue) }, ], }, ]; const router createRouter({ history: createWebHistory(), routes, }); router.beforeEach((to, from, next) { const token localStorage.getItem(token); const role localStorage.getItem(role); if (to.path ! /login !token) { return next(/login); } if (to.meta.role to.meta.role ! role) { return next(/scores); } next(); });路由里meta.role控制角色的访问范围。这里要注意这只是前端控制防止看到不该看的页面。真正的安全校验在后端接口里做前端控制更像是一种体验优化。另外提一个 Vue3 路由的坑createWebHistory模式在本地开发没问题但部署到后端服务器如果 Nginx 没配置好刷新页面会 404。新手跑通本地后改部署时经常会栽在这个 404 上。解决方案有两个用createWebHashHistory()换 hash 模式或者在 Nginx 里配置try_files $uri $uri/ /index.html;。我在下面的部署篇会细说。4. 核心功能实操成绩的录入、统计与分析怎么做4.1 成绩录入页面的体验设计成绩录入是这套系统里最核心也最容易做难用的功能。很多学生管理系统的成绩录入是“一行一行的下拉框输入框”录一节课几十个学生能点几百次鼠标体验极差。我做得稍微不同一点录成绩时按照“班级 课程”维度一次加载一个班的所有学生每个学生对应一个输入框Tab 键切到下一个回车保存。!-- ScoreManage.vue 核心部分做了简化 -- template el-form el-row :gutter16 el-col :span6 el-select v-modelselectedClass placeholder选择班级 changehandleClassChange el-option v-forc in classList :keyc :labelc :valuec / /el-select /el-col el-col :span6 el-select v-modelselectedCourse placeholder选择课程 changeloadStudentScores el-option v-forc in courseList :keyc.id :labelc.course_name :valuec.id / /el-select /el-col el-col :span6 el-button typeprimary clickbatchSave批量保存/el-button /el-col /el-row el-table :datatableData border el-table-column label学号 propstudent_no width140 / el-table-column label姓名 propname width120 / el-table-column label成绩 width180 template #default{ row } el-input-number v-modelrow.score :min0 :max100 controls-positionright changehandleScoreChange(row) / /template /el-table-column el-table-column label状态 template #default{ row } el-tag :typerow.score 60 ? success : danger {{ row.score 60 ? 及格 : 不及格 }} /el-tag /template /el-table-column /el-table /el-form /template这样做的好处是录成绩像填 Excel而不是一条一条开弹窗。这里的关键在于后端接口要支持批量创建或更新。我实现的是“批量 upsert 接口”前端把整张表格的数据一次性提交给后端后端按student_id course_id组合判断是插入还是更新。app.post(/api/scores/batch) teacher_required def batch_save_scores(): data request.get_json(forceTrue) items data.get(items, []) course_id data.get(course_id) saved 0 for item in items: student_id item.get(student_id) score_val item.get(score) existing Score.query.filter_by(student_idstudent_id, course_idcourse_id).first() if existing: existing.score score_val else: db.session.add(Score(student_idstudent_id, course_idcourse_id, scorescore_val)) saved 1 db.session.commit() return {saved: saved}这个接口配合前端表格一次提交体验比逐个 save 好太多。注意批量接口里事务的粒度所有保存都在一个事务里中途任何一条出错就整体回滚不会出现“录了一半另一半个没保存”这种数据不一致问题。4.2 成绩统计别只会算平均分统计模块是学生成绩管理系统里最有含金量的部分。除了平均分我建议至少做到以下几点单门课程的成绩分布优秀 90-100、良好 80-89、中等 70-79、及格 60-69、不及格 60班级整体及格率、优秀率、最高最低分多门课程平均分横向对比后端统计接口的核心逻辑是这样的from sqlalchemy import func, case app.get(/api/stats/course/int:course_id) login_required def course_stats(course_id): stats db.session.query( func.count(Score.id).label(total), func.avg(Score.score).label(avg_score), func.max(Score.score).label(max_score), func.min(Score.score).label(min_score), func.sum(case((Score.score 60, 1), else_0)).label(pass_count), ).filter(Score.course_id course_id).first() dist db.session.query( case( (Score.score 90, 优秀), (Score.score 80, 良好), (Score.score 70, 中等), (Score.score 60, 及格), else_不及格, ).label(level), func.count(Score.id).label(count), ).filter(Score.course_id course_id).group_by(level).all() return { total: stats.total, avg_score: round(stats.avg_score or 0, 2), max_score: stats.max_score, min_score: stats.min_score, pass_rate: round(stats.pass_count / stats.total * 100, 2) if stats.total else 0, distribution: [{level: level, count: count} for level, count in dist], }前端拿到统计结果后我用了echarts画饼图和柱状图。有人说“用 echarts 太重了项目小没必要”但分数分布饼图确实比一张表格直观太多家长查成绩、老师看班级整体情况时一眼就能看出哪个分数段的人最多。用 echarts 也就新增一个依赖成本并不高。// StatsView.vue 中的图表数据组装 const getOption (dist) ({ title: { text: 成绩分布, left: center }, tooltip: { trigger: item }, series: [{ type: pie, radius: 60%, data: dist.map(item ({ name: item.level, value: item.count })), }], });统计模块里比较容易被忽略的点是“统计口径”。比如“及格率”到底是以“参加考试人数”为分母还是“班级总人数”为分母如果有的学生缺考成绩为空这个数据会直接变化。实际做的时候我建议成绩字段加一个状态正常、缓考、缺考统计时把缺考排除在外否则平均分和及格率都会失真。4.3 成绩导出的三个坑导出功能虽然听起来像个边角料但实际使用频率很高。我看很多人直接用 Python 的csv模块输出结果打开 Excel 中文乱码。这个问题的根源是 CSV 默认用 UTF-8 编码而 Windows 上的 Excel 用 GBK 读取。解决方案是输出 CSV 时加上 UTF-8 BOMimport csv from io import StringIO from flask import Response def export_scores_csv(): result [] # ... 从数据库查询成绩数据 output StringIO() output.write(\ufeff) # UTF-8 BOM防止 Excel 中文乱码 writer csv.writer(output) writer.writerow([学号, 姓名, 课程, 成绩, 是否及格]) for row in result: writer.writerow(row) return Response(output.getvalue(), mimetypetext/csv, headers{Content-Disposition: attachment; filenamescores.csv})如果不加\ufeff你用 Windows 打开导出的文件所有中文都会变成乱码。这个坑我印象太深了第一次做导出时忘了加测试同学反馈说全是乱码我当时还以为是编码格式选错了折腾半天才发现是 BOM 的问题。另外两个坑也一并说吧。第一filename如果是中文比如“成绩表.csv”在浏览器里需要做 URL 编码否则下载的文件名是乱码最好用英文名加日期。第二如果导出大数据量别一把梭全查出来再拼接先排个序或分页能避免内存爆炸几千条成绩是不用担心但养成好习惯不会错。5. 本地运行、联调部署与高频问题排查5.1 环境准备Python 和 Node 的版本怎么选跑这套系统之前环境准备就会卡住不少人。先说 PythonFlask 3.x 建议 Python 3.9 及以上我用了 3.11。安装 Python 时有一个非常关键的勾选就是“Add Python to PATH”。很多人装完 Python 后打开命令行输入python提示不是内部命令十有八九是忘了勾这个选项。装完之后建议在项目根目录建虚拟环境python -m venv venv # Windows venv\Scripts\activate # macOS/Linux source venv/bin/activate pip install flask flask-cors flask-sqlalchemy flask-jwt-extended前端环境主要就是 Node.js。Vite 4 要求 Node 14.18Vite 5 要求 Node 18建议直接装 Node 18 LTS 或更高。检查版本就一行命令node -v npm -v有同学在 Windows 上装 Node 时遇到权限问题推荐用 nvm-windows 管理 Node 版本切版本特别方便。我在开发过程中曾因为 Node 版本过高导致某个依赖编译失败切回 Node 18 后就正常了。5.2 前后端联调CORS 与代理的取舍开发时我用了 Vite 的 proxy所以前端请求不存在跨域问题。但如果你跳过 proxy直接在前端用http://localhost:5000请求后端浏览器就会拦截跨域请求这时你就需要给 Flask 配 CORS。from flask_cors import CORS CORS(app, resources{r/api/*: {origins: [http://localhost:5173]}})我个人其实不推荐纯靠 CORS 解决联调问题因为生产环境很可能和一个域名下多个服务部署在一起代理配置更干净。但作为排查手段如果前端控制台出现CORS policy报错说明跨域配置有问题要么走代理要么配 CORS二者选一个。联调时我还习惯把 Flask 的 debug 模式打开并开启 SQLAlchemy 的 echo 日志app.config[SQLALCHEMY_ECHO] True这样每次 SQL 查询都会打印到终端排查“接口返回数据和数据库对不上”这类问题非常有用。你会看到每一次 ORM 查询真实执行的 SQL前端传了什么参数、后端查了什么数据一目了然。5.3 Vite proxy 下本地运行出现页面 404 怎么办本地开发时你有两个常见的 404 场景。第一个是 Vue Router 的 history 模式刷新 404。这个在开发环境有时不会出现因为 Vite 开发服务器本身就做了 SPA fallback但如果你用watch模式或者部署到测试环境就很容易遇到。解决方案我前面提过要么换 hash 模式要么在后端配置 SPA fallback。如果你用 Flask 直接跑打包后的前端静态文件可以加一个兜底路由from flask import send_from_directory app.route(/, defaults{path: }) app.route(/path:path) def catch_all(path): if path and (frontend_dir / path).exists(): return send_from_directory(frontend_dir, path) return send_from_directory(frontend_dir, index.html)这个兜底路由必须是最后一个路由不然会把真正的 API 请求也吃掉。第二个是 API 404。如果你发现前端调用的接口路径在 Flask 里找不到多半是路由写错了或者 Flask blueprint 没注册。建议统一在 Flask 应用的app.py里注册所有 blueprintfrom routes.auth import auth_bp from routes.student import student_bp app.register_blueprint(auth_bp, url_prefix/api) app.register_blueprint(student_bp, url_prefix/api/students)如果接口请求 404先在浏览器直接访问http://localhost:5000/api/students如果浏览器能正常返回 JSON说明接口没问题问题大概率出在前端 baseURL 或代理配置上。5.4 常见问题速查表我把实际开发中遇到的最高频问题整理成一个表格照着排查效率会高很多。症状可能原因解决方案前端请求接口后返回ERR_CONNECTION_REFUSEDFlask 服务没启动或端口不一致检查 Flask 监听端口建议统一为 5000浏览器报CORS policy错误没有走 Vite 代理直接跨域请求配置 Vite proxy或后端加 flask-cors刷新页面 404Vue Router history 模式 Nginx 无 fallback换 hash 模式或配置try_filesExcel 打开 CSV 中文乱码缺少 UTF-8 BOM导出的 CSV 加\ufeff表单日期校验不生效Vue3 里el-date-picker的value-format未设置设置value-formatYYYY-MM-DDWindows 部署时上传/导出附件路径错误路径硬编码使用了正斜杠或反斜杠混淆用pathlib.Path拼接路径不用字符串新增学生后成绩查询不到外键关联未刷新或事务未提交检查db.session.commit()是否执行同一学生同一课程录了两条成绩缺少唯一约束数据库表加 UniqueConstraint已有数据先清理重复Vue3 开发时热更新失效Vite 缓存或 node_modules 损坏删除node_modules和package-lock.json重新npm install登录后跳转首页又被踢回登录页token 存储 key 前后端不一致统一 localStorage 中的 key 名比如都叫token最后一个表看起来简单但每个问题我都实际碰过。特别是 Windows 部署时附件路径错误Flask 的路径不能用 Windows 的\硬编码因为跨平台部署时会崩溃。用Path类处理就没有这个问题。最后的实操心得如果让我总结一句话学生成绩管理系统这种项目技术不难难的是把细节做对。我最大的体会是开发之前先花半小时把数据模型理清楚是后面不返工的关键。Score表不要偷懒不加唯一约束统计口径提前定好权限控制必须不能只依赖前端路由拦截。这套系统我前后大概花了一周业余时间大部分时间其实花在调整交互体验和排查那些“看着奇怪”的 bug 上。最后一个小建议如果你准备拿这个系统做毕业设计或面试作品别只停留在“能跑”的程度。你可以给成绩统计加上导出 PDF 成绩单、加入 Excel 批量导入学生数据、用 Redis 做排行榜缓存这些扩展点都能让这个普通的 CRUD 项目变成一个有亮点可讲的完整作品。后面我会再写一篇关于成绩系统加 Excel 导入导出、以及 Flask 部署到服务器实现外网访问的具体操作如果你也在做类似项目可以先关注着。
返回列表