前段时间接了个活儿:给学校的信息学科部做一个官网。这种院系级别的网站,大家应该都不陌生——门户首页、新闻通知、师资队伍、专业介绍,外加学生最常用的下载中心。表面看需求不复杂,真做起来才发现细节一堆:新闻图片要能传上去、通知要按权限发布、活动视频要能在线播放,还得同时兼容电脑和手机。我最终落地的方案就是标题里这套 Spring Boot + Vue 前后端分离架构,配合 MySQL、MinIO 这些配套组件,也就是 "springboot信息学科部校园网站3249q-vue" 整个项目的核心。这篇文章把完整过程、选型逻辑、踩过的坑全部梳理一遍,给后面要接校园网站、内容管理系统这类项目的朋友做个参考。
1. 项目整体设计与技术选型思路
1.1 为什么锁定Spring Boot + Vue这套组合
先说选型。接到需求时,我的第一个念头就是:别把简单事情搞复杂。信息学科部的校园网站,本质是一个内容管理系统加信息展示平台,核心动作是"发布内容"和"展示内容",没有特别复杂的高并发、高实时性场景。但也不能拿纯静态页面去糊弄,后面肯定要加新闻管理、文件上传、权限控制这些动态功能,所以最终确定 Spring Boot + Vue 这套组合。
选 Spring Boot,理由是"省心"。它是构建后端服务最快的方案之一,自动装配机制帮我省掉了大量 XML 配置,Maven 依赖拉下来就能跑,内嵌 Tomcat 又不用单独装服务器。对校园网站这种业务来说,Spring Boot 的成熟生态和稳定性能完全够用,而且招人维护时,会 Spring Boot 的开发者远比会老旧 SSH 框架的多。
前端选 Vue,是因为它足够轻、组件化思路清晰,而且在国内高校和企业里的普及率极高。Vue 的单文件组件(SFC)把一个页面拆成"模板 + 脚本 + 样式",写起来直观,维护起来也友好。再加上 Element Plus 这类现成组件库,做后台管理页面基本是拼积木。这套组合还有一个隐性优势:前后端分离以后,前端只需要关注页面渲染和数据交互,后端只需要把接口设计好,两边可以并行开发,不需要互相等待。
1.2 信息学科部官网的功能模块拆解
在动工之前,我先把需求拆成了两个端:前台门户和后台管理。
前台门户主要面向师生和外部访客,核心模块有这么几块:
- 门户首页:展示学科部简介、新闻动态轮播图、通知公告、快速入口。
- 新闻通知:按分类展示学院新闻、教务通知、学工动态,支持搜索和分页。
- 师资队伍:教师列表、教师详情(研究方向、成果、联系方式)。
- 专业介绍:各本科/专科专业的信息展示。
- 下载中心:教学资料、表格模板等文件下载。
- 招生就业:面向考生的招生宣传内容。
后台管理端主要面向管理员和部门干事,设计成角色化权限管理:
- 内容管理:新闻、通知的发布、编辑、下架、置顶。
- 文件管理:下载中心的上传、分类、删除。
- 用户管理:管理员账号维护、角色分配。
- 基础配置:轮播图设置、网站参数配置。
这个拆法基本沿用了我做类似项目的经验:前台管"看",后台管"发",中间通过一套接口桥接起来。权限大体分成三类:超级管理员(管所有模块)、部门管理员(管自己负责的栏目)、普通编辑(只能发文章)。权限模型不搞太复杂,校园官网的角色贵在够用,过度设计后面反而是负担。
1.3 页面骨架与权限模型设计
页面骨架我采用的是"前台走官网风格、后台走管理风格"的双模板策略。前台用响应式布局,重点照顾手机上浏览新闻的体验,后台则用侧边栏加顶栏的管理框架,方便管理员快速切换功能。
这里有一个值得说的点:权限模型的设计决定了后面所有菜单和接口的编写方式。我没有用太重的 RBAC 表结构,而是采用"用户—角色—菜单"三层,角色对应一组菜单编码,后端返回该角色允许访问的菜单树,前端根据菜单树动态生成侧边栏。
这样做的直接好处是:后端接口通过过滤器统一校验权限,前端菜单也是同一套数据驱动生成,权限改动只需要在数据库里调整角色和菜单的关联,不用同时改前端代码和后端拦截逻辑。对一个课程设计阶段或部门自用系统来说,这是性价比非常高的方案。
2. Spring Boot后端:从零搭建到核心接口落地
2.1 环境准备与项目初始化要点
开发环境我用的 JDK 1.8 + Maven 3.6.3,Spring Boot 版本选了 2.7.x。很多朋友迷信最新版本,我这次特意没有上 Spring Boot 3.x,原因很简单:3.x 强制 JDK 17,而且大量第三方依赖把 javax 命名空间切换成了 jakarta,很多旧教程和老依赖会直接报错。对一个要快速交付、稳定上线的项目来说,2.7.x 的生态反馈最快,坑最少。这个选择也提醒各位一句——技术选型永远是为交付服务的,不是为简历服务的。
项目创建我用的是 Spring Initializr,也可以直接用 IDEA 的 Spring Initializr 窗口。关键依赖我加了这些:Spring Web、Spring Data JPA 或 MyBatis-Plus(我用的 MyBatis-Plus)、MySQL Driver、Lombok、Spring Security、JWT 相关工具包。MyBatis-Plus 在这里帮了大忙,单表 CRUD 几乎不用写 SQL,自带分页插件、逻辑删除、字段填充,非常适合内容管理这种以单表查询为主的业务。
初始化之后第一件事是配置 application.yml。除了常规的端口、数据库连接,要特别注意两个地方:一是 MySQL 的时区参数,二是上传文件大小限制。时区不配置,查询出的日期经常会差 8 个小时,这是新手最容易忽略的;上传限制不放大,新闻配图稍微大一点就直接报错。我的配置大概是:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/site_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: xxxxxx servlet: multipart: max-file-size: 50MB max-request-size: 100MB这里要补充一个容易踩坑的细节:MySQL 连接串里加上 allowPublicKeyRetrieval=true 也很重要,尤其使用 MySQL 8.x 时,某些客户端连接会报"Public Key Retrieval is not allowed"。服务能跑起来只是第一步,这些底层依赖问题越早配置越省心。
2.2 统一响应结构与全局异常处理
后端接口设计我坚持一个原则:所有接口返回统一格式。前后端分离之后,前端最怕的就是接口返回格式五花八门——一会儿返回字符串,一会儿返回对象,一会儿抛异常直接返回错误页。我在这个项目里定义了一个 Result 类,固定包含 code、message、data 三个字段:
public class Result<T> { private Integer code; private String message; private T data; }所有 Controller 的返回值都包成 Result,成功 code 是 200,业务失败用 500 或自定义业务码。前端拿到响应后,统一在 axios 拦截器里判断 code,等于 200 就正常返回,不等于就弹提示。这一套下来,前后端联调时几乎没有出现过格式对不上的情况。
与之配套的是全局异常处理。我用 @RestControllerAdvice 写了一个全局异常捕获器,把 RuntimeException、参数校验异常、文件上传异常等都统一转换成 Result 返回。这一步的价值在于:后端不把堆栈信息直接抛给前端,避免暴露内部细节,也让前端错误提示永远保持统一风格。
2.3 登录认证与权限控制:为什么我用了JWT拦截器
关于登录认证,我一开始其实犹豫过要不要上 Spring Security 全家桶。后来走访了一圈,发现自己做过的大部分同类项目,用 Spring Security 时配置复杂、放行规则容易写错,反而拖慢进度。所以我最后采用了一个更"轻"的方案:JWT 令牌 + 自定义拦截器。
具体逻辑是这样的:用户登录成功后,后端生成一个 JWT 令牌返回给前端,令牌里只携带用户 id 和角色编码,有效期设置为 24 小时。前端请求头带上 Authorization: Bearer ,后端写一个拦截器统一解析,解析成功就把用户信息放进 ThreadLocal,解析失败返回 401 状态码。首页、新闻列表、教师展示这些公开接口,通过白名单配置直接放行,不经过拦截器。
字符串这种实现,简单直接,对校园官网完全够用。但要注意几个关键点:JWT 密钥千万不要硬编码在业务代码里,我放到了配置文件;拦截器里要排除登录接口和静态资源路径,否则项目一启动就 401;前端要在 axios 响应拦截器里统一处理 401,让用户跳回登录页,而不是弹一个莫名其妙的报错。
2.4 文件上传与m3u8视频播放支持
这个项目让我最费工夫的功能是视频处理。学校希望官网能在"学院风采"栏目里放活动视频,但浏览器对普通 mp4 大文件在线播放兼容性并不好,尤其要拖动进度条时体验很差。最终我采用了 HLS 方案:把上传的 mp4 用 FFmpeg 转成 m3u8 索引文件加 ts 切片,前端用 video.js 或者 hls.js 播放。
文件存储我用了 MinIO。MinIO 是兼容 S3 协议的对象存储服务,用 Docker 就能起一个实例,相比直接存服务器本地磁盘,它的好处是文件访问走 HTTP,管理界面直观,扩展也容易。Spring Boot 集成 MinIO 的核心步骤就三步:引入 minio 依赖,配置服务地址和账号密钥,封装一个 upload 方法返回访问 URL。
视频转 m3u8 我走的是异步流程:视频上传到 MinIO 后,立即返回"上传成功,转码中"的状态给前端,后端用线程池执行 FFmpeg 命令,转码完成后更新数据库状态。这样做的好处是前端不用一直等待,后台也可以集中处理耗时的转码任务。FFmpeg 核心命令大致如下:
ffmpeg -i input.mp4 -codec copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8这里有个实操细节:如果只是转 HLS,-codec copy 可以直接复制流不重新编码,速度非常快,但它的前提是原视频编码格式兼容(H.264/MPEG-4)。如果上传的视频编码不标准,保险起见还是要加 H.264 编码参数保证兼容性。
3. Vue前端:环境、路由与组件化实践
3.1 Vue环境配置与项目创建的版本选择
前端我把 Vue 版本定在了 Vue 3,配套 Vite 构建工具和 Element Plus 组件库。Vue 3 用组合式 API 写起来更灵活,Vite 的开发服务器启动速度比老一代 Webpack 快得不是一点半点,改动代码热更新几乎是秒级,这对调试体验提升很大。
环境配置方面,我用 Node 18 LTS 版本。当时极简流程是这样:
- 先安装 Node.js,npm 随自带;
- 用 vite 模板创建项目,命令是 npm create vite@latest,交互界面里选择 vue 即可;
- 安装依赖 npm install,启动开发环境 npm run dev。
这里要强调一个经常被人忽略的问题:npm 源。默认官方源在国内拉依赖时慢到怀疑人生,建议尽早切换为国内镜像源,命令是 npm config set registry https://registry.npmjs.org/ 改成镜像即可。这不算什么高级操作,但能省出的时间非常可观。
Vue 项目创建完成后,我按模块拆分目录:views 放页面,components 放组件,router 放路由,store 放全局状态,api 放接口请求封装。分层清晰的好处,后面加功能、改 bug 时体会特别明显。
3.2 动态路由与菜单权限的落地姿势
Vue 侧的这个项目里,动态路由是我重点设计的一个环节。刚才说了,后端返回菜单树,前端要根据菜单生成侧边栏并注册路由。这里我采用的方案是:前端定义一份"路由映射表",把后端返回的菜单编码映射到具体的组件路径。
具体做法是:登录接口返回用户信息的同时带上 menuList,前端把这份菜单存到 Pinia 或 Vuex 里,然后通过 router.addRoute 动态注册路由。举个例子,后端返回一个菜单项是"新闻管理",对应前端路由路径 /admin/news,映射到 views/admin/NewsList.vue 组件,前端在路由守卫里判断该用户是否已经注册过动态路由,没有的话先 addRoute 再放行。
这套方案最核心的价值:菜单权限和路由权限来自同一份数据,不会出现"菜单看到了、点进去却 404"的尴尬。我在项目里给 Vue Router 设了一个约定——页面文件名和路径保持一致,动态映射时通过 import.meta.glob 一次性加载所有页面组件,省去手动 import 的麻烦。
3.3 组件化拆分:通用列表页、插槽与复用
做后台管理,最烦的就是重复写列表页:搜索框、表格、分页器、编辑弹窗,每个模块都长得很像。我用组件化的方式解决这个问题,抽了一个 CommonTable 组件,把搜索表单、数据表格、分页封装成一个整体,父组件只需要传入接口请求函数和列配置,就能生成一个完整列表页。
这里体现的是 Vue 插槽(slot)的强大之处。我在 CommonTable 组件里预留了几个插槽位置:搜索区域插槽、表格操作列插槽、弹窗内容插槽。不同的业务模块只需要往插槽里塞不同的内容,列表结构和分页逻辑全部复用。比如新闻管理需要"置顶"按钮,就在操作列插槽里写一个按钮,绑定对应的请求方法;下载中心需要"复制链接"按钮,也同样在插槽里追加。
这种组件化改造一开始会花点时间,但项目做到第四、五个功能模块时,效率优势极其明显。写代码第一版我是直接从新闻列表复制改成通知列表,改到一半发现到处都是冗余代码,才痛下决心抽象组件。如果大家下次做类似项目,建议从第一个列表页就开始用通用组件思路,不要走我这种"先复制再重构"的老路。
3.4 前端播放器集成:m3u8播放免转码方案
视频播放这里,我前端用的是 video.js 加 hls.js 的组合。Vue 项目里安装视频播放器的依赖方式很简单:
npm install video.js但要注意,video.js 原生默认不直接支持 m3u8 直播流的播放,通常还需要引入 hls.js 作为它的解析器,或者直接使用 video.js 自带的 contrib-hls 插件。我这里选了 hls.js 做底层流解析,video.js 做 UI 框架,封装了一个 VideoPlayer 组件。
这个组件接受一个视频地址,如果是 .m3u8 后缀就走 hls 播放逻辑,是普通 mp4 就直接喂给 video 标签。封装完以后,前台页面使用这个组件时只需要传视频 URL,其他全部内部处理。
有个经验要分享:m3u8 播放时,浏览器跨域问题容易导致加载失败。如果 m3u8 文件里的 ts 切片地址是相对路径,而播放器页面域名和存储服务域名不一致,就会出问题。最稳妥的做法是让 FFmpeg 转码时输出绝对路径的 m3u8,或者在 MinIO 桶策略里配置好访问域名,确保所有切片 URL 都能被正常访问。这种底层兼容问题排查起来非常费劲,我光在这个坑里就花了小半天。
4. 前后端联调、打包与部署实录
4.1 联调阶段的跨域与代理问题
前后端联调第一个拦路虎就是跨域。前端开发服务器跑在 5173 端口,后端接口跑在 8080 端口,浏览器默认会拦截跨域请求。解决思路有两个:后端开启 CORS,或者前端配置代理。
我在开发环境用的方案是 Vite 代理。在 vite.config.js 里配置一个 proxy,把请求 /api 开头的接口转发到 localhost:8080,前端代码里请求地址只写 /api/xxx,这样浏览器端不存在跨域问题。后端不需要额外写 CORS 配置,开发环境清净很多。
但要注意,部署到服务器后,如果前端走 Nginx 提供静态服务、后端接口仍然由 Spring Boot 提供,跨域问题会重新出现。这时候就要用 Nginx 反向代理统一路径,或者在后端配置文件里配置 CORS 允许的域名白名单。我偏好的是 Nginx 反代方案,它能同时解决静态资源服务和接口转发两个问题,而且不用改代码。
4.2 Vue打包放进Spring Boot的两种方式
这个项目里我实际上测试了两种部署方式,都在不同场景下用得上。
第一种是最省事的单应用部署:前端执行 npm run build 后生成 dist 目录,把 dist 里的文件整个复制到 Spring Boot 项目的 src/main/resources/static 下,然后重新打包 Spring Boot jar。这样整个系统只有一个应用,一个端口,部署时只需跑一个 jar 包,非常方便。缺点是前后端代码耦合在一起,每次改前端界面都要重新打包后端,不够灵活。
第二种是标准的前后端分离部署:前端 dist 交给 Nginx 托管,后端 Spring Boot 以 jar 独立运行。这种方式上线后,前端文件更新只需替换 dist 并刷新 Nginx,后端接口迭代单独重启 jar,互不影响,也更贴近生产环境。
我当时交付给学校的是第二种,因为后续部门老师可能要频繁改页面内容,如果每次都要重新打一个 jar,维护成本太高。但如果遇到内网测试环境不方便装 Nginx 的情况,第一种方案就是最快捷的兜底。
4.3 部署到服务器:端口、静态资源与Nginx反代
部署到服务器时,我的操作流程大致如下:
- 后端项目执行 Maven 打包命令 mvn clean package -DskipTests,生成 jar 文件;
- 前端执行 npm run build,生成 dist 文件夹;
- 把 jar 和 dist 上传到服务器指定目录;
- 后端通过 nohup java -jar site-server.jar & 方式启动,日志重定向到文件;
- 服务器安装 Nginx,配置一个 server 块,root 指向 dist,location /api/ 反代到本机 8080 端口。
Nginx 配置核心片段长这样:
server { listen 80; server_name your-domain.com; root /opt/site/dist; index 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; } }这里有个部署细节:Vue Router 如果开了 history 模式,刷新页面时 Nginx 会找不到对应的静态资源,返回 404。需要在 Nginx 配置里加一个 try_files $uri $uri/ /index.html;,把所有未命中的路径都回退到 index.html。这个坑我当年第一次部署 Vue 项目时就遇到得刻骨铭心,这次提前写进配置,一次通过。
端口问题也要提一嘴:Spring Boot 默认端口 8080,很多服务器上这个端口已经被占用了,启动会直接报错。我习惯在配置里额外看一眼端口占用情况,用 netstat -tlnp 查一下,如果是云服务器还要在安全组里放行对应端口,否则外部依然无法访问。防火墙和安全组是两套体系,漏掉任何一个都可能导致服务起来了却访问不了。
5. 常见问题排查与避坑指南
5.1 典型问题与解决方案速查表
我把这次项目中踩过、以及同事们经常遇到的典型问题整理成一个速查表,按问题现象、原因定位、解决方案三列列出,后面的项目直接对照查就行:
| 问题现象 | 原因定位 | 解决方案 |
|---|---|---|
| 后端启动报 mysql 时区异常 | 连接串缺少 serverTimezone | 连接串加 serverTimezone=Asia/Shanghai |
| 前端 npm install 极慢 | npm 默认使用官方源 | 切换到国内镜像源 |
| 上传图片超过 1MB 报错 | Spring Boot 默认限制 1MB | 在配置中调整 max-file-size |
| Vue 打包部署后刷新 404 | history 模式缺少回退规则 | Nginx 配置 try_files 回退到 index.html |
| 前端请求接口 502 | 后端服务未启动或端口未放行 | 检查 jar 进程、查看日志、配置安全组 |
| 登录接口被拦截器拦截 | 白名单未配置 | 在拦截器排除登录接口和静态资源 |
| m3u8 视频黑屏 | ts 切片地址跨域或未生成 | 配置 MinIO 桶策略,确保切片可访问 |
| 日期显示相差 8 小时 | MySQL 时区和 Jackson 序列化时区不一致 | 统一配置 GMT+8 |
这个表在交付时我还单独打印了一份给学校的运维同事,方便他们后面遇到问题先自查。实用价值比甩一份开发文档高得多,这也是做项目时一个很讨巧的技巧——把最容易出问题的东西总结成速查表,比写一百页说明文档都好使。
5.2 写了这么多次项目后的5条实战经验
做这类校园网站和内容管理系统,我梳理出的几条实战经验比较核心,写在这里:
第一,数据库表设计不要过度设计,但"发布时间、更新时间、排序字段、状态字段"这四个必须带上。任何内容型网站的核心操作都是"查列表、看详情、改状态",字段一次留够,后面不用反复加列。
第二,前端状态管理库不要滥用。我这个项目里用 Pinia 存的东西其实不超过五个,最核心的就是用户信息和菜单列表。很多人写项目上来就建一堆 store,反而把数据源搞乱了,调试时不知道数据是从接口来的还是从缓存来的。
第三,日志一定要打全。后端每个关键接口的开始和结束都要打日志,尤其登录、文件上传、视频转码这类操作。校园网站出问题时往往都是老师打电话说"传不上去了、发不出来了",这时候有日志能立刻定位。
第四,视频转码这类耗时任务务必异步化。用户上传视频后,如果前端一直等后台转码完成再响应,用户体验非常差。我当时用线程池解决,简单可靠,任务多时也可以换消息队列。
第五,上线前至少留两天做兼容性测试。校园网里什么浏览器都有,老版 Edge、360、火狐,还有各种手机浏览器。Vue 3 项目在低版本浏览器上偶尔会有兼容问题,提前用 babel 或者 browserslist 处理一下,免得上线后被人当客服。
5.3 维护期意外发现的配置细节
项目交付后,我陆续做了两轮小维护,期间又发现几个配置层面容易被忽略的细节。
一个是 Spring Boot 的控制台日志编码问题。服务器上默认编码如果不是 UTF-8,中文日志会显示乱码,排查问题时非常头疼。后来我在启动脚本里加了 -Dfile.encoding=UTF-8 参数,日志恢复正常。
另一个是 MinIO 文件上传的大小限制,其实不只是 Spring Boot 要限制,MinIO 服务端也要注意。如果视频文件特别大,还可能遇到访问超时的问题,这时可以在 Nginx 层增大 proxy_read_timeout。这些问题不真正跑到生产环境基本发现不了,好在它们修复成本不高,但每一条都能在关键时刻救场。
这套项目整体做下来,结构并不复杂,但每一层都有值得打磨的细节。如果后面学校提出更多需求,比如成绩查询、在线选课,这套 Spring Boot + Vue 的骨架可以直接扩展,接口和页面都在,只是新增业务模块而已。
我个人负责这种校园网站项目最深的一点体会是:方案不需要炫技,稳定、好维护、别人接手时看得懂才是真正的价值。技术栈选成熟组合,代码写清晰,文档留到位,这个项目就成功了大半。哪怕以后不做校园网站,转去做企业官网、内部管理系统,这套思路依然通用。