
1. 这是一个什么项目为什么值得做每年考研的人数都在往上走从年初的备考规划到冲刺阶段的真题模拟考研er要面对的信息量是相当恐怖的目标院校的招生简章、报录比、专业课大纲、公共课复习资料、时间节点……这些信息分散在各个网站、公众号、论坛里找起来费时费力而且很难判断哪份资料靠谱。我做这个毕业设计的时候思路其实很简单——为什么不把这些东西整合到一个平台上让考生在一个系统里完成查资讯、找资料、定计划、记进度这一整条备考链路“考研小助手”就是这样一个基于SpringBoot的研究生备考服务平台。项目本身定位很明确面向考研学生提供一个轻量级的、一站式的备考辅助系统覆盖考研资讯展示、备考资源管理、学习计划制定、复习进度打卡这几个核心场景。从功能体量上看它不大不小属于典型的“业务逻辑清晰、技术栈完整、可扩展性强”的项目选择它作为计算机毕业设计题目好处是工作量可控不会因为功能太杂导致烂尾技术覆盖全面能把SpringBoot、MyBatis-Plus、Redis、定时任务、文件上传、JWT认证这些面试常问的点全部串起来后续想升一档还能轻松加上协同过滤推荐算法、WebSocket在线讨论室这些亮点功能。这篇文章我打算从项目需求拆解、技术选型、数据库设计、核心模块实现、避坑经验这几个维度完整复盘一遍所有代码和思路都是实际跑通过、验证过的。无论你是正在选毕业设计题目的大四学生还是想练手SpringBoot全栈开发的初学者这个项目都值得从头到尾跟一遍。2. 需求拆解与功能全景设计2.1 考研场景的痛点分析在做需求分析之前我花了不少时间跟正在备考的学弟学妹聊天还翻了几十个考研论坛的帖子。总结下来考研备考过程中最让人头大的痛点集中在三个地方一是信息聚合难。研招网、学校研究生院官网、学院通知栏各有各的信息入口目标院校一多每天刷一遍都得花一两个小时还容易漏掉关键通知。很多考生直到报名快结束了才发现自己错过了某所学校的招生宣讲会。二是资料管理乱。每个考研er的网盘里都堆着几十G的资料政治、英语、数学、专业课混在一起版本新旧不一。到了复习后期想找一套2024年的真题解析翻了半天只找到一堆散零件。三是计划执行差。备考是长跑大多数考生一开始会认真做计划但坚持不了一周就放弃了。没人提醒、没有打卡反馈、没有进度可视化是计划坚持不下去的三个主要原因。2.2 功能模块的划分逻辑基于上面的痛点我把系统划分成五个功能模块按照用户身份分为前台学生端和后台管理端。前台是考生能直接用到的功能后台是运营人员维护内容的工具用户模块注册、登录支持邮箱验证码、个人信息维护、密码加密存储。这里用JWT做无状态认证不依赖Session方便后续前后端分离部署。资讯公告模块后台按分类发布考研资讯院校信息、招生简章、复习指导、政策解读前台支持条件筛选、关键词搜索、点击量统计。首页用轮播图展示置顶资讯让重要通知第一时间触达用户。备考资源模块后台上传资源文件PDF、图片、压缩包均可设置资源分类和标签前台支持按科目政治/英语/数学/专业课筛选。资源支持在线预览和下载后台能看到下载量排行。学习计划模块用户创建专属备考计划按倒计时天数自动拆分阶段目标支持每日打卡、进度统计、考前冲刺提醒。这是整个系统里最有“小助手”感觉的功能。后台管理模块管理员登录后台维护用户列表、发布资讯、审核资源、查看数据统计看板。统计部分用定时任务每晚生成报表减少实时查询压力。2.3 为什么选用SpringBoot作为主框架做这个项目之前我也对比过其他方案SSMSpringMVC Spring MyBatis太老旧写配置的时间比写业务代码还多SpringCloud那一套对毕业设计来说明显超重。SpringBoot的优势在于自动化配置机制——通过spring-boot-starter一系列依赖把Web、数据访问、缓存、定时任务等常用组件的配置全部内置开发者只需要关心业务代码。尤其适合毕设场景的是SpringBoot的起步依赖管理。比如引入spring-boot-starter-web后Maven会自动带上Tomcat、Jackson、Spring MVC相关依赖版本已经经过官方校准不需要自己处理版本冲突。这对经验不丰富、容易在依赖地狱里浪费大量时间的学生来说价值非常大。另外SpringBoot天然支持前后端分离开发模式这是现在互联网项目的主流形态。项目里我用Vue 3写的前端通过HTTP接口与后端交互部署时把前端打包产物直接放进SpringBoot的src/main/resources/static目录一个Jar包就能跑起整个系统对毕业设计答辩演示来说非常方便。3. 技术选型与开发环境准备3.1 技术栈清单与选型理由层次选型方案选择理由后端框架SpringBoot 2.7.x稳定、教程多、兼容JDK8生态资料最丰富ORM框架MyBatis-Plus 3.5.x代码量少内置分页插件、条件构造器开发效率高数据库MySQL 8.0免费、主流支持JSON类型文档丰富缓存Redis 5.x适合做验证码缓存、资讯热点数据缓存、点击量计数安全方案JWT BCrypt无状态认证适合前后端分离BCrypt加盐hash防彩虹表攻击文件存储本地磁盘 Nginx映射毕设规模用云OSS没必要本地存储够用且简单定时任务Spring Task框架自带注解化配置满足日报表生成、倒计时提醒等需求前端Vue 3 Element Plus组件齐全UI美观招聘市场认可度高这里特别说一句SpringBoot版本的问题。现在Spring Boot 3.x已经发布很久了但如果你去做毕设或者对Spring生态不是很熟我依然建议选择SpringBoot 2.7.x。原因很简单3.x要求JDK17及以上很多学校实验室电脑装的是JDK83.x里javax.*包名换成了jakarta.*网上大量旧博客代码直接复制会报错MyBatis-Plus、一些老牌第三方库的高版本适配文档还不完善。项目图稳不图新2.7.x是当前阶段最稳妥的选择。3.2 Maven项目结构与核心依赖配置项目采用标准的Maven多模块思路——虽然物理上是单模块但包结构按功能分包保证代码可维护性。先看pom.xml的核心依赖dependencies !-- Web基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Redis -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- JWT -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency !-- Lombok减少样板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies包结构沿用常见的分层层级controller层接收请求、参数校验service层处理业务逻辑mapper层对接数据库entity层定义实体类config放配置类common放通用工具类和统一返回结果。这种结构在毕设答辩时非常好讲解——老师问你某一层是干什么的你可以直接指着代码说清楚职责划分。3.3 开发环境的搭建记录我本地的环境是Windows 11 IntelliJ IDEA 2023.2 JDK 8 Maven 3.9。几个值得注意的配置点IDEA里Maven的配置要从File → Settings → Build Tools → Maven设置自定义的settings.xml把仓库地址指向阿里云镜像不然依赖下载慢到你怀疑人生。MySQL 8.0的驱动类名是com.mysql.cj.jdbc.DriverURL需要加serverTimezoneAsia/Shanghai参数否则连接时会报时区错误。Redis在Windows下没有官方版本可以下载redis-windows社区版或者直接在项目里用Docker跑一个Redis容器都行。application.yml里的关键配置片段server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/kaoyan_app?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath:mapper/*.xml global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意logic-delete-field这个配置MyBatis-Plus支持逻辑删除删除数据实际上是执行UPDATE语句把deleted字段置为1这对保留用户评论、资源记录等业务数据非常重要。4. 数据库设计五张核心表撑起整个业务4.1 表结构全景数据库是整个系统的地基。这一版数据库总共设计了7张表核心的是这5张user用户表article考研资讯表resource备考资源表study_plan学习计划表plan_record每日打卡记录表另外还有user_favorite收藏表和resource_category资源分类表。建表语句我精简一下关键的表结构如下CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, email varchar(100) DEFAULT NULL COMMENT 邮箱, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, target_school varchar(100) DEFAULT NULL COMMENT 目标院校, target_major varchar(100) DEFAULT NULL COMMENT 目标专业, role tinyint DEFAULT 1 COMMENT 1-学生 2-管理员, status tinyint DEFAULT 1 COMMENT 1-正常 0-禁用, deleted tinyint DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户信息表; CREATE TABLE article ( id int NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 标题, category varchar(50) DEFAULT NULL COMMENT 资讯分类院校信息/招生简章/复习指导/政策解读, cover varchar(255) DEFAULT NULL COMMENT 封面图, content longtext COMMENT 内容HTML, source varchar(100) DEFAULT NULL COMMENT 来源, is_top tinyint DEFAULT 0 COMMENT 是否置顶1-置顶, view_count int DEFAULT 0 COMMENT 浏览数, status tinyint DEFAULT 1 COMMENT 1-已发布 0-草稿, deleted tinyint DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_category (category), KEY idx_is_top (is_top) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考研资讯表;4.2 核心设计思路有几个设计点值得展开讲讲。首先是数据冗余的取舍。资讯表里的view_count字段就是典型的空间换时间。如果每次浏览都去做一次COUNT计数在数据量大时性能会很差。这里我在Redis里维护浏览数每隔一段时间批量同步到数据库Windows下Redis的持久化配置默认开启了RDB快照不会丢太多数据。其次是推荐系统的雏形。resource表我加了一个tags字段用逗号分隔存多个标签比如“英语一,真题,2019年”。在做“猜你想找”的推荐功能时直接查当前用户收藏的资源标签然后按标签相关性拉取其他资源。这种基于标签的推荐虽然简单但对毕设来说已经足够形成亮点。第三是学习计划的粒度设计。study_plan表里有一个total_days字段和plan_json字段plan_json用JSON存储阶段划分信息比如“基础阶段30天、强化阶段45天、冲刺阶段20天”打卡记录按天落在plan_record表。这样设计的优点是修改计划不产生历史打卡记录的变更统计“连续打卡天数”只需要查plan_record表即可。4.3 索引设计经验关于索引我的原则是先满足业务查询再用慢日志后补。article表的category和is_top字段在前台列表页会频繁作为查询条件加了索引效果明显。plan_record表的user_id和plan_date联合索引支撑每天加载打卡状态时只扫一行记录。对于小表user_favorite不建太多索引因为全表扫描本身很快。注意longtext类型字段不能加默认值也尽量不要对它建索引。资讯正文内容都是通过富文本编辑器提交的HTML有两三万字很常见。查询列表时用SELECT只查需要的字段列表不要把content字段带出来这是大字段拖垮查询性能的经典事故。5. 核心功能模块实现过程5.1 用户认证模块JWT从零到一认证模块是系统的基础几乎所有接口都要先过这一关。我采用的是JWT 拦截器方案流程是这样的用户在登录接口提交用户名和密码。服务端用BCrypt校验密码注意BCrypt每次加密结果都不同所以比较时调用matches方法而不是把密文解密对比。校验通过后用JWT工具类生成tokenpayload里放userId和role设置过期时间我设的是24小时返回给前端。前端把token存到localStorage在HTTP请求拦截器里统一加到Authorization头。后端拦截器验证token从Claims里取出用户ID放入ThreadLocal后续业务代码直接用。JWT工具类的核心代码Component public class JwtUtils { // 密钥实际应该放到配置文件中 private static final String SECRET kaoyan-secret-key-2024-example; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; public String generateToken(Integer userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }拦截器里要做异常捕获token过期、签名错误、token为空分别返回不同的错误码。这里有个很容易踩的坑签名密钥长度必须大于32字节否则HS256算法会报WeakKeyExceptionjjwt 0.11版本起增加了密钥强度校验。5.2 资讯与资源管理列表查询与缓存加速资讯模块是典型的CRUD但有几个细节值得打磨。列表接口我做了分类筛选、分页、置顶排序和关键词搜索public IPageArticleVO pageArticle(ArticleQuery query) { PageArticle page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperArticle wrapper new LambdaQueryWrapper(); // 分类筛选 if (StringUtils.hasText(query.getCategory())) { wrapper.eq(Article::getCategory, query.getCategory()); } // 关键词搜索标题和内容 if (StringUtils.hasText(query.getKeyword())) { wrapper.and(w - w.like(Article::getTitle, query.getKeyword()) .or() .like(Article::getContent, query.getKeyword())); } // 置顶排前再按时间倒序 wrapper.orderByDesc(Article::getIsTop) .orderByDesc(Article::getCreateTime); // 不查询content大字段文章详情单独查 wrapper.select(Article.class, f - !f.getProperty().equals(content)); return articleMapper.selectPage(page, wrapper); }这段代码里最值得强调的是大字段分离查询。列表接口用wrapper.select排除content字段详情接口才完整查询。这个优化在数据有几百条资讯时收益不明显但养成这个习惯以后处理千万级数据的C端接口时就知道好处了。热点资讯的缓存我用Redis做了一层。详情接口先查Redis没有才查数据库再回填缓存public ArticleVO getArticleDetail(Integer id) { String cacheKey article:detail: id; String json redisTemplate.opsForValue().get(cacheKey); if (json ! null) { return JSONUtil.toBean(json, ArticleVO.class); } Article article articleMapper.selectById(id); if (article null) { throw new BizException(ResultCode.NOT_FOUND); } String jsonValue JSONUtil.toJsonStr(article); redisTemplate.opsForValue().set(cacheKey, jsonValue, 30, TimeUnit.MINUTES); return BeanUtil.copyProperties(article, ArticleVO.class); }这里要注意缓存穿透的问题。如果查询的ID根本不存在缓存会一直为空请求会全部打到数据库。我踩过这个坑之后加了“空值缓存”——即使查不到数据也往Redis里写入一个占位符过期时间缩短到5分钟同时接口层做了基础参数校验非法ID直接拦截。5.3 学习计划模块倒计时自动生成与打卡学习计划是“考研小助手”这个产品名字的由来也是我做的时候最想做出彩的部分。功能设计是这样的用户输入目标院校、专业、考试日期系统自动计算出距离考研还有多少天然后按“基础→强化→冲刺”三个阶段把总天数平均分配自动生成学习计划。用户可以在计划内添加具体任务比如“过完高数第一轮”“背完英语高频词”。生成计划的算法很简单/** * 根据考试日期生成阶段计划 * 阶段默认比例基础45%强化35%冲刺20% */ public StudyPlan generatePlan(Integer userId, LocalDate examDate) { LocalDate today LocalDate.now(); long totalDays ChronoUnit.DAYS.between(today, examDate); if (totalDays 0) { throw new BizException(考试日期必须晚于今天); } StudyPlan plan new StudyPlan(); plan.setUserId(userId); plan.setExamDate(examDate); plan.setTotalDays((int) totalDays); plan.setStatus(1); // 三个阶段的起止日期 int baseDays (int) (totalDays * 0.45); int strengthenDays (int) (totalDays * 0.35); int sprintDays (int) (totalDays - baseDays - strengthenDays); // 构造阶段JSON MapString, Object stages new HashMap(); stages.put(base, buildStage(基础阶段, today, today.plusDays(baseDays))); stages.put(strengthen, buildStage(强化阶段, today.plusDays(baseDays 1), today.plusDays(baseDays strengthenDays))); stages.put(sprint, buildStage(冲刺阶段, today.plusDays(baseDays strengthenDays 1), examDate)); plan.setPlanJson(JSONUtil.toJsonStr(stages)); studyPlanMapper.insert(plan); return plan; }打卡功能用一张plan_record表记录每天只能打一次卡前端展示月度打卡日历。判断今天是否已打卡用user_id plan_date查一次记录即可每次打卡后更新计划表的continuous_days连续打卡天数。这块逻辑虽然简单但涉及并发时要注意同一用户同时发两个打卡请求可能造成重复记录。我的解决方案是在plan_record表加了UNIQUE KEY uk_user_date (user_id, plan_date)数据库层面兜底应用层用Redis分布式锁也可以但对这个场景来说唯一索引最简单也最有效。5.4 定时任务的落地实践Spring Task在项目里做了三件事每晚凌晨统计昨日资讯浏览排行、每天上午9点给北有学习计划的用户推送备考提醒邮件、每周一更新一次资源排行榜。核心就是一个Scheduled注解Component Slf4j public class ScheduledTasks { Autowired private ArticleService articleService; Autowired private MailService mailService; // 每天凌晨1点执行资讯浏览统计 Scheduled(cron 0 0 1 * * ?) public void statArticleViews() { log.info(开始统计资讯浏览数据...); // 从Redis取浏览数据写入article表的view_count字段 articleService.syncViewCountFromRedis(); log.info(资讯浏览统计完成); } // 每天9点给备考中的用户发送提醒 Scheduled(cron 0 0 9 * * ?) public void sendRemindEmails() { ListStudyPlan plans studyPlanService.listActivePlans(); for (StudyPlan plan : plans) { String email userService.getEmailById(plan.getUserId()); mailService.sendSimpleMail(email, 考研小助手备考提醒, 距离考试还有 plan.getRemainDays() 天记得完成今天的打卡哦); } } Scheduled(cron 0 0 10 ? * MON) public void weeklyResourceRank() { // 生成一周资源下载榜存入Redis缓存 } }cron表达式不熟的人容易写错这里记住几个速记键秒 分 时 日 月 周*代表任意值?只用在“日”和“周”两个位置表示不指定。比如0 0 1 * * ?就是“每天凌晨1点整执行”0 0 10 ? * MON是“每周一上午10点执行”。一个坑Spring Task默认是单线程串行执行的如果你有多个定时任务且每个任务耗时较长后面的任务会被阻塞。解决方法是配置一个线程池Configuration EnableScheduling public class ScheduleConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); } }5.5 文件上传与静态资源映射资源模块需要上传PDF、图片等文件。SpringBoot默认的单文件上传大小限制是1MB做资源的同学上传一套真题PDF往往几MB甚至几十MB所以必须手动调大限制spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB文件上传接口注意几个细节文件保存路径不要放项目内部建议存到绝对路径比如D:/kaoyan-files/否则打包成Jar后通过new File()写相对路径会失败。文件名要用UUID 原始后缀重新拼接避免中文文件名和重名文件覆盖的问题。保存后需要外部访问两种方案一是把文件路径作为Nginx映射的静态资源二是SpringBoot里自定义静态资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file:D:/kaoyan-files/); } }这样访问http://localhost:8080/files/xxx.pdf就能直接打开文件。提示毕设答辩演示时如果老师想看文件上传效果最好提前准备一个不超过5MB的小PDF不然上传大文件等待时间长演示卡壳就很尴尬了。6. 前后端联调与项目打包部署6.1 Vue项目如何与SpringBoot优雅共存项目前端使用Vue 3 Element Plus Vite构建开发环境中前后端通过代理解决跨域问题。在vite.config.js中配置export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })后端接口统一以/api前缀暴露前端所有请求都走/api开头开发时代理转发到后端的8080端口Vite服务器对浏览器的响应没有跨域问题。生产部署时很多人不知道Vue构建产物怎么跟SpringBoot合体。最简单的方式是前端执行npm run build把生成的dist目录里所有文件拷贝到后端的src/main/resources/static文件夹下。这样SpringBoot启动后访问http://localhost:8080/就直接进入前端页面后端接口仍然走/api前缀。一个Jar包前后端全搞定。6.2 打Jar包的配置细节后端要打可执行Jar包需要在pom.xml里包含前端构建插件或者干脆手动构建。我用的是手动方案前端构建npm run build把dist内容拷贝到后端static目录后端执行mvn clean package -DskipTests运行java -jar target/kaoyan-app-1.0.0.jar运行后有两个细节容易踩坑如果resources目录下有之前的打包残留mvn clean会清掉但要确保前端产物在clean之后再复制否则会打包进旧的index.html。数据库和Redis的连接地址要用生产环境的真实地址如果部署在云服务器上建议把敏感配置放到application-prod.yml通过--spring.profiles.activeprod参数激活。6.3 用宝塔面板部署到云服务器最近帮同学部署了好几个SpringBoot项目发现宝塔面板是相对省心的方案。基本步骤是服务器装好宝塔面板先安装Nginx、MySQL 8.0、Redis这里面MySQL和Redis直接用面板里的“软件商店”一键安装最快。创建数据库和账号导入本地导出SQL文件。上传Jar包到服务器目录用面板里的“Java项目管理器”部署填好端口号。Nginx配置反向代理把80端口的/api请求转发到Java应用的8080端口。配置SSL证书用面板申请免费的Lets Encrypt证书。反向代理配置的核心server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } client_max_body_size 100m; }client_max_body_size 100m这行一定不能少否则前端上传大于1MB的文件时Nginx直接返回413错误而后端日志里看不到任何异常。7. 常见问题与避坑指南7.1 SpringBoot版本引起的“编译通过运行报错”有同学私信环境是SpringBoot 3.2 JDK 17直接拿我这个项目的代码跑结果启动报错java.lang.NoClassDefFoundError: javax/servlet/Filter。原因就是3.x里Servlet API的包名从javax.servlet迁移到了jakarta.servlet旧项目里的javax相关依赖全部失效。这个问题其实很好避免要么跟着项目用2.7.x JDK8要么把代码里所有javax.servlet替换成jakarta.servlet。但我还是建议如果你不熟悉这两者的区别就直接用2.7.x把精力放在业务上。7.2 Redis连接失败导致登录接口502启动后访问登录接口报错原因往往是Redis服务没启动。很多同学本地装好了Redis但忘记启动然后登录时验码用到了Redis模板应用直接抛异常。建议在项目启动脚本里加一段Redis健康检查启动前先确认redis-cli ping返回PONG。如果不想装Redis也可以用Spring的CacheManager做本地缓存替代不过会失去Redis的持久化能力。7.3 定时任务“不执行”的原因排查有段时间我发现邮件提醒没有发出去查日志发现任务根本没触发。排查思路按这个顺序来先用PostConstruct在应用启动时打印一句话确认EnableScheduling生效再检查cron表达式常见错误是秒位忘记写0 0 9 * * ?和0 9 * * ?的区别很大最后看是不是项目里有两个同名Component被扫描了导致任务在另一个实例上执行日志打到了别处。我这边的最终原因是服务器时区问题。云服务器默认时区是UTC而cron表达式按系统时区计算导致每天9点的任务实际是北京时间下午5点执行。解决办法是启动时指定时区java -jar kaoyan-app.jar --spring.jackson.time-zoneGMT8或者更稳妥地在Linux服务器执行timedatectl set-timezone Asia/Shanghai。7.4 前端打包后SpringBoot页面404Vue项目npm run build生成的静态文件如果放到resources/static下运行时依旧404大概率是路径问题。Vite的base配置默认是/打包后的资源引用路径是绝对路径/assets/xxx.js如果你的项目部署在http://ip:8080根路径下没问题但如果做了子路径映射比如http://ip:8080/kaoyan/就必须把vite.config.js里的base改成/kaoyan/否则找不到静态资源。7.5 易忽略的安全问题自查清单毕设答辩时老师经常问安全问题提前做好这些能加不少印象分密码用BCrypt加密不要用MD5MD5已经能被彩虹表秒破前端提交的数据后端要二次校验不能信任任何输入JWT的密钥不要硬编码在代码里放到配置文件并加入.gitignore文件上传接口要校验文件扩展名防止上传可执行的JSP木马文件只允许PDF、图片、压缩包等白名单格式管理员的密码不要用admin/123456这种容易被弱口令爆破进来。8. 写在最后的实操心得整个项目从零写完大概用了三周多的时间。第一周做需求分析和数据库设计整天泡在ER图里反复调表结构第二周把用户、资讯、资源三个模块跑通每天都能看到接口正常返回数据第三周集中做学习计划、定时任务、文件上传最后两天联调打包。我最大的体会是毕设项目能不能顺利推进数据库设计阶段就已经定生死。中间因为资源表设计不合理我重写了一次前端列表页。当初为了图省事把资源分类设计成了多对多关系结果前端筛选时要去算关联关系代码绕来绕去不如直接在资源表里冗余一个category_id字段来得直观。经典的“适当的冗余换来的是简单和高效”在这个项目里体会得很深。另外一个建议是代码要尽量写得规范注释不是写给老师看的是写给两周后的自己看的。每完成一个模块写一份简短的功能说明记录接口的入参出参、逻辑要点和踩坑记录答辩时整理成PPT的素材也顺手。如果你打算用这个项目作为毕业设计我建议在此基础上加一个“亮点功能”比如基于协同过滤的院校推荐、基于WebSocket的考研战友聊天室、或者用ECharts展示历年分数线趋势的的可视化图表。这些功能工作量不大但能在答辩时快速吸引评委注意力。最后再分享一个小技巧开发期间前端调试可以准备一份带测试数据的SQL脚本每次测试环境挂了就重新导入一张快照比手动造数据快得多。这套项目后续如果我还要扩展大概率会先做移动端适配把打卡和资讯浏览的体验搬到小程序上让备考学生真正能够随时随地使用。无论如何动手写永远比看别人的代码强。去把项目跑起来然后在跑起来的基础上改点东西这趟毕业设计就算没白做。