前后端分离的课程管理系统:SpringBoot+Vue+MySQL开发与部署实践
1. 项目功能全景与核心设计思路1.1 这个系统到底在解决什么问题先说结论这是一个典型的“前后端分离 基础权限模型 核心业务CRUD”教学级项目。我之前接触过好几个类似的课设题——在线选课、在线考试、课程管理、教学资源管理本质上是同一套东西换了一层皮。这个系统把核心业务收敛在“课程”这个实体上管理员负责基础数据维护教师负责课程内容与作业发布学生负责选课、学习和提交作业。三方角色各管一段权限边界清晰业务逻辑完整度足够撑起一份毕业设计论文。很多人拿到这类题目会犯一个毛病——一上来就抢着做前端界面表格画得漂漂亮亮结果后端接口对不上数据表设计漏洞百出等联调的时候才发现要推翻重来。真正合理的顺序应该是先梳理角色和权限边界再画数据表关系最后才写代码。这套系统的角色模型很干净无非就是三张权限视图管理员用户管理、课程审核或下架、全局数据统计教师创建课程、上传课件、发布作业、批改打分学生浏览已上架课程、选课退课、观看视频、提交作业、查看成绩如果把这个业务模型映射到数据库表你大概需要开头就能想到七八张表。我当时给这个项目设计的表结构有用户表、课程表、选课记录表、作业表、作业提交表、公告表、课件表加起来差不多十张表。不要觉得表多表之间的关系撑起来的业务数据流动才是论文里“系统设计”章节的素材来源。1.2 技术选型为什么是 SpringBoot Vue MySQL我从一个带过不少新人做毕设的角度认真说一下技术选型背后的逻辑。SpringBoot 解决的是后端“配置地狱”的问题——没有满屏的 XML 配置文件内嵌了服务容器依赖管理用 Maven 或 Gradle 一把梭。相比传统 SSM 框架需要手动整合 Spring MVC 和 MyBatisSpringBoot 用自动配置把 90% 的样板工作吞掉了对毕设级别的项目来说能省出大量时间写业务代码。前端选 Vue 的原因也比较现实。Vue 在国内面试和课设里都是主流学习曲线平缓模板语法直观配合 Element UI 或 Element Plus 组件库可以快速组装后台管理界面。再加上 Vue Router 做前端路由Axios 做 HTTP 请求Vite 做构建工具整个前端工程化的闭环就通了。这完全是一套“拿简历上写出去不吃亏”的技术组合。数据库选 MySQL 没有悬念。对这种典型的 Web 管理系统关系型数据库在数据一致性、事务支持、联表查询上的优势是 NoSQL 替代不了的。比如选课操作需要扣减课程剩余名额同时插入一条选课记录这种操作必须在一个事务里完成。MySQL 的 InnoDB 引擎配合 Spring 的声明式事务写起来既简洁又安全。1.3 功能模块拆分方案按照功能域划分这个系统可以拆成以下模块用户认证模块登录、注册、JWT 令牌签发与校验、密码加密存储用户管理模块管理员对用户进行增删改查、角色分配课程管理模块课程信息的增删改查、上下架状态切换选课业务模块学生选课、退课、选课列表查询、防重复选课校验作业模块教师发布作业、学生提交作业、教师批改与打分公告模块系统管理员或教师发布教学公告这种模块划分好处在于每个模块都可以独立写进论文的“系统功能”章节同时也能对应到后端的 Controller、Service、Mapper 三层结构。对毕业设计而言功能粒度太粗会显得工作量不足太细又会拖慢开发进度上面的拆分刚好处于一个合理区间。2. 数据库表设计与后端核心实现2.1 数据表关系设计与字段规划数据表是整个项目的根基表结构定得不合理后面写代码时处处难受。我直接给出这套系统核心表的字段设计思路你可以根据自己的业务场景微调。用户表sys_user是整套权限体系的锚点常见的字段有id、username、password、real_name、rolestudent / teacher / admin、avatar、create_time。密码字段存的是 BCrypt 加密后的哈希值绝对不允许明文存储。角色字段用字符串枚举比用数字更直观查问题时不需要翻字典。课程表course记录了课程静态信息id、course_name、teacher_id、course_desc、cover_image、video_url、status0下架/1上架、selected_count、max_count、create_time。注意 teacher_id 要和用户表关联这个字段体现出教师和课程的从属关系。视频地址的字段建议存相对路径不要拼完整 URL后续部署换域名会省很多事情。选课记录表course_selection是学生和课程之间的多对多关系表id、course_id、student_id、select_time。为了防止学生重复选课在 course_id 和 student_id 上建联合唯一索引。这条索引会在数据库层面兜住重复数据后端代码里也要做二次判断双层保险。作业表homework和作业提交表homework_submit是最体现联表查询设计的地方。homework 表有 id、teacher_id、course_id、title、content、deadline、create_time。homework_submit 表有 id、homework_id、student_id、submit_content、file_url、score、submit_time、update_time。作业批改就是一次简单的 UPDATE 操作但涉及到“该课程下所有学生的作业列表”这种查询时必须用 JOIN 关联三张表才能出结果。这块逻辑写得好不好直接影响论文里“系统实现难点”的质量。下面我用一个表格把主要表的关系列一下方便你建表时对照。表名核心字段关联关系sys_userid, username, password, role与课程表 teacher_id 关联courseid, course_name, teacher_id, status与选课表、作业表、公告表关联course_selectionid, course_id, student_id联合唯一索引防重复homeworkid, course_id, teacher_id, deadline与提交表关联homework_submitid, homework_id, student_id, score多对一关联作业表noticeid, user_id, title, content与用户表关联2.2 后端分层结构与关键代码实现后端我遵循的标准写法是 Controller 接收请求、Service 写业务逻辑、Mapper 做数据库操作。如果你用 MyBatisMapper 接口加 XML 文件或者注解 SQL 二选一。毕设项目表数量不大我建议直接用注解 SQL省去维护 XML 文件的麻烦。看一个核心业务——选课的代码实现。这段逻辑涉及的坑比较多我贴出来逐行说明。Service public class CourseSelectionService { Autowired private CourseMapper courseMapper; Autowired private CourseSelectionMapper selectionMapper; Transactional(rollbackFor Exception.class) public Result selectCourse(Long courseId, Long studentId) { Course course courseMapper.selectById(courseId); if (course null) { return Result.error(课程不存在); } if (course.getStatus() ! 1) { return Result.error(课程未上架); } if (course.getSelectedCount() course.getMaxCount()) { return Result.error(课程名额已满); } int count selectionMapper.countByCourseIdAndStudentId(courseId, studentId); if (count 0) { return Result.error(请勿重复选课); } selectionMapper.insert(courseId, studentId); courseMapper.incrementSelectedCount(courseId); return Result.success(选课成功); } }这里有几个细节要提醒你。第一行代码就用了事务注解因为选课要同时操作两张表插入选课记录、更新课程已选人数。如果不加事务万一第二步失败学生就会定在“选课记录存在但课程人数没增加”的中间状态。另外判断selectedCount maxCount只能拦住大部分情况高并发场景下还需要用悲观锁或乐观锁来保证数据安全这块可以作为论文里的“系统优化”章节素材。用户认证我用的是 JWT 拦截器的方式。用户在登录接口拿到 token之后每次请求在请求头里带上Authorization: Bearer token后端拦截器解析 token 并取出用户身份。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write(token无效); return false; } } response.setStatus(401); response.getWriter().write(未登录); return false; } }拦截器注册的时候要设置白名单登录接口、注册接口、验证码接口这些不需要鉴权的请求放行其余接口统一拦截。这里有一个新手容易忽略的点前端路由做了权限控制不等于后端就可以放松。单纯依赖前端隐藏按钮来“防越权”是最脆弱的设计接口层面的角色校验才是底线。比如教师批改作业的接口必须在后端校验当前登录用户的角色和课程归属关系否则换个请求就能操作别人的课程数据。2.3 文件上传与静态资源配置课程封面、课件、作业附件都涉及文件上传。在 application.yml 里配置上传路径spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB file: upload-dir: ./upload/文件上传的 Controller 返回一个相对路径给前端前端直接把路径拼到请求域名后面访问。Windows 环境下部署时有一个常用的坑路径分隔符不一致。我这个项目里写过一个小工具方法统一处理File.separator避免在 Windows 环境用的是反斜杠、Linux 环境又是正斜杠的问题。3. 前端工程化设计与页面实现3.1 Vue 项目结构设计与路由权限控制前端我用的是 Vue 3 Vite Pinia Element Plus 的组合如果你用的是 Vue 2 也完全没问题核心思路相通。目录结构大致如下src/ api/ # 所有 axios 请求封装 assets/ # 静态资源 components/ # 通用组件 router/ # 路由配置 stores/ # Pinia 状态管理或 Vuex views/ # 页面视图 admin/ # 管理员页面 teacher/ # 教师页面 student/ # 学生页面 login.vue register.vue路由这块建议用动态路由方式。系统初始只加载登录页和注册页用户登录成功后根据角色动态添加路由。这样做的效果是学生账号根本拿不到管理端的路由映射表从 URL 层面就进不去。当然这个方案要在路由守卫里配合addRoute方法使用。// 路由守卫示例 router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); } else if (token to.meta.roles !to.meta.roles.includes(store.user.role)) { next(/403); } else { next(); } });这里给新手一个建议路由守卫里to.meta.roles这个 meta 字段一定要写全因为它是前端权限控制的最后一层防线。漏掉一个页面就等于给对应角色白送了一个未授权入口。3.2 核心交互流程选课与课程列表以学生视角举例。学生登录后进入课程列表页调/api/course/list接口拉取上架课程。列表渲染用 Element Plus 的el-table或el-card都行关键是操作列的状态判断得仔细——已选课程显示“退课”按钮可选课程显示“选课”按钮满员课程按钮置灰。el-table :datacourseList stripe el-table-column propcourseName label课程名称 / el-table-column propteacherName label授课教师 / el-table-column propselectedCount label已选人数 / el-table-column label操作 width160 template #defaultscope el-button v-ifscope.row.selected typewarning clickcancelCourse(scope.row.id) 退课/el-button el-button v-else-ifscope.row.selectedCount scope.row.maxCount typeprimary clickselectCourse(scope.row.id) 选课/el-button el-button v-else disabled已满员/el-button /template /el-table-column /el-table前端交互和后端接口的设计节奏要匹配。后端接口返回的数据结构最好统一封装我会在 Result 类里固定返回 code、message、data 三个字段前端 axios 响应拦截器统一判断 code 是否为 200。这样前端组件里就只需要关心数据不用每个请求都写一遍错误提示。3.3 教师工作台课程维护与作业批改教师端页面我拆成了三个核心区域课程管理、课件管理、作业管理。课程管理里教师能够创建新课程填写课程名称、封面、简介和视频素材提交后课程的状态默认是上架。部分设计里也有需要管理员审核后上架的流程这取决于你的需求文档怎么规定。作业批改页面是联调工作量最大的模块。页面需要加载当前教师所有课程、每门课程下的作业、每份作业下所有学生的提交记录。这个三层钻取结构如果没有处理好会导致前端页面出现大量嵌套循环性能很差。我的建议是后端直接提供一个“汇总查询接口”用 VOView Object对象一次性返回结构化数据前端只做渲染不在前端做内存里的二次聚合。这类查询 SQL 可以用嵌套查询或流式处理但毕设层面一次性 JOIN 出来就够了。4. 部署流程与真实环境避坑4.1 构建打包与环境配置这个项目要交付部署文档所以打包和环境配置部分值得好好写。后端的流程是 Maven 打包成 jar 包前端 npm run build 生成 dist 目录。dist 目录里的静态文件有两种接入方式一种是部署到 Nginx再通过反向代理把/api请求转发到后端端口另一种是把 dist 扔进 SpringBoot 的 resources/static 目录让 jar 包同时负责静态资源和接口服务。我推荐用 Nginx 方案。理由有三个一是 Nginx 处理静态文件性能更好二是线上环境修改前端配置不需要重新打包后端三是反向代理配好以后前后端域名完全一致没有跨域问题。我是这样配置 Nginx 的server { listen 80; server_name localhost; location / { root /opt/course-frontend/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }后端打包时要注意配置文件按环境拆分。开发环境连本地数据库生产环境连服务器数据库用spring.profiles.active参数区分。部署命令# 本地启动参数 java -jar course-system.jar --spring.profiles.activedev # 生产环境启动参数指定端口和数据库配置 java -jar course-system.jar --spring.profiles.activeprod --server.port80804.2 部署过程中的典型问题速查表我把自己和几个同事做类似项目时踩过的坑整理成了一张表这些内容在论文里不用写但对实操的人来说是保命级别的。问题现象原因分析解决方案前端请求接口报 404Nginx 配置了 history 模式但缺少 try_files 回退在 location / 里加try_files $uri $uri/ /index.html;后端接口能通但前端显示跨域前端页面和后端接口不在同一个域名下开发环境用 Vite 代理生产环境用 Nginx 反向代理上传中文文件名时访问失败静态资源映射没处理 URL 编码后端做 URL 解码或上传时统一重命名文件打包后接口地址仍是 localhost前端 .env 文件里 API 基础路径写死使用环境变量区分 dev 和 prod 环境数据库导入中文乱码MySQL 表默认字符集不是 utf8mb4建表时指定DEFAULT CHARSETutf8mb4jar 包运行一段时间内存耗尽未设置 JVM 运行时内存参数用java -Xms256m -Xmx512m -jar控制堆内存第六条我单独说一下。很多毕设项目的服务器配置都不高默认 JVM 堆内存设置过大的话小内存机器跑几天就被系统给 OOM 杀掉了。我当时就吃过这个亏部署在同学宿舍的旧电脑上跑了一个周末服务器直接挂了后面加了-Xmx512m稳了很多。4.3 几个部署前的必查项第一数据库脚本要在干净环境里完整跑一遍。我见过太多项目交付的 .sql 文件只有在原作者本地才能成功导入换一台电脑就报主键冲突或者外键缺失。原因是建表语句里混入了测试数据或者随手改过表结构但脚本没同步。解决方法是新建一个空数据库从头执行一遍 SQL 脚本确保无报错。第二所有配置文件里的密码、密钥不要用默认值。生产环境至少把数据库密码改成强密码JWT 密钥换成一长串随机字符串。虽然作为毕设项目安全威胁不大但答辩老师如果问这类问题你能说出“已修改默认配置”就是加分项。第三手册要写清楚从零到一的全过程。部署文档里我把 Java 环境安装、MySQL 初始化、Nginx 配置、项目启动分成了四个章节每步截图保证一个没接触过这个项目的人照着文档做也能跑起来。5. 论文结构安排与答辩准备建议5.1 论文的章节组织思路论文写得好不好很大程度上决定了你这个项目的最终评分。系统做得再漂亮论文逻辑混乱也会拖后腿。我建议按下面的骨架来写第一章 绪论课题背景、国内外研究现状、研究内容、论文组织结构第二章 相关技术概述SpringBoot、Vue、MySQL、前后端分离架构第三章 系统分析可行性分析、需求分析功能需求 非功能需求、用例图第四章 系统设计总体架构图、功能模块设计、数据库设计E-R 图和表结构第五章 系统实现每个核心模块的界面截图 核心代码 实现说明第六章 系统测试测试环境、功能测试用例表、部分性能测试数据第七章 总结与展望项目总结、不足与未来改进方向注意第三章和第四章绝不是抄接口文档。需求分析要有用例图和业务流程图数据库设计要有 E-R 图实现章节代码不能贴大段直接展示要挑“难点”和“重点”来讲。5.2 答辩中最高频的五个问题根据我带过的团队经验答辩现场问得最多的问题集中在以下五个方面提前准备答案效果会好很多。第一“你的系统有什么亮点”别说用了前后端分离这种话。正确说法是系统的核心亮点在于选课流程中通过数据库联合索引与事务控制保证了数据的一致性同时系统通过角色权限控制实现了学生、教师、管理员三段隔离的界面与接口访问范围。第二“为什么选这个技术栈”从开发效率、生态成熟度、资料丰富程度、就业市场匹配几个维度回答。记住不要说“因为网上教程多”。第三“如果同一时间大量学生抢同一门课系统怎么保证不超选”这是考察并发能力的经典题目。你可以回答当前方案是乐观校验也可以提可以演进为带版本号的乐观锁或 Redis 原子操作方案说明你思考过。第四“你的数据库设计有什么考虑”选课记录表联合唯一索引、外键设计、课程表冗余了已选人数字段以降低查询压力——这些都是加分项。第五“项目部署在什么环境如何保障线上稳定运行”结合部署文档回答服务器环境配置、JVM 参数、Nginx 反向代理、日志排查方式即可。5.3 答辩演示的准备建议前面再充分演示翻车也非常可惜。做一个答辩前 Checklist一是演示账号准备三个角色每个账号密码提前登录一遍确认状态不要把 token 过期或密码记混的情况留到现场。二是演示网络环境需要全程联网至少提前把数据库服务、后端服务、前端页面全部跑通一遍。三是准备一份“B 计划”如果现场网络断了提前录一份完整的操作演示视频放在桌面。我自己遇到过的情况是答辩当天校园网抖动前端资源加载不出来还好提前录了视频全程看着录像讲解评委也没觉得突兀。6. 写在最后的经验总结这个项目做完最大的收获不是什么高深技术而是对“完整交付”有了更具体的概念。源码、数据库脚本、论文、部署文档、演示账号一样都不能缺。很多同学把毕设做成了一个“能跑就行”的 Demo但真正的系统要经得起换环境、换数据、换操作者的考验。后面如果有精力我建议在现有系统上做两件事扩展一是把课程视频接入对象存储解决本地磁盘存放大视频的压力二是给选课模块叠加一个简单的消息通知功能学生选课成功或退课后能收到站内信提醒。这两个方向的改动量都不大但能让你在论文“展望”章节有话可写。最后分享一个小技巧项目交付前一定要做一次“冷启动验证”。把系统环境全部关掉数据库删掉重建然后严格按照部署文档从零操作一遍。凡是文档里漏掉的步骤、写错的路径都在这个过程中暴露无遗。我见过的很多部署文档没经过这个验证最后都变成了“只有作者本人能看懂”的备忘录。