最近把一套基于 Spring Boot + Vue 的考研资讯系统完整跑通了,仓库代号是 cv6al6e3,前后端代码加起来一万多行。这个项目不是那种看一眼就懂的 CRUD 演示,它涵盖了内容资讯展示、院校专业库、在线课程播放、用户打卡、后台运营管理这些模块,是一个比较典型的内容型前后端分离项目。
我把它从数据库建模到前端路由、从视频播放到服务器部署完整走了一遍,过程中踩了不少坑,也把 Spring Boot 自动装配、Redis 缓存、JWT 认证、Vue 路由守卫、m3u8 视频播放这些高频考点全部串进了项目里。这篇文章我就按实际开发顺序,把整套系统的设计思路、核心代码逻辑、部署方式和排错经验一次说清楚,适合正在做毕设的计算机专业学生、想积累项目经验的初级开发者,以及准备面试时想拿真实项目讲技术点的人参考。
1. 考研资讯系统:先想清楚要做什么再写代码
很多同学拿到这类项目标题第一反应是“登录注册 + 增删改查”,然后直接开写。等你写到一半就会发现,考研资讯系统真正麻烦的不是用户表,而是资讯内容怎么分类、院校数据怎么结构化、视频课程怎么播放、运营后台怎么管理内容。这些业务边界没想清楚,代码写得越快,返工越狠。
1.1 业务拆解:资讯、院校库、打卡、管理后台缺一不可
我接手这个项目后,先花了一天把需求拆成了四个大块,这是整个项目最值得先做的事。
第一块是资讯内容端。考研资讯不是简单发一篇文章,它有多个栏目,院校动态、政策解读、复习经验、调剂信息都混在一起的话,用户根本找不到想要的内容。所以资讯表必须带栏目字段,前端导航栏、首页推荐位、列表筛选全部依赖这个字段。资讯详情页还要有浏览量统计,方便后台做热门推荐。
第二块是院校与专业库。这是资讯系统区别于普通博客的核心。院校需要按省份、城市、层次(985/211/双非)、类型(综合/理工/师范)筛选,专业要关联所属院系和考试科目。我在设计时单独建了学校表、专业表、学校专业关联表三张表,没有把专业信息塞在学校表里,否则后面做筛选和详情页的时候会非常痛苦。
第三块是学习辅助功能。项目里有一个复习打卡模块,用户每天记录学习科目和时间,系统按周生成统计图表。这类功能技术难度不高,但很能体现系统完整性,放在项目描述里也是加分项。视频课程模块也是这一块的重点,管理员上传 mp4 视频,系统转成 m3u8 切片供前端播放,这个后面单独讲。
第四块是后台管理系统。资讯发布、院校数据维护、用户管理、打卡记录查询、视频上传管理,这些操作全部走独立的管理员界面。前端用 Vue Router 做嵌套路由区分前台和后台,后端通过用户角色字段配合拦截器控制接口访问权限。
这样拆完之后技术选型就很清晰了:数据关系明确、前后端交互频繁、有内容展示也有文件上传,Spring Boot + Vue 前后端分离正好是最稳妥的组合。
1.2 技术选型:Spring Boot + Vue 前后端分离的匹配逻辑
选 Spring Boot 做后端几乎没有悬念。它内嵌了 Tomcat,不用额外部署容器,打一个 jar 包就能跑;自动装配机制帮你省掉大量 XML 配置;生态里 MyBatis-Plus、Redis、JWT、EasyExcel 这些轮子都现成,学生项目最需要的就是这种“能快速落地”的框架。
前端选 Vue 是因为它对新手最友好。模板语法贴近 HTML,Vue Router 做路由跳转非常直观,Element Plus 组件库把表格、表单、弹窗、日期选择器这些后台管理界面常用的东西都封装好了。相比 React 全家桶,Vue 的学习曲线要平缓得多。
我这次选择的组合是 Spring Boot 2.7.x + JDK 8 + MyBatis-Plus + Redis,前端 Vue 3 + Vite + Element Plus + Pinia。这里特别提醒一下版本问题,现在新项目直接生成 Spring Boot 3.x 的话要求 JDK 17,很多人的本机环境还是 JDK 8,跑起来会直接报 class 版本错误。另外不少第三方 starter 对 Spring Boot 3 的兼容性更新不及时,光排查依赖冲突就要耗掉半天时间。既然项目目的是把系统完整落地,建议老老实实用 Spring Boot 2.7.x,这也是目前企业里存量项目最多的版本线。
1.3 开发环境准备:IDEA 创建 Spring Boot 项目和 Vue 工程
IDEA 创建 Spring Boot 项目时,默认连的是 Spring 官方 Initializr,网络不好的时候转圈很久才能出来。我建议在配置里把 Server URL 换成阿里云镜像https://start.aliyun.com,创建速度快很多,而且会给你配好国内源,后续下载依赖也顺畅。
Vue 工程这边,我直接用了 Vite 创建:
npm create vite@latest kyzx-web -- --template vueNode 版本建议 16.14 以上,我用的是 18,配 Vite 5 没有问题。装依赖的时候如果报 ERESOLVE 错误,通常是不问版本冲突,可以用npm install --legacy-peer-deps绕过去,但建议先看一眼是不是某些包版本过老。
2. 后端落地的核心细节与 Spring Boot 原理延伸
后端不是光把接口写出来就完事。代码结构、表设计、认证方案、缓存策略,每一项都影响后面能不能继续扩展。这个项目做完之后,我对 Spring Boot 的一些底层机制也有了更具体的理解,下面按模块拆开讲。
2.1 后端工程结构与分层设计
我按常见的四层结构组织代码,每个包职责单一,后续维护的时候不会找不到文件。
controller:接收前端请求,只做参数校验和结果封装。service:业务逻辑层,处理事务、调用多个 Mapper 组合数据。mapper:MyBatis-Plus 的数据访问层,复杂 SQL 写在 XML 里。entity:数据库表对应的实体类,和表字段一一对应。common:统一返回结果、异常处理、工具类。config:Redis、拦截器、跨域等配置类。dto/vo:接口入参和返回给前端的数据对象。
controller 里我统一返回Result<T>结构,格式是{ code, message, data }。前端 axios 拦截器里统一判断 code,请求成功返回数据,失败弹错误提示并跳登录。这样写的好处是接口风格统一,前端处理逻辑极度简单,不需要每个页面单独做异常判断。
2.2 数据表设计:别小看考研资讯这类内容型系统的建模
数据表是整个系统的地基。我在设计时画清楚了每张表的关系,核心表大概有这些:
| 表名 | 用途 | 关键字段说明 |
|---|---|---|
| user | 用户表 | username、password(加密存储)、nickname、avatar、role |
| article | 资讯文章表 | title、content、cover、category_id、status、view_count |
| category | 栏目分类表 | name、sort,资讯和视频共用 |
| school | 院校表 | name、province、city、level(985/211/双非)、type |
| major | 专业表 | name、code、category、description |
| school_major | 院校专业关联表 | school_id、major_id、exam_subjects、enrollment_count |
| course | 课程表 | title、cover、video_url、m3u8_url、teacher、difficulty |
| study_checkin | 学习打卡表 | user_id、subject、study_date、study_minutes、note |
| favorite | 收藏表 | user_id、target_type(文章/课程)、target_id |
| comment | 评论表 | user_id、target_type、target_id、content、parent_id |
举个例子,文章表单独建栏目分类表而不是直接写死字符串,是因为资讯系统后面一定会扩展栏目,写死字段会让你每次加栏目都要改代码。收藏表用target_type + target_id这种多态设计,可以一套逻辑同时支持文章和课程收藏,不用为每种内容建一张收藏表。
账号密码存数据库之前必须加密,我用的 BCrypt,Spring Security 自带的密码编码器,每次加密结果都不一样,但校验方法能正确比对,防止 数据库泄露后密码明文暴露。
2.3 登录认证与权限:JWT 拦截器方案为什么够用
权限这块是面试必问的重头戏。这个项目我用了 JWT + 拦截器,没有上 Spring Security,原因很简单:系统的角色只有普通用户和管理员,访问控制逻辑非常直接,Spring Security 的过滤器链和配置复杂度对这个项目来说属于过度设计。等以后权限模型变复杂了再引入 Security 也来得及。
JWT 的流程是:用户登录成功后,后端生成一个包含 userId 和 role 的 token,返回给前端;前端存储到 localStorage,之后每次请求在请求头加Authorization: Bearer token。后端写一个AuthInterceptor拦截所有/api/**请求,校验 token 签名、判断角色是否有权访问对应接口。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); } try { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"message\":\"未登录或token已过期\"}"); return false; } } }这里有几个细节容易踩坑。gateway 或 nginx 转发时,请求头Authorization必须正确透传,否则后端永远取不到 token 导致所有请求 401,这个我在联调时排查了很久才发现是前端代理配置把请求头丢了。另外 JWT 是无状态的,token 一旦签发只能等它过期,所以我把过期时间设为 7 天,管理员禁用用户的场景下,需要结合 Redis 黑名单才能实现立即失效,这个作为扩展点写在项目文档里就可以了。
接口权限控制我用@RequireRole("admin")注解在 controller 方法上,拦截器通过反射检查方法注解判断是否放行。这种轻量方案在面试时很好讲:你既能说出 JWT 原理,又能说明白为什么不用复杂框架,体现了你自己的工程判断力。
2.4 Redis 缓存热点数据的几个实际问题
考研资讯系统有一个非常典型的场景:首页资讯列表、院校库列表这些数据是热点数据,读的频率远大于写的频率,如果每个用户打开首页都去查一次 MySQL,数据库压力很大。我把这些列表缓存到了 Redis,key 是接口路径加查询参数,value 是序列化后的 JSON,过期时间设为 30 分钟。
写缓存的时候要考虑三个经典问题:
缓存穿透是指请求一个 id 在数据库里根本不存在的数据,缓存里自然也没有,请求每次都打到数据库。我在缓存查不到时会把空值也缓存下来,过期时间设短一点比如 5 分钟,让恶意请求打不到数据库。
缓存击穿是指某个 key 正好过期,瞬间大量请求同时打到数据库。我用的是逻辑过期而不是物理过期,查询时发现逻辑过期就加互斥锁去重建缓存,其他请求先返回旧数据,避免瞬间压垮数据库。
缓存雪崩是指大量 key 在同一时间段过期。我把过期时间加随机值,比如 30 分钟加 0 到 5 分钟的随机数,避免大面积 key 同时失效。
@GetMapping("/list") public Result<List<ArticleVO>> list(@RequestParam Integer categoryId) { String key = "article:list:" + categoryId; String cached = redisTemplate.opsForValue().get(key); if (cached != null) { return Result.success(JSON.parseArray(cached, ArticleVO.class)); } List<ArticleVO> list = articleService.getByCategory(categoryId); if (list == null || list.isEmpty()) { redisTemplate.opsForValue().set(key, "[]", 5, TimeUnit.MINUTES); } else { redisTemplate.opsForValue().set(key, JSON.toJSONString(list), Duration.ofMinutes(30).plusSeconds(new Random().nextInt(300))); } return Result.success(list); }面试时能讲清楚这三个问题,比单纯说“我用了 Redis 缓存”要加分很多,因为这说明你不只会调 API,而是理解缓存系统在高并发下的实际风险。
2.5 自动装配与 @ConfigurationProperties 到底怎么运转
这个项目里我定义了不少自定义配置,比如 JWT 的私钥、token 过期时间、文件上传路径。我通过@ConfigurationProperties把它们绑定到一个配置类,而不是散落在一个个@Value注解里。
@Component @ConfigurationProperties(prefix = "jwt") public class JwtProperties { private String secret; private long expire; // getter / setter }对应application.yml里:
jwt: secret: your-secret-key expire: 604800000很多人面试被问“Spring Boot 自动装配原理”不知道从哪答。结合这个项目其实很好解释:Spring Boot 的启动类上有@SpringBootApplication,它组合了@EnableAutoConfiguration,这个注解会通过AutoConfigurationImportSelector去读取依赖 jar 包里的META-INF/spring.factories或AutoConfiguration.imports文件,把里面配置的自动配置类加载进来。每个自动配置类用@ConditionalOnClass、@ConditionalOnMissingBean这些条件注解控制,只有满足条件才生效,这就是为什么你引入spring-boot-starter-redis后只需要配置连接信息就能直接用 RedisTemplate,因为 RedisAutoConfiguration 在类路径有 RedisTemplate 类时自动创建了相关 Bean。
我碰到过一个真实问题:自定义拦截器里@Autowired注入的 Bean 一直为 null。原因是我手动 new 了拦截器对象,没有由 Spring 容器管理。后来把拦截器做成@Component,在addInterceptors里从容器中获取,问题解决。这个案例是理解 Spring 容器管理 Bean 生命周期的绝佳例子。
3. Vue 侧开发实录:从搭建到视频播放
前端虽然看起来是“写页面”,但涉及的环境搭建、路由设计、状态管理、媒体播放、接口联调,每一步都有不少细节。我按实际推进顺序把 Vue 部分的经验完整过一遍。
3.1 环境搭建:Node 版本、Vite 还是 Vue CLI
创建 Vue 项目我建议直接用 Vite,这已经是社区的主流选择。Vue CLI 基于 Webpack,冷启动速度和热更新体验都比 Vite 差不少。用 Vite 创建项目之后npm install,然后npm run dev就能直接看到页面。
安装依赖的时候遇到过不少问题。比如npm install中途卡住,那是因为下载源访问不稳定,用npm config set registry https://registry.npmmirror.com切换到国内镜像后速度快很多。还有一次 Element Plus 安装后组件样式全部不生效,排查了半天发现是全局 CSS 文件引入了顺序不对,Vite 构建时把业务样式覆盖了组件库样式,解决办法是把 Element Plus 的基础样式放在main.js里最先引入。
3.2 路由规划与权限守卫
考研资讯系统的页面分为前台和后台两块,我在 Vue Router 里用嵌套路由把它们组织起来:
const routes = [ { path: '/', component: HomeLayout, children: [ { path: '', component: Home }, { path: 'articles/:id', component: ArticleDetail }, { path: 'schools', component: SchoolList }, { path: 'schools/:id', component: SchoolDetail }, { path: 'courses', component: CourseList }, { path: 'checkin', component: CheckIn, meta: { requiresAuth: true } } ]}, { path: '/admin', component: AdminLayout, meta: { requiresAdmin: true }, children: [ { path: 'articles', component: AdminArticle }, { path: 'schools', component: AdminSchool }, { path: 'users', component: AdminUser }, { path: 'courses', component: AdminCourse } ]} ]路由参数传值这里有个容易混淆的点。/articles/:id这种是动态路由参数,在组件里用this.$route.params.id读取;如果跳转的时候带查询条件,用this.$router.push({ path: '/schools', query: { province: '北京' } }),读取时用this.$route.query.province。两者的区别是params必须对应路由配置中的动态字段,query会变成 URL 上的?key=value,刷新后依然保留,而刷新后 params 会丢失,这是写路由时必须记牢的。
路由守卫方面,我配置了前置守卫做登录态判断和权限控制。没有 token 的用户访问打卡页面就跳转到登录页,非管理员访问/admin直接踢回首页,同时保留登录后回跳记录的参数,这个细节比较友好:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else if (to.meta.requiresAdmin && role !== 'admin') { next('/') } else { next() } })3.3 Pinia 还是 Vuex:状态管理选型
项目里需要全局共享的状态不多,主要是用户信息、登录态、购物车(课程收藏)这些。我选了 Pinia 而不是 Vuex,理由是 Pinia 的 API 简洁得多,没有mutations这个概念,直接在 store 里定义 state 和 action,而且天然支持 Composition API 和 TypeScript。
// stores/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}') }), actions: { setLogin(data) { this.token = data.token this.userInfo = data.userInfo localStorage.setItem('token', data.token) localStorage.setItem('userInfo', JSON.stringify(data.userInfo)) }, logout() { this.token = '' this.userInfo = {} localStorage.removeItem('token') localStorage.removeItem('userInfo') } } })面试如果问到 Pinia 和 Vuex 的区别,我会结合项目说三点:Pinia 去掉了 mutations,异步操作直接在 actions 里写;模块化不需要嵌套,一个文件一个 store;对 TypeScript 的推断更友好。这也是为什么新项目首选 Pinia 的原因。
3.4 让页面支持播放 m3u8 视频
视频课程是考研资讯系统的亮点功能,这里要重点讲。我们的视频资源经过转码后生成 m3u8 索引文件和 .ts 视频切片,m3u8 的本质是一个文本文件,里面记录了视频分片的地址和时长,播放器按顺序加载这些分片就能实现流畅播放,同时支持拖动进度条时按需加载,这在点播场景下能大幅节省流量。
前端播放 m3u8 我用了hls.js,兼容性很好,Safari 浏览器自带对 HLS 的支持,Chrome 和 Firefox 需要通过 hls.js 实现:
npm install hls.js播放器组件的核心逻辑:
<template> <video ref="videoRef" controls playsinline style="width:100%"></video> </template> <script setup> import Hls from 'hls.js' import { onMounted, ref } from 'vue' const props = defineProps({ src: { type: String, required: true } }) const videoRef = ref(null) onMounted(() => { if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(props.src) hls.attachMedia(videoRef.value) } else if (videoRef.value.canPlayType('application/vnd.apple.mpegurl')) { videoRef.value.src = props.src } }) </script>实现播放本身不难,但有两个坑必须提前说。第一是跨域问题,m3u8 文件里的切片地址如果在另一个域名,而那个服务器没有配置 CORS 响应头,播放器会直接报跨域错误。除了视频服务器配置Access-Control-Allow-Origin,也可以在 nginx 里对.ts和.m3u8请求统一加上跨域头。第二是要看 m3u8 里切片地址是相对路径还是绝对路径,很多新手被这个问题卡住,切片地址是相对路径时播放器会根据当前页面的域名去拼接,结果自然是 404。最好是让后端在返回 m3u8 内容时把相对路径改写成完整的请求域名地址。
视频上传这块,项目里先用MultipartFile接收上传,然后调用本地ffmpeg命令把 mp4 转成 hls 格式。实际做法可以简单一点,直接用一行命令转换:
ffmpeg -i input.mp4 -codec copy -start_number 0 -hls_time 10 -hls_list_size 0 output.m3u8这是比较节省性能的方案。如果视频量大了可以再考虑用对象存储加转码服务的方案,但对这个项目来说本地转码已经够了。
3.5 封装 axios 与跨域处理
前端所有请求我都封装在一个request.js模块里,统一配置 baseURL、请求头、超时时间和响应拦截器。请求拦截器里自动加上 token,响应拦截器里统一处理 401 和业务错误码。
跨域问题是前后端分离项目绕不开的。开发环境最简单的方式是在 Vite 配置文件里开启代理:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这里changeOrigin: true必须配,否则后端拿到的请求头里的 Host 还是前端地址,如果后端做了域名校验就会出现请求被拒的情况。生产环境则通过 nginx 反向代理解决,前端请求/api时自动转发到后端服务。这套逻辑在实际项目中通用,前端代码里不用写死后端地址,换环境只需要改配置。
4. 打包、部署与线上注意事项
系统本地跑通不算完,能打包部署到服务器上正常运行才是完整的交付。这个环节踩坑最多的是前端静态资源路径和路由刷新 404 问题,影响面很大,必须单独梳理。
4.1 前端打包常见的布局异常与刷新 404
前端执行npm run build后生成的 dist 目录,如果直接扔到 nginx 下访问,常见情况是首页白屏、样式丢失、图片加载不出来,而且刷新某个子页面时直接报 404。这些问题背后的原因完全是两码事,我分别排查过的经验如下:
样式和图片丢失,九成是publicPath的问题。Vite 默认的 base 是/,意味着打包后资源引用路径是/assets/xxx.js,如果你把项目部署在服务器根目录没问题,但如果部署在子路径像http://ip:8080/kyzx/,所有资源都会从根路径去找,自然全部 404。解决办法是在vite.config.js里按部署路径设置 base。
刷新 404 是另一个经典问题。Vue Router 如果用的是history模式,路由路径是真实的 URL,刷新时 nginx 收到这个路径,发现服务器上并没有对应的静态文件,就直接 404 了。有两种解决办法,最直接的是 nginx 配置 try_files 把所有路径都回退到 index.html:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }另一种是改用 hash 模式,URL 会带#,没有刷新 404 问题但不够美观。我用的 history 模式加 try_files,视觉效果正常,刷新也正常。
我在部署时还遇到过一次 layout 异常的隐蔽问题:Element Plus 组件库按需引入后,打包出来样式顺序和本地开发不一致。后来查了原因,把业务样式放在组件库样式之后引入,问题解决。遇到布局异常别第一反应是改代码,先确认是不是打包后资源路径和样式引入顺序的问题。
4.2 Spring Boot 服务端打包与外部配置
后端打包用 Maven 的package命令生成 jar 包。这里要注意跳过测试:
mvn clean package -DskipTests打包后生成的 jar 在target目录,运行命令:
java -jar kyzx-server.jar --spring.profiles.active=prod我把配置拆成application-dev.yml和application-prod.yml。开发环境连接本地 MySQL 和 Redis,生产环境通过环境变量注入数据库密码,避免把真实密码写进代码仓库。这种方式在多环境切换时非常方便,不会出现本地能跑、服务器上起不来的问题。
端口设置我用的是 8080,如果服务器上 8080 被占用了可以用--server.port=8081指定。内嵌 Tomcat 的好处在这里体现得很明显,不用像传统 Java Web 项目那样装独立 Tomcat 再丢 war 包,一个 jar 一条命令服务就起来了。
4.3 安全底线:监控端点暴露与信息泄露
部署上线阶段有一个安全点需要特别重视。如果项目里引入了 Spring Boot Actuator 做监控,它有heapdump、env、beans这些端点,默认情况下如果全部暴露,攻击者可以直接把运行内存的 dump 文件下载下来,再通过工具分析出配置信息和内存里的敏感数据,这就是网上常说的 Spring Boot heapdump 敏感信息泄露漏洞。
我在生产环境的配置里限制只暴露health和info端点:
management: endpoints: web: exposure: include: health,info另外所有需要登录的接口都经过拦截器校验,但像 RSS 订阅、网站地图这些公开接口不需要登录,注意绕过鉴权时只返回公开数据,不要带用户相关信息。
5. 开发中高频问题排查速查
整个项目做下来,真正花时间的不是功能开发,而是排错。我把遇到过的典型问题整理成一个速查表,每一条都是实际踩过的坑:
| 现象 | 可能原因 | 排查思路与解决办法 |
|---|---|---|
| 前端请求后端接口报跨域错误 | 后端未配置 CORS 或前端代理未生效 | 开发环境检查 Vite proxy 是否生效;生产环境检查 nginx 是否转发正确,后端CorsConfig是否配置了允许的来源 |
| 接口返回 401 但前端已登录 | token 过期、请求头没有带上 token 或 Authorization 头丢失 | 查看浏览器网络面板确认请求头;检查 axios 拦截器和网关代理配置 |
| 上传大视频超时 | nginx 默认请求体大小限制 1MB | nginx 配置client_max_body_size 200m;,同时检查 Spring 的max-file-size配置 |
| m3u8 视频无法播放 | 跨域、切片路径错误、hls.js 未正确挂载 | 先手动打开 m3u8 地址看内容,确认切片 URL 是相对还是绝对路径;检查视频服务器的 CORS 头 |
| 打包后首页白屏 | publicPath/base 路径配置错误 | 打开浏览器控制台看资源加载地址,对照实际部署路径修改 base |
| 刷新页面 404 | Vue Router history 模式未配置回退 | nginx 加try_files $uri $uri/ /index.html; |
| Redis 启动后缓存数据没生效 | Redis 服务未启动或连接配置错误 | 先redis-cli ping看是否 PONG,再看 Spring Boot 启动日志有没有报连接失败 |
| Spring Boot 项目启动失败,提示 ClassNotFound | 引入的依赖版本和 Spring Boot 版本不兼容 | 优先用spring-boot-starter-parent管理版本,第三方依赖查下兼容矩阵 |
| 页面布局样式混乱 | 组件库样式被业务样式覆盖、CSS 引入顺序错误 | 检查 main.js 里的样式引入顺序,把组件库样式放前面 |
| 数据库插入数据中文乱码 | 数据库连接未指定 UTF-8 编码 | jdbc url 加characterEncoding=utf8,检查数据库表字符集 |
排查问题有一个总原则,先分前端还是后端,再定位到具体层级。前端打开浏览器开发者工具网络面板,看请求状态码和响应内容;后端看日志文件里的异常堆栈。遇到问题别盲目改代码,先定位到最小复现路径,改一处测一处,效率最高。
我记得有一次用户反馈登录后偶尔会跳到首页而不是上次访问的页面,排查之后发现是登录接口返回数据里没有带redirect字段,前端登录成功后就走了默认跳转。后面在前端登录页把跳转逻辑改成优先读route.query.redirect,没有再出现过这个问题。这类小细节往往是面试时最有价值的素材,因为它展示了你定位问题到解决的全过程。
6. 从项目复盘到面试答题的沉淀
一个完整项目做完之后,如果不做深度复盘,面试时很容易被问倒。Spring Boot 和 Vue 的面试题,其实都可以拿这个项目当锚点来答,让理论落到实际场景里。
6.1 Spring Boot 高频问题可以怎么结合项目讲
问“Spring Boot 自动装配原理”时,我不会干背源码,而是说:我的项目里只加了spring-boot-starter-data-redis依赖,配置文件里写了 Redis 地址,项目启动后 RedisTemplate 就能直接用,这是因为RedisAutoConfiguration这个自动配置类在类路径检测到 RedisTemplate 相关类后自动创建了 Bean,内部通过@ConditionalOnClass判断依赖是否存在,通过@ConditionalOnMissingBean保证用户可以覆盖默认配置。
问“@ConfigurationProperties 和 @Value 的区别”时,我拿 JwtProperties 配置类举例。@Value 是逐个字段注入,适合少量配置项;@ConfigurationProperties 是批量绑定一个配置前缀,适合一组相关配置,还能做类型校验和复杂类型绑定。我选择后者是因为 JWT 的私钥、过期时间、请求头名称这些配置是强关联的一组数据。
问“Redis 在 Spring Boot 里的使用”时,我直接说缓存热点资讯和登录 token 的场景,然后展开缓存穿透、击穿、雪崩的解决策略,面试官基本就能判断出你的水平在哪个档位。
6.2 Vue 高频问题可以怎么结合项目讲
问“Vue 响应式原理”时,我结合项目里用户登录状态变化的场景说明。Vue 3 用 Proxy 拦截对象的读取和依赖收集,修改属性时触发依赖更新,所以 Pinia 的 state 变化能自动驱动页面刷新。
问“路由守卫怎么用”时,我讲述/admin管理后台和打卡页面的权限控制逻辑,beforeEach 中检查 token 和角色,未登录跳转登录页并带上redirect参数便于回跳。
问“computed 和 watch 的区别”时,我举了一个实际的例子:在院校筛选页面,我用 computed 根据用户选择的省份、层次、类型实时计算出筛选后的列表;用 watch 监听搜索关键词的变化,做防抖后请求后端搜索接口。computed 是依赖变化时自动重新计算,有缓存;watch 适合做异步操作或复杂业务逻辑。
问“Vue 打包后布局异常”时,我不仅说 publicPath 和顺序问题,还会讲如何用浏览器开发者工具定位是 CSS 加载失败还是 JS 报错导致的白屏,这个排查方法比答案本身更有价值。
问“为什么选 Pinia 不用 Vuex”时,我把 Pinia 去除 Mutation、天然支持 Composition API、TypeScript 友好这三点结合项目实际说一遍,比背概念强很多。
6.3 这个系统还能拓展成什么样
项目做完了不等于没有优化空间。我在项目文档里写了三个后续方向,也给想做扩展的同学一些参考。
第一个方向是检索能力升级。目前的资讯搜索就是 SQL 的 LIKE 模糊查询,数据量大了之后效率和准确性都不够。可以引入 HanLP 分词做中文分词,再结合倒排索引或者 Elasticsearch 做全文检索,让用户搜“双非逆袭”能匹配到更精准的内容。
第二个方向是办公与协作能力。后台运营人员发布资讯前可以考虑接入 OnlyOffice 实现在线编辑文档预览,这样编辑审稿流程直接在系统内完成。虽然这个集成有一定工程量,但一旦做出来项目的完整度立刻提升一个档次。
第三个方向是流程审核。如果资讯发布需要多级审核,可以集成 Flowable 工作流引擎,或者简单一点加一个audit_status字段配合管理员手动审核——后者虽然简单但也是实际可用的方案。流程引擎的扩展更适合作为后期学习方向,真正投入时多做评估。
可视化大屏也可以做,把用户打卡数据、文章浏览量、院校热度做成大屏展示,前后端都有成熟方案,视觉冲击力强,适合作品集展示。
最后分享一套我自己的调试习惯
项目做到最后,我养成了一套调试习惯,分享出来应该对做同类项目的人有直接帮助。
第一,后端启动时一定观察日志,看到Started KyzxApplication in xx seconds才能确认启动成功。很多接口报错看下启动日志就能找到原因。第二,前端遇到报错先定位是请求问题还是渲染问题,打开开发者工具 Console 和 Network 两个面板对照看。第三,测试接口时我用 Postman 建了开发环境和生产环境两套环境变量,切换环境只需要改一个 baseURL,不需要改代码。第四,全站搜索或关键词搜索接口,我都会把入参和出参打在日志里,方便排查问题,上线前再去掉冗余输出。
做项目的过程本质上就是不断遇到问题、定位问题、解决问题的过程。这套考研资讯系统从数据库建模到前后端开发,再到部署上线,每一环都有值得记录的地方。希望这篇复盘能帮到正在做类似项目的人少走一些弯路。