
简介这是一套面向计算机专业本科生及毕业设计指导教师的课程教学平台完整源码基于SpringBootVue实现前后端分离架构解决高校在线教学管理中课程发布、作业布置、成绩查询与师生互动等核心需求。资源共719个文件包含163个Java后端业务逻辑文件、123个Vue组件页面、159个SVG图标资源、86张JPG教学素材图及63个JS交互脚本辅以SQL建表语句、YML配置、BAT部署脚本和详尽的源码说明文档压缩包大小21.38MB。已有45人学习下载适合开展毕业设计、课程实训或二次开发实践。读者可直接获取模块化清晰的工程结构用户/课程/作业三大子系统、MySQL数据库设计蓝图、Spring Security权限控制实现、Vue单页应用路由与API调用范式以及含install.bat、run.bat在内的开箱即用部署方案。1. 为什么用 SpringBoot Vue 做课程教学平台不是“堆技术”而是解决真问题你可能已经见过太多“SpringBoot Vue 毕设项目”——名字雷同、功能相似、部署即崩。但真正跑起来的课程教学平台核心痛点从来不是“能不能写出来”而是教师上传 PPT 后学生打不开、课件 PDF 在线预览白屏、作业提交后时间戳错乱、多班级排课冲突校验失效、Vue 路由嵌套三层后导航守卫失灵、SpringBoot 接口返回中文字段在前端被转义成乱码……这些不是玄学是真实课堂场景下每天发生的“教学流中断”。本项目基于SpringBoot和vue的课程教学平台的设计与实现_f2840y9o_245-QQ.zip之所以能落地关键在于它把 SpringBoot 的事务控制、文件流处理、定时任务能力和 Vue 的动态路由、组件懒加载、PDF 预览封装、表单校验链路做了教学业务语义级对齐比如“发布课程公告”操作后端不是简单存一条 record而是自动触发邮件通知 学生端未读红点 教师端已读统计前端不是渲染一个 div而是绑定v-model.lazy防抖 keydown.enter.prevent防误提交 el-upload自定义 request 适配 SpringBoot 的MultipartFile接收逻辑。它适合两类人一是需要交付可演示、可答辩、可扩展的毕设/实训项目的同学尤其关注vue打包放进springboot中的实际路径二是想快速验证教学类 SaaS 核心模块课程管理、作业批改、在线测验是否能闭环的一线开发——不讲架构图只讲哪一行代码改了能让 PDF 在 Chrome 和 Edge 都正常显示。2. 从零搭建SpringBoot 后端服务与 Vue 前端工程的最小可行联调方案2.1 初始化 SpringBoot 项目选型依据与关键依赖锁定本项目采用 SpringBoot 2.7.18非最新版原因很实在SpringBoot 3.x 默认要求 JDK 17而高校机房、学生笔记本普遍仍是 JDK 8 或 11且 2.7.x 对 MyBatis-Plus 3.5.x、Shiro 1.10.x 兼容性更稳避免springboot版本太高导致的Failed to load tsconfig类编译错误传导到后端。使用 Spring Initializr 创建基础工程时必须勾选以下依赖其他如 Lombok、DevTools 可选!-- pom.xml 关键依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency提示spring-boot-starter-thymeleaf不是为了做模板渲染而是为后续vue打包放进springboot中提供静态资源托管能力——Thymeleaf 的templates/目录可被 SpringBoot 自动识别为视图根路径而static/目录则用于存放 Vue 打包后的index.html及js/css文件。这是实现前后端同源部署的底层支撑比 Nginx 反向代理更轻量、更适合毕设演示。2.2 Vue CLI 工程初始化与教学场景定制化配置使用 Vue CLI 4.5.19非 Vite创建项目原因明确Vite 在vue播放m3u8免安装场景下需额外配置videojs-player/vue插件而 Vue CLI 4.x 对vue-router动态路由、vuex状态持久化、axios请求拦截器的集成更成熟且与 SpringBoot 的CrossOrigin注解配合更少踩坑。执行命令vue create course-platform-frontend # 选择 Manually select features → Router (History mode), Vuex, CSS Pre-processors (Sass), Linter / Formatter初始化后立即修改vue.config.js关键配置如下// vue.config.js const path require(path) module.exports { // 1. 输出路径必须与 SpringBoot static 目录对齐 outputDir: path.resolve(__dirname, ../backend/src/main/resources/static), // 2. 开发服务器代理解决跨域仅开发用 devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, // SpringBoot 默认端口 changeOrigin: true, pathRewrite: { ^/api: } } } }, // 3. 关闭 eslint 强校验避免 vue安装依赖 后大量报错干扰开发 lintOnSave: false, // 4. 配置 public 目录资源拷贝如 favicon.ico、pdf.worker.min.js configureWebpack: { resolve: { alias: { : path.resolve(__dirname, src) } } } }参数说明outputDir指向 SpringBoot 项目的static目录是vue打包放进springboot中的物理落点devServer.proxy是开发阶段的救命配置让http://localhost:8080/api/course/list实际请求http://localhost:8081/course/list避免浏览器 CORS 报错lintOnSave: false是血泪经验——毕设阶段优先保证功能跑通ESLint 规则可在答辩前统一修复。2.3 前后端联调第一步打通登录接口并验证跨域与会话SpringBoot 后端编写/api/auth/login接口使用RestController// LoginController.java PostMapping(/api/auth/login) public ResultVO login(RequestBody LoginDTO dto, HttpServletRequest request) { // 1. 查询用户此处省略密码校验逻辑 User user userService.getByUsername(dto.getUsername()); if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return ResultVO.fail(用户名或密码错误); } // 2. 生成 token使用 JWT非 Session String token jwtUtil.generateToken(user); // 3. 设置响应头关键Vue axios 需要手动携带 response.setHeader(Authorization, Bearer token); return ResultVO.success(登录成功, user); }Vue 前端调用该接口src/api/auth.js// api/auth.js import request from /utils/request export function login(data) { return request({ url: /api/auth/login, method: post, data }) }其中request封装src/utils/request.js必须启用withCredentials// utils/request.js import axios from axios const service axios.create({ baseURL: process.env.NODE_ENV production ? / : /, // 生产环境走相对路径 timeout: 10000, withCredentials: true // ✅ 关键允许携带 cookie若用 Session或 Authorization 头若用 JWT }) service.interceptors.response.use( response { // 从响应头读取 tokenJWT 场景 const token response.headers.authorization if (token token.startsWith(Bearer )) { localStorage.setItem(token, token.substring(7)) } return response.data } )逻辑说明withCredentials: true是跨域请求携带认证信息的开关没有它SpringBoot 的CrossOrigin(origins *, allowCredentials true)注解无效baseURL: /确保生产环境下所有请求路径相对于根目录与 SpringBoot 的static/静态资源托管路径一致response.headers.authorization提取 JWT token 并存入localStorage为后续所有请求提供Authorization头。3. 教学核心功能落地课程管理、课件上传与 PDF 在线预览的完整链路3.1 课程 CRUD 与多角色权限隔离教师/学生/管理员后端使用 MyBatis-Plus 的QueryWrapper构建动态查询条件区分角色数据权限// CourseController.java GetMapping(/api/course/list) public ResultVO list(RequestParam(required false) String keyword, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, HttpServletRequest request) { // 1. 从 JWT 解析当前用户工具类封装 User currentUser jwtUtil.getCurrentUser(request); // 2. 教师只能查自己开的课学生查已选课管理员查全部 QueryWrapperCourse wrapper new QueryWrapper(); if (TEACHER.equals(currentUser.getRole())) { wrapper.eq(teacher_id, currentUser.getId()); } else if (STUDENT.equals(currentUser.getRole())) { // 关联 student_course 表查已选课程 wrapper.inSql(id, SELECT course_id FROM student_course WHERE student_id currentUser.getId()); } if (StringUtils.isNotBlank(keyword)) { wrapper.like(name, keyword).or().like(description, keyword); } PageCourse pageObj new Page(page, size); IPageCourse result courseService.page(pageObj, wrapper); return ResultVO.success(result); }Vue 前端使用vue动态路由实现菜单权限控制// router/index.js const routes [ { path: /course, component: () import(/views/layout/Layout.vue), children: [ { path: list, name: CourseList, component: () import(/views/course/List.vue), meta: { roles: [ADMIN, TEACHER, STUDENT] } // ✅ 角色白名单 }, { path: create, name: CourseCreate, component: () import(/views/course/Create.vue), meta: { roles: [TEACHER] } // 仅教师可创建 } ] } ] router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) return next(/login) const user JSON.parse(localStorage.getItem(user) || {}) const hasRole to.meta.roles?.includes(user.role) if (!hasRole) { next(/403) // 无权限跳转 } else { next() } })参数说明meta.roles是路由元信息存储角色数组router.beforeEach全局守卫在每次跳转前校验当前用户角色是否匹配目标路由所需角色inSql是 MyBatis-Plus 提供的原生 SQL 片段插入方式用于复杂关联查询比Select注解更易维护。3.2 课件上传与存储解决vue image能显示pdf吗的本质问题课件上传分两步前端文件选择 后端接收存储。关键在于PDF 不是图片不能直接img srcxxx.pdf显示必须通过 PDF.js 渲染。Vue 前端使用el-upload组件Element UI!-- views/course/Upload.vue -- template el-upload classupload-demo action/api/file/upload :http-requestcustomUpload :on-successhandleSuccess :on-errorhandleError :limit1 :auto-uploadfalse el-button slottrigger sizesmall typeprimary选取文件/el-button /el-upload /template script export default { methods: { customUpload({ file, onSuccess, onError }) { const formData new FormData() formData.append(file, file) this.$axios.post(/api/file/upload, formData, { headers: { Content-Type: multipart/form-data } }).then(res { onSuccess(res.data) }).catch(err { onError(err) }) }, handleSuccess(response) { this.$message.success(上传成功) this.fileUrl response.data.url // 后端返回的访问路径 this.previewPdf() // 触发 PDF 预览 } } } /scriptSpringBoot 后端接收并保存文件FileController.javaPostMapping(/api/file/upload) public ResultVO upload(RequestParam(file) MultipartFile file) throws IOException { if (file.isEmpty()) { return ResultVO.fail(文件不能为空); } // 1. 生成唯一文件名防重名 String originalFilename file.getOriginalFilename(); String extension FilenameUtils.getExtension(originalFilename); String newFilename UUID.randomUUID().toString() . extension; // 2. 保存到服务器指定目录非 static避免被直接访问 String uploadPath D:/course-files/; // 生产环境建议用 NAS 或 MinIO File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); file.transferTo(new File(uploadPath newFilename)); // 3. 返回可访问 URLSpringBoot 静态资源映射 String url /files/ newFilename; // 通过 WebMvcConfigurer 添加 /files/** 映射到 uploadPath return ResultVO.success(上传成功, url); }逻辑说明MultipartFile是 SpringBoot 对文件上传的标准抽象transferTo()安全写入磁盘/files/**映射需在WebMvcConfigurer中配置确保http://localhost:8081/files/xxx.pdf可直接访问前端拿到url后不直接iframe srcxxx.pdfChrome 会下载而是用 PDF.js 渲染!-- PDF 预览组件 -- template div idpdf-container stylewidth:100%;height:600px;/div /template script import * as pdfjsLib from pdfjs-dist/build/pdf export default { mounted() { this.renderPdf() }, methods: { renderPdf() { const loadingTask pdfjsLib.getDocument(this.fileUrl) loadingTask.promise.then(pdf { pdf.getPage(1).then(page { const viewport page.getViewport({ scale: 1.5 }) const canvas document.getElementById(pdf-container).appendChild(document.createElement(canvas)) const context canvas.getContext(2d) canvas.height viewport.height canvas.width viewport.width const renderContext { canvasContext: context, viewport: viewport } page.render(renderContext) }) }) } } } /script3.3 在线测验模块Vue 表单校验与 SpringBoot 事务一致性保障测验提交需满足单选题必答、时间截止自动交卷、答案提交后不可修改、分数实时计算。后端使用Transactional保证原子性// ExamController.java PostMapping(/api/exam/submit) Transactional(rollbackFor Exception.class) public ResultVO submit(RequestBody ExamSubmitDTO dto, HttpServletRequest request) { User user jwtUtil.getCurrentUser(request); // 1. 校验考试是否在有效期内 Exam exam examService.getById(dto.getExamId()); if (exam.getEndTime().before(new Date())) { return ResultVO.fail(考试已结束); } // 2. 保存考生答案student_answer 表 ListStudentAnswer answers dto.getAnswers().stream() .map(a - new StudentAnswer().setStudentId(user.getId()) .setExamId(dto.getExamId()) .setQuestionId(a.getQuestionId()) .setAnswer(a.getAnswer())) .collect(Collectors.toList()); studentAnswerService.saveBatch(answers); // 3. 计算分数并更新考试记录 int score calculateScore(dto.getExamId(), user.getId()); examResultService.update(new UpdateWrapperExamResult() .eq(exam_id, dto.getExamId()) .eq(student_id, user.getId()) .set(score, score) .set(submit_time, new Date())); return ResultVO.success(提交成功, score); }Vue 前端使用el-formrules实现前端校验el-form :modelform :rulesrules refformRef el-form-item v-for(q, index) in questions :keyq.id :label第 (index1) 题 q.content propanswer el-radio-group v-modelform.answers[index].answer el-radio v-foropt in q.options :keyopt.id :labelopt.content{{ opt.content }}/el-radio /el-radio-group /el-form-item /el-form script export default { data() { return { rules: { answer: [ { required: true, message: 请选择答案, trigger: change } ] } } } } /script参数说明Transactional(rollbackFor Exception.class)确保答案保存失败时分数更新也回滚el-form的prop必须与v-model数据路径一致如answers.0.answer否则校验不生效trigger: change表示选项变更时触发校验比blur更及时。4. 避坑指南SpringBoot Vue 联调中最常翻车的 5 个硬核问题4.1 现象Vue 打包后index.html能打开但所有 API 请求 404原因Vue 路由使用history模式生产环境部署在 SpringBoot 的static/目录下但 SpringBoot 默认不处理/*路径的 HTML 回退导致刷新子页面如/course/list时 SpringBoot 返回 404。解决在 SpringBoot 中添加WebMvcConfigurer将所有非 API 请求重定向到index.htmlConfiguration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/).setViewName(forward:/index.html); registry.addViewController(/course/**).setViewName(forward:/index.html); registry.addViewController(/exam/**).setViewName(forward:/index.html); // 其他前端路由路径... } }4.2 现象PDF 在 Chrome 正常显示Edge 白屏或报PDF.js worker not found原因PDF.js 依赖pdf.worker.min.jsVue CLI 4.x 默认不将其复制到public/目录且node_modules/pdfjs-dist/build/pdf.worker.min.js路径在打包后失效。解决将node_modules/pdfjs-dist/build/pdf.worker.min.js复制到public/js/目录在 PDF 渲染前设置 worker 路径import * as pdfjsLib from pdfjs-dist/build/pdf pdfjsLib.GlobalWorkerOptions.workerSrc /js/pdf.worker.min.js // ✅ 指向 public 下的文件4.3 现象vue路由参数传递中文时后端接收到乱码如?name数学变成?name%E6%95%B0%E5%AD%A6原因Vue Router 的params默认使用encodeURI编码SpringBoot 的RequestParam默认用 ISO-8859-1 解码。解决方案一推荐前端用encodeURIComponent()显式编码后端用URLDecoder.decode()解码// Vue 跳转 this.$router.push(/course/detail?name${encodeURIComponent(this.course.name)})// SpringBoot Controller GetMapping(/api/course/detail) public ResultVO detail(RequestParam String name) throws UnsupportedEncodingException { String decodedName URLDecoder.decode(name, UTF-8); // ✅ 主动解码 // ... }方案二在application.yml中全局配置 URI 编码server: tomcat: uri-encoding: UTF-84.4 现象springboot配置中spring.servlet.context-path/eduVue 打包后 API 请求路径错乱原因Vue 的axios基础 URL 写死为/api未考虑上下文路径导致请求发送到http://localhost:8081/api/xxx而非http://localhost:8081/edu/api/xxx。解决SpringBoot 配置server.servlet.context-path/eduVue 中vue.config.js添加publicPath// vue.config.js module.exports { publicPath: process.env.NODE_ENV production ? /edu/ : /, // ... }axios实例的baseURL改为相对路径// utils/request.js const service axios.create({ baseURL: /edu/api/, // ✅ 匹配 context-path // ... })4.5 现象vue打包放进springboot中后CSS 样式丢失或 JS 报Cannot find module vue原因Vue CLI 打包时outputDir指向static/但static/目录下已有index.html而js/css文件被覆盖或路径错乱或package.json中dependencies与devDependencies混淆导致生产环境缺失vue。解决确保vue.config.js中outputDir指向绝对路径且static/目录为空或已备份运行npm run build前检查package.jsondependencies: { vue: ^2.6.14, vue-router: ^3.5.3, axios: ^0.21.4 }, devDependencies: { vue/cli-service: ^4.5.19, vue-template-compiler: ^2.6.14 }注意vue-template-compiler必须与vue版本严格一致否则npm run build会报错。5. 进阶技巧把 Vue 打包产物无缝注入 SpringBoot实现真正的“一体化部署”5.1 不用mvn clean package改用maven-resources-plugin自动同步前端资源很多教程教你在backend/src/main/resources/static手动粘贴dist/文件这在 CI/CD 中不可靠。正确做法是让 Maven 在package阶段自动将 Vue 打包产物复制到 SpringBoot 的static目录。在backend/pom.xml中添加插件plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-resources-plugin/artifactId version3.2.0/version executions execution idcopy-frontend-resources/id phaseprocess-resources/phase goals goalcopy-resources/goal /goals configuration outputDirectory${project.build.outputDirectory}/static/outputDirectory resources resource directory../frontend/dist/directory !-- Vue 项目 dist 目录 -- filteringfalse/filtering /resource /resources /configuration /execution /executions /plugin逻辑说明maven-resources-plugin在process-resources阶段执行早于compile确保static/目录在打包前已就绪directory../frontend/dist/directory是 Vue 项目dist目录的相对路径需根据你的实际目录结构调整如../../course-frontend/distfilteringfalse避免 Maven 对 HTML/CSS 文件做属性替换。5.2 SpringBoot 启动时自动检查前端资源完整性失败则拒绝启动光复制不够还要验证index.html是否存在、js/app.js是否可读。在ApplicationRunner中添加校验Component public class FrontendResourceChecker implements ApplicationRunner { Value(classpath:static/index.html) private Resource indexHtml; Value(classpath:static/js/app.js) private Resource appJs; Override public void run(ApplicationArguments args) throws Exception { if (!indexHtml.exists() || !appJs.exists()) { throw new RuntimeException(前端资源缺失请先运行 npm run build); } // 可选校验文件大小防空文件 if (indexHtml.contentLength() 1000 || appJs.contentLength() 1000) { throw new RuntimeException(前端资源损坏index.html 或 app.js 为空); } System.out.println(✅ 前端资源校验通过服务启动中...); } }参数说明Value(classpath:static/index.html)利用 Spring 的Resource抽象直接定位static/下的文件contentLength()获取字节数1000 字节是index.html的合理下限含基本 HTML 结构抛出RuntimeException会让 SpringBoot 启动失败强制开发者先构建前端。5.3 使用spring-boot-maven-plugin打包成可执行 JAR并验证部署效果最终打包命令只需一步# 在 backend 目录下执行 mvn clean package -DskipTests生成的target/course-platform-backend-1.0.0.jar即为完整应用含前端静态资源。验证方式java -jar target/course-platform-backend-1.0.0.jar # 访问 http://localhost:8081 —— 应直接显示 Vue 的首页 # 查看 network tab所有 js/css 请求状态码为 200且路径为 /js/app.xxx.js # 调用 /api/course/list返回 JSON 数据关键验证点http://localhost:8081/js/app.xxx.js能直接下载证明静态资源映射成功http://localhost:8081/api/course/list返回数据证明后端接口可用刷新任意 Vue 子页面如/course/list不报 404证明WebMvcConfigurer重定向生效上传 PDF 后http://localhost:8081/files/xxx.pdf可访问且 PDF.js 渲染正常。我带过三届毕设最常看到的翻车不是代码写错而是把 Vue 当成独立 SPA 部署结果答辩现场连不上后端或者盲目升级 SpringBoot 版本导致 Shiro 权限注解失效最后三天重写鉴权逻辑。所以我的习惯是第一周只做登录 课程列表跑通axios请求、JWT 解析、角色路由第二周加课件上传和 PDF 预览重点调通MultipartFile和pdf.worker.min.js路径第三周补测验和作业模块用Transactional锁住分数一致性。剩下的时间全花在application.yml的logging.level.com.xxxdebug上——看清楚每一行 SQL 和每个请求头比背一百道springboot面试题都管用。希望帮到你。本文还有配套的精品资源点击获取