十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于Flask+Vue的毕业设计选题管理系统设计与实现

基于Flask+Vue的毕业设计选题管理系统设计与实现 毕业设计选题管理系统这应该是每年大四学生和指导教师都绕不开的一个经典课题了。如果你正在用 Python 做毕设或者帮别人搭过这类系统一定知道这个题目有多常见学生要选题、老师要出题、教务要审核整个流程管起来非常琐碎。我自己在实际开发中用 Python Flask Vue 这套组合把一个选题管理系统从零搭了出来整个过程踩了不少坑也总结了一套挺顺手的开发节奏。这篇就把完整的实现思路、关键代码设计、前后端联调细节和常见的排查经验都拆开讲清楚给正准备做类似系统的同学一条可以直接照着走的路。这个系统本质上是一个典型的前后端分离项目Flask 负责提供 RESTful APIVue 负责页面交互PyCharm 作为主力开发工具贯穿前后端。适合正在准备毕业设计的本科生、想快速上手 FlaskVue 全栈开发的初学者以及需要给学生演示完整业务系统的指导老师参考。不管你是从零开始还是已经写了一点代码卡住了这篇内容都能帮上忙。1. 整体设计拆解需求、角色与流程动手写代码之前最忌讳的事情就是拿到题目就开建表、写接口。选题管理听起来简单但实际业务里包含的细节比想象中多得多。我先花了两个晚上把需求彻底梳理了一遍这里面的核心角色有三个管理员、教师、学生。管理员的职责包括维护教师账号、审核教师提交的题目、发布统计结果、处理学生调题申请甚至临时调整选题名额。教师的职责是上报自己的研究方向和相关课题设定每个题目的可选人数上限浏览学生报名情况并且最终确认选中的学生。学生是系统里使用频率最高的角色他们要浏览所有可选题目、查看每个题目的已选人数和剩余名额然后提交选题申请等教师审核被拒绝之后还需要重新选择。一个容易被忽略的地方是选题的状态流转。我见过很多同学把管理系统的状态设计成简单的可选/不可选结果打印论文和导出表格时会出现拟选题人数超过上限、有人重复选题、有人被拒绝后没有及时释放名额之类的数据问题。所以我在设计之初就把完整的业务流程画了出来。系统的状态流转大致是这样教师提交题目后题目处于待审核状态管理员审核通过进入可选状态学生就能在系统里看到。学生提交申请后选题记录进入待确认状态指导教师登录后确认或拒绝。确认后题目剩余名额减一选题状态变为已通过。如果名额已满题目自动变为已满状态不再出现在可选列表中。整个过程虽然简单但每个状态必须被完整管理否则统计环节必出乱子。这样做的好处是任何时刻数据库里的记录都能反映真实的业务情况导出报表时也不用靠人肉去筛数据。具体到技术方案我选择 Flask Vue 而不去用现成的后台管理框架比如 Django Admin 或者若依那套原因也很实际毕设答辩时老师最常问的问题就是系统中哪部分是你自己写的使用纯手动实现的前后端分离结构更容易把自己做的工作讲清楚而且 Flask 灵活、轻量、易部署几行代码就能起一个 API 服务非常适合这种规模的管理系统。2. 数据库设计六张核心表与字段规划2.1 用户表与角色权限数据库是系统最基础的骨架。我的设计里总共用了六张核心表分别是用户表、题目表、选题申请表、课程设计表部分题目关联课程、学院/专业表、公告表。这里有一个很多初学者容易踩坑的点不要把用户角色直接硬编码成is_admin布尔字段。我最初也这么干过直到发现教师也需要部分管理权限比如审核学生选题、学生也有部分查看权限时就赶紧改成了 role 字段加权限校验装饰器的方式。具体的用户表设计如下数据库使用 MySQLCREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role ENUM(admin, teacher, student) NOT NULL DEFAULT student, real_name VARCHAR(50) NOT NULL, email VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );密码存储一定要用哈希不能明文。我在代码里用的 Werkzeug 自带的generate_password_hash和check_password_hash不用重复造轮子Flask 项目默认就依赖这个库。这里的 role 字段是后续所有权限控制的核心后面接口校验全靠它。2.2 题目表与选题申请表题目表是业务核心它承载了选题模块几乎所有关键信息。字段至少应该有题目名称、题目介绍、所属方向、指导教师ID、限选人数、已选人数、状态、创建时间。我实际建表时额外加了两个字段一个用于标记是否被推荐后期做首页热门选题直接用不用再写逻辑另一个用于存放附件材料的路径比如开题报告模板。选题申请表记录了学生选题的行为。这里特别需要注意字段的唯一约束。我在设计表结构时加了UNIQUE KEY uk_student_topic (student_id, topic_id)也就是说一个学生对同一个题目只能有一条申请记录。如果少了这个约束学生反复点击提交就会产生重复记录后边审核和统计会变得一塌糊涂。还有一个我后来才加上的字段申请时间。教师确认学生时如果同时有多个学生申请同一个题目时间先后的信息非常重要。系统设计里通常遵循先到先得的原则教师也可以根据时间信息来决定录取谁。这些细节在写文档时都是加分项。3. Flask 后端接口实现与经验要点3.1 项目结构与依赖配置后端源码结构建议按照功能模块拆分不要把所有路由写进一个 app.py。我第一次写的时候把所有接口堆在一个文件里四千多行代码后来连自己都分不清哪是哪了。推荐的结构是这样flask_backend/ ├── app.py # 应用入口与注册蓝图 ├── config.py # 配置文件数据库、密钥、跨域设置 ├── models.py # SQLAlchemy 数据模型 ├── extensions.py # db SQLAlchemy() 独立出来防循环引用 ├── api/ │ ├── auth.py # 登录、注册、刷新token │ ├── admin.py # 管理员审核 │ └── topic.py # 选题核心接口 ├── utils.py # 权限校验、响应格式化 └── requirements.txt这里最值得提醒的是extensions.py独立建模。如果你在app.py里写db SQLAlchemy()再在models.py里导入它然后在app.py里又注册蓝图很容易碰到循环导入报错ImportError: cannot import name db。把 db 实例单独放一个文件所有模块都来引用它就能从根源上避开这个问题。依赖方面requirements.txt里我锁定的关键版本是Flask2.2.3、flask_sqlalchemy3.0.3、flask_cors3.0.10、PyJWT2.6.0。其中 flask_cors 必须装否则前端 Vue 调用接口时会因为跨域直接被浏览器拦截后面都会遇到。3.2 JWT 登录鉴权实战登录鉴权这个环节我建议用 JWTJSON Web Token而不是传统的 Session。原因很直接Flask 后端和 Vue 前端是分开部署的用 Session 要额外处理 cookie 跨域和 CSRF 的问题而 JWT 是无状态的前端把 token 存到 localStorage 里每次请求带在请求头里就行简洁也方便。关键代码如下import jwt from functools import wraps from flask import request, jsonify from datetime import datetime, timedelta SECRET_KEY your-secret-key def generate_token(user_id, role): payload { user_id: user_id, role: role, exp: datetime.utcnow() timedelta(hours12) } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def token_required(f): wraps(f) def decorated(*args, **kwargs): token request.headers.get(Authorization, ).replace(Bearer , ) if not token: return jsonify({code: 401, msg: 未登录或登录已过期}), 401 try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) request.user_id payload[user_id] request.user_role payload[role] except jwt.ExpiredSignatureError: return jsonify({code: 401, msg: 登录已过期请重新登录}), 401 except Exception: return jsonify({code: 401, msg: 无效的登录凭证}), 401 return f(*args, **kwargs) return decorated实际开发时还要写一个基于角色的权限装饰器比如管理员操作接口必须校验request.user_role admin。把角色校验从 token 校验里拆分出来以后加新接口的时候复用性更高代码也更清晰。3.3 选题状态流转的接口实现选题是本系统最核心的业务接口设计上我把它拆成了三类学生操作接口、教师操作接口、管理员操作接口。学生调用提交选题申请接口后新增一条待确认记录教师调用确认选题接口后同步更新选题记录状态和题目已选人数管理员调用审核题目接口后把题目从待审核变为可选。关键事务逻辑是更新已选人数时必须加条件判断防止多人同时申请导致超员。例如# 伪代码只在剩余名额 0 时才减少 affected Topic.query.filter( Topic.id topic_id, Topic.selected_count Topic.max_count ).update({selected_count: Topic.selected_count 1}) db.session.commit() if affected 0: return jsonify({code: 400, msg: 名额已满}), 400这里用到了 SQLAlchemy 的原子更新比先查再改更安全。如果先查再改两个请求同时查到已选人数是 19上限是 20然后同时更新成 20就会有两个学生同时选中数据就错了。类似的并发现象在答辩演示时不太容易暴露但上线后是真会出事的。4. Vue 前端设计与核心页面实现4.1 环境搭建与项目初始化前端部分用 Vue 3 Vite 是当前最顺手的组合。Vite 启动速度快开发体验比 Vue CLI 舒服很多也不用额外配置 webpack。Node.js 我用的 18 LTS 版本再低的版本装 Vite 时会报 engine 警告虽然能跑但总归不稳。初始化命令很简单npm create vitelatest web_frontend -- --template vue cd web_frontend npm install npm install axios vue-router4 pinia element-plus这边特别提一下 Element Plus。毕设管理系统里表格、表单、弹窗、消息提示全是刚需Element Plus 一套组件库几乎全覆盖。如果你自己去手写这些组件光样式调优就得花三五天完全没必要。按需引入的话打包体积还能压缩不少。我的前端目录一般这样组织src/ ├── main.js ├── router/index.js # 路由 ├── store/user.js # 用户状态 ├── api/request.js # axios 实例 ├── api/topic.js # 接口封装 └── views/ ├── Login.vue ├── StudentTopicList.vue ├── TeacherTopicManage.vue └── AdminAudit.vue4.2 路由守卫与登录态持久化前端路由必须配置守卫否则用户直接在地址栏输入/admin就能进入管理员页面。虽然后端接口有权限校验前端不拦也能保证数据安全但用户体验上仍应避免未登录的人点进去看到一个空荡荡的报错页面不如直接跳转登录页友好。路由守卫的实现思路是在router.beforeEach里判断目标路由是否需要在登录状态访问如果需要就从 Pinia 或 localStorage 里取 token没有就跳转/login。最好再加一层路由 meta 的角色限制比如meta: { role: admin }管理员页面的路由加上这个标记其他角色访问时直接跳回首页。axios 请求拦截器里统一给请求头加Authorizationrequest.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这样登录态过期后用户在操作时第一次收到 401 就会被自动踢回登录页比等到手动刷新才发现过期要友好得多。4.3 选题列表页与实时名额展示选题列表页是这个系统最有辨识度的一个页面。学生的核心动作就是浏览题目和点击选它界面上需要一眼看明白的信息是题目名称、所属方向、指导老师、剩余名额数量。剩余名额少的时候我会用 tag 标红名额为 0 时按钮置灰并显示已满。列表接口返回的数据格式我统一成{ code, msg, data }这种形式。前端拿到 data 后渲染表格。注意 ** 已选人数这个字段建议在每次列表加载时重新查询数据库**不要在前端做减法、也不要缓存保证数据一致。印象比较深的一次事故是当天下午导数据时把已选人数缓存到了浏览器 localStorage学生在另一个浏览器看到名额还剩 5 个实际只剩 1 个结果提交时后端返回名额已满学生一头雾水地来问怎么回事。从那之后我再也不在前端缓存业务数据了。搜索功能和筛选功能也是这个页面的常规操作按方向筛选下拉框、按教师姓名搜索、按名额从少到多排序都用表格自带属性或者简单方法就能实现加不了多少代码量但是在答辩时很能体现系统完整度。5. 前后端联调跨域配置、统一响应与部署细节5.1 跨域问题Flask 后端跑在 5000 端口Vue 开发服务器跑在 5173 端口两个端口一不一样跨域就来了。浏览器安全策略会拦截跨域请求这时最常见的报错是Access to XMLHttpRequest at http://localhost:5000/api/login from origin http://localhost:5173 has been blocked by CORS policy解决办法用 flask_cors 扩展最省事。开发阶段可以直接放开所有来源from flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})生产部署如果前后端在同一个域下其实可以把 CORS 关掉或者配置成具体的域名。这是部署阶段需要注意到的一个差别你没有必要让 API 对全世界的网站完全开放。5.2 Vue 开发代理配置除了后端开 CORS前端 Vite 也可以配置代理// vite.config.js server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } }这样前端所有/api开头的请求都会被 Vite 代理转发到 Flask 后端浏览器视角里请求是同源的就不存在跨域了。开发环境开代理方式和后端开 CORS二选一即可两个都开也没有冲突只是实际开发中我建议用代理方式因为将来部署时的 Nginx 反向代理配置几乎是一样的思路路径迁移会很平滑。5.3 生产环境部署生产环境的部署方案比较常规Vue 执行npm run build产物是dist目录里的静态文件后端用 Nginx 托管前端静态文件同时把/api开头的请求反向代理到 Flask 服务上。核心 Nginx 配置大概这样server { listen 80; server_name your-domain.com; root /var/www/vue_dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }Flask 服务用 Gunicorn 启动端口建议统一用 8000 而不是 5000因为 macOS 的 AirPlay 接收器默认占用了 5000 端口Linux 上很多服务也会预留这个端口回头排查端口冲突不如一开始就避开。6. 常见问题与排查技巧实录6.1 高频问题速查表我把这个项目从开发到调试再到部署期间遇到的高频问题整理成了一个表格这里面的每一条都是你几乎必然会碰到的。问题现象根本原因解决方案前端访问/api报 404Nginx 未正确代理/api路径检查 proxy_pass 是否写成http://127.0.0.1:8000而不是http://127.0.0.1:8000/api/登录后接口调用还是 401请求头没带上 token检查 axios 拦截器确认每次请求都加上 Authorization数据库中文乱码MySQL 建表时默认字符集不是 utf8mb4建库写法CREATE DATABASE graduation DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;学生重复提交同一选题成功缺少唯一约束给 student_id topic_id 加联合唯一索引已选人数超过限制并发更新时线程不安全用原子 SQL 更新加条件判断UPDATE topic SET selected_count selected_count 1 WHERE id ... AND selected_count max_count打包后页面刷新 404vue-router history 模式下没有 fallbackNginx 配置try_files $uri $uri/ /index.html;跨域请求被阻止前后端端口不一致开发环境用 Vite 代理生产用 Nginx 统一转发6.2 几个值得展开说明的排查细节第一个是打包后布局异常的问题。开发时 Vue 项目引入 CSS 正常打包部署后样式全部错乱。这个通常是因为资源路径用了绝对路径/assets/...部署到子目录后找不到文件。解决办法是vite.config.js里设置base: ./让资源引用改成相对路径。这个细节不细看还真容易被忽略。第二个是 Vite 本地开发正常但打包后图片加载不出来。这个和上面原理类似都是public目录里的静态资源在部署时路径前缀不对。统一改成相对路径或者用new URL(../assets/xxx.png, import.meta.url)的方式引用资源问题就消失了。第三个是Flask 的 debug 模式。开发时要开app.run(debugTrue)这样代码改完自动热加载不用反复重启非常省时间。但是生产部署时千万别开 debug否则一旦抛异常就会在网页上直接显示完整的调用栈和源码片段这在安全上是灾难级别的隐患。我用 Gunicorn 启动时压根不会开启 debug然后再配合 Nginx 和系统日志一起排查线上错误。第四个问题是统计报表导出乱码。系统里导出的 Excel 文件是 pandas 写的用 Excel 打开时中文列名会变成乱码。原因出在编码上Excel 对 UTF-8 无 BOM 的文件的兼容性有问题。解决办法是在导出时加 BOM 头df.to_csv(选题统计.csv, indexFalse, encodingutf-8-sig)。这个细节在实际使用中很容易踩坑但修复只需要换一个 encoding 参数。7. 一些值得思考的扩展方向这个系统做完后我一直在思考还能往哪些方向优化也想给各位做毕设的同学提供点思路。第一个方向是智能推荐选题。学生信息包括专业、兴趣方向、已修课程题目包括难度、方向、所需技能。基于这些数据用简单的协同过滤或内容匹配算法给学生推荐合适的选题。毕设论文里加入算法部分内容深度和可写性都会好很多。第二个方向是公告的定时自动发布。比如截止日期前三天系统自动给未选题的学生发站内消息提醒。用 Flask 的定时任务或者 Celery 调度都可以实现。第三个方向是教学班维度的统计。当前大多数选题系统统计到班级、专业这一层就停了。实际上管理端需要更细的维度比如分导师统计指导人数、分题目统计选择热度、按时间周期统计选题计划推进情况。这些统计图表换成 ECharts 来展示出来的效果会提升很多在答辩时也更抓眼球。对比同类的 Django 实现Flask 版本的最大优势就是轻、灵活、源码清晰易讲Django 的优势在于自带 Admin 后台和 ORM 全家桶基础功能开发更快。如果时间紧、需要快速怼出后台管理页可以考虑 Django如果想在论文里详细写接口设计、权限控制、状态机流转这些内容Flask 的思路会更清楚。两种取舍没有绝对优劣看你是更想要系统复杂度还是更想要逻辑解释的清晰度。最后分享一个我做这套系统时最深的体会毕业设计类管理系统能不能拿高分往往不取决于功能有多花哨而取决于业务流程的状态设计是否严谨、代码结构是否清晰、界面的每一处交互是否对三类角色都照顾到了。多做一步流程测试比如两个学生同时选同一个题目、教师先确认后管理员把题目下架这类边缘情况系统的完整度会得到实打实的提升。做完这些再回头补文档你会发现自己能写出真正有价值的内容而不是照抄网络上的模板。
返回列表