作为一个前后端都写过不少项目的老程序员,我这两年接到的博客系统相关的咨询一直没有断过。很多人问的第一句通常是:“现在都2025年了,还有必要自己写博客系统吗?用WordPress或Hexo不香吗?” 我的答案往往是:如果你想深入理解一个Web应用从零到部署的全过程,想掌握Spring Boot在实际业务中的套路,那么自己动手做一个博客系统,依然是性价比最高的练手项目,没有之一。
这篇博客不是给你贴一段完整源码就完事,而是把我在开发“基于Spring Boot的个人博客系统”过程中,从技术选型、数据库设计、核心模块实现,到上线部署踩过的坑,整个决策链路都摊开来讲。项目配套了完整源码、SQL脚本和开发文档,你可以直接拿去用,也可以照着思路自己重写一版。无论你是刚学完Spring Boot基础、需要项目经历加持的应届生,还是想给团队搭建一个轻量内容平台的Java工程师,这篇文章都值得你花十分钟读完。
1. 为什么我最终选了Spring Boot,而不是别的方案
先聊点题外话。动笔之前,我其实纠结了很久:到底是用热门的Vue3 + Spring Boot前后端分离架构,还是用Spring Boot + Thymeleaf服务端渲染?
身边不少朋友都劝我上前后端分离,理由是“现在主流”“简历好看”。但我最后选了Spring Boot + Thymeleaf的组合。原因有三点,每一条都是实际考量:
- SEO对个人博客至关重要。博客内容需要被搜索引擎收录,服务端渲染返回完整HTML,爬虫直接就能抓到正文。而前后端分离架构下,搜索引擎虽然现在能抓取SPA页面,但效果依然不如直出HTML稳定。
- 开发效率高,维护成本低。一个人做全栈项目,最怕的是前后端接口联调时来回扯皮。服务端渲染模式下,后端一次性把页面渲染好,前端的工作量大幅减少,几乎不需要单独维护一套API接口文档。
- 架构足够轻。博客的核心是内容发布和展示,没有复杂交互。为了一个博客硬上Redis缓存、消息队列、微服务拆分,属于典型的过度设计。简单可靠的单体应用,才是个人项目的最优解。
技术栈定下来之后是这样的:
| 技术组件 | 选型版本 | 用途说明 |
|---|---|---|
| Spring Boot | 2.7.14 | 项目主框架,注意没有用3.x,下面细说 |
| Thymeleaf | 3.0.15 | 服务端模板引擎,渲染页面 |
| MyBatis-Plus | 3.5.2 | ORM框架,简化数据库操作 |
| MySQL | 8.0 | 主数据库,存储业务数据 |
| Druid | 1.2.20 | 数据库连接池,附带监控功能 |
| Lombok | 1.18.24 | 消除样板代码 |
| validation | 2.x | 后端参数校验 |
| springdoc-openapi | 2.1.0 | 接口文档生成,对标Swagger |
关于Spring Boot版本,这里单独提醒一句。Spring Boot 3.x已经把javax包替换为jakarta,Maven坐标也改了,一些老插件需要额外适配。如果你只是做学习项目,建议直接上2.7.x,资料多、坑少。如果目标是新项目落地或者找工作,可以冲3.x,但要有心理准备,遇到问题搜到的答案可能因为包名差异跑不通。
2. 数据库表设计:博客系统的地基,六张表就够了
很多新手会犯一个典型错误:一上来就在user表里塞一堆字段,觉得反正表是私有的,随便加。这样做后期会带来一连串麻烦。我的做法是先画出博客系统的数据关系,再落库。
个人博客系统的核心实体其实就六个:文章、分类、标签、评论、用户(后台管理员)、网站配置。对应到数据库里,我设计了六张核心表:
2.1 文章表和用户表
文章表article是核心中的核心,字段不能敷衍。我最终的建表语句经过了好几轮调整,最终定稿的关键字段如下:
CREATE TABLE `article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '文章ID', `title` varchar(100) NOT NULL COMMENT '文章标题', `summary` varchar(255) DEFAULT NULL COMMENT '文章摘要,列表页展示用', `content` longtext NOT NULL COMMENT '文章正文,Markdown格式', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图URL', `category_id` bigint(20) DEFAULT NULL COMMENT '所属分类ID', `author_id` bigint(20) NOT NULL COMMENT '作者ID,关联user表', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0草稿,1已发布,2回收站', `view_count` int(11) NOT NULL DEFAULT '0' COMMENT '浏览量', `like_count` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数', `comment_count` int(11) NOT NULL DEFAULT '0' COMMENT '评论数,冗余字段避免联表查询', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`), KEY `idx_status_create_time` (`status`, `create_time`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='文章表';这里有两个设计细节值得展开:一个是comment_count这个冗余字段,一开始我没有加,后来发现列表页要展示每篇文章的评论数量,如果不冗余,就得每条文章跑一次COUNT查询,当文章数量上来后,N+1问题非常明显。加了冗余字段,更新评论数时多写一次就好,对博客这种低频写入场景完全值得。
另一个是索引设计。idx_status_create_time这个联合索引,是专门为列表页“展示已发布文章并按发布时间倒序”这个高频查询设计的。MySQL的索引最左前缀原则,意味着单独按status筛选也能走这个索引,但按status + create_time排序时可以避免回表后的额外排序,实测在十万级数据量下性能差异非常明显。
用户表user就没那么复杂了:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码,BCrypt加密', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `role` tinyint(4) NOT NULL DEFAULT '0' COMMENT '角色:0普通用户,1管理员', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';用户名加唯一索引,接口层再做一次唯一性校验,双重保障。加密方式没有用MD5,直接上BCrypt,虽然加盐逻辑稍复杂,但抗彩虹表攻击的能力强得多。
2.2 分类、标签和多对多关系
分类和标签这两个概念经常被混淆。我的理解是:分类是树形结构的,一般只有一层或两层,强调内容的归属,比如“Java”“数据库”“随笔”;标签是扁平化的,强调内容的交叉描述,比如“Spring Boot”“优化”“踩坑”。一篇文章属于一个分类,但可以有多个标签。
为了支持文章和标签的多对多关系,我建了关联表article_tag:
CREATE TABLE `article_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `article_id` bigint(20) NOT NULL, `tag_id` bigint(20) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_article_tag` (`article_id`,`tag_id`), KEY `idx_tag_id` (`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章-标签关联表';唯一键uk_article_tag的作用是防止同一篇文章重复添加同一个标签。我最初设计时没有加这个唯一约束,结果测试环境下用脚本批量导入数据时,出现了大量重复关联记录,标签统计页面的数据直接没法看。教训就是:任何一张关联表,复合唯一约束都是标配。
分类表category本身很简单:
CREATE TABLE `category` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名称', `slug` varchar(50) DEFAULT NULL COMMENT 'URL访问别名', `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序权重,越大越靠前', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表';2.3 评论表和网站配置表
评论表comment用自关联来支持楼中楼回复:
CREATE TABLE `comment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `article_id` bigint(20) NOT NULL COMMENT '文章ID', `user_id` bigint(20) DEFAULT NULL COMMENT '评论用户ID,未登录则为NULL', `nickname` varchar(50) DEFAULT NULL COMMENT '游客昵称', `email` varchar(100) DEFAULT NULL COMMENT '游客邮箱', `content` varchar(500) NOT NULL COMMENT '评论内容', `parent_id` bigint(20) DEFAULT NULL COMMENT '父评论ID,NULL为根评论', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_article_id` (`article_id`), KEY `idx_parent_id` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';这个表设计最需要注意的一点是:不要用递归查询去取所有层级的回复树,那在数据量稍大时性能会很差。我采用的是“平铺展示 + 前端缩进”的方案,查询某个文章的全部评论时,一次SELECT把该文章所有评论拉出来,在Java层用parent_id组装成树形结构返回给页面。对个人博客一天几条评论的量级,这个方案简单可靠。
网站配置表site_config是后期加上的,存博客标题、副标题、页脚信息、ICP备案号等。用key-value结构:
CREATE TABLE `site_config` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `config_key` varchar(50) NOT NULL COMMENT '配置键', `config_value` varchar(500) DEFAULT NULL COMMENT '配置值', `remark` varchar(100) DEFAULT NULL COMMENT '备注说明', PRIMARY KEY (`id`), UNIQUE KEY `uk_config_key` (`config_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='网站配置表';业务代码里写一个工具类,启动时把全表加载到内存Map里,之后读取配置零数据库开销。
2.4 数据库字符集与导入导出
字符集我统一用了utf8mb4。utf8mb4是utf8的超集,能存emoji表情和生僻字。如果用了utf8,用户在评论里发一个表情符号,程序会直接报错。这个问题在真实环境里出现过一次,所以建库时务必确认:
CREATE DATABASE `blog_system` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;项目配套的SQL文件里,我加了不少测试数据,包括十几篇示例文章、几个分类和标签。有测试数据的好处是,项目启动后页面不至于一片空白,你可以直接看到效果,也方便调试分页、标签聚合等功能。
3. 核心功能模块拆解:内容管理这套逻辑,一次吃透
技术栈和表结构定好之后,就到了写代码的阶段。项目整体分层是标准的Controller-Service-Mapper三层架构,包结构如下:
com.blog ├── controller # 控制器,接收请求、参数校验、返回视图或数据 ├── service # 业务逻辑层,处理核心业务 │ └── impl ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象,接收前端参数 ├── vo # 视图对象,返回页面渲染数据 ├── config # 配置类,如WebMvc、拦截器、加密组件 ├── common # 通用工具类、异常处理、统一返回结果 └── BlogApplication.java # 启动类3.1 文章发布的完整链路
发布文章是博客的基础功能,接口设计看似简单,实际涉及不少细节。前端表单提交的字段包括标题、摘要、正文、分类ID、标签ID列表、封面图URL、状态(草稿/发布)。后端接收后做什么?
@PostMapping("/admin/article/save") public R save(@RequestBody @Valid ArticleDTO dto) { return articleService.saveOrUpdateArticle(dto); }Controller层很薄,真正的逻辑在Service里。saveOrUpdateArticle方法里我做了这样几件事:
- 参数校验。标题必填且不超过100字符,正文必填,分类ID必须存在。
- 保存或更新文章主表信息。
- 重建文章和标签的关联关系。先删除原有的
article_tag记录,再插入新的。这个操作虽然多了一次删除和插入,但逻辑清晰,避免复杂的增量比对。毕竟一篇文章的标签通常不会多到影响性能。 - 维护评论计数、标签计数等冗余数据。新增标签时,标签表的引用计数加一。
用MyBatis-Plus写这块业务时,最方便的是它的LambdaUpdateWrapper和saveOrUpdate方法。比如更新浏览量:
articleMapper.update(null, new LambdaUpdateWrapper<Article>() .eq(Article::getId, id) .setSql("view_count = view_count + 1"));这样写是数据库层面的原子自增,比“先查出来、+1、再写回去”的三步操作安全得多,不会有并发问题。
3.2 列表页分页和分类归档
博客首页的文章列表、分类页、标签页,本质上都是条件查询加分页。MyBatis-Plus提供了一个分页插件PaginationInnerInterceptor,配置如下:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }配置好之后,分页查询一行代码搞定:
Page<ArticleVO> page = articleMapper.selectPage(new Page<>(current, size), new LambdaQueryWrapper<Article>() .eq(Article::getStatus, 1) .eq(categoryId != null, Article::getCategoryId, categoryId) .eq(tagId != null, Article::getTagId, tagId) .orderByDesc(Article::getCreateTime));这里有两个小坑必须提一下。第一个是分类页和标签页的查询,如果直接查article表,就会发现tag_id这个字段根本不存在,因为标签关系在关联表里。这时候我的做法是:先根据tag_id从article_tag表查出符合条件的article_id集合,再用IN语句去查文章表。对博客系统这个量级,一条IN查询的性能完全够用。另一个坑是文章正文是longtext,列表页查询如果不加限制会把正文全部查出来,浪费网络IO和内存。我的处理是查列表时用select指定只查需要的列,不要正文。
ArticleVO selectArticleSummary(@Param("id") Long id);手写Mapper XML时,明确列出需要的字段:
<select id="selectArticleSummary" resultType="com.blog.vo.ArticleVO"> SELECT a.id, a.title, a.summary, a.cover_image, a.view_count, a.like_count, a.comment_count, a.create_time, c.name AS category_name FROM article a LEFT JOIN category c ON a.category_id = c.id WHERE a.id = #{id} </select>3.3 Markdown编辑与渲染
Markdown是博客系统的标配,纯文本的textarea早就被淘汰了。编辑器我选了Markdown Editor和highlight.js的组合。存储时直接把Markdown原文存库,展示时在服务端渲染成HTML,然后返回给Thymeleaf模板。
这一步最要注意的就是安全。直接把用户提交的Markdown转成HTML然后原样输出,会有XSS攻击风险。比如用户在正文里写一段<script>标签,渲染时会被浏览器执行,产生安全问题。我的方案是在渲染完成后,用Jsoup对HTML做白名单过滤:
public String mdToHtml(String markdown) { Markdown4jProcessor processor = new Markdown4jProcessor(); String html = processor.process(markdown); return Jsoup.clean(html, Safelist.relaxed()); }Safelist.relaxed()允许常见的标题、列表、链接、图片等标签,但会过滤掉script、iframe、object等危险标签。链接属性中的javascript:协议也会被清理。这样既保留了Markdown的排版能力,又堵住了XSS的入口。
3.4 评论模块与拦截器
评论模块我设计成支持游客评论和管理员回复。游客评论时无需注册,填写昵称和邮箱即可,但后端要加一层防刷机制。我的做法是使用Session存储最近一次评论时间,两次评论间隔不能小于30秒,一个人一天对同一篇文章的评论数限制在10条以内。用AOP在Service层做拦截,不影响Controller的代码简洁度。
管理员回复用户评论时,需要先确认当前登录用户是管理员。登录拦截器用Spring Boot的HandlerInterceptor实现:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute(SessionConstants.USER_KEY); if (user == null) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}"); return false; } return true; } }然后在WebMvcConfig中注册拦截器,只拦截/admin/**路径,放行首页、文章详情、评论等公开接口。
3.5 接口文档:springdoc-openapi的接入
项目配套的文档里,我接入了springdoc-openapi,它能在项目运行期间自动生成符合OpenAPI 3.0规范的接口文档,并且提供一个可视化的Swagger UI页面。这个对团队协作和后期维护很有用,别人接你的后端接口时,不用瞎猜参数格式。
不过要提醒的是,个人博客这种项目,接口文档不需要追求全自动化。更务实的做法是把核心接口(文章保存、评论提交、用户登录)的入参、出参、异常码写清楚,其余的交给代码注释。文档是给活人看的,清晰比完整重要。
4. 实战中那些坑,我替你先踩了一遍
这部分是最想分享的。理论写起来容易,真正落地时遇到的问题往往让人抓狂。我按照时间顺序记录了几个印象最深刻的坑。
4.1 MyBatis-Plus分页插件不生效
第一次写完分页代码,怎么测都发现分页不生效——查询结果永远是全量数据,没有LIMIT语句。检查了半天,发现是分页插件的@Configuration配置类没被Spring Boot扫描到。我的配置类放在com.blog.config包下,而启动类BlogApplication默认扫描的是com.blog包,如果启动类放在com.blog下,扫描范围没问题。但当时我把配置类放到了com.blog.system.config这个子包下,不在默认扫描范围内,导致MybatisPlusInterceptor这个Bean根本没有注入容器。解决办法是在启动类上加@ComponentScan("com.blog")强制指定扫描路径,或者把配置类放到启动类的子包下。
大家自己搭项目时,如果遇到某个配置不生效,第一反应先确认配置类是否被Spring扫描到了,这是排查Spring Boot问题的最快路径。
4.2 Thymeleaf页面引用静态资源404
Thymeleaf模板引擎会解析th:href、th:src等属性,但静态资源(JS、CSS、图片)分两种:一种放在resources/static目录下,一种放在resources/templates下。后者是由Thymeleaf解析的模板文件,不能直接作为静态资源访问。
我踩的坑是:把一些CSS文件放进templates/css/目录,引用时写th:href="@{/css/style.css}",结果页面加载了N次都是404。原因在于Spring Boot默认只把classpath:/static/、classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/这4个目录映射为静态资源所在目录。templates目录本身被Thymeleaf占用,不参与静态资源映射。把所有CSS和JS统一放到src/main/resources/static/下,问题迎刃而解。
4.3 文章详情页的“上一篇/下一篇”
这个功能看起来简单,但第一次实现时我写的是“查所有文章,在内存里找到当前文章的前后两条”。这种写法在数据量小的时候没问题,但一旦文章超过几百篇,每次详情页请求都会把全部文章ID和标题捞出来,显然不合理。正确的SQL应该是通过create_time和id做条件查询:
-- 上一篇 SELECT id, title FROM article WHERE status = 1 AND (create_time < #{createTime} OR (create_time = #{createTime} AND id < #{id})) ORDER BY create_time DESC, id DESC LIMIT 1; -- 下一篇 SELECT id, title FROM article WHERE status = 1 AND (create_time > #{createTime} OR (create_time = #{createTime} AND id > #{id})) ORDER BY create_time ASC, id ASC LIMIT 1;注意到create_time相同的场景了吗?如果两篇文章在同一秒发布,只按create_time排序会有歧义,所以必须加上id作为第二排序条件。
4.4 数据库连接耗尽问题
项目本地跑得好好的,上线后运行两天突然报Communications link failure,查看日志发现是Too many connections。分析了一圈,问题出在Druid连接池的maxActive配置。默认情况下maxActive=8,而我又没有配置minIdle和validationQuery,导致数据库在高并发场景下连接池被占满,新请求拿不到连接。
修复方案是调整连接池参数:
spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 validation-query: SELECT 1 test-while-idle: truemaxWait=60000表示请求连接的最大等待时间为60秒,避免无限等待。test-while-idle定期检查空闲连接的有效性,防止数据库关闭空闲连接后应用还在使用坏连接。
这些都调好之后,上线一个多月再没出过连接相关的问题。
4.5 图片上传与访问路径
博客文章免不了要插图。项目里我实现了简单的本地存储上传:上传的图片保存到服务器/data/blog/images/目录,访问时通过Spring Boot的静态资源映射规则暴露出来:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/images/**") .addResourceHandler("file:" + imageUploadPath); } }这里有个细节要注意:imageUploadPath必须是绝对路径,并且以/结尾。如果配置成相对路径,项目启动目录一变,图片就全挂了。生产环境建议把图片目录放到磁盘独立分区,不要把图片放到resources/static下,因为每次重新部署都可能被覆盖。
5. 构建、打包、部署:从本地到云服务器
5.1 打包方式选择
Spring Boot官方支持两种打包方式:jar和war。
jar包方式:Spring Boot内嵌Tomcat,Java环境即可运行,部署最简单。war包方式:需要把war包丢到外部Tomcat的webapps目录,适合已有运维体系的传统部署场景。
个人博客我选的是jar包。
mvn clean package -Dmaven.test.skip=true打包完成后,在target目录下会生成blog-system-1.0.0.jar,这个包包含了项目所有依赖。运行只需要一条命令:
java -jar blog-system-1.0.0.jar --spring.profiles.active=prod生产环境的配置放在application-prod.yml里,里面配的是线上数据库地址、用户名密码、文件上传路径等。
5.2 用Nginx做反向代理
生产环境不能让用户直接访问应用端口(比如8080),最好在前面挡一层Nginx。一是因为HTTP默认端口是80,用户访问时不需要输入端口号;二是因为Nginx可以对静态资源做缓存,减轻应用服务器压力。
server { listen 80; server_name yourdomain.com; # 静态资源缓存,提高加载速度 location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2?)$ { expires 30d; add_header Cache-Control "public, no-transform"; access_log off; } # 动态请求转发给Spring Boot location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }proxy_set_header Host $host这句很重要。如果不加,后端拿到的请求Host是127.0.0.1:8080,如果项目里用到了绝对地址生成(比如邮件中的链接、跳转URL),用户会跳转到错误地址。
5.3 自动化部署脚本
手敲命令部署太累,我写了一个简单的部署脚本deploy.sh,放在服务器上:
#!/bin/bash # 博客系统部署脚本 APP_NAME=blog-system-1.0.0.jar APP_PATH=/opt/blog BACKUP_PATH=/opt/blog/backup echo "===== 停止旧进程 =====" PID=$(ps -ef | grep ${APP_NAME} | grep -v grep | awk '{print $2}') if [ -n "$PID" ]; then kill -9 $PID echo "已停止进程: $PID" fi echo "===== 备份旧版本 =====" if [ -f "$APP_PATH/$APP_NAME" ]; then cp $APP_PATH/$APP_NAME $BACKUP_PATH/${APP_NAME}.$(date +%Y%m%d%H%M%S) echo "备份完成" fi echo "===== 上传新版本并启动 =====" cp /tmp/blog-system.jar $APP_PATH/$APP_NAME cd $APP_PATH nohup java -jar $APP_NAME --spring.profiles.active=prod > /dev/null 2>&1 & echo "部署完成"配合Git Hook或者手动把本地target目录下的jar包上传到服务器的/tmp目录,然后执行脚本,一分钟就能完成一次上线。对于个人博客来说,这样的部署链路已经够用了。
6. 项目文档与二次开发思路
配套的开发文档,我写了大概40多页,涵盖以下内容:环境搭建步骤、数据库初始化脚本说明、项目结构说明、核心接口文档、部署方案。文档里没有废话,每部分都是按照“拿来就能跑”的标准写的。
如果你拿到这套源码,想做一些二次开发,我建议按以下优先级顺序来:
- 接入第三方登录。目前系统只支持本地用户名密码登录。用OAuth 2.0接入GitHub或Gitee登录,让访客可以免注册评论,可以显著提高互动率。
- 增加文章搜索功能。目前搜索依赖SQL的
LIKE '%keyword%',数据量大了之后效率低。可以引入Elasticsearch或使用MySQL全文索引,做一个轻量版的站内搜索。 - 接入对象存储。把本地图片存储替换成阿里云OSS或MinIO,解决服务器存储扩容问题。
- 做数据统计。基于ECharts展示文章的浏览量趋势、来源渠道、热门文章Top10等。
每个人的技术路线不同,但有一条我很确定:与其去网上找那种几十年前老掉牙的SSH博客系统Demo,不如好好研究一套基于Spring Boot、设计合理、代码整洁、文档齐全的现代版本。这个项目做完之后,你对Spring Boot的理解会有一个质的飞跃——不再只是“会写CRUD”,而是真正理解了一个Web应用从设计到上线的完整链路。
最后再分享一个小技巧。项目上线后,我给自己配了一个“一键发布”的小工具:用GitHub Actions监听仓库的main分支,代码push后自动构建、打包、上传服务器、重启服务。从此以后,我只需要在本地写内容、提交代码,剩下的都交给流水线。对个人项目来说,这大概就是“技术改变生活”的实感了。