资讯中心

Flask+Vue毕业设计选题管理系统全栈开发实战

📅 2026/9/25 15:19:16
Flask+Vue毕业设计选题管理系统全栈开发实战
毕业设计选题管理系统这个题目几乎每年都能见到。但说实话多数网上流传的版本要么是老旧的JSPServlet要么是前后端不分的面条式代码能真正跑通、结构清晰、可以拿去答辩的少之又少。这篇博文我就把一套基于 Flask Vue 的选题管理系统从设计到落地完整拆开讲包括数据库怎么建、接口怎么定义、前端怎么对接以及部署时那些文档里不会写的坑。无论是准备做类似系统的同学还是想快速上手前后端分离开发的朋友都可以直接把这套思路抄作业。1. 项目整体设计与技术选型思路1.1 为什么不是 Django而是 Flask Vue先聊个很现实的问题标题里同时出现了 Flask 和 Django热词里也有 django 创建 app、django 教程之类的搜索很多同学一开始会纠结到底选哪个。我在实际开发里的建议很直接——如果目标是快速搭建、代码量少、自己能完全掌控每个环节选 Flask如果目标是有一个自带后台管理希望开箱即用的全家桶选 Django。两者都能完成选题管理系统但思路完全不同。这套项目选择 Flask 做后端核心原因是毕业设计选题管理系统本身业务并不复杂用户登录、课题的增删改查、学生选题、教师审核最多再加个统计看板。用 Flask 可以在一个项目里把路由、模型、业务逻辑全部串起来代码量比 Django 少三分之一左右排查问题也更容易。而且 Flask 对新手有一个很大的优势——它可以让你手动去写数据库关系、手动处理请求参数这个过程本身就是最好的学习素材答辩的时候老师问到任何一层你都能答上来。前端选择 Vue 则是因为它对中文社区很友好生态完善Element UI 这类组件库可以直接复用开发后台管理类界面效率极高。前后端分离的好处是后端只需要返回 JSON前端只管渲染页面两者通过接口通信分工清晰任何一个部分出问题都能快速定位。1.2 系统功能模块划分一个完整的选题管理系统我通常把它拆成三个端口来设计对应三类角色学生端浏览课题列表、查看课题详情、选择课题、取消选题、查看自己的选题记录。教师端发布课题、修改课题、查看自己课题的被选情况、审核或通过学生的选题申请。管理员端管理用户学生/教师账号、管理所有课题、查看全局统计数据比如每个专业的学生选题进度。这套模块划分的逻辑很简单一句话概括就是谁对哪些数据拥有操作权。学生只操作自己的选题记录教师只操作自己的课题管理员拥有全部权限。这样在后续写接口的时候权限校验的逻辑会非常清晰——每个接口先判断角色再执行对应操作。1.3 前后端分离的核心优势以及要付出的代价这里必须说实话前后端分离不是没有成本的。你至少要把项目拆成两个工程开发时要同时启动 Flask 和 Vue 两个服务接口联调时还要处理跨域问题。但换来的是什么呢第一Vue 的热更新机制可以让你改完前端代码立即刷新看到效果不用像模板渲染那样整个页面刷新第二前端写起来更灵活比如课题列表的筛选、排序、分页这些交互完全可以做成单页应用内的局部刷新用户体验好很多第三以后如果想把系统做成小程序或者 App后端接口可以直接复用不用重新开发。我在最初给这个项目做技术选型时也考虑过直接用 Flask 的 Jinja2 模板渲染减少一个前端工程。但后来发现选题管理系统的页面交互密度还是比较高的——异步校验用户名是否重复、课程列表的动态筛选、管理员后台的表格操作用模板渲染会写大量重复的 JavaScript反而更痛苦。所以最后敲定了 Flask Vue 这套组合。2. 后端 Flask 核心设计与实现2.1 数据库设计三张核心表搞定所有业务选题管理系统听起来功能很多但其实核心数据就三类人、课题、选题关系。所以数据库我设计了以下三张表。用户表user主要字段字段名类型说明idInteger, 主键自增IDusernameString(50), 唯一登录用户名通常是学号或工号password_hashString(128)密码哈希值绝不存明文roleString(10)admin / teacher / studentfull_nameString(50)姓名用于展示student_noString(20), 可空学号仅学生有课题表subject主要字段字段名类型说明idInteger, 主键自增IDtitleString(100)课题名称descriptionText课题详细要求描述categoryString(50)课题类别比如 Web开发、大数据、人工智能teacher_idInteger, 外键创建该课题的教师max_studentsInteger最多可选人数默认1statusInteger0表示未开放1表示可选2表示已选满选题记录表selection主要字段字段名类型说明idInteger, 主键自增IDstudent_idInteger, 外键选题的学生subject_idInteger, 外键被选的课题statusInteger0待审核1已通过2已拒绝created_atDateTime选题时间这个设计的核心在于学生和课题是多对多关系而 selection 表就是那个中间表。它额外记录了状态字段这样就能支持一个业务流程——学生先提交选题申请教师再审核。如果不设置这张中间表直接在课题表上记录学生ID那当教师想拒绝某个申请时你还需要把学生的ID清掉相当于把状态和关系耦合在了一起后续维护特别麻烦。2.2 RESTful API 设计与接口清单后端接口我全部按照 RESTful 风格设计统一返回 JSON 格式。这里的统一格式很重要我前后端在联调前就约定好所有接口的返回格式是{ code: 200, message: success, data: {} }code 是业务状态码200表示成功401表示未登录403表示无权限500表示服务器异常。前端只需要在拦截器里统一判断 code就可以完成全局的错误处理不用每个接口都写一遍异常逻辑。整套系统的核心接口如下方法接口路径功能权限POST/api/auth/login用户登录公开GET/api/user/info获取当前登录用户信息登录用户GET/api/subjects获取课题列表支持分页和筛选登录用户POST/api/subjects新建课题教师/管理员PUT/api/subjects/id修改课题教师/管理员DELETE/api/subjects/id删除课题教师/管理员POST/api/selection学生提交选题申请学生POST/api/selection/cancel学生取消选题学生PUT/api/selection/audit教师审核选题教师GET/api/selections按条件查询选题记录教师/管理员GET/api/stats/overview统计总览数据管理员接口这块踩过最多坑的就是 DELETE 请求。Vue 的 axios 库发 DELETE 请求时默认不带请求体所以传参要么放到 URL 的参数位比如/api/subjects/3要么用 POST 接口模拟。我在设计这个项目时统一采用了 RESTful 推荐的路径参数方式既符合规范又规避了 axios 的 delete 请求体问题。2.3 JWT 登录认证与用户权限控制登录认证我直接采用了 JWTJSON Web Token方案。思路是用户输入学号和密码后端验证成功后就签发一个带用户信息的 token 给前端前端之后每次请求都在请求头里带上这个 token后端校验通过后再处理业务。这比传统的 Session Cookie 方案更适合前后端分离场景因为跨域调用时可以天然避免 Cookie 跨域携带的问题。Flask 里实现 JWT 最省事的方案是使用flask-jwt-extended扩展关键代码如下from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity # 登录接口 app.route(/api/auth/login, methods[POST]) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username)).first() if user and check_password_hash(user.password_hash, data.get(password)): token create_access_token(identityuser.id) return jsonify(code200, messagesuccess, data{ token: token, role: user.role, full_name: user.full_name }) return jsonify(code401, message用户名或密码错误, data{}), 401 # 需要登录才能访问的接口 app.route(/api/user/info, methods[GET]) jwt_required() def get_user_info(): user_id get_jwt_identity() user User.query.get(user_id) return jsonify(code200, messagesuccess, data{ username: user.username, full_name: user.full_name, role: user.role })需要注意的一点是在创建 token 时identity 参数传的是用户 id而不是整个用户对象。一是保证 token 体积小二是如果用户修改了个人信息token 中的身份不受影响。需要更多用户信息时后端通过get_jwt_identity()拿到 id 后再去数据库查一次最新的记录。权限控制的技巧在于我写了一个装饰器来统一处理角色校验from functools import wraps def role_required(role): def decorator(fn): wraps(fn) jwt_required() def wrapper(*args, **kwargs): user_id get_jwt_identity() user User.query.get(user_id) if user.role ! role and user.role ! admin: return jsonify(code403, message权限不足, data{}), 403 return fn(*args, **kwargs) return wrapper return decorator这样在写教师发布课题的接口时只需要在函数上面加一行role_required(teacher)就能把非教师用户挡在门外代码非常优雅。2.4 Flask-SQLAlchemy 操作与分页筛选实现查询课题列表是系统里最核心、最频繁的接口。第一版我只写了最简单的Subject.query.all()返回结果但很快发现列表页如果一次返回几十条完整的课题描述前端渲染起来会卡而且网络传输的数据量太大。后来我补上了分页和筛选逻辑用 Flask-SQLAlchemy 内置的paginate方法处理from flask_sqlalchemy import SQLAlchemy from sqlalchemy import or_ db SQLAlchemy() app.route(/api/subjects, methods[GET]) jwt_required() def get_subjects(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) keyword request.args.get(keyword, , typestr) category request.args.get(category, , typestr) query Subject.query.filter(Subject.status 0) if keyword: query query.filter(or_(Subject.title.contains(keyword), Subject.description.contains(keyword))) if category: query query.filter(Subject.category category) pagination query.order_by(Subject.id.desc()).paginate( pagepage, per_pageper_page, error_outFalse) subjects [{ id: s.id, title: s.title, category: s.category, teacher_name: User.query.get(s.teacher_id).full_name, max_students: s.max_students, selected_count: Selection.query.filter_by(subject_ids.id, status1).count(), status: s.status } for s in pagination.items] return jsonify(code200, messagesuccess, data{ items: subjects, total: pagination.total, page: page, pages: pagination.pages })这里有个细节值得说明selected_count是我实时统计的数据库查询结果它统计的是该课题下 status 为 1 的已通过选题数量。虽然每次查询列表都会多执行几条 count 查询但对个人管理系统来说性能完全够用而且能保证数据绝对准确。如果以后并发量大了可以增加一个selected_count字段在每次选题通过时用事务更新。分页返回的数据包含了items、total、page、pages四个字段前端可以据此准确渲染分页组件和总条数提示。这种约定好的返回结构比前端单纯猜下一页还有没有数据要靠谱得多。3. 前端 Vue 核心实现与联调3.1 Vue 环境搭建与项目初始化前端这部分很多同学卡住的第一关往往不是写代码而是环境搭不起来。这里分享一套我在多个项目中验证过的稳定组合Node.js 16 搭配 Vue CLI 5或者用 Vite 创建 Vue 3 项目。考虑到多数同学学习时用的是 Vue 2 Element UI 的旧资料我在这个项目里用了 Vue 3 Element Plus 的选型API 风格更现代长期来看也更值得投入。创建项目时我习惯用 Vite 而不是 Vue CLI因为 Vite 的开发服务器启动速度真的快很多。操作命令如下npm create vitelatest subject-system-frontend -- --template vue cd subject-system-frontend npm install npm install vue-router4 axios element-plus npm run dev装好基础依赖之后要在main.js里注册 Element Plus 和路由import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(ElementPlus) app.use(router) app.mount(#app)特别提醒Element Plus 的样式文件必须单独引入我见过不少同学忘了 import 样式结果组件渲染出来是一堆光秃秃的 HTML 结构。这个问题你对照官方文档就能发现但在排错的时候很容易被忽略因为代码看起来没有任何报错。3.2 axios 请求封装与前端路由守卫前端最值得封装的就是 axios 请求层。如果不做统一封装每个页面组件里都要写一遍头部带上 token、捕获 401、弹出错误提示的逻辑代码会非常冗余。我推荐的封装方式是这样的在src/utils/request.js中创建一个 axios 实例import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, // 开发环境通过 Vite proxy 代理生产环境同域部署 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 { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request这段封装解决了三个核心问题统一注入 token、统一处理业务错误码、统一处理 401 未认证跳转登录页。之后每个页面调用接口时的代码就会简洁很多比如获取课题列表const res await request.get(/subjects, { params: { page: 1, per_page: 10 } })路由守卫放在src/router/index.js中本质就是在每次跳转路由前检查用户是否已登录router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.path /login token) { next(/dashboard) } else { next() } })3.3 跨域问题解决方案与 Vite 代理配置前后端联调时遇到最多的就是跨域问题。现象很典型你在 Vue 的页面里调用http://localhost:5000/api/login浏览器控制台报CORS错误接口明明能访问但就是进不去。这是因为前端服务跑在 5173 端口后端跑在 5000 端口两者端口不同浏览器默认判定为跨域请求。解决办法有两种我会根据使用场景选择。开发期间我在 Vite 配置里加一个代理把所有/api开头的请求转发给后端// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })这样一来前端代码里的请求地址依然是/api/xxxVite 开发服务器会自动转发到 Flask 的 5000 端口。这个方案的好处是浏览器认为请求是同源的完全绕开了 CORS 的限制。但如果你不想配代理或者更愿意直接让后端支持跨域那就用 Flask-CORS 扩展from flask_cors import CORS CORS(app, resources{r/api/*: {origins: http://localhost:5173}})我的建议是两者都配置上。开发时用 Vite 代理保证安全同时在后端开启 CORS 白名单方便以后有其他客户端接入。两种方案并不冲突。3.4 核心页面拆解与实现逻辑前端页面中工作量最大的是课题列表页和选题管理页。我用一个真实的页面片段来说明整体写法。课题列表页在加载时先请求/subjects接口拿到列表数据后分页渲染在 Element Plus 的表格或卡片中。搜索功能通过绑定输入框的 v-model 和搜索按钮的 click 事件触发接口请求。核心逻辑如下template el-card el-form :inlinetrue submit.prevent el-form-item el-input v-modelquery.keyword placeholder输入课题名称或关键词 clearable / /el-form-item el-form-item el-select v-modelquery.category placeholder课题类别 clearable el-option labelWeb开发 valueWeb开发/el-option el-option label大数据 value大数据/el-option el-option label人工智能 value人工智能/el-option /el-select /el-form-item el-form-item el-button typeprimary clickloadSubjects搜索/el-button /el-form-item /el-form el-table :datasubjects v-loadingloading el-table-column proptitle label课题名称/el-table-column el-table-column propcategory label类别 width120/el-table-column el-table-column propteacher_name label指导教师 width120/el-table-column el-table-column propselected_count label已选/人数 width100/el-table-column el-table-column label操作 width200 template #defaultscope el-button typeprimary sizesmall clickhandleSelect(scope.row)选择/el-button el-button sizesmall clickhandleDetail(scope.row)详情/el-button /template /el-table-column /el-table el-pagination background layouttotal, prev, pager, next :totaltotal :page-sizequery.per_page v-model:current-pagequery.page current-changeloadSubjects / /el-card /template script setup import { ref, reactive } from vue import request from ../utils/request const subjects ref([]) const total ref(0) const loading ref(false) const query reactive({ page: 1, per_page: 10, keyword: , category: }) async function loadSubjects() { loading.value true const data await request.get(/subjects, { params: query }) subjects.value data.items total.value data.total loading.value false } async function handleSelect(row) { await request.post(/selection, { subject_id: row.id }) ElMessage.success(选题申请已提交) loadSubjects() } /script管理员后台页面则用 Element Plus 的 el-menu 做侧边栏导航把学生管理、课题管理、统计报表分成不同路由页面再通过 iframe 嵌套都是不推荐的直接组件化才是 Vue 的正道。因为组件化可以让每个页面的代码组织清晰修改起来定位快后续维护成本低。需要注意的一点选题按钮在前端要加上条件判断。如果当前用户是学生且已经选过课题就不能再显示选择按钮而应该显示已选状态如果是教师登录这个按钮更是要完全隐藏。这种基于角色和状态的前端控制直接通过v-if结合 store 里的用户信息实现不用做过多封装。4. 部署上线与常见问题排查4.1 开发调试技巧两个终端并行跑这个项目是前后端分离的开发时必然要同时启动两个服务。我的工作习惯是开三个终端窗口第一个跑 Flask 后端flask run或者python app.py第二个跑 Vue 前端npm run dev第三个留着做 Git 操作和临时命令行任务。很多人不习惯这种多窗口模式但你会发现调试起来确实高效。前端保存代码后 Vite 会热更新浏览器立即刷新后端开启调试模式后代码修改也会自动重载只要你没改到模型层结构原有的数据都会保留。写接口时我习惯于用 Postman 或 Apifox 先测一遍接口确认通了再写前端调用代码这样定位问题时永远不会搞混是前端错了还是后端错了。如果遇到后端代码改了但 Flask 没有自动重载的情况多是因为启动了多个 Flask 实例端口被旧的进程占着。这时候用命令行工具查一下端口占用把对应进程杀掉重新跑就行。4.2 前端打包与 Flask 静态文件托管方案开发完成后需要部署上线整体流程是先npm run build把 Vue 项目打包成 dist 目录下的静态文件再让 Flask 把 dist 目录作为静态资源目录来服务。在 Flask 侧主初始化文件里加上这段配置import os from flask import Flask, send_from_directory app Flask(__name__, static_folder../dist) app.route(/, defaults{path: }) app.route(/path:path) def catch_all(path): if path and os.path.exists(os.path.join(app.static_folder, path)): return send_from_directory(app.static_folder, path) return send_from_directory(app.static_folder, index.html)这段代码的逻辑很好理解如果请求的路径确实对应 dist 下的一个静态文件比如 JS、CSS 或图片就直接返回否则统一返回 index.html。第二次 return 是为了配合 Vue Router 的 History 模式——前端路由跳转到/subjects时Flask 后端找不到subjects这个文件就返回 index.html让 Vue 自己去接管路由渲染。部署上我最常用的是 Gunicorn 加 Nginx。Nginx 负责监听 80/443 端口把请求代理到 Gunicorn 启动的 Flask 应用上Gunicorn 则管理多个 worker 进程稳定性和并发能力都比 Flask 自带的开发服务器强很多。如果你不想搞这么复杂直接在服务器上gunicorn app:app -b 0.0.0.0:5000也能跑只是没有静态文件的高效处理能力。但对于毕业设计演示来说已经够用了。4.3 常见问题速查表开发这套系统的过程中我积累了一张问题排查表基本都是碰到过的真实情况在这里分享给大家问题现象排查思路解决方案登录接口报 401 但账号密码正确密码哈希算法不匹配或数据库种子的原始密码被修改重新注册新账号测试或直接改数据库密码字段前端请求接口跨域报错检查 Vite 代理配置是否生效后端 CORS 是否开启确认代理 target 指向 Flask 端口后端安装 flask-corsVue 打包后页面空白静态资源路径配置错误在 vite.config.js 中设置base: ./Flask 启动报端口被占用上一次服务未正常关闭查找并 kill 对应进程或换端口启动操作数据库时报字符编码错误MySQL 或 SQLite 默认编码不是 utf8创建数据库时指定 utf8mb4连接串加上 charset 参数Element Plus 组件没有样式样式文件未全局引入在 main.js 中 import element-plus/dist/index.css文件上传接口超时Flask 默认上传大小限制为 1MB设置app.config[MAX_CONTENT_LENGTH]或在前端压缩文件数据库这块还有个容易踩的坑就是 MySQL 环境安装和配置。如果你本机没有 MySQL建议直接用 SQLite 起步Flask 对 SQLite 的支持零配置改一行连接串就能切换。等系统跑通了再迁移到 MySQL 完全来得及。4.4 几个容易踩的坑提前替你们踩了第一件事密码千万不要用明文存储。你用werkzeug.security提供的generate_password_hash和check_password_hash来处理代码只多三行但安全性提升是质的飞跃。答辩时有评委一定会问数据安全怎么做你能答上来这一个点就能加不少分。第二件事前后端字段命名要统一。经常出现后端返回的是created_at下划线风格前端 JavaScript 里习惯用驼峰createdAt如果不在后端预先处理好前端拿到的对象里到处是 undefined。我的习惯是后端 API 层统一转成驼峰或者前端用映射表转换但最省心的方案是直接在 Flask 返回字典时字段名就按前端需要的风格来。第三件事关于 Vue 打包后的布局异常。热搜词里有这个关键词我再展开说下。Vue 打包后页面布局错乱最常见的原因是样式被浏览器缓存了强制刷新或者清缓存就能解决。其次就是前面提到的base路径问题——打包后的资源路径是绝对路径部署到子目录就找不到把vite.config.js里的base改成./变成相对路径就一切正常。第四件事权限校验别只在前端做。前端隐藏按钮只是用户体验层面的优化真正的安全防线在后端接口的权限校验。我见过不少项目前端把按钮隐藏了就觉得安全了实际上直接调用接口照样能新增课题、删除记录。后端接口每一层都要校验身份和角色这一点在答辩时也经常会作为项目亮点被单独询问。5. 我对这套系统后续优化的几点思考5.1 从能用到好用的功能扩展方向如果时间有余力这套系统值得扩展的地方还很多。举例来说当前设计是学生只能选一个课题但有些学校允许多轮选题——第一轮没被老师通过的话可以继续第二轮。你可以给 selection 表加一个round字段记录当前是第几轮选题再给 subject 表加字段控制每轮的开放状态。这样做的好处是业务逻辑完全解耦代码也不需要大改。邮件通知也是一个很实用的扩展点。教师通过或拒绝学生的选题申请时系统自动发一封邮件给学生。用飞书或钉钉的 webhook 机器人推送也可以配置一个 URL 就能实现复杂度不高但体验提升非常明显。5.2 接口层性能与安全加固建议选课题这个操作从业务上应该是原子性的否则两个学生同时提交同一个只剩 1 个名额的课题就可能出现超选问题。当前实现是查一下已选人数小于限定人数就插入记录。但高并发场景下这个查一下和插入之间存在时间窗口。解决思路是给 subject 表加一个version版本号字段更新时带条件校验版本号或者用数据库行锁。虽然毕业设计场景不需要这么高的并发量但你要是能在文档里把这个思考写进去评委印象分一定会提升。API 层面还应该做限流。Flask 端可以借助flask-limiter扩展给登录接口加一个每 IP 每分钟 5 次的限制防止暴力破解密码。前端做一些基础的输入校验和防重复提交比如选题按钮点击后立即进入提交中禁用状态也能从体验上避免大半问题。5.3 团队协作中版本管理的经验如果这个项目有几个人一起做Git 的分支管理一定要走好。我的习惯是后端代码和前端代码放在同一个仓库的两个目录里使用两个分支backend分支只动 Flask 代码frontend分支只动 Vue 代码主分支main保持始终可以运行的稳定状态。接口联调阶段再合并到dev分支统一部署测试。这些经验做项目时不一定能用上但面试和答辩时聊起来会让别人觉得你是个真正完整做过项目的人而不只是照着教学视频敲了一遍代码。选题管理系统作为一个经典的 CRUD 项目难度不高但五脏俱全涉及用户认证、权限管理、数据库关系设计、前后端交互、部署上线等流程。把它吃透的成本很低但对完整掌握一门技术栈的收益非常直接。希望这篇拆解能帮你把这个毕业设计做得明明白白最好还能站在它肩膀上做出一点属于自己的新东西。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案