测试工程师个人技术博客 ——
代码永远在等待一个它从未通过的测试
基于 FastAPI + SQLAlchemy 2.0(async)+ PostgreSQL / SQLite + Redis 的后端服务,配套 Nuxt 3(SSR) 前端(见 caoxublog-web/)。
- P1:数据模型 + Alembic 迁移 + 博客/教程 CRUD + 标签多对多
- P2:首页去重、全文搜索、阅读/点赞/评论互动(Redis 去重与频次限制)、敏感词过滤
- P3:Nuxt 三栏 + 教程三层目录 + Sitemap + SEO(
caoxublog-web/) - P4:暗黑模式 + 鼠标光点 + Markdown 高亮/行号/复制
- P5:边界测试套件 + README 自动生成 + CI
| 端 | 依赖 | 版本 |
|---|---|---|
| 后端 | fastapi | >=0.115,<0.116 |
| 后端 | uvicorn[standard] | >=0.30,<0.35 |
| 后端 | sqlalchemy[asyncio] | >=2.0,<2.1 |
| 后端 | asyncpg | >=0.30,<0.31 |
| 后端 | aiosqlite | >=0.20,<0.22 |
| 后端 | alembic | >=1.13,<1.16 |
| 后端 | pydantic | >=2.7,<3 |
| 后端 | pydantic-settings | >=2.3,<3 |
| 后端 | redis | >=5,<7 |
| 后端 | pyahocorasick | >=2,<3 |
| 前端 | @nuxtjs/tailwindcss | 6.12.2 |
| 前端 | @playwright/test | ^1.48.0 |
| 前端 | @types/markdown-it | ^14.1.2 |
| 前端 | highlight.js | ^11.12.0 |
| 前端 | markdown-it | ^14.1.0 |
| 前端 | nuxt | ^3.13.0 |
| 前端 | tailwindcss | ^3.4.0 |
| 前端 | typescript | ^5.6.0 |
| 前端 | vue | ^3.5.0 |
| 前端 | vue-router | ^4.4.0 |
# 1. 创建虚拟环境并安装依赖
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linuxsource .venv/bin/activate
pip install -e ".[dev]"# 2. 准备环境变量(可选,默认已用 SQLite)
cp .env.example .env
# 3. 执行数据库迁移
alembic upgrade head
# 4. 灌入演示数据(2 篇博客、1 篇教程、3 个标签)
python scripts/seed.py
# 5. 启动服务
uvicorn app.main:app --reload启动后访问:
- 交互式 API 文档:http://127.0.0.1:8000/docs
- OpenAPI 规范:http://127.0.0.1:8000/openapi.json
- 健康检查:http://127.0.0.1:8000/health
数据库连接串通过环境变量 DATABASE_URL 切换,支持双后端:
| 环境 | 连接串 |
|---|---|
| 开发(SQLite,开箱即用) | sqlite+aiosqlite:///./caoxublog.db |
| 生产(PostgreSQL 16) | postgresql+asyncpg://USER:PASS@HOST:5432/caoxublog |
# 切到 PostgreSQLexport DATABASE_URL="postgresql+asyncpg://caoxu:secret@localhost:5432/caoxublog"
alembic upgrade head说明:主键类型使用
BigInteger().with_variant(Integer, "sqlite"), PostgreSQL 下为BIGINT(符合 SRS §5.1),SQLite 下退回INTEGER以支持自增。
| 表名 | 模型类 | 说明 |
|---|---|---|
comments | Comment | 评论模型(SRS §5.1 comments 表)。 |
post_tags | - | 博客 / 教程与标签的多对多关联表。 |
posts | Post | 博客 / 教程文章模型(Post 统一承载 blog 与 tutorial)。 |
tags | Tag | 标签模型。 |
users | User | 用户模型(预留微信登录,当前不开放自助注册)。 |
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/home | 首页:3 篇置顶 + 6 篇最新(不重复)。 |
| GET | /api/posts | 文章列表(分页 + post_type / status 过滤,软删除不返回)。 |
| POST | /api/posts | 创建文章(博客 / 教程)。 |
| DELETE | /api/posts/{post_id} | 软删除:仅置 is_deleted=True,保留数据可恢复。 |
| GET | /api/posts/{post_id} | 文章详情。 |
| PUT | /api/posts/{post_id} | 更新文章(部分更新)。 |
| GET | /api/posts/{post_id}/comments | 评论列表,时间正序(created_at ASC),可选分页。 |
| POST | /api/posts/{post_id}/comments | 发表评论:先敏感词过滤(422),再频次校验(超限 429)。 |
| POST | /api/posts/{post_id}/like | 点赞(同一 IP 对同一文章仅计一次,重复点赞不累加)。 |
| POST | /api/posts/{post_id}/tags | 给文章绑定一批标签(幂等:已绑定的标签不会重复绑定)。 |
| DELETE | /api/posts/{post_id}/tags/{tag_id} | 解绑文章与单个标签的关联。 |
| POST | /api/posts/{post_id}/view | 阅读量 +1(同一 IP 对同一文章 24h 内仅计一次)。 |
| GET | /api/search | 按关键词搜索已发布文章(标题 / 简介 / 正文),支持类型过滤。 |
| GET | /api/tags | 标签列表(分页)。 |
| POST | /api/tags | 创建标签(重名返回 409)。 |
| DELETE | /api/tags/{tag_id} | 删除标签(先解除与文章的关联,再物理删除)。 |
| GET | /api/tags/{tag_id} | 标签详情。 |
| PUT | /api/tags/{tag_id} | 更新标签(重名返回 409)。 |
统一分页响应 Page[T]:{"items": [...], "total": N}。
统一错误响应:{"detail": "..."}(404/409/429)或 {"detail": [...]}(422)。
| 路径 | 说明 |
|---|---|
/ | 首页(品牌 + 置顶 + 最新) |
/blog | 博客列表 |
/blog/:slug | 博客详情(三栏) |
/tutorial | 教程目录索引 |
/tutorial/:slug* | 教程详情(三栏,catch-all 三层) |
互动接口(阅读 / 点赞 / 评论)依赖 Redis 7.x,Key 设计:
| Key | 用途 | 命令 |
|---|---|---|
view:{post_id}:{ip} | 阅读量 24h 去重 | SET ... EX 86400 NX |
like:{post_id}:{ip} | 点赞去重 | SET ... NX |
comment:{post_id}:{ip} | 评论频次计数 | INCR + 过期 |
# 本地启动 Redis
docker run -d --name caoxublog-redis -p 6379:6379 redis:7
# 或直接使用本机 redis-server评论次数上限由 COMMENT_LIMIT 控制(默认 3),阅读窗口由 VIEW_DEDUP_SECONDS 控制(默认 86400)。
get_client_ip 依赖负责解析客户端 IP:
- 默认(
TRUST_PROXY_HEADERS=false):使用直连地址(request.client.host)。 - 反向代理之后(
TRUST_PROXY_HEADERS=true):信任X-Forwarded-For首段 /X-Real-IP。
安全提示:仅当应用部署在可信反向代理(Nginx 等)之后才应开启
TRUST_PROXY_HEADERS, 否则攻击者可伪造X-Forwarded-For绕过阅读/点赞/评论的频率限制。
敏感词库位于 app/data/sensitive_words.txt,每行一个词,# 开头为注释。
- 使用
pyahocorasick构建 Aho-Corasick 自动机,多模式匹配性能优于逐词replace。 - 词库为进程级懒加载单例:编辑文件后需重启服务才生效(热更新见后续批次)。
- 命中敏感词的评论直接返回 422,不写入数据库。
# 全量
pytest -q
# 覆盖率(要求 ≥ 80%)
pytest --cov=app --cov-report=term-missing
# 重新生成本 README
python scripts/gen_readme.py- 依赖注入必须使用
Annotated[AsyncSession, Depends(get_db)] - async 路由禁止同步 ORM / 同步
Session() - 任何删除操作必须软删除(
is_deleted=True)