简介这是一套面向计算机专业本科生的毕业设计与课程实践项目基于SpringBootVue构建课程教学平台解决教学管理数字化、前后端分离开发实战训练等核心需求特别适合毕设选题、期末大作业及Java全栈能力提升。资源包共含项目源码、MySQL数据库脚本、开发说明文档、部署操作视频与代码讲解视频等关键内容涵盖后端逻辑实现、前端交互界面、数据建模与系统部署全流程压缩包大小为19.9MB。已有45人学习下载表明其在学生群体中具备实际参考价值与落地可行性。读者可直接导入IDE运行调试结合文档理解MVC分层设计、RESTful接口开发、Vue组件化实践及前后端联调要点视频资料进一步降低了部署门槛助力快速复现系统并开展二次开发。 最近在整理一个基于SpringBoot和Vue的课程教学平台项目正好有朋友问到这个题目的设计思路和实现细节。这个题目算是高校毕设和课程设计里非常经典的选题没有特别复杂的算法但对工程能力的要求比较全面——后端要管好接口和权限前端要把页面和交互做顺还要把视频播放、文件上传、作业提交这些教学场景的细节落到地上。我知道点进这篇的人多半是准备做毕设或课设的学生也可能是刚接触前后端分离、想找个完整项目练手的开发者。这篇文章我会把项目从架构拆分、数据库设计到核心功能实现再到我实际踩过的一些坑尽量完整地讲一遍。内容不追求花哨重点是让你能真正把这个项目做出来、跑起来并且在答辩或者演示的时候能讲清楚每一个设计决策背后的理由。1. 整体方案拆解为什么用SpringBoot加Vue这套组合1.1 课程教学平台的核心需求在动手写代码之前先得想清楚一个事课程教学平台到底要管什么我见过的很多半途而废的项目问题都出在需求没想清楚就开建表结果做到一半发现表和业务对不上又推倒重来。一个常规的课程教学平台核心用户有三类学生、教师、管理员。围绕这三类角色业务可以拆成几个模块用户认证与权限管理登录、注册、角色区分不同角色能访问的页面和操作完全不一样。课程管理管理员和教师可以创建课程、编辑课程信息、上下架课程学生可以浏览课程列表、查看课程详情。选课与学习学生选课后进入课程学习页查看章节课时、观看课程视频、下载课件资料。作业管理教师发布作业、设置截止时间学生提交作业文本或附件教师批阅评分。数据统计管理员看平台整体数据比如课程数、选课人数、作业提交率教师看自己课程的学习人数。这些需求放在一起其实就是一个典型的CRUD加文件处理加简单权限控制的组合。选SpringBoot做后端、Vue做前端恰好是这个组合里最成熟、参考资料最多的方案。1.2 技术选型背后的取舍有人会问现在微服务这么火要不要拆成SpringCloud我的回答是别。一个课程教学平台的并发量单机SpringBoot完全扛得住拆微服务只会让你陷入服务注册、配置中心、分布式事务这些和业务无关的坑里。SpringBoot的优势在于“开箱即用”内置Tomcat、自动配置、生态成熟一个人开发的时候不用花大量时间在环境配置上。配合MyBatis-Plus操作数据库开发效率会高很多。尤其是MyBatis-Plus的代码生成器和条件构造器写单表CRUD基本可以少掉一半的重复代码。前端选Vue核心原因是它的渐进式设计。Vue可以只做一个页面里的局部组件也可以配合Vue Router、Pinia搭出完整的单页应用。对于这种管理后台加学习页面的项目Vue的单文件组件开发体验很舒服而且国内社区资料多遇到问题搜索一下基本都能解决。前端工程化我建议用Vite而不是Webpack。Vite在开发环境下的冷启动速度比Webpack快一个量级配置也简单。不过要注意Vite要求Node.js版本在16以上如果你的电脑上Node版本太老需要先升级。1.3 项目目录结构设计项目采用前后端分离目录分开管理。后端是一个标准的Maven工程前端是一个Vue工程。我最终的项目结构长这样course-platform/ ├── backend/ # SpringBoot后端 │ ├── src/main/java/ │ │ └── com/course/platform/ │ │ ├── controller/ # 控制层接收前端请求 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis-Plus的Mapper接口 │ │ ├── entity/ # 数据库实体类 │ │ ├── dto/ # 前端传参的封装对象 │ │ ├── vo/ # 返回前端的视图对象 │ │ ├── config/ # 配置类跨域、拦截器、Swagger │ │ ├── common/ # 通用返回结果、异常处理、工具类 │ │ └── CoursePlatformApplication.java │ └── src/main/resources/ │ ├── application.yml │ └── mapper/ # MyBatis XML文件复杂SQL时用 └── frontend/ # Vue前端 ├── src/ │ ├── api/ # 接口请求封装 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── store/ # Pinia状态管理 │ ├── views/ # 页面组件 │ ├── utils/ # 工具类axios封装等 │ ├── App.vue │ └── main.js ├── vite.config.js └── package.json这个结构有几个好处后端分层清晰每个类职责明确前端的api目录单独管理所有请求后续业务扩展时不用在页面里到处找接口views目录按角色或模块分子目录比如views/admin、views/teacher、views/student权限控制的时候也方便。2. 数据库设计与核心表结构2.1 六大核心表梳理数据库设计是这种平台类项目的地基。我见过太多人一上来就建表结果表之间关系混乱查询的时候各种嵌套循环性能差还容易出错。我的建议是先画ER图理清楚实体之间的关系再动手写SQL。课程教学平台的实体关系其实不复杂核心就六个表用户表、课程表、课程分类表、章节表、选课表、作业表。如果要做作业提交记录再加一个作业提交表。把这些表的关系理清楚整个业务模型就清晰了。用户表user所有角色统一存这一张表通过role字段区分学生、教师、管理员。这样登录逻辑只写一套权限控制靠角色判断。课程分类表course_category课程的一级分类比如“编程开发”“设计创意”“职场技能”分类下挂课程。课程表course归属某个教师属于某个分类包含课程标题、简介、封面图、价格如果是免费课可以置0、状态草稿/已上架/已下架。章节表course_chapter属于某门课程字段包括章标题、所属课程ID、排序号。为什么单独建表因为一个课程会有多个章节如果课程表里直接存章节内容既难维护又难扩展。选课表course_selection记录学生和课程的选课关系一个学生可以选多门课一门课可以被多个学生选典型的多对多关系。选课表除了存用户ID和课程ID还应该存选课时间和学习进度。作业表homework属于某个章节或某门课程包含作业标题、要求、截止时间、满分分值。作业提交表homework_submit学生提交作业的记录包含提交内容、附件地址、提交时间、批阅分数和批语。2.2 用户表与权限字段设计用户表的设计有个值得注意的点账号、密码、角色这些字段必须单独建出来密码不要明文存储用MD5加盐或者BCrypt加密。我在项目里用的是Spring Security自带的BCryptPasswordEncoder虽然引入Spring Security配置起来麻烦一点但密码安全这个底线不能放。用户表的核心字段大概是id、username、password、nickname、avatar、role、status、create_time。role用整数存1代表管理员2代表教师3代表学生。status用来控制账号是否被禁用比如学生毕业了或者教师离职了直接修改状态就能禁止登录不用删数据。有一点要提醒真实项目里不要用外键约束。数据库表之间用逻辑外键关联也就是在业务层保证数据一致性。MySQL的外键在数据量大时会影响写入性能而且做分库分表的时候外键根本没用。MyBatis-Plus的设计哲学也是这样关联查询通过SQL完成不依赖数据库约束。2.3 核心建表SQL参考下面是我整理过的核心建表SQL去掉了无关字段保留了主干。你在实现的时候可以直接参考也可以根据自己的需求调整。-- 用户表 CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint(4) NOT NULL DEFAULT 3 COMMENT 角色1管理员 2教师 3学生, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 课程表 CREATE TABLE course ( id bigint(20) NOT NULL AUTO_INCREMENT, teacher_id bigint(20) NOT NULL COMMENT 授课教师ID, category_id bigint(20) DEFAULT NULL COMMENT 课程分类ID, title varchar(100) NOT NULL COMMENT 课程标题, cover varchar(255) DEFAULT NULL COMMENT 封面图地址, intro text COMMENT 课程简介, price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 课程价格, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0草稿 1已上架 2已下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 章节表 CREATE TABLE course_chapter ( id bigint(20) NOT NULL AUTO_INCREMENT, course_id bigint(20) NOT NULL COMMENT 所属课程ID, title varchar(100) NOT NULL COMMENT 章节标题, video_url varchar(255) DEFAULT NULL COMMENT 视频地址, duration int(11) DEFAULT NULL COMMENT 视频时长秒, sort int(11) NOT NULL DEFAULT 0 COMMENT 排序号, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程章节表; -- 选课表 CREATE TABLE course_selection ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 学生ID, course_id bigint(20) NOT NULL COMMENT 课程ID, progress int(11) NOT NULL DEFAULT 0 COMMENT 学习进度百分比, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_course (user_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课表;选课表的联合唯一索引很重要它保证了一个学生同一门课只能选一次在代码层做好这层约束能避免很多重复数据问题。另外需要注意的是课程表里为什么要state区分草稿和上架因为教师创建课程并不想立即发布草稿状态给了教师一个缓冲同时也方便管理员审核。3. 后端工程落地SpringBoot接口与权限设计3.1 后端工程搭建与配置SpringBoot后端的搭建现在非常简单直接去Spring Initializr网站start.spring.io生成一个基础工程或者用IDEA自带的Spring Initializr创建。要注意的是SpringBoot版本选择尽量选2.7.x不要追新。我在热搜词里看到有“springboot版本太高”的条目这确实是个普遍问题——SpringBoot 3.x要求JDK 17及以上而且很多老版本的依赖不兼容网上能搜到的教程大部分还是基于2.x写的。你做毕设或者课程设计稳定压倒一切不要在这个地方给自己找麻烦。后端依赖方面我最终选了这些关键依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi2-spring-boot-starter/artifactId version4.3.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency这里多说一句为什么不直接引入Spring Security做权限Spring Security功能很强大但学习曲线陡峭配置繁琐对新手不友好。课程教学平台的权限模型很简单登录、角色校验、接口放行用拦截器加JWT完全可以搞定而且逻辑透明自己写的代码在答辩时还能讲清楚。所以我选择用JWT做Token鉴权拦截器做角色校验。如果你后续想增加更复杂的权限策略再引入Spring Security也不迟。application.yml里的核心配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0数据库连接串里的serverTimezoneAsia/Shanghai是必须的不然会报时区错误。MyBatis-Plus的配置里我把log打印打开了开发阶段方便看SQL执行情况上线前再关掉。3.2 JWT登录鉴权的完整实现登录鉴权是后端最核心的模块。我的实现思路是用户提交用户名和密码后端校验通过后生成一个JWT Token返回给前端前端把Token存在本地每次请求时在请求头里带上后端通过拦截器校验Token合法性和角色权限。JWT工具类里主要有三个方法生成Token、解析Token、判断角色。Token里我放的是用户ID、用户名和角色过期时间设置24小时。下面是我的核心代码Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; // 单位秒 // 生成Token public String generateToken(Long userId, String username, Integer role) { Date now new Date(); Date expireDate new Date(now.getTime() expire * 1000); return Jwts.builder() .setHeaderParam(typ, JWT) .claim(userId, userId) .claim(username, username) .claim(role, role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } // 解析Token获取Claims public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }登录接口的逻辑接收用户名和密码从数据库查出用户用BCrypt校验密码如果通过就生成Token返回。注意不要用MD5MD5已经被证明不安全而且彩虹表攻击很容易破解常见的弱密码。拦截器的实现要注意两个点。第一要放行登录接口和Swagger文档接口不然前端没法调试。第二从Token里取出角色信息后要先判断当前请求的路径是否要求特定角色再做拦截。我把角色判断放在拦截器里做通过请求路径的前缀区分比如/api/teacher/**只允许教师角色访问/api/admin/**只允许管理员访问。这样代码简单逻辑也清晰。Component public class AuthInterceptor implements HandlerInterceptor { Autowired private JwtUtils jwtUtils; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 跨域预检请求直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录或登录已过期); } try { Claims claims jwtUtils.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); // 判断路径权限 String uri request.getRequestURI(); Integer role (Integer) claims.get(role); if (uri.startsWith(/api/admin/) role ! 1) { throw new BusinessException(403, 无权限访问); } if (uri.startsWith(/api/teacher/) role ! 1 role ! 2) { throw new BusinessException(403, 无权限访问); } return true; } catch (Exception e) { throw new BusinessException(401, Token无效或已过期); } } }Token过期时间的设置值得说一下。24小时对用户来说比较友好不用频繁重新登录但安全上稍弱。如果是在生产环境建议用双Token机制一个短期Token比如30分钟用于正常请求一个长期Refresh Token用于自动续期。课程设计项目做到单Token就够了但如果你在答辩时提到这个方案会让老师觉得你考虑得比较全面。3.3 Swagger接口文档与Knife4j配置接口写完了文档不能少。我用的Swagger增强版是Knife4j界面比原生Swagger好看而且自带调试工具。在SpringBoot里集成Knife4j非常简单加依赖后就配一个配置类Configuration EnableKnife4j public class SwaggerConfig { Bean public OpenAPI openAPI() { return new OpenAPI() .info(new Info() .title(课程教学平台API) .version(1.0) .description(课程教学平台后端接口文档)); } }配置好之后启动项目浏览器访问http://localhost:8080/doc.html就能看到接口文档页面。每个接口的请求参数、返回结果都一目了然前端对接的时候非常方便。这里要提醒一个坑Swagger配置不当会报空指针异常。常见原因是SpringBoot版本和Swagger版本不兼容。如果你用SpringBoot 2.7.x建议用Knife4j 4.3.0如果你用了SpringBoot 3.x必须用Knife4j的openapi3版本。这个问题的报错信息还不友好经常是各种奇怪的NPE你按版本对照关系排查基本就能解决。3.4 课程视频与文件上传处理课程教学平台的另外一个核心功能是视频播放和课件上传。视频和文件不能直接放到数据库里应该存到服务器磁盘或者对象存储比如阿里云OSS数据库里只存文件的访问路径。我在项目中处理的思路是上传接口接收MultipartFile把文件保存到服务器的指定目录比如/upload/video/和/upload/file/然后把文件的访问路径返回给前端。为了让前端能直接通过URL访问我配置了静态资源映射把/upload/**映射到磁盘目录Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadPath /); } }这里有个文件上传大小的坑。SpringBoot默认的上传文件大小限制是1MB视频文件动辄几百MB不修改配置的话上传必然失败。需要修改application.ymlspring: servlet: multipart: max-file-size: 1024MB max-request-size: 1024MB如果你的项目要支持HLS视频流播放m3u8格式那么视频处理这块就是另一个复杂度了。m3u8是苹果制定的流媒体协议原理是把视频切成很多个ts分片通过m3u8索引文件播放。前端播放的时候用video.js配合hls.js插件就能实现。具体的处理方式我放到前端部分再讲。4. 前端工程实现Vue路由、状态管理与页面交互4.1 前端工程化初始化和依赖安装前端这块我选了Vue 3加Vite的组合。Vue 3的Composition API写业务代码很顺手而且性能比Vue 2好不少。如果你之前学的是Vue 2也不用担心Vue 3的模板语法大部分和Vue 2类似Options API也是支持的。创建项目用Vite官方脚手架npm create vitelatest frontend -- --template vue cd frontend npm install然后安装路由、状态管理、UI库和HTTP库。前端选型这块我花过不少时间最终确定如下组合npm install vue-router4 npm install pinia npm install element-plus npm install axios npm install hls.jsElement Plus是Vue 3对应的Element UI升级版表单、表格、弹窗这些组件都挺好用。有些教程推荐用Ant Design Vue也可以看个人习惯。我对Element Plus更熟所以选了它。安装完依赖后一个非常常见的坑是npm install报错或者安装完项目运行不起来。出现这种情况先看node_modules目录是否存在如果不存在删掉package-lock.json后重新执行npm install。如果是因为网络问题可以换成淘宝镜像源npm config set registry https://registry.npmmirror.com还有一个我踩过的坑Node版本太新也可能导致Vite运行报错。Vite 5要求Node 18如果你用Node 20在部分Linux环境下会报Digital Envelope Routines错误。解决办法是设置环境变量NODE_OPTIONS--openssl-legacy-provider不过这个属于老版本Vite的问题新版基本已经修复了。4.2 路由设计与前端权限控制Vue Router的路由配置我的做法是分成两套一套是公开路由登录页、注册页、首页一套是需要登录的路由学习中心、个人中心、管理后台。通过路由守卫来控制访问。// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /register, component: () import(../views/Register.vue) }, { path: /, component: () import(../layout/MainLayout.vue), redirect: /home, children: [ { path: home, component: () import(../views/Home.vue) }, { path: course/:id, component: () import(../views/CourseDetail.vue) }, { path: learn/:courseId, component: () import(../views/LearnPage.vue), meta: { requiresAuth: true } }, ] }, { path: /admin, component: () import(../layout/AdminLayout.vue), meta: { requiresAuth: true, role: 1 } }, ] router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.role store.role ! to.meta.role) { next(/home) } else { next() } })按需引入组件用() import()做代码分割这样首屏加载不会把整个项目的JS都拉下来。路由守卫里做两件事判断登录状态和判断角色权限。这里要记住一个原则前端的权限控制只是提升用户体验真正的安全边界在后端接口前端隐藏按钮并不能阻止恶意请求。热搜词里提到“vue keep-alive切换路由子组件el-table滚回头部”这个问题的场景是学习中心的某个页面里有表格用户往下滚动表格后切到其他页面再切回来表格滚动条回到了顶部。解决方案是在列表中给ElTable加ref在activated钩子里恢复滚动位置。如果需要保持状态可以用keep-alive的include属性指定要缓存的组件。4.3 Axios封装与接口请求统一管理Axios封装这块做得好不好直接影响你后端接口对接的效率。我在项目里做了一个统一的请求模块把Token注入、错误处理、Loading处理都集中管理。// utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带Token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization 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 }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) window.location.href /login } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } )这里最大的好处是前端所有页面都不用重复写Token注入和错误弹窗的逻辑页面里只关心数据。后端返回的数据统一封装成{ code, message, data }格式前端响应拦截器直接解包。如果后端接口报错弹窗、跳转登录这类公共逻辑在拦截器里已经处理掉了。在baseURL里我用了/api同时开启了Vite的代理转发。这个方案的好处是避免了跨域问题。开发环境下在vite.config.js里配置代理// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })如果你不用代理直接在Axios里写后端地址http://localhost:8080那就必须在后端做跨域配置。两种方式都行。但生产环境下通常用Nginx把前端和后端的请求统一代理到同一个域名上这样就完全不存在跨域问题。4.4 视频播放功能的实现与m3u8播放问题课程播放页是学生用户最常用的页面。视频播放有两种情况普通MP4文件和m3u8流媒体文件。MP4播放简单直接用HTML5的video标签就能播放。但有一个坑原生的video标签对MP4编码格式要求比较严格如果视频是H.265编码的Chrome浏览器可能播放不了。解决办法是转码成H.264编码或者在项目里集成video.js播放器。m3u8播放就需要专门处理了。m3u8是把视频切成很多个小分片索引文件里记录了分片地址。浏览器原生不支持需要借助hls.js库。我在封装一个视频播放组件时的核心代码template video refvideoRef controls classvideo-player/video /template script setup import Hls from hls.js const props defineProps({ src: { type: String, required: true } }) onMounted(() { const video videoRef.value if (Hls.isSupported()) { const hls new Hls() hls.loadSource(props.src) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () { video.play() }) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari原生支持m3u8 video.src props.src } }) /script如果视频地址是HTTP的还要注意Nginx的CORS配置因为hls.js加载ts分片时会有跨域请求。Nginx需要在location块里加上跨域头。这个问题排查起来比较隐蔽因为报错信息只是“跨域请求被阻止”不会直接告诉你是因为ts分片加载失败。5. 常见问题与排查实录5.1 环境与脚手架问题把项目运行起来的过程往往是新手最容易卡住的地方。我整理几个高频问题和解决方案。第一个是“IDEA新建项目没有SpringBoot选项”。这个大概率是你本地IDEA版本是社区版Community Edition社区版不内置Spring Initializr插件需要手动安装Spring Assistant插件或者直接去start.spring.io网站下载工程包再导入IDEA。如果你已经装了插件还是没有SpringBoot 3.4.3选项那是因为IDEA的Spring Initializr默认是从Spring官网拉取版本列表网络原因可能导致列表加载不出来用start.spring.io手动生成最靠谱。第二个是“npm install后提示The project can not found node_modules”。这个其实就是依赖没装成功。遇到这个提示先删掉node_modules目录和package-lock.json再重新npm install。如果还不行看看npm版本是不是太老执行npm install -g npm升级npm到最新版。第三个是“springboot版本太高导致启动报错”。我遇到过不少同学直接用IDEA默认的Spring Boot 3.x版本建项目结果各种依赖不兼容启动报ClassNotFound或者BeanCreationException。解决办法很简单把pom.xml里的SpringBoot版本改成2.7.18JDK环境切到1.8或者11SpringBoot 2.x对JDK 8兼容最好。在毕设这个场景下SpringBoot 2.7.18是稳定性最高的选择。5.2 运行期典型问题运行期的坑我挑几个最典型的说。第一个是数据库连接失败。报错信息通常是Access denied for user rootlocalhost要么是密码不对要么是用户没有远程访问权限。本地开发先确认MySQL服务已经启动并且用你在application.yml里填的账号密码能够直接连上。如果MySQL密码是空注意SpringBoot的连接池默认会拒绝空密码连接可以在密码位置填一个特殊符号或者不填但配置里要加上allowPublicKeyRetrievaltrueuseSSLfalse。第二个是后端返回数据前端拿不到控制台报404。这个大概率是路径不匹配。SpringBoot的Controller路径是/api/course/list前端请求的路径也是/api/course/list但Vite代理只把/api前缀转发给后端所以前端Axios的baseURL就不要再加/api了直接写/course/list。这种前缀重复的问题排查起来很烦躁但只要你把前端baseURL和后端接口路径的拼接逻辑理清楚就能避免。第三个是文件上传失败报错MaxUploadSizeExceededException。这个就是前面说的SpringBoot默认1MB限制。除了修改配置外还有一个隐蔽的坑如果你用了Nginx做反向代理Nginx默认的client_max_body_size也是1MB需要在Nginx配置里调到合适的大小。所以即使后端把限制打开了上传大文件时依旧可能被Nginx拦截。5.3 部署与演示准备项目的部署我建议用最直接的方式后端打成Jar包前端构建成静态文件放到同一台服务器的Nginx下。后端在服务器上执行java -jar course-platform.jar前端npm run build生成dist目录配置Nginx把dist目录作为站点根目录把/api前缀的请求代理到后端的8080端口。server { listen 80; server_name your-domain.com; root /var/www/course-platform/dist; index index.html; location / { 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; } location /upload/ { alias /var/www/upload/; } }这里的关键是try_files $uri $uri/ /index.html它保证前端路由在刷新页面时不会因为找不到对应的静态文件而返回404。这是SPA应用部署时最容易踩的坑。还有一个小细节Jar包运行的端口不要用80和443这两个端口通常需要root权限直接用8080或者别的端口让Nginx做反向代理。另外我建议在交付前自己完整走一遍核心流程管理员创建课程分类、教师创建课程并上传视频、学生注册登录选课、查看视频、提交作业、教师批改作业。这个流程走通项目就基本没问题了。我见过太多项目功能点单独测没问题但串联流程跑不通原因往往是某个环节的状态不一致比如课程是草稿状态学生端看不到或者作业的截止时间没设置导致学生无法提交。6. 项目演示与答辩亮点设计课程设计或者毕设最终要做演示这块我的建议是提前准备一个演示脚本按顺序走核心流程。演示的关键不是把所有功能都过一遍而是挑最有代表性的链路讲清楚业务逻辑和技术点。我的演示顺序是这样的先用管理员账号登录后台展示课程列表和用户管理再切换到教师账号演示创建课程、上传视频、发布作业然后切到学生账号选课、看视频、提交作业。这个链路覆盖了全部三类角色和核心业务。答辩时常见的问题是“这个项目有什么亮点”不要说自己做了多少功能要说自己解决了什么问题。比如JWT无状态认证解决了传统Session在多节点部署下无法共享的问题文件上传时做了类型校验和大小限制防止恶意脚本上传视频播放兼容了MP4和m3u8两种格式适配了不同网络环境。另外如果把项目对应的实际课表或者教学设计考虑进去比如教师可以按周发布课程内容学生端可以查看教学日历这种结合具体教学场景的细节会让老师觉得你做项目的时候思考过业务而不是纯为了交作业。不过这就属于加分项了时间不充裕的话先保证核心功能稳定跑通。我在实际做这个项目的过程中最大的体会是前后端分离的项目真正的难点不在单个技术栈里而在两边对接的时候。数据格式怎么定、错误码怎么统一、接口文档怎么维护、权限怎么在前端和后端同时生效这些才是你花时间打磨后能拉开差距的地方。这篇文章里的代码和配置都是我在项目中实际验证过的按着这个思路走你的课程教学平台一定能稳定跑起来。本文还有配套的精品资源点击获取