SpringBoot个人博客系统开发全流程:选题设计到部署上线

发布时间:2026/9/9 1:21:14
SpringBoot个人博客系统开发全流程:选题设计到部署上线 每年毕业设计季总有人来问我选题怎么定。如果你走Java方向又想要一个既能完整覆盖业务闭环、又不至于庞大到做不完的题目我一般首推这种基于SpringBoot的个人博客网站系统也可以叫个性化内容发布与分享平台或者自媒体内容管理与社交互动系统。叫法不一样内核是同一套——用户注册登录、文章发布、标签分类、评论点赞、个人主页、后台管理这些功能串起来就是一个标准的内容型Web项目。这类题目最大的优势是“有得讲又不至于失控”。比起单纯的学生管理系统、图书馆管理系统那种全是增删改查的CRUD项目它多了一层内容生态的逻辑比如文章的富文本编辑、标签体系、评论互动、搜索过滤这些点随便挑一个都能在答辩时往深了说。而比起商城、秒杀那种动不动就扯上高并发、分布式、消息队列的重型项目它又不会被复杂度拖死一个人在两三个月内完全能保质保量地完成。这篇内容我就以自己实际做过的一个版本为例把从选题思路、技术栈选型、数据库设计、核心功能实现到部署上线的完整路径捋一遍也把开发过程中踩过的坑、被问过的问题一并整理出来。不管你是准备用它当毕设还是打算拿来做个人站都能直接参考。1. 项目定位与技术选型思考1.1 这个题目到底在解决什么问题先想清楚这个项目存在的意义才能在开题报告和答辩的时候站得住脚。传统的个人博客很多还是静态页面或者简单的内容展示用户只能看不能参与。而标题里提到的“个性化内容发布与分享”“社交互动”本质上是要把博客从一个“展示窗口”变成“互动社区”。具体来说有三个核心诉求内容发布用户能注册登录能写文章、传图片、设置标签系统能对内容进行统一存储和管理。个性化体验用户可以管理自己的专栏、关注感兴趣的标签、按分类浏览内容系统会根据标签和分类做内容聚合而不是把所有文章堆在一个列表里。社交互动登录用户可以评论文章、点赞、收藏博主能收到互动通知形成最基本的社区氛围。这三个诉求对应到系统设计上就是三个核心模块用户模块、内容模块、互动模块。项目里所有技术细节都是为这三个模块服务的。提示答辩时老师最容易问的一句话就是“你这个系统解决了什么痛点”。如果你能说出“从单向展示到双向互动”这个转变逻辑就已经比大多数同学高了一个层次。1.2 为什么选SpringBoot而不是SSM或Spring Cloud很多同学会纠结框架选型。我直接给结论毕设层面SpringBoot就是最优解没有之一。先说SSMSpring SpringMVC MyBatis。这个组合放在五年前是主流但现在的问题在于配置太繁琐大量的XML配置、web.xml配置、spring-mvc.xml配置光搭建环境就能劝退很多人。对毕设来说时间应该花在业务实现上而不是花在“把项目跑起来”上SpringBoot的自动配置和起步依赖把这一层几乎全部省掉了一个带内嵌Tomcat的jar包就能跑环境搭建半小时内搞定。再说Spring Cloud。这是微服务框架毕设项目撑不起这个复杂度。如果你在个人博客这种单体项目里强行引入服务注册、配置中心、网关、熔断不仅自己写起来痛苦答辩时老师追问服务之间如何通信、分布式事务怎么处理你也会非常被动。单体应用、单数据库这个量级用SpringBoot刚刚好。从面试的角度看SpringBoot也是Java后端岗位的高频考点缓存机制、自动配置原理、约定优于配置、Starter机制这些在面试八股里出现频率非常高。做完这个项目你对SpringBoot的理解就不再是背题而是真正知道它帮你省了哪些事。1.3 整体技术栈清单我用的这套技术栈核心原则是“主流、稳定、不花哨”。具体如下表层次技术选型说明后端框架SpringBoot 2.7.x稳定版资料多避免盲目追新版本带来的兼容问题持久层MyBatis-Plus单表操作不用写SQL复杂查询也能用Wrapper比纯MyBatis省事数据库MySQL 8.0主流数据库性能足够缓存Redis用于验证码存储、文章浏览量缓存、热点数据查询安全认证JWT Spring Security无状态登录前后端分离场景下的标准方案文件存储本地磁盘 Nginx映射毕设阶段不需要接OSS本地存储足够演示前端Vue 2 Element UI后台管理界面的主流组合学习成本低构建工具Maven必选不解释接口文档Knife4j基于Swagger封装比原生Swagger好看也好用之所以强调版本是因为SpringBoot的版本坑太多了尤其是SpringBoot 3.x开始强制要求JDK 17。如果你是第一次做项目建议老老实实用2.7.x加JDK 1.8这个组合最成熟网上搜问题一搜一个准。JDK 17虽然新但很多旧教程里的依赖写法已经不适用了排查起来费劲。2. 系统功能设计与数据库建模2.1 用户端和管理端功能拆解拿到题目之后不要急着写代码先画功能图把“有哪些角色、每个角色能干什么”理清楚。这个系统我用两个维度拆功能用户端前台门户用户注册、登录、退出登录支持邮箱验证码注册。首页文章列表按最新、最热、推荐排序支持分页浏览。文章详情页展示正文内容支持点赞、收藏、评论功能。个人中心用户可修改头像、昵称、密码查看自己发布过的文章和互动记录。文章发布/编辑富文本编辑器录入内容可设置封面、标签、分类支持草稿保存。标签和分类聚合页点击标签或分类能看到该主题下的全部文章。搜索按文章标题、标签关键词搜索内容。管理端后台登录认证基于JWT令牌校验管理员身份。用户管理禁用/启用用户账号重置密码。文章管理审核文章、置顶文章、删除违规文章。评论管理删除违规评论。标签/分类管理新增、修改、删除标签。数据统计文章数、用户数、评论数的简单统计面板。功能拆分的原则是“够用就行”。你不需要做得像CSDN那样复杂但用户、内容、互动这三个闭环必须完整每个闭环都包含一张主表和一到两张关联表图穷匕见数据模型自然就出来了。2.2 数据库表设计与关系梳理数据库设计是这个项目的灵魂。我见过很多同学上来就建表结果表与表之间的关系混乱后面写SQL的时候痛苦到怀疑人生。合理的设计应该遵循“一个核心实体对应一张主表实体之间的多对多关系用中间表”的规则。我用到的核心表如下表名用途关键字段user用户表id, username, password, nickname, avatar, email, status, create_timearticle文章表id, user_id, title, summary, content, cover_image,status, view_count, like_count, comment_count, create_time, update_timetag标签表id, tag_name, create_timearticle_tag文章标签关联表id, article_id, tag_idcategory分类表id, category_name, create_timecomment评论表id, article_id, user_id, content, parent_id, create_timeuser_like点赞记录表id, article_id, user_id, create_timeuser_favorite收藏记录表id, article_id, user_id, create_timeuser_follow关注表有社交需求时可加id, user_id, follow_user_id, create_time这里面有几个设计上的细节值得说一下文章表里的点赞数、评论数、浏览数为什么要冗余存字段因为每次点开文章都去count一次评论表、点赞表数据量大了之后性能会很差。冗余字段的做法是写入时更新计数读取时直接取出来这就是典型的空间换时间。评论表里的parent_id用于支持楼中楼回复。如果parent_id为null说明是一级评论如果不是说明是某条评论的回复。这里注意不要在同一张表里把所有层级嵌套都做出来两级的结构够用再深就是给自己找麻烦。user_like和user_favorite两张表的核心作用是做“幂等控制”。用户点了赞之后再次点击就是取消赞靠的就是表里的唯一记录判断当前状态。联合唯一索引article_id, user_id是必须的防止重复插入脏数据。2.3 核心建表SQL参考直接贴出两张核心表的DDL剩下的表格子都差不多。注意MySQL的字符集建议用utf8mb4不然存Emoji表情会报错。这个坑我当年踩过存评论里的表情符号直接变成问号折腾半天才发现是字符集问题。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL UNIQUE COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, email varchar(100) DEFAULT NULL COMMENT 邮箱, status tinyint(1) DEFAULT 1 COMMENT 账号状态1启用0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE article ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 作者ID, title varchar(100) NOT NULL COMMENT 文章标题, summary varchar(255) DEFAULT NULL COMMENT 文章摘要, content longtext COMMENT 文章正文富文本HTML, cover_image varchar(255) DEFAULT NULL COMMENT 封面图地址, status tinyint(1) DEFAULT 1 COMMENT 状态1发布0草稿2禁用, view_count int(11) DEFAULT 0 COMMENT 浏览量, like_count int(11) DEFAULT 0 COMMENT 点赞数, comment_count int(11) DEFAULT 0 COMMENT 评论数, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status_time (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章表;注意article表里我把create_time和update_time都加上了。很多同学只建一个时间字段后面做“按时间排序最近更新”的需求时就会变得很被动。update_time字段加ON UPDATE CURRENT_TIMESTAMP每次更新数据它会自动刷新省得在代码里手动维护。3. 核心模块实现与关键代码3.1 JWT认证与登录拦截器实现登录认证这块我选择了Spring Security加JWT的组合。有不少同学觉得Spring Security配置太复杂想绕过它直接用拦截器。我的建议是不建议绕。Spring Security在面试中的出现频率很高而且它的过滤器链机制现在搞懂了以后做企业项目会遇到同样的场景早点学不亏。设计思路是登录接口放行其余接口经过JWT过滤器验证令牌。用户登录成功后后端生成一个带用户ID和用户名的JWT令牌返回给前端前端把它存在localStorage里后续每次请求在请求头里带上Authorization: Bearer token。后端通过过滤器解析令牌把当前用户信息塞进SecurityContext方便后续业务代码直接获取当前登录用户。JWT工具类的核心代码大致如下Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; public String generateToken(Integer userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } public boolean isTokenValid(String token) { try { parseToken(token); return true; } catch (Exception e) { return false; } } }这里有个很容易被忽略的点jwt.secret这个密钥不能写死在代码里必须放到application.yml中并且要用足够长的随机字符串长度最好在32位以上否则HS256算法会直接因为密钥太短报错。如果一个同学的项目里密钥是123456这种答辩时基本会被老师一眼看出安全意识不过关。3.2 文章发布与富文本处理文章发布是内容型系统的核心操作。我的实现方案是前端用wangEditor国内的开源富文本编辑器中文文档友好把编辑好的HTML内容通过接口传给后端后端存储到article表的content字段。为了安全性HTML内容不能直接原样存储和展示必须做XSS过滤否则用户发一篇带有恶意脚本的文章管理员的Cookie就可能被窃取。XSS过滤这里我推荐使用Jsoup的clean方法public String cleanHtml(String html) { return Jsoup.clean(html, Safelist.relaxed()); }Safelist.relaxed()允许基础的标签如p、img、a、ul、ol等但会过滤掉script、iframe、onclick等危险内容。这一步极其重要很多毕设项目完全没有XSS防护的概念属于硬伤。文章发布接口的流程是接收前端传来的表单数据标题、摘要、正文HTML、封面、标签ID列表先做参数校验标题非空、正文长度不低于100字然后生成Article实体插入数据库再批量插入文章标签关联表。注意必须开启Spring的Transactional事务保证文章主体和标签的关联要么同时成功、要么同时失败。3.3 评论、点赞与收藏的幂等处理这三个功能的本质都是“用户与文章建立一种关联关系”。我用同样的思路处理先查关联表有没有记录再决定是新增还是删除。以点赞为例按用户维度去重一个用户对同一篇文章只能有一条点赞记录。执行点赞时先查询user_like表如果记录不存在就插入一条同时将article表里的like_count加1如果记录已存在就删除同时like_count减1。这样做天然支持了“点赞再点一下取消”而且不会出现重复数据。这里有一个并发隐患用户狂点点赞按钮会不会导致计数不对我的做法是用Redis的分布式锁来包住这段逻辑锁的key为like:article:{articleId}:user:{userId}保证同一用户对同一篇文章的操作串行执行。如果不想引入Redis也可以用数据库的唯一索引加INSERT ... ON DUPLICATE KEY UPDATE来实现效果接近。前端也要配合做防抖处理。用户快速点击时按钮在请求返回前禁用避免发出重复请求。前后端同时做约束才能保证数据最终一致。3.4 文章搜索与标签聚合搜索功能我一开始想用Elasticsearch后来还是放弃了。毕设时期引入ES太重而且在评分上也不会比MySQL基础的LIKE查询高出太多分。我最终采用的是MySQL的LIKE模糊匹配配合title和summary两个字段SELECT * FROM article WHERE status 1 AND (title LIKE CONCAT(%, #{keyword}, %) OR summary LIKE CONCAT(%, #{keyword}, %)) ORDER BY create_time DESC如果你的项目里有搜索高亮的需求可以在MySQL里先查出来再在后端用Java做高亮处理。代码很简单把匹配到的关键词用span stylecolor: red包起来返回给前端即可。标签聚合页的逻辑稍微复杂一点点击标签后要根据标签ID反查所有关联的文章ID然后再根据文章ID查文章详情。如果不做处理两条SQL分别查询先查article_tag表拿到文章ID列表再用IN查询文章数据。数据量小的时候这个方法够用。4. 开发过程中的坑与排查实录4.1 前后端联调时的跨域问题这个问题几乎一定会遇到。前后端分离部署的情况下前端跑在8080端口后端跑在9090端口浏览器会拦截跨域请求控制台报错“Access-Control-Allow-Origin”。我的排查思路先确认请求有没有真的发到后端如果发了但没回来那基本就是跨域。解决方案是写一个CorsFilter并注册到过滤器链里Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意如果用了Spring SecurityCORS过滤器的注册位置必须在Spring Security的过滤器链之前否则请求在进入控制器前就被Security拦截了跨域配置不会生效。这个问题排查起来比较隐蔽建议直接看SecurityConfig里http.cors()是否开启。4.2 文件上传与静态资源映射文章封面和用户头像都需要文件上传功能。我的方案是把图片存放到本地的一个upload目录然后用Nginx做静态资源代理前端访问时通过/images/**路径访问图片。后端Controller的核心代码如下PostMapping(/api/upload) public Result upload(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; File dest new File(uploadDir fileName); file.transferTo(dest); return Result.success(/images/ fileName); }这里有两个很重要的细节文件名一定要用UUID重命名严禁直接用用户上传的原始文件名。否则别人上传一个../../a.jpg再加路径拼接处理不当就可能产生目录穿越漏洞导致任意文件写入。要限制上传文件的类型和大小application.yml里配置spring.servlet.multipart.max-file-size: 5MB类型校验用file.getContentType()判断不是image/开头的直接拒绝。4.3 浏览量统计的并发问题最初我是在文章详情接口里直接执行UPDATE article SET view_count view_count 1逻辑没问题但高并发下一篇文章被刷几次行锁竞争就会比较激烈。后来我把浏览量先存到Redis里key为article:view:{articleId}用户访问接口的时候执行INCR操作浏览量直接查Redis然后每隔一段时间把Redis的值同步回数据库。另一种更简单的做法是前端每隔一段时间批量上报浏览记录后端按时间段合并更新。但毕设阶段Redis方案已经足够有说服力答辩时也能顺势讲出热点数据缓存的思路。如果项目部署在内网或者根本没有Redis环境那就老老实实用数据库自增配合uid限流比如同一个IP一分钟内对同一篇文章只算一次浏览。这也能体现你对反作弊的考虑。4.4 MyBatis-Plus自动填充时间字段MyBatis-Plus提供了一个MetaObjectHandler接口可以帮我们自动填充create_time、update_time这类公共字段省得每次insert和update都手动set。但很多第一次用的人会在配置自动填充时踩坑实体类字段上忘了加TableField(fill FieldFill.INSERT)导致填充不生效。正确姿势是先实现MetaObjectHandler接口Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }然后在实体类的时间字段上加注解TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;排查这类问题时先看控制台有没有报错没报错就看SQL里有没有带上这两个字段。如果有注解但SQL里没有值多半是实体类字段名和数据库列名映射不上。MyBatis-Plus默认开启驼峰转下划线如果数据库列名是create_time实体字段是createTime配置map-underscore-to-camel-case: true就能自动映射。4.5 常见问题速查表现象可能原因快速解决启动报端口被占用上一个项目没有停止内嵌Tomcat默认8080改端口或杀掉占用进程中文乱码数据库字符集不是utf8mb4建库时指定CHARSETutf8mb4Redis连接失败Redis未启动、密码没配置、地址写错先在本机执行redis-cli ping验证JWT过期后接口报401前端没有捕获异常并跳转登录页前端统一在axios拦截器里处理401状态码图片上传成功但访问404静态资源映射路径没配置在WebMvcConfigurer里addResourceHandlers映射本地磁盘目录接口返回null字段实体类没有序列化确认Getter/Setter存在或加JsonProperty注解5. 从毕设到面试的复盘与扩展5.1 答辩之前把这些点吃透很多同学做完项目却不知道怎么写文档、怎么讲PPT。我的经验是答辩PPT里面不要堆代码而是用一张系统架构图加三张核心表结构图把“用户进来发生什么、管理员进来发生什么、数据在哪里流转”讲清楚。老师看重的不是你的代码量而是你是否理解自己的系统。答辩时高频问题我整理过大致包括登录是怎么做认证和授权的JWT过期了怎么处理文章标签的多对多关系是怎么设计的为什么要用中间表点赞量的数据一致性怎么保证如果用户量变大你会怎么优化这个系统数据库索引为什么这样建哪里用到了索引优化不管最终做出什么样把这些问题的答案准备扎实答辩基本就没问题了。顺便说一句面试的时候这些问题也同样高频提前准备就是提前刷经验值。5.2 几个值得做的功能扩展方向如果做完核心功能后还有时间我建议按优先级挑一到两个方向做深化关注与Feed流用户关注其他创作者后在首页看到关注对象的文章流。这需要一张关注关系表然后查询时用子查询过滤文章列表。文章定时发布通过SpringBoot的Scheduled定时任务每分钟扫描一次待发布文章表到时间就把状态从草稿改成发布。评论通知用户收到新评论后站内信提醒本质是写一张通知表等用户登录后轮询未读数量。接入WebSocket在线聊天、实时滚动展示评论这个扩展技术含量高在很多答辩里属于加分项。扩展功能不要贪多一个就好。把扩展功能的技术难点讲透比罗列十个功能但每个都是增删改查更有说服力。5.3 个人实战中的体会写这个项目的时候我最大的感受是代码量并不是最难的部分最难的是“想清楚再动手”。前期数据库设计多花了两天后期写业务代码反而非常顺畅几乎没返工。反观那些上来就写代码的同学后面基本都在改表结构、改接口、改前端字段痛苦程度完全不同。另一个感想是SpringBoot这个框架真的很适合用来建立完整的项目认知。它把Spring繁琐的配置收敛掉了但又不至于像一些低代码平台那样把底层全部隐藏起来。你仍然需要理解依赖注入、事务管理、拦截器、过滤器、ORM映射这些核心概念而这些东西弄懂了以后不管换什么框架底层逻辑都是通的。如果这篇文章能帮你在毕设路上少走一点弯路我就觉得很有价值了。最后再分享一个小技巧做项目的时候一定记得把开发文档、数据库设计文档、接口文档三个文件同步维护好不要等到最后一天再补。答辩的时候一份像模像样的技术文档往往比代码本身更能撑起整个项目的专业度。