SpringBoot+Vue博客系统开发实战:从数据库设计到部署答辩全攻略
发布时间:2026/10/1 19:46:32来源:尧图网络
SpringBoot搭Vue的博客系统几乎是计算机毕业设计题库里出现频率最高的题目。标题换一个说法叫“个人内容发布与互动平台”再换一个叫“轻量级在线创作社区系统”核心要解决的其实是同一件事用户能注册登录、发文章、看文章、评论点赞管理员能管内容。这篇文章我就基于自己做过的这个项目把从数据库设计、后端接口、前端页面到联调部署的完整链路过一遍哪些地方是坑、哪些地方能成为答辩亮点都会讲到。适合正在做毕设的同学直接抄作业也适合想快速搭一个内容平台的开发者参考。1. 项目整体设计与技术选型1.1 先拆解需求这个“博客系统”到底要做什么很多同学拿到这个题目第一反应是“不就是个发文章的网站嘛”结果做着做着就发现功能边界全是模糊的。其实标题里藏了三个关键词博客系统、个人内容发布与互动平台、轻量级在线创作社区。这三个词是层层递进的拆开看就清楚需求边界了。博客系统是最基础的理解解决“文章怎么发布、怎么展示”。个人内容发布与互动平台重点是“互动”两个字。也就是说这个系统不能只让用户闷头写文章还得有评论、回复、点赞、收藏这类能让读者和作者产生连接的功能。我在设计时把互动模块单独拎出来做而不是把评论简单挂在文章下面就是因为这个“互动平台”的定位决定了评论不能只是几句话最好有楼中楼回复有点赞计数甚至能在个人主页看到自己发过的评论。轻量级在线创作社区系统强调的是“轻量”和“创作”。轻量意味着不要一上来就套 Spring Cloud、Redis、消息队列那套重架构一个好用的单体应用完全够撑起一个学习项目创作意味着编辑器体验要合格支持 Markdown 是底线代码高亮、图片上传、草稿保存这些都值得做进去。所以我的功能清单最终收敛成这样模块功能是否必须用户注册、登录、个人主页必须文章发布、编辑、逻辑删除、草稿状态必须文章展示分页列表、分类筛选、详情、热门排行必须互动评论、楼层回复、点赞、收藏必须辅助搜索、标签、浏览量统计建议做那些大而全的后台管理、多角色权限、敏感词审核这个阶段不建议碰。做得再花哨答辩时被问一句“你这个和 WordPress 有什么区别”就露馅了不如把用户端这条主链路做扎实。1.2 为什么选 SpringBoot Vue版本怎么定技术选型这件事我在开题报告里写了很多理由但真正动手时发现核心就三条生态成熟、资料多、前后端分离好分工。SpringBoot 解决了传统 SSM 项目配置地狱的问题内嵌 Tomcat打成一个 jar 就能跑这对部署和答辩演示都极其友好。Vue 这边组件化开发特别适合博客这种页面结构固定的系统文章列表、评论框、侧边栏都是天然组件拆开写互不干扰。版本这里有个非常值得注意的坑。我刚开始创建项目时直接选了最新的 SpringBoot 3.2结果发现 3.x 全家桶把javax换成了jakarta网上查资料时一大半老教程都是javax.servlet对不上。如果你用的是 JDK 8老老实实选 SpringBoot 2.7.x如果机器上只有 JDK 17那就用 SpringBoot 3.x看教程时留意教程的版本说明别让版本问题消耗掉大量调试时间。配套选型我给一套在毕设场景下最稳的组合数据库MySQL 8.x存储引擎 InnoDB字符集utf8mb4ORMMyBatis-Plus 3.5.x分页插件、自动填充字段都很省事前端构建Vite Vue3 Pinia Vue Router编辑器md-editor-v3支持 Markdown 编辑和预览对中文环境很友好请求库Axios统一封装拦截器这套组合的好处是每个组件的资料都足够多哪怕项目写到一半发现某个点不会搜索一下立刻能找到对应方案的解决方案不会卡死在冷门技术上。1.3 整体架构与前后端目录怎么组织架构上就是标准的前后端分离。前端通过 Axios 调用后端 REST 接口后端只负责业务逻辑和数据存取前端只负责渲染和交互。开发阶段用 Vite 的代理转发解决跨域生产环境可以打成单 jar 运行。后端代码按包分好别一股脑塞进 controller。我习惯的目录结构是com.example.blog ├── controller # 接口层 ├── service # 业务层 ├── mapper # MyBatis-Plus 数据访问 ├── entity # 数据库实体 ├── dto # 接口入参出参对象 ├── common # 统一返回结果、异常、常量 └── config # 拦截器、跨域等配置前端目录对应这样src ├── api # 按模块拆分的接口文件 ├── router # 路由与守卫 ├── stores # Pinia 状态管理 ├── views # 页面组件 ├── components # 通用组件 └── utils # axios 实例、工具函数目录规范不是形式主义。项目后期要加功能时你只需要知道“接口调用放 api、页面逻辑放 views、状态管理放 stores”就不会出现一个文件堆几千行的问题。答辩时考官看代码结构第一眼印象也很重要。2. 后端实现从建表到认证把核心逻辑写扎实2.1 数据库设计五张核心表最关键博客系统的数据库设计并不复杂但有几个细节会直接影响后续开发体验。用户表是基础中的基础CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50) DEFAULT , avatar VARCHAR(255) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );密码字段一定要留到 100 长度因为 BCrypt 加密后的字符串长度在 60 左右50 不够用。用户名加唯一索引是底线。文章表是设计的重心CREATE TABLE article ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, category_id BIGINT DEFAULT NULL, title VARCHAR(100) NOT NULL, summary VARCHAR(255) DEFAULT , content LONGTEXT, cover VARCHAR(255) DEFAULT , status TINYINT DEFAULT 1 COMMENT 0草稿 1发布 2删除, view_count INT DEFAULT 0, like_count INT DEFAULT 0, comment_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_category_id (category_id) );这里有几个设计决策值得展开说。第一content用LONGTEXT因为 Markdown 原文可能很长加上还要存渲染后的 HTMLTEXT类型容易触顶。第二summary字段是冗余设计的典型用法。文章列表页只需要展示标题和摘要如果每次查列表都把content全文捞出来既浪费网络带宽又拖慢查询。发布文章时前端摘要自动截取正文前 100 个字符列表查询时不查 content这个优化效果很明显。第三status字段做的是逻辑删除。用户删除文章时只是把状态改成 2数据还留在库里。这比物理删除多一道保险答辩时还能顺势讲一句“考虑到用户误删后恢复的场景我选择了逻辑删除”。这句话就是加分项。评论表采用自关联设计CREATE TABLE comment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, article_id BIGINT NOT NULL, user_id BIGINT NOT NULL, parent_id BIGINT DEFAULT NULL COMMENT 父评论IDNULL为顶级, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_article_id (article_id) );楼中楼回复不需要单独建一张回复表parent_id指向同一张表的id就能形成树形结构。前端递归渲染子评论后端查询时按article_id一次捞出来再在内存中组装成树不用写递归 SQL。这个方案在评论量不大的场景下非常简洁可靠。2.2 统一返回结构与全局异常处理后端接口设计要先定一个统一的返回格式否则每个接口返回结构都不一样前端 axios 拦截器根本没法写。我定义一个R对象Data public class RT { private int code; private String message; private T data; public static T RT ok(T data) { RT r new R(); r.code 200; r.message success; r.data data; return r; } public static T RT fail(int code, String message) { RT r new R(); r.code code; r.message message; return r; } }配合全局异常处理器把业务异常、参数校验异常、兜底异常统一转换成R对象返回。这样前端就只需要认准一个结构code为 200 代表成功否则弹出message即可。整个项目里不需要每个接口单独写 try-catch代码能干净不少。2.3 JWT登录认证与权限校验登录认证我建议直接手写 JWT 拦截器不要在这个项目里引入 Spring Security。原因很简单毕设的核心是讲清楚业务逻辑Spring Security 那套过滤器链和配置类会占据大量篇幅而且很多同学只是配置好了但并没有真正理解答辩被追问反而容易翻车。手写拦截器只有几十行代码但你能把“token 如何签发、如何校验、如何获取当前用户”讲得明明白白。登录流程是这样的注册时密码用BCryptPasswordEncoder加密绝不存明文。登录成功后用用户的 id 和用户名生成 JWT设置过期时间 7 天。前端把 token 存到localStorage每次请求在请求头带上Authorization: Bearer token。后端写一个AuthInterceptor拦截需要登录的接口解析 token把用户 id 放到 request 的 attribute 里。核心代码大致长这样public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BizException(401, 未登录); } Long userId JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(currentUserId, userId); return true; } }在 WebMvcConfig 里注册拦截器时放行登录、注册、文章列表、文章详情这些无需登录的接口其他写操作全部拦截。Controller 需要拿当前用户时通过RequestAttribute(currentUserId) Long userId获取这样接口内部就不会出现到处从 session 取用户的混乱代码。越权校验是另一个容易被忽略的坑。比如编辑文章接口不能只根据前端传的 articleId 就去更新必须校验当前用户是不是这篇文章的作者Article article articleMapper.selectById(articleId); if (!article.getUserId().equals(currentUserId)) { throw new BizException(403, 无权操作); }删除评论同理。这种校验逻辑就一句话但没有的话别人登录后就能改你的文章属于严重漏洞。答辩时主动提一句“我在修改和删除接口做了归属校验”比什么炫酷功能都加分。2.4 文章发布与查询的接口细节文章接口是核心我把它拆成几个场景分别处理。发布和编辑走同一个保存逻辑如果传入了id就是编辑没有就是新增status传入 0 存草稿传入 1 直接发布。保存之前校验标题不能为空、长度在 1 到 100 之间、正文不能为空。这里有个性能细节文章详情接口GET /api/article/detail/{id}需要把浏览量加 1。我一开始写成先selectById再把 viewCount 加一后 update等于两次数据库操作。优化成一句话更好articleMapper.updateViewCount(articleId);对应 SQL 就是UPDATE article SET view_count view_count 1 WHERE id ?这种原子更新在高并发下不会丢计数也省掉了一次查询。列表接口用 MyBatis-Plus 的分页插件PageArticleVO page new Page(pageNum, pageSize); LambdaQueryWrapperArticle wrapper new LambdaQueryWrapper(); wrapper.eq(Article::getStatus, 1) .eq(categoryId ! null, Article::getCategoryId, categoryId) .like(StrUtil.isNotBlank(keyword), Article::getTitle, keyword) .orderByDesc(Article::getCreateTime);查询结果不要直接返回实体而是转成 VO 对象放在ArticleVO里。VO 里只包含 id、title、summary、cover、authorName、viewCount、commentCount 这些列表页需要的数据不包含 content 全文。字段裁剪这件事既为了性能也为了接口语义清晰。分页返回结构里total字段来自 MyBatis-Plus 自动执行的 count 查询前端表格组件直接拿它算总页数就行。3. 前端实现Vue3页面、编辑器与交互3.1 项目初始化、路由与axios封装前端我用的 Vite 创建项目npm create vitelatest blog-web -- --template vue装完基础依赖后先装核心库npm install vue-router4 pinia axios element-plus md-editor-v3 dayjs路由规划要提前想清楚页面虽然不多但守卫逻辑要安排明白路径页面是否需要登录/首页文章列表否/article/:id文章详情否/write创作中心是/login登录否/profile/:id个人主页否在路由配置里给需要登录的路由加一个meta: { requiresAuth: true }然后在全局前置守卫里判断router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else { next(); } });axios 封装是所有页面交互的地基。我建了一个utils/request.js把 baseURL、请求拦截器、响应拦截器集中处理const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; } return config; }); request.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(error); } );这样业务代码里调用接口时拿到的是一个干净的数据对象不需要每个页面都处理错误码和 token非常省心。开发环境的跨域问题用 Vite 的 proxy 解决vite.config.js里加一段server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }前端所有请求都以/api开头Vite 自动转发到后端 8080 端口浏览器端完全不感知跨域。3.2 Markdown编辑器选型与文章渲染文章编辑是创作系统的门面。我做项目前对比过两个方案一是传统的富文本编辑器比如 wangEditor二是 Markdown 编辑器。最终选了 Markdown原因有三Markdown 源文件存储更省空间渲染成 HTML 的样式容易统一控制代码块编辑体验远超富文本。我用的 md-editor-v3 中文文档齐全安装后使用非常简单template MdEditor v-modeltitle / /template script setup import { MdEditor } from md-editor-v3; import md-editor-v3/lib/style.css; /script编辑器支持上传图片我在toolbar里配置了图片上传按钮后端提供一个统一的上传接口返回图片 URL 后编辑器自动插入到正文中。文章详情页的渲染用MdPreview组件MdPreview :modelValuearticle.content /代码高亮这块md-editor-v3 默认支持 highlight.js我只需要在引入样式时选一个主题比如 GitHub 风格的主题代码展示效果立刻变得很专业。这一步花费很小但对观感提升是决定性的。3.3 评论与点赞的数据流转评论组件是互动功能的核心载体。我的实现思路是详情页加载时并行请求文章详情和顶级评论列表评论列表接口一次性返回该文章所有评论包含层级关系。前端用一个递归组件CommentItem.vue渲染template div classcomment-item div classcomment-content{{ comment.content }}/div div classcomment-meta span{{ comment.nickname }}/span a clickshowReply !showReply回复/a /div div v-ifshowReply textarea v-modelreplyContent / button clicksubmitReply(comment.id)提交/button /div CommentItem v-forchild in comment.children :keychild.id :commentchild / /div /template递归组件的写法注意子组件要写上name属性Vue3 里用script setup时还需要通过defineOptions({ name: CommentItem })声明组件名否则无法自我引用。这个点很隐蔽我当时卡了十几分钟才反应过来。点赞功能我单独建了一张like_record表用户对同一篇文章只能点一次赞数据库层用唯一索引(article_id, user_id)兜底防止并发重复点赞。前端点击点赞按钮后调接口后端如果发现已经点过就返回“已经点过赞了”的提示正常则更新article.like_count。这样的设计虽然简单但数据一致性是可靠的。互动功能还有一个容易忽略的细节评论成功后文章详情页的评论总数要加 1。最简单的方式是评论接口返回评论 id前端重新请求评论列表保证列表的一致性也可以在发表成功后对commentCount 1做纯前端更新。刷新页面时以数据库为准最终不会出问题。4. 联调阶段常见问题与踩坑实录4.1 跨域问题的两种解法联调第一天肯定会遇到跨域。前端跑在http://localhost:5173后端跑在http://localhost:8080这俩端口不同浏览器默认拦截。我的做法是开发环境一口气配完 Vite proxy如上文所说前端请求走/api前缀转发。这样开发时完全不需要后端配 CORS代码也更干净。但如果你直接请求后端完整地址比如http://localhost:8080/api/article/page那就必须在后端配 CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(*) .allowedHeaders(*) .allowCredentials(true); } }这里有个常见报错点allowedOrigins配置成*且开启allowCredentials(true)时浏览器会直接报错。因为携带凭证的情况下不允许使用通配符必须写明确的前端地址。如果你的 token 通过 Authorization header 传递其实不依赖 cookie开发时可以把allowCredentials设为 false 来规避这个报错。4.2 JSON序列化与时间显示的坑后端实体里用LocalDateTime很顺手但序列化给前端时SpringBoot 默认会输出成类似2024-05-16T10:30:00的 ISO 格式前端直接展示很难看而且如果不做处理还会因为缺少依赖直接序列化失败。解决方式是在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai注意timestamp类型字段要确保控制台输出为北京时间否则会发现create_time比实际时间少了 8 小时。这时检查一下 JDBC 连接串有没有带serverTimezoneAsia/Shanghaijdbc:mysql://localhost:3306/blog?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai前端拿到yyyy-MM-dd HH:mm:ss字符串后直接用 dayjs 解析展示不需要再做本地时区换算。4.3 富文本内容的XSS安全问题博客系统的核心风险点是文章内容支持 HTML 渲染如果用户提交了scriptalert(1)/script前端渲染时就会执行恶意脚本。这是 XSS 攻击的典型场景毕设阶段很容易被忽略。我的处理分两层。后端在文章保存时用 jsoup 做白名单过滤只保留必要的标签String safeContent Jsoup.clean(content, Safelist.relaxed() .addTags(h1, h2, h3, pre, code, img) .addAttributes(img, src, alt));前端渲染时再用xss库做一次过滤双保险。这个细节写到论文里是实打实的“安全性设计”章节内容答辩老师基本都会认可因为它是博客系统真实存在的隐患而不是空泛地写一句“系统具有安全性”。另外一个容易踩的坑是接口越权前面已经讲了编辑文章、删除评论时的归属校验。把校验逻辑做成统一模式每个写操作的 service 方法开头先确认当前用户与资源归属一致就能把这一整类安全问题兜住。4.4 分页与MyBatis-Plus的使用细节使用 MyBatis-Plus 分页必须先注册分页插件否则Page对象只会返回全部数据total 永远为 0Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }我最初漏配了这段导致前端列表页永远只有一页数据排查了好一会儿才发现是插件没注册。这类配置类问题在项目初期就要检查一遍不要等到联调时才找原因。5. 部署上线与毕业设计收尾5.1 打包部署的两种方案项目写完要能够现场演示部署方案直接影响演示的稳定性。我准备了两种方式这里都分享出来。第一种是前后端分离部署。前端执行npm run build产物在dist目录由 Nginx 做静态托管后端打成 jar 通过java -jar blog-server.jar运行。Nginx 配置里把/api的请求反向代理到后端location /api/ { proxy_pass http://localhost:8080; }第二种更省事适合毕设演示场景。把前端的dist目录里的所有文件复制到后端src/main/resources/static下重新打包后端 jar。这样一个 jar 就同时包含前端页面和后端接口启动后默认端口直接访问页面无论答辩的电脑是否安装 Nginx 都能跑起来。我实际答辩演示用的是第二种方案并且提前在application-prod.yml里把数据库连接、端口配置好现场启动再加一份数据初始化脚本确保演示环境干干净净不会翻车。5.2 论文结构和答辩准备的几个关键点论文结构不用标新立异按学校要求的套路走即可但以下细节决定了论文质量需求分析章节把系统分为“游客、注册用户、管理员”三类角色逐一列出用例不要只写“用户能发布文章”这种一句话需求要写出前置条件、主流程和异常流程。系统设计章节重点展示数据库设计的三范式分析和 ER 图以及 JWT 认证流程图、文章发布时序图。图表质量比文字数量重要答辩老师翻论文首先看图。系统实现章节不要在论文里堆大段代码。我当时的策略是每部分截取 10 到 20 行关键代码配上功能描述和设计原因比如 JWT 拦截器为什么要放进 HandlerInterceptor 而不是直接在 Service 里判断。这种“为什么”比“是什么”更能体现理解深度。测试章节至少准备二十条功能测试用例覆盖正常流程、异常输入、权限场景。用表格列出功能模块、操作步骤、预期结果、实际结果。另外可以加几条简单的安全测试比如“非作者访问编辑接口应返回 403”这是加分亮点。答辩演示顺序我建议固定为注册两个账号 → 账号A发布一篇带代码块和图片的 Markdown 文章 → 账号B评论并回复 → 点赞 → 个人主页展示创作列表 → 修改文章 → 删除文章 → 扩展功能搜索、热门。整个流程控制在五分钟以内每一步之前想好停顿和讲解词不要一上来就机械地点页面。6. 实际项目落地后的几点感悟项目做完回看最深刻的体会是“先审计数据库后写前端”。我第一版做完突然发现文章列表没有摘要字段前端列表页显示很难看回头补字段、补接口、改前端展示反复折腾浪费了两天。如果你能先把建表脚本写出来对着表字段逐一把所有页面能想到的信息写清楚再开始写代码后面的返工能少一大半。还有一个小技巧后端每个接口打印一下耗时日志。我写了个非常简单的 AOP 切面统一打印请求路径、参数、耗时联调阶段排查问题效率翻倍。答辩时被问到“项目有没有优化点”这个日志设计也能作为系统可维护性的佐证。最后关于学习路径如果你的基础比较薄弱我的建议是不要一上来就研究深层源码。先按本文的链路把项目完整跑通一次遇到问题搜索答案记录下来跑完一遍你自然会发现哪些原理值得深挖——到那时候再去看 SpringBoot 自动装配或者 Vue 响应式原理理解成本会低很多。毕竟做毕设的目标不是写一个“完美”的系统而是把一个“完整且能跑”的系统彻底讲清楚。
网站建设高端定制企业官网