SpringBoot个人博客系统实战:从数据库设计到部署上线全解析

发布时间:2026/10/21 14:12:48
SpringBoot个人博客系统实战:从数据库设计到部署上线全解析 直接说结论计算机毕业设计选SpringBoot个人博客系统是目前性价比最高的一档选题。别觉得它不够炫真正能把用户注册登录、文章发布、标签分类、评论互动、文件上传这些基础功能做扎实再配一套干净的前端页面答辩现场足以讲清楚工作量和设计思路。这篇文章把我从一个实际完整做完的博客系统项目里拿出来的东西全部摊开需求边界怎么划、数据库表怎么设计、登录认证选哪种方案、Markdown文章怎么存怎么渲染、部署上线踩过哪些坑。不是教材式的罗列而是一份能对着操作的实践笔记正在准备毕设、或者想给自己搭个专属博客写文章的朋友都能在里面找到可以直接抄的部分。1. 选题分析与整体设计1.1 需求清单先做减法别让功能膨胀拖垮进度博客系统是典型的CRUD项目但它的坑恰恰藏在看起来什么都能加里。我见过一份需求清单光功能列了三十多条文章管理、分类、标签、评论、点赞、收藏、私信、消息通知、访客统计、友链、音乐播放器……单看每一条都不难组合起来工作量立刻失控一个人一个月根本做不完。正确做法是先定最小可用版本核心就四件事能写、能发、能管、能看、能评。用户模块注册、登录、退出、个人信息修改、头像上传。文章模块发布、编辑、删除、查看文章详情、按分类和标签筛选。评论模块对文章发表评论、查看评论列表、删除自己的评论。后台视角每个用户只管理自己的文章与评论管理员额外拥有内容审核权限。这个边界跑通之后系统已经是一个完整可用的博客产品。点赞、收藏、阅读数属于提完成度的功能我实际加了点赞和阅读数因为这两个功能能让答辩演示更有说服力但更复杂的消息通知、私信这类功能果断砍掉。做减法不是偷懒是保证每个功能都做得踏实而不是十个功能每个都是半成品。1.2 技术选型的核心逻辑为什么SpringBoot是这题的答案单机Web系统里SpringBoot是这个体量项目最稳的选择。核心原因有几个第一零配置起步。内嵌Tomcat解决了传统SSH时代部署麻烦的问题一个可执行Jar包直接跑起来部署成本很低。第二生态成熟。官网文档、社区案例、各种踩坑帖都非常多学生遇到问题能快速搜到解决方案。第三和毕业设计的评审习惯契合。SpringBoot MyBatis-Plus MySQL Redis这套组合评审老师一看就知道你掌握了当前主流的后端开发方式技术含量比单纯的JSPServlet高出一截。我的技术栈落定如下组件选择说明核心框架SpringBoot 2.7.x稳定、资料多兼容主流JDK8ORMMyBatis-Plus单表CRUD不用写SQL适合快速开发数据库MySQL 8.0utf8mb4存储支持中文全文检索扩展缓存Redis存验证码、热点文章数据认证JWT无状态认证前后端分离友好前端Thymeleaf Bootstrap服务端渲染环境搭建简单构建工具Maven依赖管理打包部署方便有人纠结要不要用Spring Boot 3.x。我的建议是除非你对新特性有明确需求否则2.7.x就够用3.x要求JDK17某些老版本的MyBatis-Plus和第三方组件会有兼容问题调试成本会增加。前端为什么不用前后端分离的Vue因为个人博客本身页面交互不复杂Thymeleaf服务端渲染可以大幅减少代码量。你只有一个人时间摆在那里把精力花在后端核心逻辑和论文撰写上更划算。这个决策本身就是一种工程能力在有限资源下选择成本最低的路径。2. 数据库设计与关键表结构2.1 实体关系梳理六张表搞定全部核心业务博客系统实体关系并不复杂我用一句话概括一个用户拥有多篇文章一篇文章属于一个分类、可以打多个标签一篇文章下有多条评论。核心实体就是用户、文章、分类、标签、评论加上文章和标签之间的多对多关联表一共六张表。画ER图很多人会纠结要不要用工具。我实际做的时候用Navicat直接建表边建边调整这样最直观。设计阶段需要想清楚几个关键决定文章正文用LONGTEXT存Markdown原文不存渲染后的HTML。是因为原文可编辑、可备份、可迁移渲染在服务端实时做页面上用编辑器组件预览效果即可。标签表和文章表之间是多对多所以必须有中间表。有些项目把标签直接冗余在文章表里用逗号分隔短期省事但统计标签文章数、按标签筛选时会把自己绕晕不推荐。阅读数和评论数作为冗余字段存在文章表里。每次浏览量都去count一次评论表是灾难级的低效直接在文章表维护计数查询列表时一次搞定。2.2 核心建表SQL与字段设计说明用户表的设计要点是密码绝不存明文用BCrypt加密后存储username加唯一索引这是登录查询的入口。CREATE TABLE user ( id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(32) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(32) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像地址, role TINYINT(4) DEFAULT 1 COMMENT 角色 1-普通用户 2-管理员, status TINYINT(4) DEFAULT 1 COMMENT 状态 1-正常 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;文章表要特别注意索引布局。列表页最常见的查询是查某分类下的文章查某作者的文章按时间倒序拉取最新文章所以user_id、category_id、create_time这三个字段都需要普通索引。CREATE TABLE article ( id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 文章ID, user_id BIGINT(20) NOT NULL COMMENT 作者ID, title VARCHAR(100) NOT NULL COMMENT 标题, summary VARCHAR(255) DEFAULT NULL COMMENT 摘要, content LONGTEXT COMMENT Markdown原文, cover VARCHAR(255) DEFAULT NULL COMMENT 封面图地址, category_id BIGINT(20) DEFAULT NULL COMMENT 分类ID, status TINYINT(4) DEFAULT 1 COMMENT 状态 1-已发布 0-草稿 2-回收站, view_count INT(11) DEFAULT 0 COMMENT 阅读数, comment_count INT(11) DEFAULT 0 COMMENT 评论数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_category_id (category_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章表;评论表用parent_id字段支持楼中楼回复0代表顶级评论。这样既保留简单的层级结构又不会让查询复杂度爆炸。二级回复场景下列表页先查article_id的顶级评论再批量查各自的子评论两条SQL就能搞定。2.3 数据访问层的几个实用经验MyBatis-Plus的BaseMapper提供的selectById、selectPage、deleteById已经覆盖了80%的CRUD操作不需要手写XML。但有几种场景绕不开自定义SQL一定要用注解或XML写清楚文章列表需要关联查出作者昵称、分类名称需要left join。标签文章数统计需要group by tag_id然后count。阅读数1这种操作用UpdateWrapper构造SQLUPDATE article SET view_count view_count 1 WHERE id ?不要先查出来再更新避免并发丢失更新。分页查询我统一用MyBatis-Plus的Page对象前端传pageNum和pageSize后端返回总记录数和当前页数据。这里有个细节容易被忽略前端列表页要支持按分类、按标签、按关键词三种筛选方式且它们可以叠加。设计查询条件时不要把参数写死而是构造一个QueryWrapper对象根据条件动态追加QueryWrapperArticle wrapper new QueryWrapper(); wrapper.eq(StringUtils.hasText(categoryId), category_id, categoryId); wrapper.in(tagIds ! null !tagIds.isEmpty(), id, articleIdList); wrapper.like(StringUtils.hasText(keyword), title, keyword); wrapper.eq(status, 1); wrapper.orderByDesc(create_time);组合条件像搭积木每增加一个筛选条件就追加一行wrapper主流程代码完全不用改。3. 核心功能模块的落地实现3.1 用户认证模块注册、登录与密码安全认证模块是安全审查的重点很多毕设在这里栽跟头。首先要明确不能只用Session存登录状态。如果你的前端用了部分前后端分离的思路、或者要支持小程序端调用Session就非常别扭。我用JWT做无状态认证流程是这样注册时用户名先查重校验通过后用BCrypt加密密码再落库。BCrypt的gensalt方法自带随机盐每次加密结果不同即使两个用户密码一样密文也不一样可以有效对抗彩虹表攻击。登录时用BCrypt.checkpw校验明文和密文而不是简单equals比较。PostMapping(/login) public ResultString login(RequestBody LoginRequest request) { User user userService.getByUsername(request.getUsername()); if (user null || !BCrypt.checkpw(request.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } if (user.getStatus() 0) { return Result.error(账号已被禁用); } String token JwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); return Result.success(token); }生成JWT时我往payload里放了userId、username、role三个字段过期时间设为24小时。后续接口从token里解析userId就不需要每次查库确认身份。权限控制用一个HandlerInterceptor拦截除登录注册和文章浏览外的所有请求校验Authorization头的token有效性。实操中有一个容易漏掉的细节JWT字符串里带点号前端放在请求头时没问题但如果你把它放在URL参数里传点号可能会被框架处理掉。我统一走请求头传递避免这个坑。Redis在这个模块里真正发挥作用的场景是登录失败次数限制。连续失败5次锁定账号15分钟。这个功能不加也能跑但加了之后整个系统的健壮性观感完全不同而且代码量不大强烈建议做。3.2 文章管理模块发布、编辑与Markdown渲染链路文章模块涉及三个核心问题编辑器选型、数据存储与回显、渲染展示。编辑器我用的是开源的Markdown编辑器组件开箱即用支持工具栏、代码高亮和实时预览。提交到后端的数据是Markdown原文前面提到过存储层只存原文。编辑场景有个我踩过的坑文章内容字段是LONGTEXT回传给前端编辑时内容里的换行符会被HTML转义导致编辑器里显示一堆br标签。解决方法是后端接口返回原文时确保不经过HTML转义处理用ResponseBody返回JSON时Spring默认的JSON处理不会动换行符真正的问题出在前端把内容塞回编辑器时用错了DOM属性。用innerText赋值而不是innerHTML即可保证原文完整还原。文章发布的完整流程我整理成一个Service方法校验标题非空、内容长度不小于10字符。写入文章表初始状态为草稿。处理标签将标签名字符串按逗号拆分查询已存在的标签新标签自动创建再建立文章与标签的关联。发布操作将status置为1。列表查询这里要重点说。首页文章列表如果不做缓存每次请求都会查库、渲染、再返回压力不大但体验一般。我引入Redis做了两级缓存第一级缓存热点文章的详情页key是article:detail:{id}过期时间15分钟第二级缓存首页前20篇文章的ID列表用来快速响应首页请求。缓存失效策略是写操作后主动删除对应缓存而不是设置短TTL等它自然过期这样可以避免用户刚发布完文章首页却看不到这种尴尬问题。3.3 标签分类与内容检索分类是单选的一篇文章只属于一个分类标签是多选的一篇文章可以有多个标签。这种设计很接近真实博客平台的逻辑分类解决归档问题标签解决相关性推荐问题。标签云是一个很好展示的页面遍历所有标签按文章数量决定字体大小。我实际做的时候写了一条带子查询的SQLSELECT t.id, t.name, COUNT(at.article_id) AS article_count FROM tag t LEFT JOIN article_tag at ON t.id at.tag_id LEFT JOIN article a ON a.id at.article_id AND a.status 1 GROUP BY t.id, t.name ORDER BY article_count DESC这里特意用LEFT JOIN而不是INNER JOIN是为了让没有文章的新标签也能展示出来。搜索功能我用了MySQL的LIKE模糊查询覆盖标题和摘要两个字段。数据量超过十万条时LIKE的%keyword%写法会放弃索引但毕设和中小型博客场景完全够用不需要引入Elasticsearch这种重量级搜索引擎。后续如果要升级可以在文章表加一个关键字字段或者接全文索引在优化章节我会说。3.4 评论互动模块的设计要点评论模块看起来简单几个细节决定了体验好坏。第一未登录用户不能评论前端要显示友好提示而不是直接报错第二评论内容要做HTML转义防止XSS攻击第三评论成功后文章表的comment_count要加一。删除评论时有个一致性陷阱直接执行DELETE删掉评论记录但文章表里的comment_count字段没有同步减少数据就对不上了。我的做法是删除评论后在同一事务里更新文章的评论数。这块代码我一开始没注意后来在做后台列表时发现评论数对不上排查了半天才发现是删除操作没有同步计数。这个问题最好在写接口的第一时间就处理掉。楼中楼回复的展示我做了简化处理列表页只显示二级结构顶级评论按时间倒序每条顶级评论下的子评论按时间正序。不搞无限层级逻辑简单且足够满足日常使用。3.5 图片上传与静态资源访问博客系统不可避免要处理图片用户头像、文章封面、编辑器里的插图。SpringBoot默认上传大小限制是1MB实际使用肯定不够。我在application.yml里显式放开spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB文件保存路径我放在项目运行目录下的uploads文件夹里没有存数据库。数据库里只保存访问URL格式是/uploads/20250328/xxx.jpg按日期分目录存储避免单个目录下文件过多。静态资源映射需要手动配置。SpringBoot默认只映射classpath:/static/本地磁盘路径不会自动暴露。我这样处理Configuration public class WebConfig implements WebMvcConfigurer { Value(${blog.upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadPath /); } }上传文件时还应该限制文件类型只允许jpg、png、gif、webp四种格式。校验文件扩展名不够我还会读取文件头魔术字节做二次校验防止有人把脚本改成jpg后缀上传。这块代码思路简单先读文件前几个字节判断真实格式不在白名单直接拒绝。4. 实操过程与排坑实录4.1 环境搭建与初始化顺序项目从零搭建时我的操作顺序是固定的先装依赖跑通空项目再连数据库建表然后做登录模块打通前后端传参最后才写业务功能。很多人喜欢先把所有实体类和表建完再动手写代码我建议反过来。先跑通最小闭环即启动项目、连上数据库、完成一个注册登录接口你的开发环境才算真正就绪。后续每个模块都是在这个闭环上做加法出问题的概率会小很多。pom.xml的核心依赖大概长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency /dependencies有个版本兼容问题必须注意MyBatis-Plus的3.5.x版本和SpringBoot 2.x搭配没问题但如果你用了SpringBoot 3.x必须用MyBatis-Plus 3.5.4以上版本因为后者才支持JDK17和jakarta命名空间。这种细节网上帖子说法不一建议以官方文档的版本兼容表为准。4.2 高频踩坑问题速查表我把实际开发过程中遇到的典型问题整理成了一张表每一个都是真实踩过的不是查资料拼出来的。问题现象根本原因处理方案控制台日志中文乱码IDEA控制台编码与项目编码不一致Settings里统一设置UTF-8加上VM参数-Dfile.encodingUTF-8数据库中文显示问号JDBC连接串缺少编码参数URL加useUnicodetruecharacterEncodingutf8mb48080端口被占用本地有其他Java进程换端口或netstat查占用进程上传图片访问404磁盘静态资源映射未配置自定义WebMvcConfigurer映射/uploads/**到本地目录同一个token一段时间后请求401用户密码修改导致JWT签名失效密码修改后强制下线重新登录前端提交内容带表情符存库报错数据库字符集不是utf8mb4表和库都改成utf8mb4Maven构建时依赖下载失败网络源不稳定配置阿里云镜像仓库4.3 一次中文乱码排查的完整过程乱码问题是最容易让新手崩溃的。我记录一次实际排查过程供参考现象是前端发表中文评论后数据库里显示的是问号。排查顺序要按链路来。先看数据库表结构确认字符集是不是utf8mb4结果正常。再看JDBC连接串发现url里没有带characterEncoding参数这是第一处问题。加上之后重启项目测试新评论正常了但之前写入的乱码数据已经不可逆。所以数据库连接参数最好在项目初始化时就配置正确否则上线后换编码配置只能救新数据。4.4 评论区XSS漏洞的一次教训评论功能上线测试时某同学特意试了在评论里插入scriptalert(1)/script结果真的弹窗了。问题出在Thymeleaf渲染评论内容时用了th:utext而不是th:text。th:text会自动HTML转义th:utext是原样输出。我的修复方案是评论存库之前只做纯文本处理把和转义掉展示时统一用th:text。这里的经验是凡是用户输入的内容无论是评论区、昵称还是文章摘要一律当成不可信数据展示时默认转义需要富文本的页面才用白名单机制放行特定标签。5. 打包部署与上线优化5.1 Maven打包与生产环境配置本地开发跑起来只是第一步毕业设计如果能现场展示一个能通过公网访问的线上版本加分效果非常明显。打包命令很简单mvn clean package -DskipTests打包后的jar在target目录下。直接运行前要把开发环境的配置和线上区分开。我在resources下建了application-prod.yml数据库、Redis地址都换成线上配置启动时指定profilenohup java -jar blog-system.jar --spring.profiles.activeprod blog.log 21 nohup和21的作用是让程序在SSH断开后继续运行日志重定向到文件方便事后排查。这一步很多第一次部署的人不知道关掉终端窗口服务就挂了还以为是自己代码问题。5.2 部署之后一定要做的几件事服务跑起来不等于部署完成还有几件收尾工作配一个管理后台的健康检查接口返回数据库连接状态和当前版本号。数据库开启定时备份用crontab每天凌晨执行mysqldump保留最近7天。日志按天滚动避免单个log文件无限膨胀。Nginx代理前把上传大小限制同步调大否则大图上传会出现413错误。这些工作单拎出来都不难但它们合在一起才是能上线运行的系统和能跑起来的demo之间的差别。5.3 性能优化与项目后续演进的建议当前规模下系统性能瓶颈基本不会出现。但如果后续文章量涨到十万级有几个方向值得做文章列表页缓存的维度可以再加一层按分类和标签分别缓存页面片段。搜索从LIKE换成MySQL全文索引或者引入Elasticsearch。图片存储从本地磁盘迁移到对象存储服务减轻服务器带宽压力。阅读数、点赞数这类高频访问的字段可以先用Redis做原子自增再定时批量回写数据库。但从毕业设计的角度我的建议是不要过度设计。把已实现的功能打磨稳定、把论文里的设计思路和踩坑记录写具体比在答辩前临时加一个花哨功能靠谱得多。就我个人的实际体会而言做这个项目的最大收获不是学会了某个框架而是建立了从需求到落地的完整闭环意识。拿到题目先划边界、设计表结构前先理清关系、实现功能时先把最核心的主链路跑通再回头补细节。按这个节奏走下来代码量不大但每一步都经得起问答辩的时候心里才有底。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询