毕业设计实战:SpringBoot+Vue+MySQL在线课程管理系统全解析
每年到这个时间段“毕业设计选什么题目比较稳”就成了后台私信里最高频的问题。如果让我给一个既能体现主流技术栈、又有足够业务逻辑可以展开讲的方案SpringBoot Vue MySQL 组合做一套在线课程管理系统绝对是最不容易翻车的选择之一。这个选题我前后辅导过不少学生也亲眼见过从需求梳理、建表、后端接口、前端对接到最后打包部署的全过程。这篇就把整条链路拆开揉碎讲清楚——从项目定位到数据库设计从鉴权方案到接口实现从前端路由到部署上线再到论文素材怎么攒全盘过一遍。想拿这个题目做毕设、或者正在做同类管理系统的同学可以直接把这篇当操作手册用。1. 项目定位这个系统到底解决什么问题在线课程管理系统本质上解决的是高校或培训机构里课程资源数字化管理的业务问题。上课点名靠口头通知、课件资料靠微信群文件、选课靠接龙Excel这些都是真实场景里反复出现的痛点。这个系统要回答三个核心问题学生怎么找课、选课、上课教师怎么发布课程、维护内容、查看选课名单管理员怎么统筹用户和课程数据。以角色维度拆开来看功能边界学生端注册登录、浏览课程列表、按分类或关键词检索、查看课程详情、选课退课、我的课程、个人信息维护教师端课程信息增删改查、课程封面与简介维护、查看选课学生名单管理员端用户管理启停用、分配角色、课程审核与上下架、分类管理、基础数据统计这套功能设计最聪明的地方在于三套角色界面工作量足够撑起一篇毕设论文又不至于复杂到无法收尾。整个系统是一个完整的业务闭环——从角色权限、课程发布、选课记录到个人中心每一环都有数据流可以追踪这在答辩时非常加分因为老师顺着业务逻辑就能问出“为什么这样设计”“表结构怎么关联”这些问题你每个答案都能落地到具体代码和表字段上。1.1 为什么这个选题适合毕业设计毕业设计评审最看重的其实是三件事系统能不能跑通、工作量是否饱满、你能否讲清楚设计与实现过程。课程管理系统在这三点上天然占优势业务模型清晰不会出现“选题太大做不完”的情况数据库表结构有典型的多对多关系选课有讲解深度前后端分离架构可以直接展示工程化能力换个角度想一个校园二手交易平台、一个社团管理系统、一个实验室预约系统本质上和课程管理系统属于同一类“业务CRUD 权限控制”的模式但课程管理系统的业务边界更贴近教育教学场景数据和逻辑关系更丰富复用性也更强。这个选题做完了后续想扩展成在线考试系统、培训报名平台底子都是现成的。1.2 功能模块全景展示我习惯在动手写代码前先把功能模块图画出来这比直接建表要高效得多。这个系统的核心模块可以划分为模块子功能涉及角色用户模块注册、登录、个人资料修改、密码重置全部课程模块课程发布、列表查询、详情展示、上下架教师、管理员选课模块选课、退课、选课名单查看、选课记录学生、教师分类模块课程分类维护、分类查询管理员公告模块公告发布与展示管理员、全部用户统计模块课程数量统计、选课人数排行管理员模块划分得越清晰后续的编码和论文章节就越好写。每个模块都可以对应论文里的一个小节页面截图、核心代码、测试用例全部能对上号。2. 技术选型SpringBoot、Vue、MySQL各自承担什么角色技术选型是论文第一章简介里必写的内容也是答辩时老师必然追问的方向。这套组合被称为“Java Web开发三板斧”三个角色在业务层、表现层、数据层各司其职各有一个非常清晰的定位。2.1 SpringBoot负责什么SpringBoot在系统里承担后端服务层的全部职责接收前端请求、处理业务逻辑、访问数据库、返回JSON数据。它的价值在于大幅降低了Spring框架的配置成本——以前搭一个SSH项目要写一堆XML配置文件现在SpringBoot通过自动配置和starter机制几行代码就能把一个可运行的Web服务拉起来。它内部集成了内嵌的Tomcat容器这意味着后端打完包就是一个可以直接执行的Jar文件部署时不用再单独装Tomcat或配置Web服务器。这一点对整个项目的交付和演示非常重要在答辩现场环境不可控的情况下一条java -jar命令能跑起来比什么都强。技术上核心会用到几个组件Spring MVC处理HTTP请求路由和参数绑定Spring Data / MyBatis负责数据库的ORM映射和持久化Spring Security 或 自定义拦截器负责登录鉴权和角色权限控制JWT前后端分离场景下的无状态token方案2.2 Vue在系统中的位置Vue负责的是整个用户界面层。前端通过Axios发请求访问后端接口拿到JSON数据后渲染成页面组件。Vue的核心价值在于组件化开发——把页面拆成一个个独立组件每个组件只关注自己那部分UI和逻辑头部导航栏、课程卡片、分页组件、表单弹窗都是可以复用的模块。Vue的响应式数据绑定让页面和业务数据天然联动修改了数组里的一个字段页面上对应的DOM会自动更新不需要手动操作DOM。这在做课程列表筛选、选课状态切换这些交互时效率极高。对于毕设项目而言Vue可以让前端代码的组织结构保持清晰Vue Router管路由、Vuex/Pinia管全局状态、Axios管网络请求整个前端工程化的思路可以在论文里单独成章。2.3 MySQL凭什么成为数据底座MySQL在系统里承担数据持久化的职责。SpringBoot应用程序一重启内存里的数据就可能丢失所有用户信息、课程数据、选课记录都必须落进数据库。MySQL选型的原因很朴素——开源免费、轻量可靠、生态成熟并且是Java技术栈联合使用频率最高的数据存储方案遇到问题网上资料一搜一大把。虽然很多公司实际生产环境会用PostgreSQL或收费版数据库但以课程管理系统的数据规模和并发量MySQL的性能和功能表现都绰绰有余。配合Navicat或DataGrip这类图形化工具建表、导数据、调试SQL都很方便。在论文里你可以顺带写一句“基于MySQL的事务机制保证选课操作的原子性和一致性”这句话就体现出你对数据库的理解不是停留在增删改查上。3. 数据库设计表结构才是整个系统的地基很多新手写毕设喜欢先写代码写到哪缺表就补一张表这种做法的坑会在后期全面爆发字段命名不统一、关系混乱、数据冗余、业务逻辑没法落地。正确顺序永远是先画ER图、建表、再写代码。课程管理系统的核心关系其实就一句话一个用户可以学多门课程一门课程可以被多个用户选。这是典型的多对多关系必须通过中间表来解耦。3.1 五张核心表的角色分配这里给出最精简且完整的五表方案表名作用关键字段user存储三类角色账号id、username、password、role、statuscourse_category课程分类id、name、sortcourse课程信息id、title、category_id、teacher_id、cover_url、statuscourse_enroll选课中间表id、course_id、student_id、enroll_time、statusnotice公告信息id、title、content、create_time需要特别注意的是中间表 course_enroll。它既承载“选课”这个业务动作的时间、状态选课中/退课也把course和user两张表的多对多关系拆成了两个一对多关系一个用户可以拥有多条选课记录一门课程也可以拥有多条选课记录。这条设计本身就可以作为论文里数据库设计章节的重点内容来写。3.2 字段设计里的几个讲究字段设计决定系统的扩展性和查询效率。基于实际踩坑经验下面几个点非常值得重视主键统一用 Long 自增 id不要用 UUID 做主键。自增索引在InnoDB下有更好的顺序写入性能密码字段不能存明文至少用BCrypt或MD5加盐处理。毕设虽不至于被攻击但这点体现了安全意识状态字段用 tinyint 而不用 varchar比如用户 status 0禁用/1正常、课程 status 0待审核/1已上架/2下架查询过滤时既快又不容易出错时间字段统一用 datetime配合数据库的 CURRENT_TIMESTAMP 默认值避免前后端时区不一致问题建表SQL参考用户表CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role varchar(20) DEFAULT STUDENT COMMENT ADMIN/TEACHER/STUDENT, phone varchar(20) DEFAULT NULL, email varchar(100) DEFAULT NULL, status tinyint(1) DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这里插一句踩坑提醒user是MySQL的保留字之一虽然目前多数版本不直接报错但某些场景下仍然可能触发解析问题。稳妥做法是表名加反引号user或者直接换成sys_user、t_user这类前缀命名。后端写SQL时要格外注意字段名不要和order、group等关键字冲突。3.3 ER关系图在论文里的呈现论文里的数据库设计章节通常需要放一张ER图。这里不推荐话大段文字描述而是建议用表格配合一段说明文字user 与 course 之间通过 course_enroll 关联一个学生可以选多门课一门课可以被多个学生选course 与 course_category 是多对一关系一个分类下可以有多门课程course 与 user教师是多对一关系一个教师可以发布多门课程ER图画清楚老师一眼就能判断你的业务逻辑是否成立。更重要的是你在答辩时要能指着ER图把每张表的主外键关系、中间表的作用讲清楚讲明白就是一次高质量的加分。4. 后端核心实现三层架构与登录鉴权后端代码的核心在三层架构和登录授权逻辑。这部分也是论文详细设计章节的主要素材来源。用SpringBoot MyBatis-Plus或MyBatis PageHelper的组合能省掉大量重复的DAO代码。4.1 Controller、Service、Mapper各管什么后端严格遵循分层结构这个结构不是套路而是有明确职权划分的Controller层接收前端请求、参数校验、返回统一响应体不写任何业务逻辑Service层承载所有业务规则例如选课时的重复校验、退课时的状态判断Mapper层操作数据库一个方法对应一条SQL或一个MyBatis-Plus条件构造器以“课程分页查询”为例Controller只负责把请求参数拿过来丢给ServiceRestController RequestMapping(/api/course) public class CourseController { Resource private CourseService courseService; GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { // Service层返回统一分页结果对象 return Result.ok(courseService.queryCoursePage(page, size, categoryId, keyword)); } }Service实现里如果用的是MyBatis-Plus代码非常简洁Service public class CourseServiceImpl extends ServiceImplCourseMapper, Course implements CourseService { Override public PageCourseVO queryCoursePage(Integer page, Integer size, Long categoryId, String keyword) { LambdaQueryWrapperCourse wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Course::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Course::getTitle, keyword) .eq(Course::getStatus, 1) // 只查已上架课程 .orderByDesc(Course::getCreateTime); return this.page(new Page(page, size), wrapper); } }这段代码里值得在答辩时展开讲的是条件构造器的用法eq条件里的第一个参数是布尔判断满足条件才拼接该SQL条件从而实现了“分类可选、关键词可选”的灵活查询避免手工拼接SQL导致的SQL注入风险。4.2 JWT登录鉴权怎么设计最稳妥前后端分离架构下最推荐的毕设方案不是把Spring Security全家桶都搬进来而是用“JWT工具类 HandlerInterceptor拦截器”的组合。原因很简单Spring Security的过滤器链和配置项对于毕设项目来说是黑盒一旦被老师追问细节就非常难过关。而JWT这套方案从生成token到解析token每一行代码都能讲出明确逻辑。登录接口的核心流程PostMapping(/login) public Result login(RequestBody LoginDTO loginDTO) { // 1. 根据用户名查用户 User user userService.getUserByUsername(loginDTO.getUsername()); // 2. 校验密码BCrypt加密存储 if (user null || !BCrypt.checkpw(loginDTO.getPassword(), user.getPassword())) { return Result.error(账号或密码错误); } // 3. 账号状态校验 if (user.getStatus() 0) { return Result.error(该账号已被禁用); } // 4. 签发JWT携带用户id和角色 String token JwtUtil.createToken(user.getId(), user.getRole()); return Result.ok(new LoginVO(token, user)); }JWT工具类里做的事情拆开讲就是用HMAC算法对payload用户id、角色、过期时间做签名生成一段三段式的token字符串。后续每次请求在请求头带上这个token后端通过拦截器解析就能确认请求者身份和权限。拦截器注册这里有个细节要放行登录、注册、课程列表查询等白名单接口只拦截需要身份的接口。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求避免跨域时OPTIONS请求被拦截 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.isEmpty(token) || !JwtUtil.validateToken(token)) { throw new BusinessException(401, 登录状态已失效); } // 解析token并存入request作用域后续Controller可直接取用户信息 request.setAttribute(userId, JwtUtil.getUserId(token)); request.setAttribute(role, JwtUtil.getRole(token)); return true; } }4.3 选课接口的唯一性约束和并发思考选课是整个系统业务逻辑的“题眼”也是答辩时老师最容易深挖的地方。选课接口如果只做“插入一条course_enroll记录”那只能算完成50%。真正的关键问题是同一个学生重复选同一门课怎么办最直接的做法是在Service层加校验代码Override Transactional(rollbackFor Exception.class) public void enroll(Long studentId, Long courseId) { Course course this.getById(courseId); if (course null || course.getStatus() ! 1) { throw new BusinessException(课程不存在或未上架); } // 重复选课校验 long count enrollMapper.selectCount(new LambdaQueryWrapperCourseEnroll() .eq(CourseEnroll::getCourseId, courseId) .eq(CourseEnroll::getStudentId, studentId) .eq(CourseEnroll::getStatus, 1)); if (count 0) { throw new BusinessException(您已选过该课程请勿重复选择); } // 插入选课记录 CourseEnroll enroll new CourseEnroll(); enroll.setCourseId(courseId); enroll.setStudentId(studentId); enroll.setStatus(1); enrollMapper.insert(enroll); // 课程选课人数1 courseMapper.incrementStudentCount(courseId); }这段代码里值得展开讲的重要底层原理是数据库层面的唯一性保障。如果只靠Java代码里的count校验在高并发场景下仍可能出现两个请求同时通过校验、插入两条重复记录的情况。更稳妥的做法是在course_enroll表上建唯一索引course_id student_id status让数据库在物理层面兜底。不过在毕设答辩阶段能把这个思考过程讲出来——先业务校验、再数据库约束兜底已经完全能体现你对并发和数据一致性问题的理解了。另外一个容易被忽略的细节是事务。选课操作涉及“插入选课记录”和“课程人数1”两个写操作必须放到同一个事务里要么全部成功、要么全部回滚。Transactional注解在这里不是装饰而是实实在在数据一致性保障。退课接口也是同理但退课时需要判断当前状态是否为“学习中”否则会重复退课。5. 前端实现Vue工程的结构与关键配置Vue项目最大的好处是文件结构清晰一个页面就是一个目录组件可以拆得稀碎再拼装前端代码量看起来也足够饱满。如果论文里要截图展示代码Vue的组件结构比一坨JS更直观。5.1 路由设计与权限控制前端路由建议按角色划分模块而不是把所有页面堆在一起。登录后跳转的首页要根据当前用户的角色动态决定// router/index.js 核心路由配置 const routes [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /dashboard, meta: { requiresAuth: true }, children: [ { path: dashboard, component: Dashboard, meta: { title: 首页 } }, { path: course/list, component: CourseList, meta: { title: 课程列表, role: [STUDENT, TEACHER, ADMIN] } }, { path: course/add, component: CourseEdit, meta: { title: 发布课程, role: [TEACHER] } }, { path: course/enroll-record, component: EnrollRecord, meta: { title: 我的选课, role: [STUDENT] } }, { path: user/manage, component: UserManage, meta: { title: 用户管理, role: [ADMIN] } } ] } ]路由守卫的逻辑是每次跳转前检查localStorage是否有token没有就强制回登录页有token还要额外检查目标路由的meta.role是否包含当前用户角色不匹配就跳回首页并给出无权限提示。这套route meta声明式权限写法简洁易读答辩时也很容易讲清楚。5.2 Axios封装与Token携带前端所有网络请求统一走封装过的Axios实例避免每个页面都写一遍拦截逻辑。Token统一在请求拦截器里从localStorage取出、放入请求头响应拦截器里统一处理业务状态码和HTTP异常// utils/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use(response { const res response.data // code为401表示token过期或未登录 if (res.code 401) { localStorage.removeItem(token) router.push(/login) } if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { Message.error(网络请求异常请检查后端服务是否启动) return Promise.reject(error) }) export default service这里的统一错误处理有一个很实际的体格价值后端如果启动失败、接口路径写错、参数类型不对页面不会白屏而是弹出一条清晰的错误提示这对于答辩演示时临场调试非常有用。5.3 页面组件拆分的思路以前端“课程列表页”为例可以拆成这些组件CourseList.vue页面入口负责数据加载和组合子组件CourseCard.vue单张课程卡片展示封面、标题、教师、选课人数SearchBar.vue分类筛选和关键词搜索Pagination.vue分页器父组件监听页码变化后重新请求接口组件拆分的好处是每个组件代码量控制在100行左右阅读负担小且同一套CourseCard可以在“课程列表”“我的课程”“推荐课程”多个页面复用。这也是前端章节论文素材的重要来源贴代码时只需贴一个组件并解释其props、emit设计即可。另外打包时有一个SpringBoot后端项目鲜有人注意的坑前端开发时和后端联调用的是代理地址但打包部署后接口地址可能变成了线上域名建议不要直接把接口地址硬编码在代码里而是通过.env.development和.env.production两个环境变量文件区分构建时自动切换。6. 打包部署与论文素材准备开发完的那一天不是结束真正让人熬夜的是部署和录屏截图。可能听上去很俗但这部分是决定最终交付体验的关键。6.1 后端打Jar包后端项目使用Maven构建在项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成一个可执行的Jar文件比如course-system.jar。在服务器或本地测试环境用java -jar course-system.jar就能启动。这里要提醒的是JDK版本必须和开发环境保持一致否则会报UnsupportedClassVersionError数据库连接地址、账号密码建议放配置文件打包时排除部署时用外部配置覆盖如果用了文件上传功能上传目录不要放在Jar包内部路径否则重启后文件会丢6.2 前端打包与Nginx配置前端在项目根目录执行npm run build生成dist目录。传统方案是把dist目录交给Nginx托管并配置反向代理解决跨域server { listen 80; server_name localhost; location / { root /usr/share/nginx/html/dist; index index.html; # 解决Vue Router刷新404问题 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; } }这个配置里try_files是关键。如果不加这行在Vue Router的history模式下用户刷新/course/list页面时Nginx会找不到了真实文件而报404。增加try_files后所有前端路由都回退到index.html由Vue Router接管。如果想完全省掉这个配置也可以在路由初始化时改成createWebHashHistory()路径会变成/#/course/list形式。6.3 论文结构和素材积累方法论文结构建议直接对标开发流程各学校模板不同但骨架相通绪论背景、意义、国内外现状、技术选型需求分析角色分析、用例图、功能与非功能需求概要设计架构图、功能模块划分、数据库设计ER图 表结构详细设计核心流程、类图、接口设计、时序图系统实现页面截图 关键代码 功能说明系统测试测试用例表、测试结果、结论总结与展望完成内容、不足、改进方向素材积累最重要的原则是“边开发边截图”。很多学生最后一周才开始补截图结果系统早就被改得跟论文描述对不上了。建议每完成一个模块就顺手记录测试数据和页面效果后续写实现章节就不是从零开始而是直接整理素材。测试数据也一个重要技巧用脚本一次性生成几十个用户、课程和选课记录截图才好看统计图才有数据量。7. 实测踩坑记录新手最容易卡住的四类问题这一节的价值来自真实调试经验。下面这些问题基本每个做这个项目的同学都会碰到提前排雷能省下大量时间。7.1 跨域问题前后端分离联调时前端地址是http://localhost:5173开发服务器后端是http://localhost:8080浏览器会拦截跨域请求。常见表现就是前端请求报CORS错误请求根本到不了后端。如果后端用了拦截器跨域配置必须和拦截器配置共存才能正确生效。推荐实现WebMvcConfigurer统一配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这段代码里最坑的一点是allowCredentials(true)时allowedOrigins不能用*必须用allowedOriginPatterns(*)否则浏览器依然会拦截。前端请求会先发OPTIONS预检拦截器必须放行。7.2 MySQL驱动和时区导致连接失败使用较新版本的MySQL驱动比如8.x时数据库连接URL必须加上时区参数否则启动报时区异常spring: datasource: url: jdbc:mysql://localhost:3306/course_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver历史老项目中常见com.mysql.jdbc.Driver写法在MySQL 8版本下会提示已废弃或报ClassNotFoundException需要改成com.mysql.cj.jdbc.Driver。这个报错信息非常迷惑人很多新手以为是驱动没导入其实只是类名写旧了。7.3 前端打包后接口调不通开发模式一切正常打包部署后接口全挂多半是因为baseURL写死了开发环境的地址。上述proxy_pass方案已经能解决跨域后端就不用在代码里写CrossOrigin。前端生产环境请求路径建议直接用相对路径/api由Nginx转发到后端。还有一个比较隐蔽的问题是后端上下文路径。如果后端的server.servlet.context-path配了/api前端baseURL就不要再带/api前缀否则等于出现/api/api/login。我见过学员在这里调了大半天最后发现路径重复了。7.4 答辩演示的临场翻车场景演示阶段最容易翻车的几个问题提前照做即可规避演示前用测试账号录好一段“从登录到选课成功”的完整流程视频哪怕现场数据库被还原也有完整素材数据库初始化脚本准备好演示前一键重置数据防止上次调试把某个账号状态改了或数据弄脏后端启动方式使用java -jar而不是直接在IDEA里跑避免临场依赖开发环境准备一份“功能速查表”每个角色能做什么操作分别对应哪个菜单、哪个接口答辩时老师问起来对答如流写在最后整套系统做下来我个人最大的体会是毕业设计不在于引入多高级的技术而在于把一件小事的链路完整走通。从业务分析到数据库设计从后端接口到前端联调从本地测试到部署上线每一个环节都不算难但合在一起需要的是条理和耐心。课程管理系统恰好是这样一条完整链路的最优载体做完之后无论是对SpringBoot的理解、对数据库设计的敏感度还是对前后端分离工程化流程的体感都会有一个质变。如果你正卡在这个项目的某个环节建议从数据库的表关系重新捋一遍十有八九问题就出在最底层的数据模型上。项目做完以后想继续扩展的话可以往在线考试、视频点播、线上作业提交这些方向延伸底子都是这套代码的变体。最后再分享一个小技巧整个项目里最值得你多花时间打磨的不是代码写得多么炫而是“演示数据的真实性”。预先准备一套带真实感的课程数据——课程名像模像样、封面图风格统一、选课数量有梯度演示效果会立刻高一个档次。这个细节往往比你在代码里多写几个接口更让人眼前一亮。