← 返回博客
架构2026-09-06 16:11:425 分钟 · 1,494 1

给博客造一个管理后台:从选技术栈到一键上线

博客一直缺一个管理后台,只能直接改库发文章。这次用 FastAPI + Next.js 16 从零搭了一套,SDD+TDD 驱动,最后用 GitHub Actions 构建镜像、服务器 docker-compose 统一管理,域名 mp.blog.wxbuluo.com 已上线。

博客跑了一年多,发文章、改配置一直靠直接改数据库或登服务器。这种玩法我自己用着还行,但每次动库都提心吊胆。这个月终于把管理后台做出来了,域名是 https://mp.blog.wxbuluo.com。下面把技术选型和部署路上踩的坑记下来。

技术选型

后端我选了 FastAPI,初衷是想正经学一遍 Python Web 框架。前端本来在 Vue3 和 Next.js 之间犹豫,看了现有的 web/(Next.js 16)和 gin-server(Go BFF),决定后台前端也用 Next.js 16 App Router + React 19 + TypeScript + Tailwind 4,技术栈统一,心智负担小。

数据库直接复用线上的 wxbuluo 库(MySQL,root 密码走服务器 mysql 容器)。之前 ThinkPHP 时代有一套后台用户和 RBAC 权限表,但后台就我自己用,不做权限系统,重新设计了一张 admin_user 表。文章正文从 2026-08 起就改明文存储了(废弃了早期的 base64 方案),所以后端读写直接走明文,不用再编解码。

先写文档,再写代码

这次强制走 SDD + TDD:先把需求、架构、API 规范、数据模型、测试计划五份文档落到 docs/admin/,再动手写实现。后端用 SQLite 内存库 + TestClient 跑测试,最终 58 个用例全绿,覆盖了登录、改密、文章增删改查、标签/分类联动。

article.py 服务层里两个细节值得记一下:

  • 列表接口补了 category_name,前端表格不用再额外查分类名;
  • 标签用 label_id 逗号串存储,后端负责字符串和数组互转,前端只管传数组。

前端那些事

前端一开始用 npm,后来统一切到 pnpm(省磁盘、装得快),顺手把 dev.sh 写成一键启动脚本——一条命令把后端(:8100)、前端(:4000)、gin(:8080)、web(:3001)全拉起来,关掉时按 PID 精准清理,不会误杀 3000 端口的 web。

联调时撞了几个典型问题:

  • CORS 拦截:后端没挂 CORSMiddleware,加一个 allow_credentials=True 的配置就解了;
  • 登录 404:前端 API base 漏了 /api/admin 前缀,补齐后正常;
  • 登录后列表崩:后端列表缺 category_name,前端又把 /labels(结构是 {total, items})当成数组 .map,改成取 data.items 后稳定。

UI 最初走"编辑工作室"墨绿风,后来嫌丑,先换成黑白灰,再换成 Zinc 极简灰;状态标签之前会被挤成竖排,给 Badge 和表格列加了 whitespace-nowrap 和固定列宽修好。最后用 shadcn/ui 整体重做了一套组件(按钮、表格、对话框、下拉菜单、主题切换等 21 个),浅色/暗色双主题,pnpm build 通过。

CI/CD:构建在云端,服务器只拉镜像

按我的部署铁律——一律用 GitHub 镜像,绝不在 2G 内存的生产服务器上 docker build。后台拆成独立仓库 ningbnii/wxbuluo-admin,两个服务各打一个镜像:

服务技术镜像端口
前台 adminNext.js 16 standaloneghcr.io/ningbnii/wxbuluo-admin4000
后端 admin-serverFastAPIghcr.io/ningbnii/wxbuluo-admin-server8100

CI 流程(GitHub Actions):push main → 构建两个镜像推到 ghcr.io → SSH 到服务器 docker pull + docker compose up -d

服务器侧,后台两个容器直接并进中央的 /docker/docker-compose.yml,和 gin、twin 一样挂在 docker_default 网络,由 slb(nginx 入口,监听 80/443)做反代。前台走 mp.blog.wxbuluo.com,API 走同域 mp.blog.wxbuluo.com/api/admin 反代到后端 8100,免 CORS、一张证书搞定。certbot 已经装在 slb 容器里,签发就一句:

docker exec slb certbot certonly --webroot -w /etc/nginx/conf.d/acme \
  -d mp.blog.wxbuluo.com --email admin@wxbuluo.com \
  --agree-tos --no-eff-email --non-interactive

部署路上踩的四个坑

镜像在 GitHub Actions 构建没问题,真上服务器时几个坑花了不少时间。

1. ghcr 拉取卡死。 腾讯云这台机器从 ghcr.io 拉大镜像会 stall(Go 二进制还好,Python 镜像几百 MB 特别明显)。用了「超时 280s + pkill 杀残留 + 重试 16 次」的兜底,已下载的层会缓存,多跑几轮就续拉完了。绝不退回去服务器本地 build——这条是硬约束。

2. DB_URL 用了 Go 语法。 我一开始照 gin 的写法填了 mysql+pymysql://root:xxx@tcp(mysql:3306)/wxbuluo,结果 SQLAlchemy 不认 tcp() 包裹,探活时直接炸:

db not ready: invalid literal for int() with base 10: '3306)'

改成 SQLAlchemy 标准写法 @mysql:3306 就好了,不需要重建镜像,改服务器上的 /docker/admin.env 重新 up 即可。

3. create_admin.py 找不到 app 包。 容器启动脚本用 python scripts/create_admin.py 建初始账号,Python 把 scripts/ 加进 sys.path 而不是 /app,导致 from app.core.security import ...ModuleNotFoundError: No module named 'app'。在 docker-compose.ymladmin-api 段加一行 environment: - PYTHONPATH=/app 解决。

4. slb 容器里没有 wget CI 健康检查时我想用 docker exec slb wget ... 探活,结果 slb 镜像根本没装 wget,全报失败误判。改成 admin-api 用容器内 python 自测 /health,前端用服务器宿主 curl 公网 443 端到端验证。

上线结果

https://mp.blog.wxbuluo.com 已经能访问,登录用 ning 账号(首次启动容器自动建号),发布文章会联动 gin 的 /api/admin/revalidate 清 ISR 缓存。整条链路:SDD 文档 → FastAPI(58 测试)→ Next.js 前台 → 镜像推 ghcr → 服务器 compose 统一管理 → certbot 签发,全通了。

后面打算把"改密码""标签管理"这些界面再打磨一轮,顺便把部署脚本里 PYTHONPATH 的注入固化进 Dockerfile 镜像层,少依赖服务器侧 compose 的临时配置。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/171