Spring Boot + Vue 个人博客活动报名系统全栈开发实战
发布时间:2026/9/16 3:48:39 作者:尧图编辑部 阅读量:1,286

最近不少读者在后台问我spring boot vue 的全栈项目到底怎么做才不显得学生气尤其是个人博客这类老掉牙的需求怎么做出亮点。刚好我这边才帮一个朋友梳理过他的毕业设计——基于 spring boot 的个人博客活动报名系统题目编号 ofwhh2c6功能不算复杂但里面踩坑和优化空间特别多。今天直接把完整思路、核心实现、常见问题都拆开讲一遍从数据库设计到前后端联调再到面试时怎么讲这个项目一次说清楚。这个系统说白了就是两件事一是个人博客的文章管理、分类和展示二是活动报名也就是用户能看到活动列表、填信息报名、管理员审核统计。技术栈是 java vue spring boot老组合但正因为老网上资料多、坑也都被踩得差不多了很适合作为入门全栈的第一个完整项目。不过也正因为它很常见把细节做好才显得专业——比如并发控制防止超报、事务处理保证数据一致、前端权限控制这些点。1. 项目整体设计与技术选型思路1.1 为什么是 Spring Boot Vue 这套组合先说选型。很多人问为什么不用前后端不分离的 jsp 或 thymeleaf非要把 spring boot 和 vue 拆开。我的真实答案是为了分工和复用。前后端分离以后前端只管渲染和交互后端只管数据和业务两边可以并行开发而且以后想换前端框架、做小程序、做移动端后端 api 完全不用动。从个人博客活动报名系统的实际需求出发这套组合还有一个很现实的好处就业方向对口。市面上大部分 java 后端岗位都要求你会 vue而 vue 也基本是前端三大框架里最适合后端程序员快速上手的。你想想java vue spring boot 这三样单独拎出来任何一个都有大量的面试题组合在一起做成项目面试时能聊的东西就特别多。但选型也不能盲目。如果你的博客系统只是给自己写写文章、不做活动报名那用 thymeleaf 反而更省事因为不用处理跨域、不用做前后端分离的鉴权。所以这个博客活动报名系统选 vue 的核心驱动是活动报名这个交互功能——用户报名、管理员审核、数据统计这种交互密集的场景用 vue 的响应式数据管理确实比服务端渲染舒服得多。1.2 项目结构与模块划分一个干净的全栈项目拿到手首先得能让人一眼看懂目录结构。我习惯把项目分成两个独立目录前端blog-front和后端blog-server。注意网上很多教程喜欢把 vue 打包后的文件丢进 spring boot 的 static 目录里然后打成一个大 jar 包这种方式部署是省事但开发体验很差而且违背了前后端分离的初衷。分开开发、分开部署联调通过后再考虑用 nginx 统一入口。后端blog-server内部我建议按业务模块分包而不是按技术分层分包。所谓按业务分包就是controller、service、mapper这些技术层不单独建顶级包而是先按业务模块分比如blog模块、activity模块、user模块每个模块下再放自己的controller、service、entity、mapper。这样做的优势是后续加功能、找 bug、甚至拆分微服务你只需要定位到对应模块不需要在整个项目里来回跳。前端blog-front用 vue cli 或 vite 创建项目后我一般会建这几个核心目录目录作用src/api统一封装 axios 请求按后端接口模块拆文件src/views页面级组件比如博客首页、文章详情、活动列表、活动报名src/components可复用的业务组件比如文章卡片、报名表单弹窗src/router路由配置包含路由守卫做登录校验src/store如果用 vuex 或 pinia 就放这里管理用户状态、token这种结构的核心思想是页面和组件分离、接口请求集中管理。很多新手喜欢在页面里直接写 axios短期看很爽但项目一旦超过十个页面接口地址散落得到处都是改一次后端地址或者接口前缀就等着通宵吧。1.3 功能边界与角色划分这个系统虽然叫个人博客活动报名系统但它实际上有三种角色得想清楚不然数据库设计会乱。第一种角色是管理员也就是博客的主人。管理员负责发布文章、删改文章、创建活动、审核报名、查看报名统计。第二种角色是普通用户可以浏览文章、查看文章详情、查看活动列表、报名活动、取消报名。第三种角色是游客只能看文章列表和活动列表想报名必须先登录。这三种角色对应到 vue 前端就是三套不同的页面权限。实现起来不复杂后端用 token 识别用户身份前端在路由守卫里判断角色然后跳转。但要注意前端的路由守卫只是体验优化不是安全措施。真正的权限校验必须落在后端接口上也就是每个管理接口都要校验当前用户的角色是不是管理员。原理很简单前端的代码是透明的别人直接在浏览器控制台调你的登录接口伪造个 token 也不是不行所以后端校验才是安全底线。2. 数据库设计与后端核心实现2.1 数据表设计的关键思考把三种角色和两类核心业务理清楚以后数据库的表设计就顺理成章了。核心表我认为有六张用户表、博客文章表、文章分类表、活动表、报名表还有一个个人资料/关于我表看个人需求。这里重点说文章表和报名表因为这两个表的设计直接影响系统能不能撑住真实使用。文章表建议大家不要把所有字段都堆在一张表里。文章内容用text类型存正文但标题、摘要、封面图、创建时间、分类id 这些字段单独拎出来方便列表页做分页查询。如果有标签功能再建一张blog_tag的表和文章表做多对多关联。别怕表多表设计得越规整后续加功能就越省心。报名表的设计要重点关注 一个用户能不能重复报名同一个活动 这个问题。实现方案是给报名表加一个user_id和activity_id的联合唯一索引。这样即使代码里有并发问题数据库层面也能兜底。这个索引非常关键我在后面并发控制部分会再展开讲。用户表在个人博客场景下不用搞得太复杂注意username和avatar字段就好。密码加密别用 MD5显存库用BCryptPasswordEncoderspring security 自带的直接用。2.2 后端分层从 Entity 到 Controller 的一次完整链路后端代码写多了你会发现分层结构其实就三板斧Entity 映射表Mapper 访问数据库Service 处理业务逻辑Controller 暴露接口。关键是把每一层的职责理清楚。拿用户报名活动这个操作来举例。很多人会把判断活动是否存在、校验名额是否满、插入报名记录这三件事全塞进 Controller 里。这不是不行但问题很大一旦后续要做报名成功后发送邮件通知、报名成功后给用户加积分这类扩展Controller 会越来越臃肿而且事务边界很难控制。正确的做法是Controller 只做参数接收和返回结果业务逻辑全放在 Service 层。报名活动的 Service 层核心代码大概是这样的Transactional(rollbackFor Exception.class) public ActivitySignupResult signUp(SignupRequest request, Long userId) { // 1. 活动是否存在 Activity activity activityMapper.selectById(request.getActivityId()); if (activity null) { throw new BizException(活动不存在); } // 2. 校验报名时间 if (activity.getEndTime().before(new Date())) { throw new BizException(报名已截止); } // 3. 校验是否重复报名 int count signupMapper.countByUserIdAndActivityId(userId, request.getActivityId()); if (count 0) { throw new BizException(请勿重复报名); } // 4. 检查名额 int signed signupMapper.countByActivityId(request.getActivityId()); if (signed activity.getMaxNumber()) { throw new BizException(名额已满); } // 5. 插入报名记录 Signup signup new Signup(); signup.setUserId(userId); signup.setActivityId(request.getActivityId()); signup.setStatus(0); signupMapper.insert(signup); return ActivitySignupResult.success(); }看到没有一个方法把校验和写入串起来外层加了一个Transactional事务注解保证中间任何一步出错都不留脏数据。说到事务Transactional有个特别容易踩的坑默认只在 RuntimeException 下回滚如果抛的是检查异常它不会回滚。所以最好显式写上rollbackFor Exception.class这个习惯能救你很多次。2.3 活动报名的并发控制数据库层面怎么防超报网上关于活动报名的并发控制讲乐观锁悲观锁的文章一大把但落到这个个人博客系统里我的建议是分层拦截、数据库兜底。先说理论。传说中的秒杀场景下超卖问题放到活动报名里就是最后一个名额被三个人同时抢到。数据库层面最稳的做法是用UPDATE语句做条件更新比如维护一个already_signed字段每次报名前执行这样的 SQLUPDATE activity SET already_signed already_signed 1 WHERE id #{activityId} AND already_signed max_number这条 SQL 是原子操作数据库自己保证不会超报。受影响行数为 1 说明成功占住名额受影响行数为 0 说明名额满或者活动不存在直接返回名额已满就行。实际项目中我一般还会在 Service 层先查一次名额做提前快速失败减少无效的数据库更新操作。但核心防线永远是 SQL 的条件更新光靠 Service 层的 if 判断在高并发下一定会出问题。原因很简单两个请求同时 select 到already_signed 99、max_number 100两个都通过 if 判断然后一起 update名额就变 101 了。但如果你直接把判断写在 update 的 where 条件里数据库锁机制会保你平安。这就是我前面说数据库加唯一索引的原因就算业务判断和条件更新都漏了唯一索引也能挡住重复报名三重保险踩坑概率直线下降。2.4 博客文章的增删改查与 Markdown 渲染博客系统的核心本来就是文章。这里我建议直接让用户用 Markdown 语法写文章前端用marked或markdown-it做渲染。后端存储就用text类型存 Markdown 原文然后通过接口返回给前端渲染展示。这样做的好处是数据库里保留的是纯文本搜索、编辑都方便。需要提一下的是 XSS 安全问题。Markdown 渲染成 HTML 以后如果文章里有恶意脚本那点开文章的用户可能被攻击。所以前端展示之前要对 HTML 做清洗过滤可以用dompurify能有效过滤掉script标签和危险属性。个人博客虽然流量不大但安全习惯从第一天养成最省事。后端文章接口注意分页查询用pagehelper插件或者 mybatis-plus 自带的分页能力都行。分页字段要传页码和每页条数接口返回结构统一包装成{ code, message, data }前端才好统一处理。3. Vue 前端实现与联调要点3.1 路由设计与页面骨架前端路由设计核心页面就几个/博客首页、/article/:id文章详情、/activities活动列表、/activities/:id活动详情、/signup报名页、/login登录页还有管理员后台/admin。vue router 的设计有一个细节把静态路由和动态路由分开。静态路由是大家都能访问的公开页面比如首页、活动列表动态路由是登录后才能访问的比如报名页、后台管理页。在路由守卫里先判断用户是否登录、再看路由是否需要权限逻辑会清晰很多。后端返回给前端的用户信息里包含角色字段前端登录成功后把用户信息存到 vuex 或 pinia路由守卫里根据角色做跳转。这里有个经验之谈不要把完整的路由表存在后端让前端动态生成。对于个人博客这种中小型项目前端直接写好管理后台的路由然后在路由守卫里判断角色即可。动态路由适合大型平台在这里属于过度设计。3.2 报名表单前端校验与后端校验的分工活动报名页前端表单通常包含姓名、手机号、邮箱、备注这些字段。前端要做的校验包括手机号格式、邮箱格式、必填项是否填写。但注意前端校验只是提升用户体验后端校验才是数据质量的底线。后端在接收报名请求时也要做一样的校验比如邮箱格式、手机号合法性。别嫌重复恶意用户跳过前端直接调接口是很容易的事。专业项目从来都是前后端双重校验。前端校验用 element-ui 或 element-plus 的rules配置就能搞定每个字段写清楚规则show 出对应的中文提示。这个部分不用写太多花哨的组件页面干净、校验明确、提交后 loading 状态处理得当就算合格。3.3 axios 封装与 token 认证在 Vue 项目里axios 的封装是必须做的。如果不封装每个页面都写一遍axios.get({ headers: token })不仅累而且以后 token 失效要全局处理时就傻眼了。我的封装思路是这样创建 axios 实例设置基础 URL然后在请求拦截器里从 vuex 里取 token 并加到 Authorization 头。响应拦截器里统一处理code 200的情况非 200 的弹提示遇到 401 就清除用户信息并跳转登录页。// api/request.js import axios from axios import store from ../store import router from ../router const request axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL || /api, timeout: 10000 }) request.interceptors.request.use(config { const token store.state.token if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { alert(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response.status 401) { store.commit(clearUser) router.push(/login) } alert(error.message) return Promise.reject(error) } )登录接口成功后后端返回 token 和用户信息前端存进 vuex 的同时放一份到sessionStorage刷新页面后从 sessionStorage 恢复状态。这个流程是前后端分离项目的基本操作必须非常熟。4. 核心业务逻辑从用户点击到报名成功4.1 完整链路梳理从用户角度,一次完整的报名流程是用户浏览活动列表页点击某个活动查看详情活动详情报名的按钮会判断用户是否已登录没登录就跳登录页已登录用户填写报名表单前端校验通过后调后端接口后端校验活动状态、名额、是否重复报名写入报名记录前端收到成功响应后跳转到报名成功提示页或弹窗这个链路中最容易出问题的是第 3 步到第 4 步之间的体验。网络慢的时候用户点了一下没反应又点一下结果就提交了两条报名。这个问题的解决方案分两端前端在提交过程中给按钮加 loading 状态、禁掉再次点击后端用联合唯一索引兜底。4.2 报名后的邮件通知异步处理的正确姿势这个项目如果做得好一点可以在报名成功后给用户发一封确认邮件。这个功能本身不难用spring-boot-starter-mail就能实现。但要注意发邮件的操作不应该拖慢接口响应速度因为邮件服务不稳定如果同步发送一个 SMTP 超时可能导致接口 5 秒后才返回。正确的做法是使用异步线程池。在 Service 方法上注入一个Async标注的 send 方法然后主流程里调用时不等待发送结果。当然使用Async前必须在启动类或配置类上加EnableAsync。还有个小坑异步方法和调用方不能在同一个类否则代理失效Async不生效。这是因为 spring boot 的异步是基于代理实现的同类调用绕过了代理。如果你还想更稳一点可以引入 mq 做消息队列把发邮件的事件丢进队列消费者慢慢处理。但考虑到个人博客项目的体量线程池 Async 就完全够用了不要为了用技术而用技术。4.3 管理后台报名名单与导出管理员的报名审核模块核心功能有三个报名详情列表、审核状态变更、报名名单导出。导出功能我推荐用 easy-excel 这个库几行注解就能把 List 数据导出成 xlsx 文件。审核状态用数字表示0 待审核1 通过2 拒绝。前端列表页用标签展示状态不同颜色区分。管理员可以批量通过、批量拒绝后端对应接收一个 id 列表的批量处理接口。这里要注意事务边界批量操作必须在一个事务里要么全部成功要么全部回滚。4.4 数据统计与可视化如果想让项目有亮点报名数据的统计可视化是性价比很高的一个模块。后端提供一个统计接口返回活动报名人数随时间变化的趋势数据前端用 echarts 画折线图或者柱状图。这个功能不复杂但放在个人博客系统里很出彩面试时拿出来讲也很有说服力。public ListSignupStatDTO getSignupTrend(Long activityId) { return signupMapper.selectSignupTrend(activityId); }对应的 SQL 大概是SELECT DATE(create_time) AS signup_date, COUNT(*) AS signup_count FROM signup WHERE activity_id #{activityId} GROUP BY DATE(create_time) ORDER BY signup_date前端拿到这个数据以后传给 echarts 就能画出一张漂亮的报名趋势图。这种后端出数据前端做可视化的分工很清晰也是真实项目里最常见的协作方式。5. 常见问题与排查技巧实录5.1 Spring Boot 版本太高导致的启动失败这个问题在相关热词里被搜爆了说明遇到的人非常多。spring boot 3.x和spring boot 2.x有一个巨大的差异2.x 用的是javax.servlet前缀3.x 改成了jakarta.servlet。如果你在 3.x 项目里引入了很多依赖老版本的代码启动时会报找不到javax.servlet的类。解决方案有两种一种是把项目降回 2.x适合刚学习的新手生态资料多另一种是坚守 3.x同时把相关的第三方依赖版本上升到支持 jakarta 的版本比如 mybatis-plus 需要 3.5.3 以上版本才支持 spring boot 3。我的建议是如果自己学习或者做毕设直接选 spring boot 2.7.x稳定且资料多如果是工作中接的新项目再看团队的技术栈约定。另外用 idea 创建 spring boot 项目时spring initializr 默认可能指向的是最新稳定版注意手动改成 2.7.x。5.2 Vue 打包后布局异常vue 打包后布局异常也是一个高频搜索词。最常见的表现是本地npm run serve一切正常npm run build部署到服务器以后页面白屏、图片资源 404、二级路由刷新后 404。白屏问题九成是publicPath没配对。打包后资源的默认路径是根目录/如果你的项目部署在服务器根目录就没事但如果部署在子路径比如http://ip:8080/blog/就必须在vue.config.js里配置publicPath: ./。同时图片资源建议用相对路径引用避免写死/assets/xxx.png。5.3 前后端联调中的 CORS 跨域问题前后端分离开发时候前端跑在localhost:8080后端跑在localhost:9090浏览器会拦截跨域请求。解决方案有两种一种在后端添加CrossOrigin或者全局跨域配置类另一种通过前端代理vue cli 的devServer.proxy或者 vite 的server.proxy把/api代理到后端地址。我推荐开发阶段用前端代理这样可以避免后端接口的地址写死在前端代码里。等部署上线时再用 nginx 做统一入口把前端静态资源和后端/api反向代理到同一个域名下彻底规避跨域。这个方案在真实项目里是首选。5.4 其他高频问题排查速查表问题原因解决办法登录后刷新页面用户信息丢失用户信息只存在 vuex 内存里没做持久化使用 sessionStorage 或 localStorage 存储初始化时恢复接口报 500日志显示空指针mapper 返回 null业务层没判空Service 层对查询结果统一判空处理上传图片后刷新就找不到图片存在本地磁盘临时目录配置独立的静态资源映射目录前端报 Failed to parse multipart servlet request上传大小超出默认 1MB 限制在 spring boot 配置文件里调整 max-file-size 和 max-request-size线程等待都完成再返回结果用了多线程但不知道如何汇总使用 CompletableFuture 的 allOf().join() 等待所有线程完成这里展开说下多线程等待的问题。活动报名完可能要发统计邮件、更新用户积分、记录日志这三件事不互相依赖可以并行执行。用CompletableFuture.runAsync()发起三个异步任务最后用CompletableFuture.allOf().join()等待所有任务完成。这个模式在真实项目里非常常见也是面试问 java 线程时很喜欢让现场写的题目。6. 项目上线与代码质量6.1 从本机到服务器部署实操本地跑通了怎么部署到服务器我建议用 docker 部署虽然学起来有点门槛但一劳永逸。后端写一个Dockerfile前端构建后的 static 文件用 nginx 镜像承载再用 docker-compose 把后端、前端、mysql、redis 串起来。如果不想用 docker那手动部署的流程是后端打包成 jar 包放到服务器java -jar blog-server.jar启动前端npm run build生成 dist 目录配置 nginx 指向这个目录再配一个/api反向代理到后端的9090端口。这里要强调的是不要用 spring boot 内置的 tomcat 去承载前端静态资源让专业的人干专业的活nginx 处理静态资源比 tomcat 高效得多顺带还能配 HTTPS。6.2 项目安全加固的几件小事个人博客系统看似不需要高规格安全但作为练习项目做点安全加固对面试加分很有用。密码一定要加密存储用BCryptPasswordEncoder。接口层面要加统一的参数校验spring boot 里的Validated和NotBlank注解就能做省得自己写一堆 if。登录接口防暴力破解可以加一个简单的失败次数限制比如失败超 5 次锁 30 分钟。最后MySQL 连接串要配useSSLfalsecharacterEncodingutf-8不然中文乱码能折腾你一天。6.3 面试角度这个项目怎么讲出深度很多读者问这种项目别人都做过面试官会不会觉得没有含金量我的看法是项目本身不重要你基于项目讲出来的思考才重要。比如面试官问你项目有没有遇到并发问题你就可以讲活动报名的超报问题然后展开数据库条件更新的方案、唯一索引兜底、前端 loading 防重复提交三条线一起说。面试官再追问为什么不用乐观锁你就回答乐观锁适合更新某个数据的次数问题而报名场景是插入一条记录 更新报名人数的复合操作用条件更新的原子性更合适。这种追问拆解能力比用一个冷门项目的效果要好得多。还有一个加分的点讲一讲你遇到的问题和排查过程。比如 vue 打包后布局异常、spring boot 版本导致启动失败、跨域问题这些都是真实的项目经历讲的时候带一点施工现场感面试官是能感受到的。背八股文和真做过项目的讲话的底气完全不一样。7. 一些值得参考的扩展方向最后聊一下扩展。这个博客活动报名系统的排期如果还有余量我建议优先做这几个方向第一个是全文检索。文章多了以后数据库LIKE %关键字%的查询效率会越来越差可以用 elasticsearch 做全文检索或者退一步用 mysql 自带的全文索引虽然性能略弱但部署成本低。第二个是评论系统。文章和活动都支持评论用户之间可以互动系统就从单向输出变成社区了。评论系统要注意防刷和敏感词过滤。第三个是如果是活动报名可以考虑接入微信登录或者小程序端让报名流程更顺滑。后端接口如果设计得合理前端加一个小程序只是多一套客户端的事。第四个是监控与日志。back-end 接入 logback 滚动日志和 spring boot actuator 的健康检查前端接入 sentry 收集报错整个项目就能做到出问题有日志、状态有监控。这些方向不是说都要做而是给项目预留好扩展的点。我做这个项目时的真实体会是一开始把模块边界理清楚、数据库索引设计好、接口返回格式统一好后面扩展起来会很顺畅。别等到代码写了一堆再回头重构那时候成本就高了。如果你正在做类似的项目我的建议是先把博客文章的展示和活动报名这条主链路跑通再做管理后台和统计报表最后考虑部署上线。每完成一个里程碑都自己测一遍记录一下遇到什么问题、怎么解决的。这样坚持下来你收获的不仅是一个能提交的项目更是一套完整排错和交付的经验这套能力才是面试时真正值钱的東西。