Files
raisfast/docs/plan.md
T
2026-04-13 11:40:37 +08:00

18 KiB
Raw Blame History

博客系统 — 详细实现计划

验收方式:每一步完成后,执行 cargo fmt --check && cargo clippy -- -D warnings && cargo build && cargo test 全部通过,即可标记 [x]

文档约定:[ ] 未完成,[x] 已完成。


Phase 1:项目骨架

1.1 配置 Cargo.toml 依赖

  • [dependencies] 中添加以下依赖:
    • axumtokiofeatures = full)、towertower-httpfeatures = cors, trace, limit
    • sqlxfeatures = runtime-tokio, sqlite)、serdeserde_json
    • uuidfeatures = v7, serde)、chronofeatures = serde
    • argon2jsonwebtoken
    • thiserroranyhow
    • tracingtracing-subscriberfeatures = env-filter
    • dotenvyvalidatorfeatures = derive
    • comrakslugammonia
    • imagerss
  • src/main.rs 顶部添加 #![deny(unsafe_code)]
  • cargo build 编译通过

验收:cargo build 无报错

1.2 创建目录结构与模块声明

  • 创建以下目录和文件:
    src/
    ├── main.rs
    ├── lib.rs
    ├── config/
    │   └── mod.rs
    ├── db/
    │   └── mod.rs
    ├── models/
    │   └── mod.rs
    ├── handlers/
    │   └── mod.rs
    ├── services/
    │   └── mod.rs
    ├── middleware/
    │   └── mod.rs
    ├── errors/
    │   └── mod.rs
    └── utils/
        └── mod.rs
    
  • 每个模块文件包含基本内容(可为空 pub mod xxx; 或占位)
  • src/lib.rs 声明所有子模块:pub mod config; pub mod db; ...
  • cargo build 编译通过

验收:cargo build 无报错,目录结构完整

1.3 环境变量与配置加载

  • 创建 .env.example,包含 APP_HOSTAPP_PORTAPP_ENVDATABASE_URLJWT_SECRETJWT_ACCESS_EXPIRESJWT_REFRESH_EXPIRESUPLOAD_DIRMAX_UPLOAD_SIZERUST_LOG
  • 创建 .env(从 .env.example 复制,填入开发用默认值)
  • .env 加入 .gitignore
  • src/config/mod.rs 中定义 AppConfig 结构体,从环境变量加载:
    pub struct AppConfig {
        pub host: String,
        pub port: u16,
        pub env: String,
        pub database_url: String,
        pub jwt_secret: String,
        pub jwt_access_expires: u64,
        pub jwt_refresh_expires: u64,
        pub upload_dir: String,
        pub max_upload_size: usize,
    }
    
  • 实现 AppConfig::from_env() 方法,使用 dotenvy::dotenv() 加载 .env
  • cargo build 编译通过

验收:单元测试验证 AppConfig::from_env() 正确读取 .env 中的值

1.4 SQLite 连接池初始化

  • src/db/mod.rs 中实现 init_pool(database_url: &str) -> SqlitePool
  • 使用 SqlitePoolOptions 配置连接池(max_connections = 5
  • 启用 SQLite PRAGMA journal_mode = WALPRAGMA foreign_keys = ON
  • src/main.rs 中调用 init_pool 并将 SqlitePool 放入 axum State
  • cargo build 编译通过

验收:运行程序不报数据库连接错误;可在测试中执行简单查询 SELECT 1

1.5 统一错误处理 AppError

  • src/errors/mod.rs 中定义 AppError 枚举:
    #[derive(Debug, thiserror::Error)]
    pub enum AppError {
        #[error("bad request: {0}")]
        BadRequest(String),       // 400
        #[error("unauthorized")]
        Unauthorized,             // 401
        #[error("forbidden")]
        Forbidden,                // 403
        #[error("not found: {0}")]
        NotFound(String),         // 404
        #[error("conflict: {0}")]
        Conflict(String),         // 409
        #[error("internal server error")]
        Internal(#[from] anyhow::Error), // 500
    }
    
  • AppError 实现 IntoResponse,返回 JSON 格式 { "code": xxx, "message": "xxx", "data": null }
  • 定义 AppResult<T> = Result<T, AppError> 类型别名
  • 为 sqlx::Error 实现到 AppError 的转换(唯一约束冲突 → Conflict)
  • cargo build 编译通过

验收:Handler 返回 AppError::NotFound("post".into()) 时 HTTP 响应 404 + 正确 JSON body

1.6 统一响应格式 ApiResponse

  • src/errors/mod.rs(或独立 src/utils/response.rs)中定义:
    #[derive(Serialize)]
    pub struct ApiResponse<T: Serialize> {
        pub code: i32,
        pub message: String,
        pub data: Option<T>,
    }
    
  • 实现 ApiResponse::success(data)ApiResponse::error(code, message) 构造方法
  • 定义分页结构 PaginatedData<T>
    pub struct PaginatedData<T> {
        pub items: Vec<T>,
        pub total: i64,
        pub page: i64,
        pub page_size: i64,
    }
    
  • cargo build 编译通过

验收:ApiResponse::success(Some("hello")) 序列化为 {"code":0,"message":"success","data":"hello"}

1.7 tracing 日志初始化

  • src/main.rs 中初始化 tracing
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .init();
    
  • 在关键位置添加 tracing::info! 日志(服务器启动、数据库连接等)
  • cargo build 编译通过

验收:设置 RUST_LOG=hello_axum=debug 运行程序,终端可见结构化日志输出

1.8 axum 路由与服务器启动 + 健康检查

  • src/handlers/mod.rs 实现健康检查 handler
    GET /api/v1/health → { "code": 0, "message": "success", "data": { "status": "ok" } }
    
  • src/main.rs 中组装 axum Router
    • 挂载 /api/v1/health 路由
    • 使用 tower::ServiceBuilder 添加 TraceLayer(请求日志)
    • 使用 axum::extract::State 共享 SqlitePoolAppConfig
  • 使用 tokio::net::TcpListener 绑定地址,axum::serve 启动
  • 添加 graceful shutdown(监听 ctrl+c
  • cargo build 编译通过

验收:cargo run 启动后 curl http://localhost:3000/api/v1/health 返回 200 + JSON

1.9 初始数据库迁移

  • 安装 cargo install sqlx-cli --no-default-features --features sqlite
  • 创建 migrations/ 目录
  • 编写 migrations/001_init.sql,包含所有表:
    • users
    • categories
    • tags
    • posts
    • posts_tags
    • comments
    • media
    • refresh_tokens
  • 每张表的字段类型、约束、外键、索引与 docs/guide.md 第 4 节一致
  • posts 添加索引:slugstatusauthor_idcategory_idcreated_at
  • comments 添加索引:post_idstatus
  • users 添加索引:emailusername
  • 运行迁移成功执行
  • 验证表结构:sqlite3 data/blog.db ".tables".schema users

验收:sqlite3 data/blog.db ".tables" 输出 8 张表,schema 与文档一致

1.10 Phase 1 最终验证

  • cargo fmt --check 通过(无格式问题)
  • cargo clippy -- -D warnings 通过(零警告)
  • cargo build 通过
  • cargo test 通过
  • cargo run 启动后健康检查返回 200
  • 数据库迁移正常

Phase 2:用户认证

2.1 用户数据模型

  • src/models/user.rs 中定义数据库模型
  • 实现查询函数:find_by_emailfind_by_idcreateupdate_profile
  • src/models/mod.rs 中声明 pub mod user;
  • cargo build 通过

验收:模型和查询函数编译通过

2.2 用户注册

  • 定义请求结构体 RegisterRequestemail, username, password)并使用 validator derive 校验
  • 定义响应结构体 UserResponse(不含 password_hash
  • src/services/auth.rs 实现 register(校验唯一性、argon2 哈希、UUID v7
  • src/handlers/auth.rs 实现 POST /api/v1/auth/register handler
  • 在 Router 中注册路由
  • cargo build 通过

验收:

  • 注册返回 200 + 用户信息(无密码)
  • 重复注册返回 409

2.3 用户登录与 JWT 签发

  • 定义 LoginRequestLoginResponse
  • 实现 JWT 工具函数(generate_access_token, verify_token
  • src/services/auth.rs 实现 login
  • src/handlers/auth.rs 实现 POST /api/v1/auth/login handler
  • cargo build 通过

验收:

  • 正确邮箱+密码登录返回 200 + tokens
  • 错误密码返回 401
  • 数据库中 refresh_tokens 表有记录

2.4 JWT 认证中间件

  • src/middleware/auth.rs 实现 AuthUserAdminUserAuthorUser 提取器
  • cargo build 通过

验收:

  • 带 Bearer Token 请求受保护接口 → 200
  • 不带 Token → 401

2.5 Token 刷新

  • 定义 RefreshRequest
  • 实现 refreshtoken rotation
  • 实现 POST /api/v1/auth/refresh handler
  • cargo build 通过

验收:

  • 有效 refresh_token 刷新返回新 token 对
  • 旧 refresh_token 不可重复使用

2.6 登出

  • 实现 POST /api/v1/auth/logout(需认证)
  • cargo build 通过

验收:登出后 refresh_token 不可用

2.7 用户信息接口

  • 定义 UpdateUserRequestUpdatePasswordRequest 并校验
  • 实现 GET /api/v1/users/me(需认证)
  • 实现 PUT /api/v1/users/me(需认证)
  • 实现 PUT /api/v1/users/me/password(需认证)
  • 实现 GET /api/v1/users/{id}(公开)
  • 实现 GET /api/v1/users(需 Admin
  • cargo build 通过

验收:

  • 各接口返回正确状态码和数据
  • 修改密码后旧密码登录失败,新密码登录成功
  • 非 Admin 访问用户列表返回 403

2.8 Phase 2 最终验证

  • cargo fmt --check 通过
  • cargo clippy -- -D warnings 通过
  • cargo test 通过
  • 手动测试完整认证流程:注册 → 登录 → 访问受保护接口 → 刷新 Token → 登出
  • 数据库中数据正确(用户密码已哈希、refresh_token 可查)

Phase 3:文章核心

3.1 分类 CRUD

  • src/models/category.rs 定义 Category 模型和请求结构体
  • 实现 model 层:create, find_all, find_by_id, update, delete
  • 实现 src/handlers/category.rsGET/POST/PUT/DELETE
  • 注册路由
  • cargo build 通过

验收:分类 CRUD 正常,Reader 角色无法创建(403)

3.2 标签 CRUD

  • src/models/tag.rs 定义 Tag 模型和 CreateTagRequest
  • 实现 model 层:create, find_all, delete
  • 实现 src/handlers/tag.rsGET/POST/DELETE
  • 注册路由
  • cargo build 通过

验收:标签 CRUD 正常,重复 name 返回 409

3.3 文章 CRUD

  • src/models/post.rs 定义完整模型和请求/响应结构体
  • 实现 service 层:create, update, delete, get, list(含 slug 自动生成、excerpt 提取)
  • 实现 src/handlers/post.rs:完整 CRUD + 权限校验
  • 注册路由
  • cargo build 通过

验收:文章 CRUD 正常,权限控制正确,slug 自动生成

3.4 文章-标签关联

  • 创建/更新时处理 tag_idssync_tags
  • 查询详情时返回标签列表
  • cargo build 通过

验收:文章详情返回对应标签

3.5 Markdown 渲染

  • src/utils/markdown.rs 实现 render_markdowncomrak + ammonia
  • PostResponse 包含 html_content 字段
  • cargo build 通过

验收:Markdown 正确渲染为 HTMLh1 + strong 等)

3.6 分页工具

  • PaginationParams 使用 axum Query 提取
  • page 默认 1page_size 默认 20,最大 100
  • 所有列表接口统一使用
  • cargo build 通过

验收:分页参数正确

3.7 Phase 3 最终验证

  • cargo fmt --check 通过
  • cargo clippy -- -D warnings 通过
  • cargo test 通过
  • 完整流程:创建分类 → 创建标签 → 创建文章 → 列表 → 详情 → 更新 → 删除

Phase 4:评论系统

4.1 评论数据模型与 CRUD

  • src/models/comment.rs 定义 Comment 模型、CreateCommentRequest
  • 实现 src/services/comment.rscreate, find_approved_by_post, delete
  • 实现 src/handlers/comment.rs:游客和登录用户分开的评论接口
  • 注册路由
  • cargo build 通过

验收:游客评论关联 nickname,登录用户关联 author_id

4.2 嵌套评论

  • CreateCommentRequest 支持 parent_id 字段
  • 验证 parent_id 对应评论存在且属于同一文章
  • 限制嵌套深度不超过 3 层
  • 查询时构建树形结构返回(build_tree
  • cargo build 通过

验收:嵌套回复正常显示在 replies 字段中

4.3 评论审核

  • 评论默认 status = "pending"
  • 实现 PUT /api/v1/comments/{id}/status(需 Admin
  • 公开评论列表仅返回 status = "approved" 的评论
  • cargo build 通过

验收:pending 不可见 → approve 后可见

4.4 Phase 4 最终验证

  • cargo fmt --check 通过
  • cargo clippy -- -D warnings 通过
  • cargo test 通过

Phase 5:媒体与辅助功能

5.1 图片上传

  • src/models/media.rs 定义 Media 模型
  • 实现 src/services/media.rs
    • save_file — 验证文件类型和大小,保存到 UPLOAD_DIR,生成唯一文件名,写数据库
    • find_all — 分页列表
    • delete_media — 删除文件和数据库记录
  • 实现 src/handlers/media.rs
    • POST /api/v1/media/upload(需认证)— multipart 上传
    • GET /api/v1/media(需认证)
    • DELETE /api/v1/media/{id}(需认证,仅本人或 Admin
  • 挂载 tower_http::services::ServeDir 提供 /uploads/ 静态文件访问
  • 注册路由
  • cargo build 通过

验收:

  • 上传图片返回 URL,文件存在于 uploads 目录
  • 超过 5MB 返回 400
  • 非图片类型返回 400

5.2 RSS 订阅

  • src/handlers/rss.rs 实现 GET /feed.xml
    • 查询最新 20 篇 published 文章
    • 使用 rss crate 生成 RSS 2.0 XML
    • 返回 Content-Type: application/xml
  • 注册路由
  • cargo build 通过

验收:curl /feed.xml 返回合法 RSS 2.0 XML,包含最新文章

5.3 全文搜索(基础版)

  • GET /api/v1/posts 增加 q 查询参数
  • 使用 SQLite LIKE '%keyword%' 搜索 title 和 content
  • cargo build 通过

验收:GET /api/v1/posts?q=rust 返回标题或正文包含 "rust" 的文章

5.4 浏览量统计

  • src/services/post.rsfind_by_slug 中增加浏览量 +1
  • 基于简化方案:每次访问 detail 接口 view_count += 1(不做去重)
  • PostResponse 包含 view_count 字段
  • cargo build 通过

验收:多次访问文章详情,view_count 递增

5.5 Phase 5 最终验证

  • cargo fmt --check 通过
  • cargo clippy -- -D warnings 通过
  • cargo test 通过

Phase 6:安全与优化

6.1 Rate Limiting 中间件

  • src/middleware/rate_limit.rs 实现基于 IP 的限流:
    • 自定义 RateLimitertokio::sync::Mutex + HashMap)作为 axum Extension + middleware
    • 登录接口:10 次/分钟/IP
    • 注册接口:5 次/小时/IP
    • 评论接口:3 次/分钟/IP
    • 通用:60 次/分钟/IP
  • 挂载到对应路由组
  • cargo build 通过

验收:超过限制后返回 429 Too Many Requests

6.2 CORS 配置

  • 使用 tower_http::cors::CorsLayer 配置:
    • 开发环境允许 Any origin(生产应改为白名单)
    • 允许的方法:Any
    • 允许的头部:Any
  • cargo build 通过

验收:跨域请求返回正确的 CORS 头

6.3 输入校验完善

  • 所有 POST/PUT 接口的请求体使用 validator derive 校验(含 UpdateCommentStatusRequest
  • Handler 中统一调用 validate(&req)?,校验失败返回 400 + 具体错误信息
  • cargo build 通过

验收:发送空 body 或非法字段返回 400 + 错误描述

6.4 数据库索引与查询优化

  • migrations/002_add_indexes.sql 中添加复合索引:
    • idx_posts_status_created — 覆盖文章列表查询
    • idx_posts_status_category — 覆盖按分类筛选
    • idx_posts_status_author — 覆盖按作者筛选
    • idx_comments_post_status — 覆盖评论列表查询
    • idx_media_user_created — 覆盖媒体文件列表查询
  • 所有分页查询使用单独 count 查询
  • cargo build 通过

验收:索引覆盖常用查询路径

6.5 Phase 6 最终验证

  • cargo fmt --check 通过
  • cargo clippy -- -D warnings 通过
  • cargo test 通过
  • 限流正常工作
  • CORS 正常工作

Phase 7:部署

7.1 Dockerfile

  • 编写多阶段 Dockerfile
    • Stage 1rust:1.85 编译 release
    • Stage 2debian:bookworm-slim 运行
    • 复制二进制 + migrations 目录
    • 暴露 3000 端口
  • docker build -t hello-axum . 成功
  • docker run -p 3000:3000 hello-axum 启动正常

验收:curl http://localhost:3000/api/v1/health 返回 200

7.2 docker-compose

  • 编写 docker-compose.yml
    • app 服务(构建镜像、挂载 data + uploads
  • docker compose up -d 正常启动

验收:docker compose up -d 后健康检查通过

7.3 Nginx 配置

  • 编写 Nginx 反向代理配置(deploy/nginx.conf
  • 配置 gzip、静态文件缓存、client_max_body_size
  • 文档中包含配置说明

验收:配置文件语法正确(nginx -t

7.4 CI/CD

  • 编写 .github/workflows/ci.yml
    • 触发条件:push / PR to main
    • 步骤:cargo fmt --checkcargo clippy -- -D warningscargo test
    • 含 cargo cache 加速

验收:CI workflow 语法正确,本地 act 可执行(或推送到 GitHub 后验证)

7.5 Phase 7 最终验证

  • Docker 构建和运行正常
  • docker-compose 启动正常
  • 所有 Phase 1-6 的验收项仍然通过
  • cargo fmt --check && cargo clippy -- -D warnings && cargo test 全通过