从零搭建个人博客系统:Spring Boot 多模块实践详解

题外话:最近沉寂了好久,随着ai的兴起,感觉随时都面临淘汰出局,于是花了2月学习了下java,写了个项目练习,目前算是完成了所有前后端的开发,本篇主要分享java部分,有兴趣的初学者可以拿去参考。预览和远远都留在了最后
本文将完整介绍一个基于 Spring Boot 2.7 + MyBatis + Redis + JWT 的多模块个人博客后端系统设计与实现,适合想学习模块化项目架构的同学。


一、项目简介

这是一个前后端分离的个人博客系统后端服务,采用 Maven 多模块架构组织代码,后端提供 RESTful API 供前台用户端和后台管理端分别调用。

核心技术栈:

技术 版本 说明
Spring Boot 2.7.14 基础框架
MyBatis 2.3.1 ORM 持久层
MySQL 8.0+ 主数据库
Druid 1.2.18 数据库连接池
Redis - 缓存 + Token 存储
JWT (jjwt) 0.9.1 无状态身份认证
Swagger3 3.0.0 接口文档
Lombok 1.18.28 简化 Java 代码

二、模块结构设计

bash 复制代码
blog-service/
├── blog-common        # 公共模块:工具类、JWT、Redis、统一返回、全局异常
├── blog-user          # 用户模块:注册/登录、评论、点赞、收藏
├── blog-article       # 文章模块:文章 CRUD、阅读数统计
├── blog-admin-user    # 管理员模块:后台登录、轮播图、分类管理
├── blog-api           # 网关入口模块:路由聚合、跨域、权限控制
└── pom.xml            # 父 POM,统一依赖版本管理

各模块职责清晰,blog-common 被其他所有模块依赖,避免重复代码;blog-api 作为统一入口模块对外暴露接口,内部通过 WebClient 调用其他服务。


三、数据库设计

系统共设计 7 张核心数据表 ,全部使用 utf8mb4 字符集以支持 emoji 等特殊字符。

3.1 表结构总览

bash 复制代码
blog 数据库
├── app_user          # 前台用户表
├── admin_user        # 后台管理员表
├── article           # 文章表
├── category          # 文章分类表
├── banner            # 首页轮播图表
├── comment           # 文章评论表(支持层级回复)
├── article_like      # 文章点赞表
└── article_favorite  # 文章收藏表

3.2 核心表字段设计

文章表 article(blog-article 模块)

sql 复制代码
CREATE TABLE `article` (
  `id`             INT          NOT NULL AUTO_INCREMENT COMMENT '文章ID',
  `title`          VARCHAR(200) NOT NULL COMMENT '文章标题',
  `content`        LONGTEXT     DEFAULT NULL COMMENT '文章正文(支持 Markdown)',
  `category_id`    VARCHAR(100) DEFAULT NULL COMMENT '分类ID,多个用逗号分隔',
  `article_cover`  VARCHAR(500) DEFAULT NULL COMMENT '文章封面图URL',
  `read_counts`    INT          NOT NULL DEFAULT 0 COMMENT '阅读数',
  `comment_counts` INT          NOT NULL DEFAULT 0 COMMENT '评论数',
  `like_count`     INT          NOT NULL DEFAULT 0 COMMENT '点赞数',
  `favorite_count` INT          NOT NULL DEFAULT 0 COMMENT '收藏数',
  `is_hot`         TINYINT      NOT NULL DEFAULT 0 COMMENT '是否热门',
  `is_top`         TINYINT      NOT NULL DEFAULT 0 COMMENT '是否置顶',
  `is_delete`      TINYINT      NOT NULL DEFAULT 0 COMMENT '软删除标记',
  `create_time`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计亮点:category_id 以逗号分隔存储多个分类 ID,Mapper 中使用 CONCAT(',', category_id, ',') LIKE CONCAT('%,', #{categoryId}, ',%') 实现精确匹配查询,避免误匹配。

评论表 comment(blog-user 模块)

评论表通过 parent_id 字段实现无限层级回复reply_to_user_id 标记被回复人,前端可据此构建评论树。

sql 复制代码
CREATE TABLE `comment` (
  `id`               BIGINT   NOT NULL AUTO_INCREMENT,
  `article_id`       BIGINT   NOT NULL COMMENT '文章ID',
  `user_id`          BIGINT   NOT NULL COMMENT '评论者ID',
  `parent_id`        BIGINT   DEFAULT NULL COMMENT 'NULL=根评论',
  `content`          TEXT     NOT NULL,
  `reply_to_user_id` BIGINT   DEFAULT NULL COMMENT '被回复用户ID',
  `create_time`      DATETIME NOT NULL,
  `is_delete`        TINYINT  NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_article_id` (`article_id`),
  KEY `idx_parent_id`  (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

四、JWT + Redis 双重 Token 认证

这是整个系统安全性的核心设计,采用 JWT 签发 + Redis 存储的方案,兼顾无状态与可主动失效两大需求。

4.1 认证流程

ini 复制代码
用户登录 → 生成 JWT Token → 写入 Redis(key=token,value=userId,TTL=7天)
                          ↓
后续请求 → JwtInterceptor 拦截 → 先查 Redis(token 是否存在)→ 再验 JWT 签名 → 通过
                                                          ↓
                                                    主动注销时 → deleteToken(Redis)

4.2 JwtUtils 核心实现

java 复制代码
// 生成 Token,同时写入 Redis
public static String generateToken(Long userId, String mobile, String username) {
    Map<String, Object> claims = new HashMap<>();
    claims.put("userId", userId);
    claims.put("username", username);

    String token = Jwts.builder()
            .setClaims(claims)
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
            .signWith(SignatureAlgorithm.HS512, SECRET)
            .compact();

    // 写入 Redis,TTL 与 JWT 过期时间一致
    staticRedisUtils.setToken(token, userId, EXPIRATION / 1000);
    return token;
}

// 验证 Token:先查 Redis 再验签名
public static boolean validateToken(String token) {
    try {
        if (!staticRedisUtils.hasToken(token)) {
            return false; // Redis 中不存在,视为已失效
        }
        parseToken(token);
        return !isTokenExpired(token);
    } catch (Exception e) {
        return false;
    }
}

4.3 JwtInterceptor 拦截器

java 复制代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 放行 OPTIONS 预检请求
    if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
        response.setStatus(HttpServletResponse.SC_OK);
        return true;
    }

    String token = request.getHeader(Constants.TOKEN_HEADER);
    if (!StringUtils.hasText(token)) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("{\"code\":401,\"message\":\"未登录或token已过期\"}");
        return false;
    }

    // 去除 Bearer 前缀
    if (token.startsWith(Constants.TOKEN_PREFIX)) {
        token = token.substring(Constants.TOKEN_PREFIX.length());
    }

    if (!JwtUtils.validateToken(token)) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("{\"code\":401,\"message\":\"token无效或已过期\"}");
        return false;
    }

    // 将用户信息注入 request attribute,供 Controller 使用
    request.setAttribute(Constants.USER_ID, JwtUtils.getUserId(token));
    request.setAttribute(Constants.USERNAME, JwtUtils.getUsername(token));
    return true;
}

五、图形验证码实现

登录时需要通过图形验证码防止暴力破解,使用 Java AWT 绘制并以 Base64 返回给前端展示。

核心思路

  1. 随机生成 4 位字母数字组合(排除易混淆字符 0/O/1/I)
  2. 绘制随机颜色、随机旋转角度的字符
  3. 添加干扰线(20 条)和干扰椭圆(20 个)
  4. 将验证码文本存入 Redis,有效期 60 秒
  5. 返回 Base64 编码的图片给前端
java 复制代码
public String generateCaptchaCode() {
    String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 去除易混淆字符
    StringBuilder code = new StringBuilder();
    Random random = new Random();
    for (int i = 0; i < CODE_LENGTH; i++) {
        code.append(chars.charAt(random.nextInt(chars.length())));
    }
    return code.toString();
}

public void saveVerifyCode(String key, String code) {
    redisUtils.setString("verify_code:" + key, code, 60L); // 60秒有效期
}

六、服务间通信:WebClient 替代 RestTemplate

blog-user 模块在处理评论、点赞、收藏时需要同步更新 blog-article 模块中的计数字段,使用 Spring WebFlux 的 WebClient 进行服务间 HTTP 调用。

java 复制代码
@Service
public class ArticleServiceClient {

    private final WebClient webClient;

    public ArticleServiceClient() {
        this.webClient = WebClient.builder()
                .baseUrl("http://localhost:8082") // blog-article 服务地址
                .build();
    }

    // 发表评论后,通知 article 服务 +1 评论数
    public void incrementCommentCount(Long articleId) {
        try {
            String result = webClient.post()
                    .uri("/article/comment/increment/{articleId}", articleId)
                    .accept(MediaType.APPLICATION_JSON)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();
            log.info("增加文章评论数结果: {}", result);
        } catch (Exception e) {
            log.error("调用 article 服务增加评论数失败", e);
        }
    }
}

为什么用 WebClient 而不是 RestTemplate? RestTemplate 在 Spring 5 之后已进入维护模式,WebClient 是官方推荐的替代方案,支持同步/异步两种调用模式,此处使用 .block() 以同步方式调用保证数据一致性。


七、阅读数统计:Redis 防刷设计

文章阅读数统计需要防止同一用户短时间内重复刷取,设计如下:

  • ip:articleId 作为 Redis key
  • 每次访问先检查 key 是否存在
  • 不存在则计数 +1,并写入 Redis,TTL = 24 小时
  • 存在则直接返回,不更新计数

这种方案以 IP + 文章 ID 维度去重,实现简单、性能高,适合中小规模博客系统。


八、统一响应格式

所有接口统一返回如下 JSON 结构,前端处理更便捷:

json 复制代码
{
  "code": 200,
  "message": "success",
  "data": { ... }
}
java 复制代码
@Data
public class Result<T> {
    private Integer code;
    private String message;
    private T data;

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        return result;
    }

    public static <T> Result<T> error(String message) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMessage(message);
        return result;
    }
}

九、项目快速部署

9.1 环境要求

  • JDK 1.8+
  • MySQL 8.0+
  • Redis 6.0+
  • Maven 3.6+

9.2 初始化数据库

执行 blog-user/src/main/resources/db/schema.sql 即可完成所有表的创建和初始数据写入。

bash 复制代码
mysql -u root -p < schema.sql

9.3 修改配置文件

各模块 application.yml 中填写实际的数据库地址、账号密码及 Redis 连接信息:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://your-host:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: your-username
    password: your-password
  redis:
    host: your-redis-host
    port: 6379
    password: your-redis-password

9.4 启动顺序

由于 blog-user 调用 blog-article 的接口,建议按如下顺序启动:

less 复制代码
1. blog-article  (port: 8082)
2. blog-admin-user (port: 8081)
3. blog-user / blog-api (port: 8083)

十、接口文档

各模块均集成 Swagger3,启动后访问以下地址查看接口文档:

模块 地址
blog-admin-user http://localhost:8081/swagger-ui/index.html
blog-article http://localhost:8082/swagger-ui/index.html
blog-api http://localhost:8083/swagger-ui/index.html

十一、后续规划

  • 引入 Spring Cloud Gateway 替代手动路由
  • 接入 OSS 对象存储替换本地文件上传
  • 引入消息队列解耦服务间通信(评论数、点赞数更新)
  • 添加全文搜索支持(Elasticsearch)
  • 前端项目开源(Vue3 + Vite)

结语

本项目完整地演示了一个 Spring Boot 多模块项目从数据库设计、模块拆分、认证方案到服务间通信的完整实践路径。代码结构清晰,注释完善,适合作为学习参考或个人博客系统的起步模板。

预览

预览地址:www.messln.cn/

源代码地址

gitee

欢迎 Star 和 Fork,有问题欢迎在评论区交流!

相关推荐
用户9003486133463 小时前
GO语言基础:反射
后端
陆枫Larry3 小时前
图片预览前先 filter 掉空地址:一个容易忽略的细节
前端
进击的尘埃3 小时前
基于 Claude Streaming API 的多轮对话组件设计:状态机与流式渲染那些事
javascript
用户1474853079743 小时前
Git-stash产生的冲突
后端
UrbanJazzerati3 小时前
Python Scrapling反爬虫小技巧之Referer
后端·面试
我叫蒙奇3 小时前
rem 适配全过程
前端
陆枫Larry3 小时前
小程序中按固定宽高比展示图片并去除黑边的实现思路
前端
HelloReader3 小时前
Tauri 2.1 新特性自定义 HTTP Headers 配置详解
前端
程序员爱钓鱼3 小时前
Go语言WebP图像处理实战:golang.org/x/image/webp
后端·google·go