
1. 项目梳理一个“博客 报名”双核心系统到底在做什么如果你刷到过“javavue基于springboot的个人博客活动报名系统”这类标题多半是在找毕设或者练手项目的阶段。这个标题看着长其实拆开就是一句话用 SpringBoot 做后端接口用 Vue 做前端页面实现一个既能发文章、又能搞活动报名的个人博客平台。我刚接触这个项目时也琢磨过为什么非要把“博客”和“活动报名”绑在一起后来想明白了这恰恰是很多真实场景的缩影。比如一个技术社区、一个线下分享会的主办方主页既需要文章展示来沉淀内容又需要报名通道来收集参与者信息。两个需求放在同一个系统里正好覆盖了内容管理和用户交互两条主线。从学习角度看也是把增删改查、权限控制、文件上传、列表分页这些高频技能一网打尽的最佳组合。这个项目适合谁来参考在我看来有三类人第一类是正在准备毕设的计算机相关专业学生它的功能粒度适中数据库表也不复杂容易讲清楚第二类是自学 Java 和 Vue 想找综合实战的初学者它能帮你把前后端零散的知识串成一条线第三类是确实想给自己搭一个带活动功能的小型网站的人改造一下就能直接上线用。我最初拿到这个题目时习惯先画一张功能脑图再动手避免写着写着东一榔头西一棒子。整套系统按角色拆其实就两块管理员端负责发布文章、管理活动、审核报名记录和查看统计数据用户端负责浏览博客、注册登录、报名活动、查看自己的报名状态。把这两个角色的需求列清楚后台接口设计基本就有数了。这个项目还有一个容易被忽略的亮点它的命名后缀“ofwhh2c6”其实只是代码生成器之类的工具产生的唯一标识不需要太在意。但换个角度想它也提醒我们真正让一个项目有价值的不是名字而是里面的技术选型和架构取舍。下面我就按我实际开发的顺序把这套系统的关键设计和踩坑经历完完整整分享出来。2. 后端工程落地SpringBoot 是如何撑起整套系统的2.1 为什么会选 SpringBoot而不继续用 SSM很多教程到现在还在教 SSMSpring SpringMVC MyBatis的 XML 配置但现实是新启动的项目绝大部分已经直接用 SpringBoot 了。核心原因就一个字快。SpringBoot 通过自动装配把“依赖引入、配置加载、对象实例化”这套流程尽量标准化我只需要加上对应的 starter再在配置文件里写少量自定义参数就能跑起一个可用的 Web 服务。当初在 IDEA 里创建 SpringBoot 项目时直接选 Spring Initializr勾上 Web、MyBatis、MySQL、Redis 这些依赖一分钟不到项目骨架就出来了。这里想多说一句很多人在“springboot版本太高”上翻车。我当时选了 3.x 版本结果 MyBatis 的 starter 还在用旧坐标导致启动直接报错。后来统一把版本降到 2.7.x配合 JDK 8才彻底消停。如果你也是做毕设或者练手没有特殊需求就老老实实选 2.7.x 或 2.5.x生态兼容性最稳网上搜到的问题解决方法也最多。自动装配是这个阶段必须搞明白的概念。简单说SpringBoot 在启动时会去读 META-INF/spring.factories 或者 AutoConfiguration.imports 文件把里面列出的配置类全部加载进来再根据条件注解比如 ConditionalOnClass、ConditionalOnMissingBean决定要不要实例化对应的 Bean。我项目里加了 spring-boot-starter-data-redis 之后RedisTemplate 就自动出现在容器里了我自己一行配置都没多写拿过来就能用。面试时如果被问到 SpringBoot 核心特性把“自动装配 starter 机制 约定优于配置”这条线讲清楚就足够体现你的理解了。2.2 后端项目结构怎么划分才能既清晰又方便扩展我见过很多人的新手项目一个 Controller 写几百行Service 层形同虚设工具类堆在 utils 包下面没有归类。这种代码自己维护半个月就开始头疼了。做这套个人博客活动报名系统时我采用的是经典的分层结构但是做了一点小优化按业务模块拆分 Controller 和 Service而不是按技术类型一股脑堆在一起。controller负责接收请求、参数校验、调用服务、返回统一结果service核心业务逻辑事务边界也放在这里mapper数据库操作对应 MyBatis 的接口entity数据库表映射的实体类dto接收前端参数的传输对象避免直接把 Entity 暴露出去vo返回给前端的视图对象config各类配置类比如 CORS、拦截器、Redis 序列化utils通用工具类如 JWT 工具、日期处理工具每一层各司其职前端需要的数据结构发生变化时我只改 vo 就好不会牵连到数据库映射层。使用 MapStruct 或者 BeanUtils.copyProperties 做对象属性拷贝也能省不少代码减少每加一个字段就要手动补三个地方的低级遗漏。2.3 核心数据表设计博客与活动报名需要哪些表支撑没设计好表结构就急着写 Controller往往会造成后续反复重构。我这个项目里一共设计了六张核心表简单说下每张表的职责和关键字段。第一张是用户表 user字段包括 id、username、passwordBCrypt 加密后的密文、nickname、avatar、email、phone、roleUSER/ADMIN 区分普通用户和管理员、status是否被禁用、create_time。第二张是文章表 article字段包括 id、title、content长文本、cover_image、summary、category_id、tags、view_count、like_count、status草稿/发布、create_time、update_time。第三张是分类表 category记录 id、name、description用于博客文章的分类展示。第四张是活动表 activity也是本系统里功能比较重的内容字段包括 id、title、description、location、start_time、end_time、deadline报名截止时间、max_people最大人数、current_people当前已报名人数、status报名中/已截止/已结束、cover_image。第五张是报名表 registration字段包括 id、activity_id、user_id、real_name、contact_phone、remark备注信息、status待审核/已通过/已拒绝/已取消、create_time。第六张是评论表 comment记录文章或者活动下的用户评论。设计表时要特别注意两个细节。活动表的 current_people 字段在并发报名时会出现超卖所以我实际做法是把它作为冗余计数报名时在事务里用UPDATE activity SET current_people current_people 1 WHERE id ? AND current_people max_people这样一条带条件的原子更新语句来保证不超卖。另外所有关联用户和活动的逻辑删除字段 is_deleted 也建议默认加上不然以后要恢复误删的数据会非常痛苦。2.4 登录认证与权限控制JWT 方案为什么够用个人博客和活动报名系统里大部分操作都是普通用户身份完成管理员只是极少数角色。我采用了 JWT SpringBoot 拦截器实现轻量的登录认证而不是引入 Spring Security 那一套重框架。主要是因为项目需要保护的接口不算多JWT 的“无状态、好扩展”特性足够支撑这个场景而且实现逻辑直观作为毕设或者简历项目也更容易讲清楚原理。具体流程是用户登录成功后后端生成一个包含 userId、username、role 和过期时间的 JWT 字符串返回给前端。前端把它存在 localStorage之后每次请求在 Authorization 头里带Bearer token。后端写一个拦截器在 WebMvcConfigurer 里注册拦截路径在 preHandle 方法中解析 token校验签名和过期时间然后把用户信息放进 ThreadLocal 供后续业务使用。这里要特别提醒JWT 的密钥一定不要硬编码在业务代码里我在项目里是放在 application.yml用 Value 注入并且本地和线上使用不同的配置。token 过期时间也不要设得太长我设置的是 24 小时需要提高安全性时可以再加一个 refresh_token 机制不过这对小型系统来说属于可选项。3. 前端交互实现Vue 的组件化与状态管理实战3.1 从 Vue 入门到搭建后台基础框架关键几步怎么走如果你刚从 Vue 的 Hello World 切到这种完整项目第一个感觉肯定是“怎么这么多文件”。把 Vue 安装及环境配置捋顺了后面其实是套路。我当时用 Vue CLI 创建项目选择 Router 和 Vuex 插件然后把目录按 views、components、router、store、api、utils 组织起来。views 放页面级组件components 放可复用的子组件api 目录专门封装 Axios 请求避免每个页面写一堆重复的请求代码。有一个比较容易踩的坑是 Vue 依赖安装时报 ERESOLVE 错误特别是 node 版本比较高而项目里某个依赖版本比较老的时候。我用的解决方案是删掉 node_modules 和 package-lock.json然后重新npm install --legacy-peer-deps实在不行再把 node 版本切回 16.x。搞定了环境问题之后后面写代码就顺多了。对于组件化设计我建议把博客列表、活动卡片、分页组件、富文本编辑器这些能够复用的部分都抽成独立组件。这样一来文章页和活动页都能共用分页和列表样式改一个组件所有引用它的页面同步更新。Vue 的单文件组件机制把 template、script、style 封装在一起维护起来特别舒服也是提升开发效率的关键手段。3.2 Vue Router 设计与路由参数传递的几种方式这个系统里Vue Router 主要承担页面跳转和权限拦截两件事。我配置的路由分成两块一部分是公开页面比如首页、博客列表、活动列表另一部分需要登录才能访问比如“我的报名”“个人中心”。在全局前置守卫 router.beforeEach 里读取本地 token没有 token 且目标路由需要认证时就重定向到登录页。路由参数传递在实际开发中特别常用比如从博客列表点进详情页需要带上文章 id。我用的方式是路径参数/article/:id跳转时用this.$router.push({ path: /article/ id })详情页里用this.$route.params.id获取。如果是查询条件这种语义不太重要的参数可以直接用 query 方式比如/article?categoryId2取的时候用this.$route.query.categoryId。项目中我建议优先使用路径参数因为它在语义上更清楚刷新页面之后参数依然存在不会丢掉。再说一个从实战中发现的问题当路由层级越来越深的时候组件复用时 created 钩子里的请求可能不会重新触发。比如从文章 A 切到文章 B组件实例被复用了created 不会再次执行页面内容还停留在上一篇文章。解决方法是 watch$route.params.id的变化参数改变时重新拉数据或者在组件上加上:key$route.fullPath强制重建组件这个细节可以说面试加分项也是实际开发中很容易被人忽视的问题。3.3 状态管理Vuex 里应该放什么不该放什么Vuex 很容易被无脑塞数据导致刷新一下就全部丢失。我在这个项目里的原则是只有全局共享且多个页面都需要使用的数据才放进 Vuex比如用户信息、全局配置、未读消息数量。而像博客详情、活动报名表单这种单页面内的临时数据用组件内部的 data 管理就够了不需要全局状态来增加复杂度。为了防止页面刷新后用户状态丢失我在 module 里初始化时先从 localStorage 读取用户信息登录成功后同时提交 mutation 更新 state 并写入 localStorage退出登录时做反向清理。这里要注意安全localStorage 容易被 XSS 脚本读取所以存放的是不敏感的用户基本信息而真正的 JWT 我仍然存在 localStorage 里因为这是前后端分离项目的常规做法只要注意渲染时转义用户输入内容风险可控。Vuex 的 action 通常拿来发异步请求比如登录、退出、拉取当前用户信息。组件里通过mapActions或mapGetters来绑定模板里少了很多this.$store.state.xxx的冗长写法代码整洁度明显提升。3.4 富文本与视频播放博客详情页的媒体处理细节文章详情页除了展示纯文本还需要支持图文混排和视频嵌入。富文本编辑器我用的是 wangEditor它基于原生 JS前后端分离模式下集成不费力核心思路是初始化一个编辑器实例然后把 HTML 内容通过接口保存到后端。读取文章详情时将后端返回的 content 用v-html渲染出来注意在服务端或前端做一次 XSS 过滤防止脚本注入风险。热词里提到过“vue播放m3u8”和“vue视频m3u8”如果你的活动说明里需要插入直播流或切片视频播放 m3u8 格式就需要引入 hls.js。我这里项目其实没有强需求但仍然踩过一次坑直接用video标签放到 Chrome 里是无法播放 m3u8 的因为原生不支持 HLS 协议。后来我在详情页封装了一个视频播放组件如果检测到视频链接是 .m3u8 结尾或者 content-type 是 application/vnd.apple.mpegurl就用 hls.js 来处理逻辑大概是先判断Hls.isSupported()如果支持就 new Hls() 把视频源接上否则回退给 video 标签原生播放。封装视频播放组件还有一个好处以后在活动回放或课程回放场景中可以直接复用不至于每个页面都重新写一遍 hls.js 的初始化逻辑。4. 活动报名业务并发、幂等与状态流转的设计要点4.1 用户报名流程中要考虑哪些高频场景活动报名看起来只是一个 INSERT 操作但真实世界里用户行为没那么简单高频场景至少有下面这几种用户未登录直接点报名需要先跳到登录页登录成功后再回到活动页活动报名人数已满按钮要置灰或点击后提示“名额已满”同一个用户重复点击报名按钮可能会导致重复报名报名后想取消但截止时间过了就不能再取消管理员可能需要审核报名信息审核前后用户看到的状态不一样针对这些场景我把报名流程设计成前端注册报名时先调checkCanRegister接口查询当前活动是否在报名期限内、是否还有名额、该用户是否已经报名过后端在事务里做同样的校验并在 activity 表中执行原子更新计数 SQL只有当更新影响行数大于 0 时才允许插入报名记录。这样从前端到后端做双重校验能挡住绝大多数无效请求。4.2 并发报名场景怎么保证不超卖且不重复这里我要重点说一下并发控制。如果你只写了 “先查 current_people如果小于 max_people 就执行插入”在并发量上来的时候一定出问题。两个请求同时查到 current_people 49max_people 50两个都判断还有名额于是两条报名记录都插进去了人数变成 51超卖就发生了。我再实际开发里采用了两个办法叠加。第一件是数据库层面的原子更新update activity set current_people current_people 1 where id #{activityId} and current_people max_people and deadline now()这条 SQL 的 where 条件里把“还有名额”和“未截止”都放在同一条语句里MySQL 在行锁保护下只会让一个事务成功更新到那一行失败的事务影响行数为 0我们就抛出“名额已满”异常。第二层是给 registration 表加一个唯一索引索引字段为 activity_id 和 user_id这样即使前端漏了判断数据库也会拒绝同一用户对同一活动的重复报名。为了进一步挡住用户频繁点击按钮造成的瞬时压力我在后端又加了一个简单的 Redis 幂等判断用户发起报名时用SETNX register_lock:{userId}:{activityId} 1 EX 10做一个 10 秒内的重复提交拦截第一次请求设置成功继续走业务第二次请求发现 key 已存在就直接返回“正在处理中”。这样系统在高并发下的稳定性就有了保障。4.3 报名状态流转和管理员审核怎么设计报名记录不能只是简单存下来还需要一个状态机来管理它的生命周期。我定义了四个状态待审核0、已通过1、已拒绝2、已取消3。用户提交报名后默认是待审核状态管理员在后台看到待审核列表后可以操作“通过”或“拒绝”。用户在我的报名页面中可以看到每条报名记录当前属于哪个状态。这里有个容易出错的地方用户申请取消报名在截止时间之前应该允许但取消之后如果还想再报需要把 activity 表的 current_people 减回去。所以取消报名不能只改 registration 状态还要加上一条递减操作最好和状态更新放在同一个事务里。我在这里踩过坑最早做的时候忘记减人数结果报名人数慢慢变成负的排查了半天才发现问题是取消时没回填人数。管理员审核的时候还可以做简单统计比如按活动查看报名人数、按时间维度看每天的新增报名量。Excel 导出就用 EasyExcel 实现编译期内生成模板、build 数据列表、一行代码输出流这是后台管理功能中非常加分的能力。5. 部署环境与性能优化从开发机到服务器的完整链路5.1 前后端分离项目打包上线需要注意哪些配置本地开发跑得好好的一打包部署就出各种问题这是前后端分离项目的通病。我总结了一下其实就三类问题一是接口地址不匹配二是跨域配置失效三是前端路由刷新后 404。接口地址问题很好理解前端在开发环境用/api代理到 localhost:8080但打包之后页面是静态文件不存在代理服务器所以必须将 API 地址写成实际服务器地址。我的做法是在项目根目录放一个.env.production文件定义VUE_APP_BASE_URL http://你的服务器IP:8080/apiAxios 的 baseURL 读取这个环境变量这样换服务器只改配置文件不用重新合并代码。跨域配置在后端也要做一道保障。我在 SpringBoot 里写了一个 CorsConfig只允许指定域名跨域同时设置允许的请求方法、请求头和携带凭证。这里要说一个细节如果后端同时使用了拦截器拦截器里的响应功能可能会被跨域拦截器挡住需要把 OPTIONS 请求直接放行。我当时在拦截器里加了一个判断只要请求方法是 OPTIONS 就直接返回 true否则前端预检请求会报跨域错误。5.2 Vue 打包后布局异常的常见原因和修复思路热词里专门提到“vue 打包后 布局异常”我排查过很多次这个问题最终发现绝大多数情况都出在静态资源路径上。Vue CLI 默认的 publicPath 是/如果你把打包后的 dist 目录放在 Tomcat 的 webapps/ROOT 下并且通过http://ip:8080/访问那没问题但如果放在子路径比如http://ip:8080/blog/访问那么默认配置会导致 CSS、JS 请求路径变成/js/app.js全部 404页面看起来就像“布局全崩了”。解决办法是在 vue.config.js 里设置publicPath: ./这样所有资源都变成相对路径。不过要注意这会影响路由的 history 模式刷新二级页面会出现 404所以配合路由的 hash 模式使用是最稳妥的。项目里我选择改成 hash 模式URL 会多个#但部署简单不需要服务器配置 fallback比较适合个人博客这类小型项目。5.3 Redis 在系统中的实际作用与性能优化点这个项目里 Redis 不是必须的但加上之后确实体验有质的提升。我主要用它做了三件事第一件事是文章浏览量统计。博客列表页每次请求都会查数据库做 count 累加访问量上来后对数据库是有压力的。我改成在 Redis 里维护一个article:view_count:{id}的计数器用 INCR 命令增加后台定时任务每隔一段时间把增量同步回 MySQL。如果还想再精简可以直接异步写日志定时批处理就不用每次请求都触发数据库更新了。第二件事是热点数据的缓存。比如首页的公告、活动推荐列表、博客分类列表这些数据变化频率很低却每次都被查询我设置了 10 到 30 分钟的缓存过期时间请求进来先查缓存缓存缺失再查 MySQL 并回填。这块代码最好封装一个带有“缓存穿透”保护的通用方法至少空值也要短暂缓存避免恶意请求高频打库。第三件事是接口防刷。用户报名提交和发送验证码这类接口我会用 Redis 做频率限制同一个 IP 一分钟内超过指定次数就直接拦截。用 SpringBoot 的拦截器加注解实现不会侵入业务代码。这里也顺便说一句“redis在springboot中的使用”如果你想让面试官眼前一亮可以说出 RedisTemplate 的 StringRedisTemplate 与普通 RedisTemplate 的区别——前者默认用 String 序列化不会有 Java 序列化对象出现的一堆乱码问题这也是很多人在 Redis 里存字符串却读出来带\xAC\xED...前缀的原因。5.4 项目后续扩展方向还能往哪些方向升级做完一个版本不等于项目结束我认为有价值的升级方向有几个。第一是接入 OAuth 2.0 第三方登录比如 GitHub、微信扫码登录免去用户注册流程这也会让登录模块的完整度提升一个档次。第二是引入消息队列来做异步通知比如活动报名成功之后发送邮件或者短信通知用 RabbitMQ 或者 RocketMQ 解耦避免同步调用太消耗时间。第三是给博客增加全文搜索能力如果数据量增大到 MySQL like 查询扛不住就可以考虑 Elasticsearch 或者轻量级的全文索引组件。如果再进阶一些可以用 Docker Compose 把 MySQL、Redis、SpringBoot 应用、Nginx 静态资源服务编排起来实现一键部署。也可以给系统加上简单的监控比如 Spring Boot Actuator 暴露健康检查接口配合前端页面展示 JVM 内存、线程池状态等指标。这些扩展不是必须实现但在面试中说出来能让人看到你对系统演进方向有自己的思考。6. 常见问题与排查技巧实录6.1 后端启动失败端口占用与依赖版本冲突问题现象SpringBoot 启动时报Web server failed to start. Port 8080 was already in use.排查思路先看是不是本地有旧进程占用了端口Windows 下可以用netstat -ano | findstr 8080找出 PID再taskkill /F /PID 进程号结束进程。Linux 服务器上用lsof -i:8080和kill -9同样可以处理。问题现象启动时 MyBatis 相关 Bean 无法创建报Invalid value type for attribute factoryBeanObjectType之类。排查思路这类问题九成是 SpringBoot 版本和 MyBatis starter 版本不兼容。SpringBoot 3.x 需要 MyBatis starter 3.0 并且包名从org.mybatis.spring.boot调整过如果你还依赖旧版本的 mybatis-plus直接降到 SpringBoot 2.7.x 是最省心的方案。6.2 跨域问题总是报错明明配置了 CORS跨域问题排查要分三个层面浏览器 Network 面板看请求有没有发出、是否被预检拦截、响应头里有没有 Access-Control-Allow-Origin。我在实际项目中遇到过前端的 axios 设置了withCredentials: true但后端 CORS 配置里却没有设置Allow-Credentials浏览器照样拦截。配置 CORS 时的对应关系是前端允许携带 cookie 时后端必须allowCredentials(true)同时 allowedOrigins 不能用*必须写明具体域名这是浏览器规范明确禁止的行为。还有一个坑是 SpringBoot 中同时依赖了 Spring SecuritySecurity 的跨域配置和后端自定义的 CORS 配置会叠加生效导致请求被拦截在 Security 层。此时只需要在 SecurityConfig 里也开启http.cors()保持配置一致性即可。6.3 MyBatis-Plus 分页查询失效total 一直为 0问题现象分页接口返回的 total 始终为 0列表数据却正常。排查思路MyBatis-Plus 3.4 之后分页插件需要显式配置一个MybatisPlusInterceptorBean而不是只添加PaginationInnerInterceptor。如果少了这段配置分页会被当作普通查询处理total 自然就是 0。配置好之后建议在日志里打印 SQL 确认 limit 语句是否拼接成功这样能快速定位问题。6.4 Vue 页面白屏控制台报 “Cannot read properties of undefined”问题现象某个对象属性读取报错比如Cannot read properties of undefined (reading name)。排查思路大多数情况是接口返回的数据结构和前端预期不一致或者页面初始化时数据还没加载完就渲染了。前端面对这种情况要养成习惯渲染对象属性之前先判断对象是否存在或者使用可选链操作符?.。比如模板里写user?.nickname || 游客就不会因为后端偶尔返回 null 导致整个页面崩溃。6.5 上传图片接口报 500MultipartFile 没有拿到文件问题现象前端 FormData 上传图片后端接口的 MultipartFile 参数为空控制台报出类型转换异常。排查思路第一步检查前端有没有设置Content-Type: multipart/form-data如果用 axios 上传建议不要手动设置这个 header让浏览器自动生成带 boundary 的完整格式第二步检查 SpringBoot 配置文件里spring.servlet.multipart.max-file-size和max-request-size是否够大默认 1MB 上传大图很容易受限第三步检查接口参数是否加了RequestPart或直接作为方法参数接收我用的是直接参数接收只要前端字段名匹配就没有问题。6.6 打包之后刷新页面 404前端路由要如何处理这个问题我在 5.2 小节提过解决方式有两种一是把 Vue Router 改成 hash 模式通过createWebHashHistory()创建路由刷新不会发请求不存在 404二是继续使用 history 模式但需要在 Nginx 里配置try_files $uri $uri/ /index.html;把所有不存在的路径回退到前端入口文件由前端路由来接管页面渲染。如果部署在 Tomcat还需要配置 web.xml 的 error-page 或者使用 rewrite 规则相对麻烦一些。总的建议是小型项目直接用 hash 模式省时省力。7. 从项目延伸如果你的目标是毕设加分或面试突破做完这套个人博客活动报名系统之后我最大的体会是一个项目值不值得写进简历不在于功能多么炫酷而在于你踩过哪些真实问题、解决思路是什么、有没有自己独特的选型取舍。比如你能讲清楚为什么用 JWT 而不是 Session为什么报名接口要加 Redis 幂等判断为什么文章浏览量要异步写回数据库这比堆砌一堆“熟悉 SpringBoot、熟悉 Vue”的形容词有说服力得多。最后分享一个我特别推荐的小技巧给系统加一个简单的操作日志模块用 AOP 注解拦截 Controller 请求记录用户操作时间、操作类型、IP 地址和操作结果。这个功能看似简单却非常能体现你对项目可维护性的理解而且面试时聊到“你在项目里做过什么有亮点的事情”这个点很容易让面试官追问下去。毕竟一个能自己发现问题、解决问题、主动提升工程质量的开发者才是团队最需要的人。