SpringBoot+Vue+MySQL在线课程管理系统设计与实现
发布时间:2026/9/9 4:24:06来源:尧图网络
又是一套被问烂了但永远有人需要的“在线课程管理系统”后端SpringBoot、前端Vue、数据库MySQL三件套整整齐齐。说实话这类项目在GitHub和各大源码站上一抓一大把但真正能直接跑起来、结构还清晰的反而没几个。我手里正好维护着一套能用的版本前后端分离该有的功能都有最关键的是——它在Windows和Mac上都能顺利启动不需要你翻山越岭去改一堆配置。今天就把这套系统的设计思路、核心实现和部署过程中的那些坑一次性讲清楚。这套东西适合谁一是拿来做毕业设计的在校生二是想学SpringBootVue全栈但一直停留在看教程阶段的新手三是确实需要一个轻量教学管理后台的培训机构和中小型教育团队。它解决的核心问题就一个让你用最少的时间跑通一套完整的前后端分离项目并且能看懂每个模块为什么这么写、每个表为什么这么建。1. 系统整体设计与技术栈选型1.1 为什么是SpringBootVueMySQL这套组合先说说技术栈的选型逻辑。SpringBoot在Java后端领域的统治地位不用我废话它最大的价值不是性能有多极致而是把SSM那套繁琐的XML配置全部消灭了一个注解搞定Bean管理一个application.yml搞定数据源和端口配置对中小型系统来说开发效率是真的高。Vue火到现在核心原因就一条组件化开发。像课程列表、轮播图、个人中心这类的UI块抽成组件后能在不同页面复用维护成本直线下降。配合Vue Router做SPA页面切换用户体验接近原生App不需要每次点击都刷新整个页面。MySQL作为关系型数据库在数据一致性要求高的场景下仍然是首选。课程管理涉及用户、订单、选课关系等多张表的关联查询用MySQL的事务机制能保证数据不会写到一半断电就丢了。这套组合的分工很清晰SpringBoot只负责提供RESTful API不掺和页面渲染Vue只负责页面交互通过Axios发请求拿数据MySQL在底层老老实实存数据。三层解耦每一层都能单独替换这也是为什么企业招聘对这三个技术栈的需求常年排在前列。1.2 功能模块划分与角色权限设计整套系统分了三个角色学生、教师、管理员。每个角色看到的功能入口完全不同这是通过后端的权限拦截器和前端的路由守卫双保险实现的。学生端注册登录、浏览课程列表、查看课程详情、选课、观看课程视频、查看已选课程、提交课程评价教师端创建课程、管理课程章节、上传课程视频、查看选课学生列表、回复课程评价管理端用户管理禁用/启用账号、课程审核上架/下架、分类管理、数据统计概览权限这块用的是JWTJSON Web Token方案。用户登录成功后后端生成一个Token返回给前端前端存在localStorage里每次请求都在Header里带上Authorization: Bearer token。后端通过拦截器解析Token获取用户角色再判断当前请求是否有权限访问。这里有个设计细节值得说前端路由守卫只控制“看得到的页面”真正拦住违权操作的是后端的接口拦截。比如学生手动调用教师的创建课程接口后端一定要返回403不能光靠前端藏按钮这个安全意识从一开始就要养成。1.3 项目目录结构与代码规范拿到源码后先看目录结构。后端遵循标准的Maven分模块结构course-backend/ ├── src/main/java/com/course/ │ ├── controller/ # 控制器层接收请求 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis接口层 │ ├── entity/ # 实体类 │ ├── dto/ # 数据传输对象 │ ├── config/ # 配置类WebMvc、跨域、JWT拦截器 │ ├── common/ # 统一返回值、异常处理 │ └── utils/ # 工具类JWT工具、MD5加密 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML文件 │ └── application.yml └── pom.xml前端目录则遵循Vue CLI的标准结构course-frontend/ ├── public/ ├── src/ │ ├── api/ # 接口请求封装 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── store/ # Vuex状态管理 │ ├── views/ # 页面视图 │ ├── utils/ # 工具函数 │ ├── App.vue │ └── main.js └── package.json前后端分离项目的目录结构直接影响团队协作效率。我看到不少新手项目把Controller里塞满了业务代码Service层形同虚设导致后期想加个缓存、加个事务都无从下手。这套源码的层级划分比较干净你照着分层规范去改代码不会越改越乱。2. 核心细节解析与实操要点2.1 数据库表设计五张核心表的关系梳理这套系统的MySQL库一共8张表最关键的是这五张表名说明关键字段user用户表学生/教师/管理员id, username, password, role, statuscourse课程表id, title, cover, teacher_id, category_id, statuschapter章节表id, course_id, title, sortvideo视频表id, chapter_id, url, durationuser_course选课关系表id, user_id, course_id, create_time设计上有一个关键点user_course是中间表把用户和课程做成多对多关系。一个学生可以选多门课一门课可以被多个学生选中间表的存在就是用来化解这种复杂关系的。user表的role字段用int类型0管理员、1教师、2学生不搞字符串理由很简单数字在索引和比对时效率更高而且后续扩展角色不用改表结构。course表的teacher_id关联user表的id没有设置实际的外键约束但逻辑上存在关联关系。这样做的原因是机房环境里经常有人手动删数据物理外键容易导致删除失败线上系统的普遍做法是保留逻辑关联而不是物理约束ORM层面控制好就行。建表SQL文件在源码的sql/目录下文件名带日期比如course_db_20240101.sql。导入之前先确认字符集CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4是必须的因为课程评价里可能有Emoji表情老的utf8存不了四字节字符一存就报错。这个坑我见过太多次了。2.2 后端接口设计RESTful风格与统一返回格式先看后端接口的返回格式这是前后端能顺利协作的第一步。源码里定义了一个Result类所有接口都返回这个统一结构public class ResultT { private Integer code; // 200成功4xx业务错误500系统错误 private String message; // 提示信息 private T data; // 数据体 public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message 操作成功; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }前端拿到返回体后if (res.data.code 200) { // 正常渲染数据 } else { // 弹出错误提示 }这种统一返回格式的意义在于前端拦截器可以集中处理错误码不用每个接口单独判断。比如Token过期时后端返回401前端Axios拦截器里统一跳转登录页一次写好全项目通用。接口路径严格遵循RESTful规范POST /api/user/login # 登录 GET /api/course/page?page1size10 # 分页查询课程 POST /api/course # 创建课程教师权限 PUT /api/course/{id} # 修改课程教师/管理员 DELETE /api/course/{id} # 删除课程管理员权限 POST /api/user/course # 学生选课 GET /api/user/course/my # 查看已选课程动词全部交给HTTP方法路径只放资源名这个习惯从搭框架第一天就要养成。有不少人喜欢写/api/getCourseByTeacherId这种也不是不行但长线维护下来RESTful风格的优势会越来越明显——看到路径就能猜出语义新成员接手时的沟通成本低很多。2.3 关键实现拆解选课业务与事务管理选课这个操作看似简单实际上包含了事务和并发两个经典问题。源码中StudentCourseServiceImpl的选课方法值得细看Override Transactional(rollbackFor Exception.class) public ResultString selectCourse(Long courseId, Long userId) { // 1. 校验课程是否存在且已上架 Course course courseMapper.selectById(courseId); if (course null || course.getStatus() ! 1) { return Result.error(400, 课程不存在或已下架); } // 2. 校验是否已经选过 Integer count userCourseMapper.checkSelected(userId, courseId); if (count 0) { return Result.error(400, 请勿重复选课); } // 3. 插入选课记录 UserCourse userCourse new UserCourse(); userCourse.setUserId(userId); userCourse.setCourseId(courseId); userCourse.setCreateTime(new Date()); int rows userCourseMapper.insert(userCourse); if (rows 0) { return Result.error(500, 选课失败请联系管理员); } return Result.success(选课成功); }有几个细节说明一下Transactional(rollbackFor Exception.class)把整个方法包进事务只要中间任何一步抛异常前面的插入操作都会回滚。如果不加这个注解可能出现“用户课表多了记录但订单没生成”的数据不一致情况。条件判断全部前置先校验后操作减少无谓的数据库写入。这里没有加Redis分布式锁对于课容量不限制的在线学习平台来说单机下的乐观锁和去重表设计已经够用。如果是抢课业务就得考虑更严密的并发控制方案。关于事务我再多说一句Transactional只对运行时异常RuntimeException生效如果代码里捕获了异常不抛出去事务是不会回滚的这是初学者最容易踩的坑之一。3. 实操过程与核心环节实现3.1 环境准备JDK、Maven、Node.js、MySQL的版本搭配在把项目跑起来之前先把环境对齐。这套系统的版本是有讲究的配错了轻则启动报错重则依赖冲突出一堆看不懂的异常。我建议的环境搭配是组件版本说明JDK1.88u202及以上SpringBoot 2.x基于JDK8开发最稳定Maven3.6.3与JDK8兼容性最好MySQL5.7或8.0都支持注意驱动版本就行Node.js16.x或18.x对应Vue CLI 4.x/5.xVue CLI4.5.13及以上脚手架版本不要太旧IDEA2021.3建议使用专业版社区版也够用特别提醒一下MySQL 8.0的坑8.0的默认密码插件是caching_sha2_password而SpringBoot 2.x自带的MySQL驱动版本如果过低连8.0数据库会报Public Key Retrieval is not allowed错误。解决办法有两个把驱动升级到mysql-connector-java 8.0.x在连接串里加上allowPublicKeyRetrievaltrueuseSSLfalse源码里已经处理过这个问题pom.xml中MySQL驱动的版本锁定在8.0.33dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency如果你用5.7的数据库这个驱动也能兼容不用改。3.2 后端启动流程0到1跑通SpringBoot后端启动在IDEA里操作顺便把application.yml里的关键配置也说清楚server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.course.entity configuration: map-underscore-to-camel-case: true jwt: secret: your_jwt_secret_key_here expire: 604800000注意三处serverTimezoneAsia/Shanghai必须加否则MySQL 8.0会在日期处理上报时区错误。map-underscore-to-camel-case: true开启下划线转驼峰这样数据库字段create_time能自动映射到实体类的createTime属性不用每个字段都写Result注解。JWT密钥建议改成自己的随机字符串别用源码里默认的毕竟是从网上下的项目默认密钥等于裸奔。启动步骤就三步打开IDEAFile - Open选择course-backend目录等待Maven自动下载依赖第一次会比较慢可以配置阿里云镜像加速找到CourseApplication.java右键Run即可启动成功后控制台输出一行Started CourseApplication in 3.2 seconds类似的信息然后在浏览器访问http://localhost:8080/api/user/info能看到JSON返回就说明后端已经活了。3.3 前端启动流程Node依赖安装与跨域调试前端启动前先确认Node环境node -v npm -v然后进到course-frontend目录npm config set registry https://registry.npmmirror.com npm install用国内镜像源的原因不用多说几十个依赖包从官方源下载等得花儿都谢了。npm install完成后执行npm run serve默认会启动在http://localhost:8081如果8080被占会自动切换到8081。启动完成后浏览器自动打开能看到登录页面就算成功。前后端分离的项目必然面临跨域问题。前端在8081端口后端在8080端口浏览器出于同源策略会拦截前端发往8080的请求。源码在SpringBoot后端做了跨域配置Config包下有个CorsConfig.javaConfiguration 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); } }这段配置的意思是允许任意来源、任意方法的跨域请求。开发阶段这么配完全没问题但上线前一定要收紧只允许你自己的域名访问否则等于给别人留了一把打开你API的钥匙。3.4 用一个完整业务流程串联前后端用户登录我们通过“用户登录”这个最基础的流程把前后端的协作逻辑完整过一遍你就知道整套系统是怎么串起来的了。第一步前端发起请求用户在登录页输入账号密码点击登录按钮Vue组件里调用登录接口// src/api/user.js import request from /utils/request; export function login(data) { return request({ url: /api/user/login, method: post, data }); }request.js里做了一件关键的事从localStorage读取Token附加到请求头// src/utils/request.js import axios from axios; import { Message } from element-ui; import router from /router; const service axios.create({ baseURL: /api, // 配合vue.config.js中的代理 timeout: 10000 }); // 请求拦截器 service.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); // 响应拦截器 service.interceptors.response.use( (response) { const res response.data; if (res.code 200) { return res; } Message.error(res.message); return Promise.reject(new Error(res.message)); }, (error) { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录); localStorage.removeItem(token); router.push(/login); } else { Message.error(网络异常请稍后再试); } return Promise.reject(error); } ); export default service;baseURL: /api配合vue.config.js里的代理配置开发环境下把请求转发到后端8080端口从而绕开跨域限制// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };第二步后端校验逻辑后端的登录接口在UserController里PostMapping(/login) public ResultLoginDTO login(RequestBody LoginVO loginVO) { String username loginVO.getUsername(); String password loginVO.getPassword(); if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { return Result.error(400, 用户名和密码不能为空); } User user userService.login(username, MD5Util.md5(password)); if (user null) { return Result.error(400, 用户名或密码错误); } if (user.getStatus() 0) { return Result.error(403, 账号已被禁用请联系管理员); } // 生成Token String token JwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); // 返回用户信息和Token LoginDTO loginDTO new LoginDTO(); loginDTO.setToken(token); loginDTO.setUserinfo(user); return Result.success(loginDTO); }注意密码是MD5Util.md5(password)不是明文比对。虽然MD5在今天看来安全性偏弱但对于毕设和中小系统已经够用更稳妥的做法是加盐后再MD5或者直接用BCrypt这个看你的时间成本。第三步前端保存状态并跳转登录成功后前端把Token存到localStorage同时通过Vuex保存用户信息然后根据角色跳转到不同页面login(form).then(res { localStorage.setItem(token, res.data.token); localStorage.setItem(userInfo, JSON.stringify(res.data.userinfo)); const role res.data.userinfo.role; // 0-管理员1-教师2-学生 let redirectPath /student/home; if (role 0) redirectPath /admin/dashboard; if (role 1) redirectPath /teacher/home; router.push(redirectPath); }).catch(() { // 错误提示已由拦截器统一处理 });至此一次完整的用户登录流程就串通了。其他业务模块的逻辑大同小异套路都是前端发请求、后端做校验和数据处理、返回统一格式的JSON、前端渲染。4. 常见问题与排查技巧实录4.1 数据库连接失败问题这是出现频率最高的问题报错信息一般是Cannot create PoolableConnectionFactory (Access denied for user rootlocalhost)或者Communications link failure CommunicationsException: Communications link failure排查顺序从三方面走检查application.yml里的用户名密码确保和本地MySQL一致。MySQL的root密码经常被忘记建议在MySQL 8.0里单独创建一个业务账号而不是一直用rootCREATE USER courselocalhost IDENTIFIED BY course123; GRANT ALL PRIVILEGES ON course_db.* TO courselocalhost; FLUSH PRIVILEGES;检查MySQL是否已启动。Windows下按WinR输入services.msc找到MySQL80或对应版本服务确认状态是“正在运行”。检查端口。3306端口被占用时连接也会失败。用netstat -ano | findstr 3306查看谁占了3306如果是其他程序占用要么改MySQL的端口要么改SpringBoot的连接串。注意如果MySQL控制台能登录但Java连不上大概率是serverTimezone参数缺失加上就对了。4.2 前端依赖安装报错Node.js版本过高或过低都会导致一些依赖包安装失败。Vue CLI 5.x搭配Node 18.x基本没问题但如果你用的是Node 21有概率碰到OpenSSL相关的ERR_OSSL_EVP_UNSUPPORTED错误。解决方案是在package.json的scripts里加上serve: set NODE_OPTIONS--openssl-legacy-provider vue-cli-service serveLinux/Mac环境用serve: export NODE_OPTIONS--openssl-legacy-provider vue-cli-service serve这个问题的本质是Node 17的OpenSSL默认策略变更老版本Webpack用了不兼容的哈希算法。从根上解决的办法是锁定Node LTS版本16.x或18.x一劳永逸。4.3 前端正常但接口404前端页面能打开但登录时报404这是一个典型问题多半是vue.config.js中的代理配置没有生效。检查顺序确认前端是通过npm run serve开发服务器启动的而不是直接在浏览器里打开HTML文件。确认vue.config.js里proxy.target指向后端的地址端口后端启动在8080target就得是8080。修改配置后必须重启前端服务CtrlC停掉再npm run serve代理修改不会热更新。还有一个常见原因后端的Context Path设置。如果application.yml里配了server: servlet: context-path: /course那么所有接口路径都要加/course前缀即/course/api/user/login而前端代理还指向/api就会404。这套源码默认没有配context-path你如果自己加了记得同步改前端。4.4 跨域问题处理不当后端配了CorsConfig前端也配了代理但有时候仍然报跨域错误。这种时候要确认是不是走上了别的请求路径。跨域错误的表现是浏览器控制台出现Access to XMLHttpRequest at http://localhost:8080/api/user/login from origin http://localhost:8081 has been blocked by CORS policy最常见的原因是后端的allowedOriginPatterns(*)被拦截器截胡了。如果JWT拦截器在CorsConfig之前先处理了OPTIONS预检请求就会导致跨域失败。源码里已经处理了这个问题在JwtInterceptor中放行了预检请求if (OPTIONS.equals(request.getMethod())) { return true; }如果你自己改造时遇到类似问题先加上这个放行逻辑再说。4.5 表格问题速查问题描述报错关键字解决方案数据库连接失败Access denied核对账号密码授权或重建用户时区错误serverTimezone连接串加上serverTimezoneAsia/Shanghai端口被占用Port already in use修改后端server.port或前端devServer.port热门依赖下载失败NETWORK ERROR切换npm镜像源npm config set registry https://registry.npmmirror.comMaven依赖下载慢Could not transferpom.xml加阿里云镜像或IDEA中配置Maven镜像登录后接口403Forbidden检查Token是否过期检查用户状态是否被禁用Vue页面白屏Cannot read properties of undefinedF12看Console报错多半是API路径漏了或字段名不匹配中文乱码数据库插入后显示问号建表库时指定utf8mb4字符集连接串加characterEncodingutf8mb4MySQL 8.0认证失败Authentication plugin给用户改成mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY password;前端端口冲突Port 8081 is already in use修改vue.config.js的devServer.port4.6 从源码库里学到的三个实战技巧这套系统虽然不算复杂但有几个细节值得学习技巧一分页参数封装后端的分页接口都用了统一的PageVO对象public class PageVOT { private ListT records; private Long total; private Integer current; private Integer size; }配合MyBatis的PageHelper插件一行代码实现分页查询前端接收这个结构后配合Element UI的el-pagination组件刚好对得上。技巧二系统初始化数据course_db.sql里预置了管理员账号admin/admin123、教师和学生账号方便测试。但上线前务必删除初始测试账号连密码带账号都改掉这是安全底线。技巧三文件上传路径配置视频上传功能里的文件保存路径在application.yml中配置file: upload-path: D:/course-upload/ access-path: /upload/**这类路径配置建议统一放在配置文件里管理不要写死在代码中。换服务器时改一行配置就能迁移不用全局搜索替换。5. 后续扩展从毕设项目到生产级系统的升级路线如果停留在这个层面它只是一个毕业设计。但如果你真想把这个系统用在真实教学场景有几处要补强的地方第一个是密码加密升级。把MD5换成BCrypt算法Spring Security已经内置了BCryptPasswordEncoder改造起来成本不高但安全性提升了一个量级。第二个是接口限流。学生选课高峰期大量并发打到后端单机全靠数据库扛会用性能风险。建议在选课接口加Redis缓存限流用令牌桶或滑动窗口控制QPS。第三个是视频点播优化。现在的实现是直接返回视频文件URL大并发下带宽扛不住。可以对接阿里云OSS或腾讯云COS利用CDN分发视频流量这是真实场景下课程平台的标配方案。第四个是日志巡检。给系统接入logbackELK或至少用spring-boot-starter-log4j2把日志切分归档不然出了问题连排查的入口都没有。第五个是测试用例。补上JUnit单元测试和MockMvc接口测试覆盖核心的登录、选课、课程管理业务流程。这套源码目前测试代码基本是空的有精力的话建议补一补面试时讲到这也是加分项。这套系统从编码到调通再到部署我前前后后跑了不下十遍每一遍都能发现一些小问题。不是代码本身问题而是环境的差异实在太大了——不同版本的JDK、Maven、MySQL之间互相掐架是常态。不过也正因为如此调试过程中积累的这些经验恰恰是你在面试和实际工作中最值钱的部分。希望这份拆解能帮你少走一些弯路把时间花在真正需要打磨的业务逻辑和代码质量上。
网站建设高端定制企业官网