mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-24 00:02:28 +00:00
361 lines
10 KiB
Markdown
361 lines
10 KiB
Markdown
# 改进计划
|
||
|
||
> 基于项目审计,按优先级列出待修复和改进项。
|
||
|
||
## P0 — 关键问题(影响正确性/安全)
|
||
|
||
### 1. 特定限流器完全失效
|
||
|
||
**文件:** `src/middleware/rate_limit.rs`
|
||
|
||
**问题:** `rate_limit_fn!` 宏在每次请求中创建新的 `RateLimiter` 实例(空 HashMap),导致登录(10次/分)、注册(5次/时)、评论(3次/分)的限流永远不触发。仅全局限流器(通过 `Extension` 共享)有效。
|
||
|
||
**修复方案:** 将特定限流器也放入 `AppState`,或通过 `Extension` 共享多个命名限流器实例。
|
||
|
||
```rust
|
||
// 方案 A:AppState 中持有多个限流器
|
||
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.rs` — `build_post_response()`
|
||
|
||
**问题:** 文章列表每篇文章执行 3 次额外查询(tags、author_name、category_name),N 篇文章共 3N+1 次查询。
|
||
|
||
**修复方案:** 改用 JOIN 一次查询,或批量查询后 HashMap 映射。
|
||
|
||
```sql
|
||
-- 方案 A:JOIN 查询
|
||
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.rs` — `RegisterRequest`
|
||
|
||
**问题:** 仅校验长度 ≥ 8,未检查必须包含字母和数字。`aaaaaaaa` 可通过验证。
|
||
|
||
**修复方案:** 增加 `validate` 自定义函数,或使用 `validator` 的 `regex` / `custom` 校验。
|
||
|
||
```rust
|
||
#[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"))
|
||
}
|
||
}
|
||
```
|
||
|
||
**验收标准:** `aaaaaaaa`、`12345678` 被拒绝,`abc12345` 通过。
|
||
|
||
---
|
||
|
||
### 4. 全局请求体大小限制缺失
|
||
|
||
**文件:** `src/server/mod.rs`
|
||
|
||
**问题:** 仅 `/media/upload` 有 `RequestBodyLimitLayer`,其他 POST 端点无限制。恶意请求可发送超大 JSON body。
|
||
|
||
**修复方案:** 在 `api_v1` 路由组外层添加默认限制,upload 路由使用更大的限制。
|
||
|
||
```rust
|
||
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:
|
||
|
||
```rust
|
||
.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` 中集成。
|
||
|
||
```rust
|
||
// 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!`(编译时校验)。
|
||
|
||
**修复方案:** 启用 `sqlx` 的 `offline` feature,运行 `cargo sqlx prepare` 生成查询元数据,逐步迁移为宏形式。
|
||
|
||
```toml
|
||
# Cargo.toml
|
||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "offline"] }
|
||
```
|
||
|
||
**迁移优先级:** 从 `models/` 层开始(SQL 最集中),`services/` 层次之。
|
||
|
||
**注意:** 这是一项大规模重构,建议分批进行,每批一个 model 文件。
|
||
|
||
---
|
||
|
||
### 8. 评论列表分页
|
||
|
||
**文件:** `src/models/comment.rs` — `find_approved_by_post()`
|
||
|
||
**问题:** 一次查询文章所有已审核评论,无分页。热门文章评论数可达数千条。
|
||
|
||
**修复方案:** 增加分页参数,前端支持懒加载/无限滚动。
|
||
|
||
```rust
|
||
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()` 调用时触发。低流量时过期条目长期驻留内存。
|
||
|
||
**修复方案:** 添加后台定时清理任务。
|
||
|
||
```rust
|
||
// 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 类型(`AppError`、`ApiResponse`、请求/响应结构体)缺少 `#[non_exhaustive]`,无法安全地添加字段/变体而不破坏向后兼容。
|
||
|
||
**修复方案:** 为所有公开类型添加 `#[non_exhaustive]`。
|
||
|
||
```rust
|
||
#[non_exhaustive]
|
||
pub enum AppError {
|
||
BadRequest(String),
|
||
Unauthorized,
|
||
Forbidden,
|
||
NotFound(String),
|
||
Conflict(String),
|
||
Internal(String),
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 11. sync_tags 批量插入
|
||
|
||
**文件:** `src/services/post.rs` — `sync_tags()`
|
||
|
||
**问题:** 逐条 INSERT + DELETE,N 个标签需要 N 次数据库操作。
|
||
|
||
**修复方案:** 使用事务 + 批量操作。
|
||
|
||
```rust
|
||
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`(未来)实现。
|
||
|
||
```rust
|
||
#[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.rs` — `update_role()`
|
||
|
||
**问题:** 手动解析 `serde_json::Value`,绕过了 `validator` 校验。
|
||
|
||
**修复方案:** 定义带验证的请求结构体。
|
||
|
||
```rust
|
||
#[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. 错误日志级别调整
|
||
|
||
**问题:** `BadRequest`、`NotFound` 等客户端错误记录为 `ERROR` 级别。
|
||
|
||
**修复:** 4xx 错误用 `tracing::warn!`,5xx 才用 `tracing::error!`。
|
||
|
||
### 16. view_count 并发安全
|
||
|
||
**文件:** `src/services/post.rs` — `get_post()`
|
||
|
||
**问题:** `find_published_by_slug` 和 `increment_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 测试
|