从数据库到用户界面,这篇文章记录我在个人博客系统中每一层技术选型背后的思考。整个系统包含 4 个独立仓库,涵盖 Java 后端、两个 Next.js 前端、和 Docker 部署配置。
用户浏览器
│
▼
Caddy(反向代理 + 自动 HTTPS)
│
├── chonkybird.com → Next.js 博客前端(SSR)
├── admin.chonkybird.com → Next.js 管理后台(CSR)
├── api.chonkybird.com → Spring Boot 后端(REST API)
└── monitor.chonkybird.com → Spring Boot Admin
所有服务运行在同一台 VPS 上,通过 Docker Compose 编排,Caddy 统一管理 TLS 和流量分发。
| 技术 | 版本 | 选型原因 |
|---|---|---|
| Spring Boot | 3.5 | 生产级 Java 框架,生态完善 |
| Java | 21 LTS | 虚拟线程、Record 类型、新 Switch 语法 |
| MyBatis-Plus | 3.5 | 灵活的 ORM,兼顾复杂 SQL 和简单 CRUD |
| MySQL | 9.x | 原生支持向量索引(Vector Index),用于相关文章推荐 |
| Redis | 7.4 | 缓存、Token 黑名单、限流 |
为什么选 MyBatis-Plus 而不是 JPA/Hibernate?
JPA 的优势是对象模型映射,适合 DDD 风格的开发。但博客系统的查询并不复杂,MyBatis-Plus 的 LambdaQueryWrapper 写 SQL 条件更直观,遇到复杂查询也可以直接写 XML,灵活度更高。
项目采用**模块化单体(Modular Monolith)**而非微服务:
personal-blog-backend/
├── blog-common/ # 公共基础层:工具类、异常、基础框架
├── blog-modules/
│ ├── blog-module-system/ # 系统模块:用户/角色/认证
│ ├── blog-module-article/ # 文章模块
│ ├── blog-module-comment/ # 评论模块
│ └── blog-module-file/ # 文件模块(S3 对象存储)
└── blog-application/ # 启动入口 + 全局配置
每个业务模块内部分为两层:
*-api:契约层,只包含 DTO、VO、跨模块调用接口*-service:实现层,包含 Controller、Service、Entity、Mapper这个架构的核心价值:模块间只能通过 *-api 层交互,禁止直接跨模块 JOIN 数据库表。这个规则用 ArchUnit 在测试阶段强制检验,防止模块边界腐化成"大泥球"(Big Ball of Mud)。
Spring Security 以 SecurityFilterChain 为核心,我的配置使用了 3 条链:
// @Order(1):公开白名单(Actuator 健康检查、文章列表等)
// @Order(2):JWT 认证链(/api/** 下的接口)
// @Order(3):HTTP Basic 兜底(监控等敏感端点)
permitAll() 跳过认证JwtAuthenticationFilter,解析 Bearer Token,把 userId 存入 Authentication.details,通过 SecurityUtils 在任意地方取用@EnableMethodSecurity 开启方法级权限控制,在 Service 层用 @PreAuthorize("hasRole('ADMIN')") 做细粒度控制,不需要把权限逻辑全部塞进 Controller。
所有 Service 类继承 BaseServiceImpl<M, E, V, D, C>,5 个类型参数分别对应:
| 参数 | 含义 |
|---|---|
M | Mapper 类型 |
E | 数据库实体 |
V | 返回给前端的 VO |
D | 更新用的 DTO |
C | 创建用的 DTO |
抽象层封装了 JSR-303 参数校验、MapStruct 类型转换、安全更新(fetch-then-update)、生命周期钩子(preSave/preUpdate)。
安全更新是核心设计,先查数据库再增量覆盖,而不是直接覆盖整个实体:
// 先查出完整实体
E entity = getEntityById(id);
// 只把 DTO 中非 null 的字段覆盖到 entity
updateEntityFromDto(dto, entity);
// 再更新
updateById(entity);
避免了"前端传了什么就存什么"导致未传字段被清空的 Bug。
项目包含两个独立的前端:博客展示前端(用户访问)和管理后台(个人使用)。
| 技术 | 版本 | 选型原因 |
|---|---|---|
| Next.js | 16 | SSR/SSG,SEO 友好 |
| React | 19 | 组件模型 |
| Tailwind CSS | v4 | 原子化 CSS,开发快 |
| react-markdown | latest | Markdown 渲染 |
| remark-gfm | latest | GFM 扩展(表格、代码块等) |
| rehype-highlight | latest | 代码语法高亮 |
核心定位:纯展示,无用户认证。所有页面尽量用 Server Component 在服务端渲染,对 SEO 和首屏加载速度都有好处。
Markdown 渲染链:
后端存储原始 Markdown → API 返回 content 字段
→ react-markdown 解析
→ remark-gfm 增强(表格、任务列表、删除线)
→ rehype-highlight 语法高亮
→ 渲染到 <div className="prose ...">
Tailwind CSS Typography 插件(@tailwindcss/typography)提供 prose 系列类名,让 Markdown 渲染出来的 HTML 拥有统一美观的排版样式。
| 技术 | 版本 | 职责 |
|---|---|---|
| Next.js | 15 | App Router 框架 |
| TanStack Query | v5 | 服务端数据获取与缓存 |
| Zustand | v5 | 客户端全局状态(认证) |
| Zod | v3 | 表单 Schema 验证 |
| Shadcn/UI | latest | UI 组件库 |
状态管理的职责划分是这套技术栈的设计核心:
服务端状态 → TanStack Query(文章/评论/文件/监控数据)
客户端状态 → Zustand(登录用户信息、UI 偏好)
两者不混用。所有来自 API 的数据由 TanStack Query 统一管理,利用 staleTime、refetchInterval、invalidateQueries 处理缓存生命周期,避免手写 useEffect + useState 的样板代码。
Shadcn/UI 的特殊之处在于它不是传统 npm 包:
npx shadcn@latest add table # 把组件源码复制到 components/ui/
组件代码在项目里,完全可改,不受库版本升级影响。
services:
├── mysql — 数据持久化
├── redis — 缓存与 Token 黑名单
├── backend — Spring Boot API(仅 expose,不暴露到宿主机)
├── frontend — Next.js 博客前端
├── admin — Next.js 管理后台
├── monitor — Spring Boot Admin
└── caddy — 唯一对外开放 80/443 的服务
只有 Caddy 有 ports: ["80:80", "443:443"],其他服务只用 expose(Docker 内网),不直接对外暴露端口。Caddy 通过 Docker 内网服务名(如 http://backend:8080)访问各服务。
使用 Cloudflare 作为 DNS 和 CDN,后端 TLS 证书用 Cloudflare Origin CA(而非 Let's Encrypt),避免 VPS 需要通过 ACME 挑战自动续签的复杂配置。证书由 Caddy 加载,存放在宿主机挂载到 Caddy 容器的目录下。
git push main
│
▼
GitHub Actions(ubuntu-latest Runner)
│ 1. Checkout
│ 2. Docker Hub 登录
│ 3. Buildx 安装
│ 4. 构建镜像(GHA 缓存加速)
│ 5. 推送:latest + sha-{commit}
▼
Docker Hub
(手动)VPS: docker compose pull && docker compose up -d
镜像同时打两个 Tag:latest(方便日常 pull)和 sha-{github.sha}(方便回滚到特定版本)。
| 场景 | 技术 | 核心理由 |
|---|---|---|
| 后端架构 | 模块化单体 | 单人开发效率优先,保留微服务演进路径 |
| ORM | MyBatis-Plus | 灵活 SQL,复杂查询不受限 |
| 安全 | Spring Security 三链 | 细粒度、可维护的访问控制 |
| 认证 | JWT + Redis 黑名单 | 无状态认证,兼顾安全 |
| 博客前端 | Next.js SSR | SEO + 快速首屏 |
| Markdown | react-markdown + remark-gfm | 完整 GFM 支持 |
| 管理后台状态 | TanStack Query + Zustand | 服务端/客户端状态严格分离 |
| 表单 | Zod | Schema 推导类型,一处定义 |
| 组件库 | Shadcn/UI | 可控源码,无版本锁定 |
| 容器化 | Docker Compose | 单机部署,7 个服务统一编排 |
| 反向代理 | Caddy | 配置简洁,原生支持 HTTPS |
| CI/CD | GitHub Actions | 免费 Runner,与 Docker Hub 生态完整 |
这套选型的核心原则:个人项目以开发效率为第一优先级,但所有选型都有清晰的向上演进路径——模块化单体可以拆成微服务,单机 Compose 可以迁移到 K8s,Caddy 可以替换为 Nginx + Cert-Manager。
在设计个人博客后端时,我面临一个经典抉择:用简单粗暴的单体应用,还是拥抱时髦的微服务?最终我选择了两者之间的 模块化单体架构(Modular Monolith),事实证明这是正确的决定。 为什么不用微服务? 微服务固然优秀,但对于个人项目来说成本太高: - 需要服务注册、配置中心、网关、链路追踪…… - 每个服务独立部署,运维复杂度指数级增长 - 分布式事务难以处理 - 单人开发,维护多个仓库效率...
在做个人博客后端时,我发现每个业务模块都在重复同样的代码:创建实体、校验参数、调用 Mapper、处理异常……于是我设计了 BaseServiceImpl,一个基于 MyBatis-Plus 和 MapStruct 的通用 CRUD 基类。
作为一个人项目,我的博客系统包含 4 个独立仓库:后端(Spring Boot)、前端(Next.js 博客展示)、管理后台(Next.js Admin)、部署配置(Docker Compose)。最初我每次更新代码都要手动构建镜像、推送 Docker Hub、SSH 到 VPS 拉取最新版——这套流程繁琐且容易出错。
博客系统的管理后台需要处理文章的增删改查、评论审核、文件上传、系统监控等功能。这类界面有一个共同特点:大量表单 + 大量数据展示 + 频繁的服务端交互。 本文记录我在管理后台中选用的技术栈,以及每个选型背后的理由。 --- 技术栈总览 | 层级 | 技术 | 版本 | 用途 | |:---|:---|:---|:---| | 框架 | Next.
这篇文章记录我在开发个人博客系统全程中使用 AI 工具的真实体验——包括它在哪些环节显著提升了效率,在哪些地方需要谨慎,以及如何正确地使用 AI。 整个系统包含 Spring Boot 后端、两个 Next.js 前端、Docker Compose 部署配置,共 4 个独立仓库,全程有 AI 深度介入。