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

10 KiB
Raw Blame History

改进计划

基于项目审计,按优先级列出待修复和改进项。

P0 — 关键问题(影响正确性/安全)

1. 特定限流器完全失效

文件: src/middleware/rate_limit.rs

问题: rate_limit_fn! 宏在每次请求中创建新的 RateLimiter 实例(空 HashMap),导致登录(10次/分)、注册(5次/时)、评论(3次/分)的限流永远不触发。仅全局限流器(通过 Extension 共享)有效。

修复方案: 将特定限流器也放入 AppState,或通过 Extension 共享多个命名限流器实例。

// 方案 AAppState 中持有多个限流器
pub struct AppState {
    pub pool: SqlitePool,
    pub config: Arc<AppConfig>,
    pub rate_limiters: RateLimiterSet,
}

pub struct RateLimiterSet {
    pub global: RateLimiter,
    pub register: RateLimiter,
    pub login: RateLimiter,
    pub comment: RateLimiter,
}

验收标准: 连续发送 11 次登录请求,第 11 次应返回 429。


2. N+1 查询

文件: src/services/post.rsbuild_post_response()

问题: 文章列表每篇文章执行 3 次额外查询(tags、author_name、category_name),N 篇文章共 3N+1 次查询。

修复方案: 改用 JOIN 一次查询,或批量查询后 HashMap 映射。

-- 方案 AJOIN 查询
SELECT p.*, c.name AS category_name, u.username AS author_name
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN users u ON p.author_id = u.id
WHERE p.status = 'published'
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?;

-- tags 批量查询
SELECT pt.post_id, t.id, t.name, t.slug
FROM posts_tags pt
JOIN tags t ON pt.tag_id = t.id
WHERE pt.post_id IN (...);

验收标准: list_posts 查询次数从 3N+1 降至 2~3 次。


3. 密码验证不完整

文件: src/models/user.rsRegisterRequest

问题: 仅校验长度 ≥ 8,未检查必须包含字母和数字。aaaaaaaa 可通过验证。

修复方案: 增加 validate 自定义函数,或使用 validatorregex / custom 校验。

#[derive(Debug, Deserialize, Validate)]
pub struct RegisterRequest {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 2, max = 50))]
    pub username: String,
    #[validate(length(min = 8, max = 128), custom(function = "validate_password"))]
    pub password: String,
}

fn validate_password(pwd: &str) -> Result<(), validator::ValidationError> {
    let has_letter = pwd.chars().any(|c| c.is_ascii_alphabetic());
    let has_digit = pwd.chars().any(|c| c.is_ascii_digit());
    if has_letter && has_digit {
        Ok(())
    } else {
        Err(ValidationError::new("password_strength"))
    }
}

验收标准: aaaaaaaa12345678 被拒绝,abc12345 通过。


4. 全局请求体大小限制缺失

文件: src/server/mod.rs

问题:/media/uploadRequestBodyLimitLayer,其他 POST 端点无限制。恶意请求可发送超大 JSON body。

修复方案:api_v1 路由组外层添加默认限制,upload 路由使用更大的限制。

let api_v1 = axum::Router::new()
    // ... 所有路由 ...
    .layer(RequestBodyLimitLayer::new(2 * 1024 * 1024)) // 全局 2MB
    .layer(from_fn(global_rate_limit))
    .layer(Extension(RateLimiter::new(...)));

upload 路由单独覆盖为 5MB

.route("/media/upload", http_post(media::upload)
    .layer(RequestBodyLimitLayer::new(5 * 1024 * 1024)))

验收标准: 发送 >2MB 的 JSON 到任意 POST 端点返回 413。


P1 — 高优先级(影响功能/规范符合度)

5. CSRF 防护

问题: 无 CSRF token 生成和校验。规范 guide.md 6.3 要求实现。

修复方案: 使用 axum-csrf 或手动实现 Double Submit Cookie 模式。

由于 API 使用 Bearer Token(非 Cookie),CSRF 风险较低。若前端改为 Cookie 认证则必须实现。

优先级评估: 当前 Bearer Token 方案下可降为 P2。若未来改用 Cookie 认证则必须立即实现。


6. 代码语法高亮

问题: Markdown 渲染(comrak)未配置语法高亮。规范 guide.md 5.1 要求使用 syntect

修复方案: 添加 syntect 依赖,在 render_markdown 中集成。

// src/utils/markdown.rs
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;

pub fn render_markdown(input: &str) -> String {
    // comrak 渲染 markdown → HTML
    // 对 <code> 块使用 syntect 高亮
}

注意: syntect 增加约 10MB 编译体积和启动时语法集加载时间。可考虑在构建时预生成 HTML。


7. SQL 编译时校验

问题: 所有 SQL 查询使用 sqlx::query() / sqlx::query_as()(运行时检查),未使用 sqlx::query! / sqlx::query_as!(编译时校验)。

修复方案: 启用 sqlxoffline feature,运行 cargo sqlx prepare 生成查询元数据,逐步迁移为宏形式。

# Cargo.toml
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "offline"] }

迁移优先级:models/ 层开始(SQL 最集中),services/ 层次之。

注意: 这是一项大规模重构,建议分批进行,每批一个 model 文件。


8. 评论列表分页

文件: src/models/comment.rsfind_approved_by_post()

问题: 一次查询文章所有已审核评论,无分页。热门文章评论数可达数千条。

修复方案: 增加分页参数,前端支持懒加载/无限滚动。

pub async fn find_approved_by_post_paginated(
    pool: &SqlitePool,
    post_id: &str,
    page: i64,
    page_size: i64,
) -> AppResult<(Vec<Comment>, i64)> {
    let offset = (page - 1) * page_size;
    // 查询 + COUNT
}

P2 — 中优先级(影响可扩展性/代码质量)

9. 限流器内存无限增长

文件: src/middleware/rate_limit.rs

问题: 清理仅在 check() 调用时触发。低流量时过期条目长期驻留内存。

修复方案: 添加后台定时清理任务。

// server 启动时 spawn 一个清理任务
tokio::spawn(async move {
    let mut interval = tokio::time::interval(Duration::from_secs(300));
    loop {
        interval.tick().await;
        limiter.cleanup_expired().await;
    }
});

10. #[non_exhaustive] 缺失

问题: 公开 API 类型(AppErrorApiResponse、请求/响应结构体)缺少 #[non_exhaustive],无法安全地添加字段/变体而不破坏向后兼容。

修复方案: 为所有公开类型添加 #[non_exhaustive]

#[non_exhaustive]
pub enum AppError {
    BadRequest(String),
    Unauthorized,
    Forbidden,
    NotFound(String),
    Conflict(String),
    Internal(String),
}

11. sync_tags 批量插入

文件: src/services/post.rssync_tags()

问题: 逐条 INSERT + DELETE,N 个标签需要 N 次数据库操作。

修复方案: 使用事务 + 批量操作。

pub async fn sync_tags(pool: &SqlitePool, post_id: &str, tag_ids: &[String]) -> AppResult<()> {
    let mut tx = pool.begin().await?;
    sqlx::query("DELETE FROM posts_tags WHERE post_id = ?")
        .bind(post_id)
        .execute(&mut *tx)
        .await?;
    for tag_id in tag_ids {
        sqlx::query("INSERT INTO posts_tags (post_id, tag_id) VALUES (?, ?)")
            .bind(post_id)
            .bind(tag_id)
            .execute(&mut *tx)
            .await?;
    }
    tx.commit().await?;
    Ok(())
}

12. 限流器水平扩展

问题: 内存存储,多实例部署时限流状态不共享。

修复方案: 抽象 RateLimitStore trait,提供 MemoryStore(当前)和 RedisStore(未来)实现。

#[async_trait]
pub trait RateLimitStore: Send + Sync {
    async fn increment(&self, key: &str, window_secs: u64) -> u64;
    async fn get(&self, key: &str) -> u64;
}

当部署为多实例时,切换为 Redis 后端即可。


13. update_role 使用类型化请求

文件: src/handlers/user.rsupdate_role()

问题: 手动解析 serde_json::Value,绕过了 validator 校验。

修复方案: 定义带验证的请求结构体。

#[derive(Debug, Deserialize, Validate)]
pub struct UpdateRoleRequest {
    #[validate(length(min = 1))]
    pub role: String,
}

pub async fn update_role(
    _admin: AdminUser,
    State(state): State<crate::AppState>,
    Path(id): Path<String>,
    Json(req): Json<UpdateRoleRequest>,
) -> AppResult<ApiResponse<UserResponse>> {
    if !["reader", "author", "admin"].contains(&req.role.as_str()) {
        return Err(AppError::BadRequest("invalid role".into()));
    }
    // ...
}

P3 — 低优先级(锦上添花)

14. 单元测试 & 测试基础设施

当前状态: 60 个集成测试通过,但无单元测试、无 mock、无快照测试、无基准测试。rstest 已引入但未使用。

改进项:

  • services/ 层添加单元测试(使用 mockall 隔离数据库)
  • utils/markdown.rs 添加快照测试(insta
  • 为热点路径(文章列表、JWT 验证)添加基准测试(criterion
  • CI 中使用 cargo nextest 替代 cargo test

15. 错误日志级别调整

问题: BadRequestNotFound 等客户端错误记录为 ERROR 级别。

修复: 4xx 错误用 tracing::warn!5xx 才用 tracing::error!

16. view_count 并发安全

文件: src/services/post.rsget_post()

问题: find_published_by_slugincrement_view_count 不是原子操作。高并发下可能出现计数偏差。

修复方案: 使用 SQL 单条语句 UPDATE posts SET view_count = view_count + 1 WHERE slug = ? RETURNING *,或在查询后用原子计数器。

17. 静态文件目录

问题: 规范中的 static/ 目录未创建。

修复: 创建 static/ 目录,用于 favicon、robots.txt 等静态资源,通过 ServeDir 挂载。


实施建议

按以下顺序分批推进:

  1. 第一批(1-2 天): P0 全部 — #1 限流器、#2 N+1、#3 密码验证、#4 请求体限制
  2. 第二批(2-3 天): P1 — #8 评论分页、#13 类型化请求、#5 CSRF 评估
  3. 第三批(持续): P2/P3 — #6 语法高亮、#7 SQL 编译时校验、#9-12 扩展性改进、#14 测试