从零搭建全栈博客系统:Go + Vue 3 + Docker 全流程实战 从零搭建全栈博客系统Go Vue 3 Docker 全流程实战前言一直想拥有一个属于自己的博客系统不想用现成的 WordPress 或 Hexo而是想从零开始亲手搭建一个前后端分离的全栈应用。经过一段时间的开发最终完成了JunBlog—— 一个基于Go Vue 3的全栈个人博客系统。本文将分享整个项目的技术选型、架构设计、核心功能实现以及 Docker 部署方案希望能给同样想造轮子的朋友一些参考。 项目地址https://github.com/super164/junblog技术栈总览层级技术选型后端Go 1.25 Gin GORM前端Vue 3 Vite Vue Router 4数据库MySQL 8.0 Redis 7认证JWT GitHub OAuth部署Docker Compose Nginx Jenkins CI/CD选择 Go 做后端是因为它的高性能、简洁语法和出色的并发能力Vue 3 则是因为上手快、生态完善配合 Vite 开发体验极佳。项目架构设计后端分层架构后端采用经典的分层架构参考了 Go 社区推荐的标准项目布局blog_backend/ ├── cmd/server/main.go # 程序入口 ├── configs/config.yaml # 配置文件 ├── internal/ │ ├── app/app.go # 应用初始化依赖注入 │ ├── api/ │ │ ├── router.go # 路由注册 │ │ └── v1/ # API 版本控制 │ │ ├── auth/ # 认证模块 │ │ ├── article/ # 文章模块 │ │ ├── category/ # 分类模块 │ │ ├── tag/ # 标签模块 │ │ ├── comment/ # 评论模块 │ │ ├── interaction/ # 互动模块点赞/收藏 │ │ ├── user/ # 用户模块 │ │ └── setting/ # 站点设置 │ ├── middleware/ # 中间件JWT、CORS、日志 │ ├── model/ │ │ ├── entity/ # 数据库实体 │ │ └── dto/ # 数据传输对象 │ ├── repository/ # 数据访问层 │ └── service/ # 业务逻辑层 ├── pkg/ # 公共包 │ ├── config/ # 配置加载 │ ├── database/ # 数据库连接 │ ├── jwt/ # JWT 工具 │ ├── logger/ # 日志Zap │ ├── response/ # 统一响应 │ └── errors/ # 错误处理 └── Makefile # 构建脚本核心设计原则关注点分离每层只关心自己的职责Repository 层只做数据操作Service 层只做业务逻辑依赖注入通过app.go统一初始化和注入依赖避免循环依赖接口驱动Repository 和 Service 都定义了接口方便测试和替换实现前端模块化组织bolg_forntend/src/ ├── App.vue # 根组件 ├── main.js # 入口 ├── router/index.js # 路由配置 ├── services/api.js # Axios API 封装 ├── pages/ # 页面组件 │ ├── HomePage.vue │ ├── ArticlePage.vue │ ├── LoginPage.vue │ └── ... ├── components/ # 通用组件 ├── composables/ # 组合式函数 ├── modules/admin/ # 后台管理模块 ├── data/ # 静态数据 └── style.css # 全局样式前端保持轻量没有引入 Vuex/Pinia 等状态管理库而是通过组合式函数Composables管理共享状态对于个人博客来说完全够用。核心功能实现1. JWT 认证体系JWT 认证是整个系统的安全基石。实现思路Token 双令牌机制Access Token有效期 15 分钟用于接口鉴权Refresh Token有效期 7 天用于无感刷新认证中间件核心逻辑// middleware/auth.gofuncAuth()gin.HandlerFunc{returnfunc(c*gin.Context){// 1. 从 Header 获取 AuthorizationauthHeader:c.GetHeader(Authorization)// 2. 解析 Bearer Tokenparts:strings.SplitN(authHeader, ,2)iflen(parts)!2||parts[0]!Bearer{response.Unauthorized(c,令牌格式错误)c.Abort()return}// 3. 验证 Token 有效性claims,err:jwt.ParseToken(parts[1])iferr!nil{response.Unauthorized(c,err.Error())c.Abort()return}// 4. 检查用户状态是否被封禁userRepo:repository.NewUserRepository(database.GetMySQL())user,err:userRepo.FindByID(claims.UserID)if!user.Status{response.Forbidden(c,账号已被封禁)c.Abort()return}// 5. 将用户信息存入 Context供后续 handler 使用c.Set(user_id,claims.UserID)c.Set(role,claims.Role)c.Next()}}路由分层设计很清晰// 公开路由 - 无需认证r.articleCtrl.RegisterPublicRoutes(v1)// 需认证路由 - 必须登录authorized:v1.Group(/)authorized.Use(middleware.Auth())r.userCtrl.RegisterRoutes(authorized)// 管理路由 - 必须 admin 角色admin:v1.Group(/admin)admin.Use(middleware.Auth())admin.Use(middleware.RequireRole(admin))r.articleCtrl.RegisterAdminRoutes(admin)2. GitHub OAuth 第三方登录这是项目中比较有意思的功能。整体流程如下用户点击「GitHub 登录」 ↓ 前端跳转 GitHub 授权页面 ↓ 用户授权后GitHub 回调带 code ↓ 前端将 code 发送给后端 ↓ 后端用 code 换取 access_token ↓ 后端用 access_token 获取用户信息 ↓ 查找/创建用户 → 生成 JWT → 返回前端关键实现 - OAuth 回调处理// service/auth_service.gofunc(s*authService)GitHubLogin(codestring)(*AuthResponse,error){// 1. 用 code 换取 access_tokentoken,err:s.getGitHubToken(code)iferr!nil{returnnil,err}// 2. 获取 GitHub 用户信息githubUser,err:s.getGitHubUser(token)iferr!nil{returnnil,err}// 3. 查找或创建用户user,err:s.userRepo.FindByGitHubID(githubUser.ID)iferr!nil{// 首次登录创建用户userentity.User{GitHubID:githubUser.ID,GitHubLogin:githubUser.Login,Username:githubUser.Login,Avatar:githubUser.AvatarURL,Role:user,}s.userRepo.Create(user)}// 4. 生成 JWTreturns.generateToken(user)}前端回调页面处理// pages/GithubCallbackPage.vueonMounted(async(){consturlParamsnewURLSearchParams(window.location.search)constcodeurlParams.get(code)if(code){try{constresawaitapi.githubLogin(code)// 保存 token 并跳转首页localStorage.setItem(token,res.data.token)router.push(/)}catch(error){console.error(GitHub 登录失败:,error)}}})3. 文章管理功能博客的核心功能支持Markdown 编辑器md-editor-v3图片上传限制 10MB支持 jpg/png/gif/webp/svg分类和标签管理文章状态管理草稿/已发布按热度、时间排序关键词搜索Docker 部署方案采用 Docker Compose 一键部署包含 4 个服务# docker-compose.ymlservices:mysql:image:mysql:8.0volumes:-mysql_data:/var/lib/mysql-./init.sql:/docker-entrypoint-initdb.d/init.sql# 只绑定 127.0.0.1外部无法访问ports:-127.0.0.1:3306:3306redis:image:redis:7-alpinevolumes:-redis_data:/data# 不暴露端口到宿主机command:redis-server--appendonly yes--requirepass junblog_redis_2026backend:build:context:../blog_backenddockerfile:Dockerfilevolumes:-./uploads:/app/uploads-./logs:/app/logsdepends_on:mysql:condition:service_healthyredis:condition:service_healthyfrontend:build:context:../bolg_forntenddockerfile:Dockerfileports:-80:80depends_on:-backend安全设计亮点MySQL 只绑定127.0.0.1外部无法直接访问数据库Redis 不暴露端口到宿主机仅 Docker 内部网络可访问数据库密码通过.env文件管理不硬编码在 docker-compose.yml 中后端也不直接暴露端口通过 Nginx 反向代理访问Nginx 配置# 前端静态资源 location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; # SPA 路由支持 } # 后端 API 代理 location /api { proxy_pass http://junblog-backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传文件代理 location ^~ /uploads { proxy_pass http://junblog-backend:8080; } # 静态资源缓存1年 location ~* \.(js|css|png|jpg|ico|svg|woff2)$ { expires 1y; add_header Cache-Control public, immutable; } # Gzip 压缩 gzip on; gzip_types text/plain text/css application/javascript application/json;CI/CD 流水线项目配置了 Jenkins 流水线实现自动化构建和部署后续碍于我的服务器性能不够我舍弃了Jenkins自动化部署Jenkins Pipeline 流程 ┌─────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────┐ │ Checkout │ → │ Build Backend│ → │Upload Files │ → │ Deploy │ │ 检出代码 │ │ 交叉编译 Go │ │ SCP 上传 │ │ 重启服务 │ └─────────┘ └──────────────┘ └─────────────┘ └─────────┘后端交叉编译# 在 Windows 上编译 Linux 二进制setCGO_ENABLED0setGOOSlinuxsetGOARCHamd64 go build-oserver.exe ./cmd/serverGo 的交叉编译能力非常方便一条命令就能在 Windows 上编译出 Linux 的可执行文件。Dockerfile 极简设计# 后端 - 基于 Alpine最终镜像仅约 15MB FROM alpine:latest RUN apk --no-cache add ca-certificates tzdata WORKDIR /app COPY server . COPY configs ./configs RUN mkdir -p uploads logs chmod x server EXPOSE 8080 CMD [./server]功能模块一览模块功能说明认证注册/登录/JWT/GitHub OAuth双令牌机制支持第三方登录文章CRUD/Markdown/图片上传支持分类、标签、状态管理分类/标签树形分类、标签关联分类支持层级结构评论发表/审核/管理支持评论审核机制互动点赞/收藏Redis 缓存计数用户个人信息/角色/封禁管理员可管理用户站点设置关于页面/系统配置后台可配置站点信息后台管理全功能管理面板文章/用户/评论/设置管理踩坑与经验1. GORM 的 N1 查询问题在查询文章列表时如果每篇文章都单独查一次分类和标签会产生 N1 问题。解决方案是使用 GORM 的 Preload// 一次性预加载关联数据db.Preload(Category).Preload(Tags).Find(articles)2. Docker 网络安全最初把 MySQL 端口直接映射到0.0.0.0:3306这意味着任何人只要知道服务器 IP 就能尝试连接数据库。后来改为127.0.0.1:3306:3306只允许本机访问。3. 前端 SPA 路由刷新 404Vue Router 使用 history 模式时刷新页面会 404。解决方案是在 Nginx 配置中添加try_files $uri $uri/ /index.html;快速开始Docker 一键部署gitclone https://github.com/yourname/junblog.gitcdjunblog/deploy# 配置环境变量cp.env.example .envvim.env# 设置数据库密码等# 启动docker-composeup-d# 访问# 前端http://your-server-ip# 后端 APIhttp://your-server-ip/api/v1/health本地开发# 后端cdblog_backend go run ./cmd/server# 前端cdbolg_forntendnpminstallnpmrun dev# 访问 http://localhost:5173总结这个项目虽然定位是个人博客但在架构上并没有偷懒后端分层架构 接口驱动 中间件链具备良好的可测试性和可扩展性前端模块化组织 组合式函数代码清晰易维护部署Docker Compose Nginx 反向代理 Jenkins CI/CD一键部署安全JWT 双令牌 RBAC 角色控制 Docker 网络隔离整个项目从零搭建到现在收获了很多。如果你也想搭建自己的博客系统欢迎参考这个项目。有问题欢迎在评论区交流