在高校里做资源共享平台,最麻烦的从来不是代码,而是“资源分散”这件事本身。最近帮一位学弟完整实现了一个基于Spring Boot + Vue的前后端分离高校教育资源共享平台,从需求梳理、数据库设计、接口开发,到前端联调、Docker部署,整个过程踩了不少坑,也沉淀了一套可以复用的方法。这篇文章就把这套完整方案拆开来讲,包括技术选型背后的原因、核心功能实现的实操代码,以及那些文档里查不到的排错经验。不管你是准备做毕业设计、课程设计,还是第一次完整做一个前后端分离项目,都可以直接参考。
1. 项目定位与整体设计思路
1.1 先搞清楚平台到底要解决什么问题
很多同学拿到这种题目第一反应是“快点搭个登录注册,然后写上传下载”,结果做完发现只是个文件管理Demo,根本算不上“教育资源共享平台”。我建议动手前先花一天时间想清楚使用场景:老师的课件还散落在各个QQ群,视频课存在网盘里链接三天就过期,学生想找某门课的历年试卷只能靠学长学姐口口相传。平台要解决的正是这种“资源分散、无统一入口、无法检索、没有互动”的问题。
所以这个平台的核心定位不是单纯存文件,而是把“上传—审核—分类—检索—预览/播放—下载—互动”这条链路打通。围绕这个链路,我把它拆成了六个基础模块:用户模块(登录注册、角色管理)、资源模块(上传、审核、分类、检索)、在线预览模块(视频m3u8播放、文档预览)、互动模块(评论、收藏、点赞)、统计模块(下载量、浏览量、排行榜)、后台管理模块(用户、资源、分类、数据看板)。
用户角色也要提前划分。我最终定了三种:学生可以浏览、检索、预览、下载资源,也可以评论和收藏;教师除了学生权限,还可以上传自己的课件和视频,上传的资源需要管理员审核后展示;管理员则负责用户管理、资源审核、分类维护和统计数据查看。角色划分清晰了,后面的权限代码才不用反复改。
1.2 为什么最终选了Spring Boot + Vue这套组合
选题阶段学弟问过我,能不能用Python Django,能不能直接用若依框架,或者干脆前后端不分离用Thymeleaf。我的建议很明确:如果你是从零开始独立完成一个项目,Spring Boot + Vue是目前综合成本最低、参考资料最多、也最容易被面试官认可的组合。原因有三点。
第一,前后端分离让职责边界非常清楚。后端只提供JSON接口,前端专注页面交互,两边可以并行开发,接口通过Swagger或YAPI对接。后面如果学校要求再做个移动端H5,前端代码可以直接打包复用,接口完全不用动。第二,Spring Boot把配置简化到了极致,内嵌Tomcat、起步依赖、自动装配,对新手非常友好。配合MyBatis Plus连SQL都不用手写太多,数据处理效率很高。第三,Vue3的组件化和响应式机制特别适合做资源管理这类交互密集的页面,像资源卡片列表、视频播放器、评论楼层这些功能都能拆成独立组件复用。
为什么不推荐一上来就上微服务?教育资源平台说到底还是一个中后台管理系统,事务、权限、文件存储都集中在单体应用里反而更容易管理。微服务拆出来网关、注册中心、配置中心,对学习能力要求高,而且部署成本也上来了。单体能解决的问题,就没必要为了“看起来高级”制造复杂度。
1.3 动手前先把数据模型画清楚
我习惯先把核心表的关系画出来再写代码。这个平台最关键的表有:用户表(user)、分类表(category)、资源表(resource)、评论表(comment)、收藏表(favorite)、下载记录表(download_record)。表之间的关系可以这样理解:一个分类下有多个资源,一个用户可以上传多个资源,一个资源可以有多条评论和多个收藏,用户和资源通过收藏表、下载记录表产生关联。
这里有个容易被忽略的点:收藏和下载记录不要直接在设计上做成资源表里的冗余字段,虽然那样更新下载次数很方便,但后续做“用户是否收藏过”“下载排行榜”时会非常痛苦。我建议单独建关联表,并用联合唯一索引控制重复收藏,资源表里只保留download_count、view_count这类统计字段,每次下载成功后异步加一。这样数据统计和业务逻辑分离,后面做排行榜就是一条order by语句的事,不用去翻所有历史记录。
2. 技术选型与核心依赖详解
2.1 后端依赖清单与版本选择的坑
后端我用的还是稳定的Spring Boot 2.7.x,而不是最新的3.x,这一点在学弟的项目里差点酿成大问题。Spring Boot 3.0强制要求JDK17,并且底层从javax.servlet迁移到了jakarta.servlet,很多第三方组件,比如老版的Swagger、Druid、代码生成器,版本跟不上会直接启动报错。如果你只是想做学业项目或者快速交付,2.7.18加JDK8/11是兼容性最好、教程最多的组合。
下面是一份可以直接使用的核心依赖清单:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.25</version> </dependency> </dependencies>注意mysql-connector-java在Spring Boot 2.7里不需要写版本号,但写上更稳妥,避免本机仓库缓存导致版本混乱。连接串一定记得加:useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,否则数据库时间会差8个小时甚至直接连不上。
2.2 前端依赖与Vue环境配置
前端我选的是Vue 3 + Vite + Element Plus + Pinia + Vue Router + Axios,这套组合接近现在企业的真实用法。Vue 2虽然还有大量存量项目,但官方已经停止维护新功能,新项目直接用Vue 3更合理。Vite比Webpack启动速度快非常多,改代码热更新几乎是秒级,开发效率明显提升。
环境配置方面,Node.js版本建议用16.20.2以上,最好直接用18 LTS。安装完Node后先执行:
node -v npm -v确认版本没问题,再用官方脚手架创建项目:
npm create vue@latest创建过程中会询问是否安装Vue Router、Pinia、ESLint等,建议全选Yes。进入项目后继续安装UI库和请求库:
npm install element-plus axios hls.jsElement Plus做后台管理类的界面非常适合,表格、表单、上传组件、分页组件都直接可用。Vue Devtools建议在浏览器里装一个调试扩展,Vue 3项目组件状态、路由跳转、Pinia状态都能实时查看,排查问题效率高很多。很多新手写了半天代码不知道数据流哪里断了,其实就是没开Devtools看组件响应式数据。
2.3 文件存储方案选型:本地存储、OSS还是MinIO
教育资源共享平台的核心资产是文件,所以文件存哪里一定要先想好。我见过很多毕设直接把文件存到数据库BLOB字段里,数据库体量很快膨胀,备份和迁移都很痛苦。这里对比一下三类常见方案:
| 方案 | 成本 | 访问速度 | 扩展性 | 适合场景 |
|---|---|---|---|---|
| 本地磁盘存储 | 最低 | 快 | 差,单机磁盘有限 | 课程设计、校内小规模使用 |
| 云OSS | 按量付费 | 快,有CDN | 很强 | 生产环境,大文件多 |
| MinIO自建 | 中等成本 | 快 | 强,兼容S3协议 | 校内私有化部署 |
高校平台我建议先用本地存储放在项目外的独立目录,比如/data/uploads,后端配置静态资源映射,前端通过URL直接访问。这样做的好处是部署简单,不依赖第三方云服务,代码也容易迁移到MinIO。视频文件则单独放在/data/videos,后面讲m3u8切片时你会明白为什么视频跟普通文件要分开存。
3. 核心功能实现与实操记录
3.1 基于JWT的用户认证与权限拦截
登录认证我选了JWT而不是传统Session,原因很直接:前后端分离后,前端可能部署在Nginx,后端是独立服务,Session跨域管理麻烦,需要额外配置;而JWT是无状态Token,后端不用保存登录状态,客户端每次请求带上Token就行,扩展性更好。
登录接口的核心逻辑是这样:先用用户名查出用户,然后用BCrypt校验密码,校验通过后用用户的id、角色、过期时间生成Token,返回给前端。生成Token的代码可以封装成工具类:
public String createToken(User user) { return Jwts.builder() .setSubject(user.getId().toString()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }后端还需要一个拦截器,在请求进入Controller之前校验Token。我实现的JwtInterceptor核心逻辑很简单:从Header的Authorization字段取Token,去掉“Bearer ”前缀,调用JWT解析,解析失败直接返回401,解析成功就把用户id放入ThreadLocal,方便后续接口里获取当前用户。注意要把拦截器注册到Spring MVC中,并排除登录、注册、资源列表等公开接口:
registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register", "/api/resource/list");角色控制可以和权限注解配合。最简单的方案是定义一个@RequireRole("ADMIN")注解,配一个AOP切面,从ThreadLocal中取当前用户角色,不匹配就抛异常返回403。这个方案比Spring Security全量接入轻量很多,学习成本低,适合中小型项目。
3.2 文件上传接口与防XSS设计
文件上传是整个平台的基础功能,但它并不是接收一个MultipartFile然后就完事了。我建议把上传接口分成两步:第一步前端把文件传到后端,后端返回文件URL;第二步前端把文件描述、分类、URL等元信息一起提交到资源发布接口。这样好处是文件上传失败时不需要回滚数据库记录,也方便后续做断点续传。
后端上传接口核心代码:
@PostMapping("/api/file/upload") public Result upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件不能为空"); } long maxSize = 1024L * 1024L * 1024L; // 1GB if (file.getSize() > maxSize) { return Result.error("文件大小超过限制"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String allowedExt = ".pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.zip,.mp4,.avi,.jpg,.png"; if (!allowedExt.contains(ext.toLowerCase())) { return Result.error("不支持的文件类型"); } String filePath = "/data/uploads/" + LocalDate.now() + "/" + UUID.randomUUID() + ext; File dest = new File(filePath); dest.getParentFile().mkdirs(); file.transferTo(dest); return Result.success(filePath); }这里有几个细节容易被忽视:文件名一定要用UUID重命名,否则用户上传一个“期末试卷.pdf”,别人就能通过URL猜到服务器上的其他文件;路径必须隔离到日期目录,方便后续按时间清理;文件类型不能只看前端传的扩展名,后端还要再校验一次,防止有人绕过前端上传恶意文件。
XSS攻击也得重点防,尤其是富文本编辑资源描述时,很容易被人插入<script>标签。常用做法是用Jsoup对富文本内容做白名单过滤,只保留安全的标签和属性:
String safeHtml = Jsoup.clean(htmlContent, Safelist.basic());有同学问过滤PDF文件时怎么处理XSS。PDF本身不会执行脚本,但文件名和资源描述会存进数据库并在页面上展示,测试时如果在上传文件的名字里加上<script>,列表页直接渲染文件名就会触发弹窗。所以除了后端统一过滤富文本,在文件上传接口里也要对原始文件名做HTML转义,页面展示时优先使用后端的转义字段,而不是直接拼接文件名。
3.3 MyBatis Plus分页插件与多条件检索
资源列表页不能一下查出所有数据,必须分页。MyBatis Plus的分页插件是我用得最多的功能,但它有个经典坑:只引入依赖不配置分页插件,分页查询会查全表但不生效。配置其实很简单:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(100L); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }配置好之后,查询代码典型如下:
public IPage<Resource> searchResource(int page, int size, String keyword, Long categoryId) { Page<Resource> pageInfo = new Page<>(page, size); LambdaQueryWrapper<Resource> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Resource::getTitle, keyword) .eq(categoryId != null, Resource::getCategoryId, categoryId) .eq(Resource::getStatus, 1) .orderByDesc(Resource::getCreateTime); return resourceService.page(pageInfo, wrapper); }wrapper.like的第一个参数是boolean condition,这个设计特别好,只有条件成立时才拼接SQL,避免了写一长串if判断。分页插件底层会拦截SQL,自动拼接LIMIT,返回的IPage里除了当前页数据,还有总条数、总页数等信息,直接返回给前端渲染分页组件即可。
多条件检索还需要注意一个性能问题:如果keyword很长,like '%xx%'会全表扫描。数据量不大的高校平台没关系,但如果资源到了十几万条,建议给资源标题建全文索引,或者用Elasticsearch;不过对于毕设和中小规模项目,MySQL的LIKE查询足够。
3.4 视频点播与m3u8播放链路
视频资源是资源平台里最特殊的一类,单个MP4文件动辄几百MB,直接让浏览器加载再播放非常浪费流量,用户拖动进度条时也容易卡顿。所以我采用的做法是把视频转成m3u8切片格式,利用ffmpeg生成索引文件和多个ts分片:
ffmpeg -i input.mp4 -codec copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8这条命令会把MP4切成10秒一个的ts分片,播放器通过m3u8文件自动按顺序加载分片。改成这种方案后有两个明显好处:一是拖动进度条时只需要加载对应分片,不会整个文件下载;二是可以配合CDN缓存分片,缓解服务器带宽压力。
前端播放我用hls.js,Vue3组件里可以这样写:
import Hls from 'hls.js'; export default { props: { videoUrl: String }, mounted() { if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(this.videoUrl); hls.attachMedia(this.$refs.video); } else if (this.$refs.video.canPlayType('application/vnd.apple.mpegurl')) { this.$refs.video.src = this.videoUrl; } } }这里有个关键点,m3u8播放接口要考虑跨域。如果前端域名是http://localhost:5173,后端是http://localhost:8080,浏览器请求m3u8时会被CORS拦截,播放器一直报错。我建议不要只依赖后端开CORS,更稳妥的办法是在Nginx或Vite代理中把/api和/videos都转发到后端,这样浏览器看到的就是同源请求。视频播放页的URL也很容易踩坑,如果用动态路由/video/:id,在组件里取参数时要注意:
watch(() => route.params.id, (newId) => { loadVideo(newId); }, { immediate: true });不监听route.params变化的话,你在同页面切换视频时会发现视频地址没更新,这是Vue路由复用组件的经典问题。
3.5 评论、收藏、下载次数与排行榜实现
评论模块如果做得简单,就是一张表一个接口一次查询。我建议加一点细节:评论支持一级回复,用parent_id区分楼层和回复;查询时一次性查出当前资源的所有评论,在内存里组装成树形结构返回前端,避免“查询递归疯了”的问题。前端渲染时就能做成楼中楼效果,代码也不算复杂。
收藏表和下载记录表建立联合唯一索引:
ALTER TABLE favorite ADD UNIQUE KEY uk_user_resource (user_id, resource_id);这样用户重复收藏时会触发唯一约束异常,后端捕获后直接返回“您已收藏”即可。下载次数和浏览量,我用的是最简单的方式:每次下载成功后在数据库更新download_count字段,并插入一条下载记录。资源访问量大的话可以先用Redis计数器,然后定时刷新到数据库,但高校平台并发量不高,直接数据库更新完全没有压力。
排行榜就是统计页面的核心:
select * from resource order by download_count desc limit 10;资源多了以后建议只统计最近一个月的数据,避免历史老资源常年霸榜,影响新资源曝光。我后来在排名SQL里加了create_time筛选条件,同时支持按“下载排行”“最新上传”“最多收藏”三个维度切换,前端用三个Tab展示,体验就好很多。
4. 前端工程化与联调细节
4.1 项目初始化和环境配置实操
我每次新开前端项目都会先把目录结构规划好,而不是一股脑往App.vue里塞代码。推荐的结构是:
src/ ├── api/ # 接口请求模块 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由配置 ├── stores/ # Pinia状态 ├── views/ # 页面组件 ├── utils/ # 工具封装 └── App.vue创建好后,第一步配置Vite代理,解决本地联调的跨域问题:
// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样前端请求/api/resource/list时,Vite会自动转发到后端的http://localhost:8080/api/resource/list,浏览器地址栏看到的域名始终是localhost:5173,就不会有跨域报错。注意代理配置必须在npm run dev启动后生效,改了配置要重启开发服务器。
4.2 Vue Router路由设计与参数传递
路由设计不要只用路径字符串硬编码,我强烈建议用命名路由,特别是页面跳转需要带参时,可维护性会好很多。资源相关页面可以这样配置:
const routes = [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue') }, { path: '/resource', name: 'ResourceList', component: () => import('@/views/ResourceList.vue') }, { path: '/resource/:id', name: 'ResourceDetail', component: () => import('@/views/ResourceDetail.vue') }, { path: '/video/:id', name: 'VideoPlay', component: () => import('@/views/VideoPlay.vue') } ];跳转时用router.push({ name: 'ResourceDetail', params: { id: row.id } })。需要保留搜索关键字时,用query传参更合适,例如router.push({ name: 'ResourceList', query: { keyword: keyword } })。区别在于params参数不会出现在URL里,刷新页面后丢失;query参数会出现在?keyword=xxx中,刷新后仍然存在。我通常把资源ID放在params里,把搜索条件放在query里,这样列表页刷新后还能保持搜索状态。
路由守卫也得写好,否则用户在未登录状态就能打开个人中心,然后接口401报错。全局前置守卫:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }); } else { next(); } });登录页跳回原页面时,用route.query.redirect解析一下就能实现。
4.3 Axios封装与统一错误处理
后端返回的数据结构如果设计得好,前端封装会省很多事。我统一使用这样的封装:{ code: 200, message: "操作成功", data: {} },code为200表示成功,其他code表示业务失败,HTTP状态码只用于真正的系统级错误。Axios实例的完整封装:
import axios from 'axios'; import { ElMessage } from 'element-plus'; import router from '@/router'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res.data; } ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } else { ElMessage.error('网络异常,请稍后重试'); } return Promise.reject(error); } ); export default request;封装完以后,页面请求不需要关心Token和错误提示,逻辑代码干净很多。这里提一下,有时候response已经是二进制文件流(比如导出Excel、下载文件),拦截器不能统一按JSON处理,需要单独对responseType: 'blob'做判断,否则下载下来的文件会损坏。
4.4 Element Plus上传组件的使用与文件上传踩坑
Element Plus的el-upload组件功能很强,但真正用起来有几个细节。首先它默认用Action指定上传地址,如果你用了我上面封装的Axios,会发现Token带不上去。解决办法是自定义http-request:
<el-upload :show-file-list="true" :http-request="handleUpload" :before-upload="beforeUpload" > <el-button>选择文件</el-button> </el-upload>async handleUpload(options) { const formData = new FormData(); formData.append('file', options.file); try { const url = await request.post('/file/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); options.onSuccess(url); } catch (error) { options.onError(error); } }前端beforeUpload里可以做文件大小、类型的预校验,提前拦截不符合条件的文件,避免上传到后端才返回错误,用户体验会好很多。另外,后端Spring Boot有默认的multipart.max-file-size限制,默认只有1MB,不配置的话视频文件一传就报错。生产环境记得在application.yml中调大:
spring: servlet: multipart: max-file-size: 1024MB max-request-size: 1024MB前端Nginx的client_max_body_size也需要同步调整,否则后端配了也没用,Nginx会先拦下来返回413。
5. 部署上线与运维排错
5.1 用Docker Compose一键部署前后端
项目开发完,手动在服务器上装JDK、Node、Nginx、MySQL太容易出错了。我用Docker Compose把服务编排起来,四件事:MySQL数据库、Redis缓存、后端应用、前端Nginx。后端Dockerfile用多阶段构建,先Maven打包再运行jar包:
FROM maven:3.8-openjdk-8 AS builder COPY . /app WORKDIR /app RUN mvn clean package -DskipTests FROM openjdk:8-jre COPY --from=builder /app/target/*.jar /app/app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]前端Dockerfile要先把源码构建成静态文件,然后用Nginx镜像托管:
FROM node:18 AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:stable-alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80Nginx配置里最关键的三个点:开启gzip压缩静态资源、配置资源目录的访问路径、处理Vue history路由刷新404问题:
server { listen 80; server_name _; gzip on; gzip_types text/plain text/css application/javascript application/json; client_max_body_size 1024m; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:8080; } location /videos/ { proxy_pass http://backend:8080; } }try_files ... /index.html;这一行解决的就是刷新页面404的问题。服务器不小心把/resource/12当成后端接口去找,肯定找不到,回退到index.html后Vue Router自己会解析路由。
5.2 核心数据库表设计与初始化数据
数据库是项目的根,设计好了后面开发会非常顺。这里给一份核心表的精简结构,字段以实际项目为准。
用户表:id、username、password(BCrypt加密)、real_name、role(STUDENT/TEACHER/ADMIN)、avatar、create_time。
资源表:id、title、description、category_id、file_url、file_type、file_size、uploader_id、status(待审核/已发布/已下架)、download_count、view_count、create_time。
分类表:id、name、parent_id、sort。分类做两级就够了,比如“计算机课程”下挂“Java”、“数据结构”。
评论表:id、resource_id、user_id、content、parent_id、create_time。
收藏表:id、user_id、resource_id、create_time,加联合唯一索引。
下载记录表:id、user_id、resource_id、create_time。这张表是为后续排行榜和统计服务的。
初始化数据时,管理员账号要预先插入,密码记得用BCrypt加密,不能直接放明文。分类数据也建议写在SQL脚本里,方便重复部署。生产环境MySQL连接串里要加上useSSL=false,避免本地环境没有证书导致连接报错。
5.3 高频报错与排查速查表
这部分内容是我和学弟联调时总结出来的,很多问题光看报错信息根本不知道从哪下手。我整理成表格,方便你直接对照排查。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| Spring Boot启动失败,提示端口被占用 | 8080端口已被其他进程占用 | 改server.port或杀掉占用进程,用lsof -i:8080查看 |
| MyBatis Plus分页不生效,查出来全表数据 | 没配置PaginationInnerInterceptor | 检查配置类是否被Spring扫描到 |
SQL报错Table doesn't exist | 表名和实体类不一致,比如数据库里是resource但实体注解写成了resources | 检查@TableName注解 |
| 前端访问接口跨域报错 | 没走Vite代理或后端CORS配置缺失 | 前端配proxy,生产环境用Nginx反代 |
| Vue页面刷新后404 | History路由没有fallback | Nginx配置try_files |
| m3u8播放黑屏,控制台报跨域 | 视频流请求被CORS拦截 | 用Nginx反代统一请求域名 |
| 文件上传提示413 Request Entity Too Large | Nginx限制上传大小 | 调大client_max_body_size |
| npm install报权限错误 | Node全局目录没有写权限 | 用npm config set prefix指定用户目录 |
| Element Plus按需引入后组件不生效 | 没自动导入样式 | 用unplugin-vue-components或全量引入排查 |
| Vue Devtools看不到组件 | 当前是生产构建或者未允许访问文件URL | 开发模式运行,扩展启用“允许访问文件URL” |
排查问题最重要的是顺序感:先看浏览器Network请求有没有发出,再看后端日志有没有异常,最后看数据状态是否正确。很多同学一上来就看代码,找半天发现是Nginx没重启或者代理配置写错,白白浪费时间。
还有一个非常容易踩的坑是Spring Boot版本太高导致第三方库冲突。有次我把项目升级到Spring Boot 3.2,结果Druid连接池和Swagger全部报错,后来检查才发现javax包被换成jakarta包,所有依赖都得换新版。如果你用的是网上找的老教程代码,建议不要轻易升级Spring Boot大版本,先用2.7系列跑通,后续再慢慢迁移。这一点在毕设阶段能省下大量时间。
最后再分享一点个人体会
做完整套项目,我最深的体会是:技术选型别追新,能跑通、能上线、能维护,这三点比什么都重要。Spring Boot + Vue这套组合真正难的不是某个框架的用法,而是把“资源从上传到播放/下载”这条链路串起来,很多坑都出在版本、跨域和文件路径上。如果你正在做类似的高校教育资源共享平台,建议先把文件存储和视频播放的链路单独做一个Demo,再往里面填业务功能,而不是一上来就写登录页面。登录、权限、列表这些都是通用模板,真正需要反复打磨的永远是资源处理的核心链路,把这部分跑顺了,整个平台的骨架就稳了。希望这些经验能帮你少踩几个坑。