从零搭建个人博客系统: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,有问题欢迎在评论区交流!

相关推荐
To_OC3 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC3 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic3 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy3 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志4 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble5 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长5 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯6 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
戮漠summer6 小时前
Missing Semester 计算机教育中缺失的一课 Lecture 01 Shell
开发语言·后端·scala
名字还没想好☜6 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js