# 平台化演进路线图 > 基于当前架构的扩展性评估,分析从「博客 API」演进为「可扩展平台」的差距与路径。 --- ## 1. 当前架构评估 ### 1.1 已具备的扩展基础 | 维度 | 现状 | 评级 | |------|------|------| | 分层架构 | Handler → Service → Model,职责清晰 | 优 | | 插件系统 | WASM 沙箱、11 个 Hook、热加载、安全模型 | 优 | | 多数据库 | SQLite/PostgreSQL/MySQL + 编译时 SQL 检查 | 优 | | 错误处理 | 统一 AppError + i18n + IntoResponse | 优 | | 认证 | JWT + Refresh Token + 角色权限 | 良 | | 测试覆盖 | 125 个测试(65 单元 + 60 集成) | 良 | | 代码质量 | `#![deny(unsafe_code)]`、零 clippy 警告 | 优 | ### 1.2 核心判断 当前系统是一个**功能完整的博客 API**,但还不是一个**平台框架**。 差距在于缺少一层「平台基础设施」: ``` 当前: HTTP → Handler → Service → Model → DB ↓ Plugin (WASM) 目标: HTTP → Handler → Service → Model → DB ↓ ↓ ↓ Cache EventBus FullText (Redis) (通知/任务) (Meilisearch) ↓ ↓ Plugin Background System Workers ``` --- ## 2. 必须补齐的短板 ### P0 — 不补就做不了复杂平台 #### 2.1 缓存层(Redis) **现状:** 无任何缓存。所有查询直接打到数据库。 **问题:** 热点数据(文章列表、分类树、标签云)在高并发下会成为瓶颈。复杂平台的配置、会话、计数器都需要缓存。 **方案:** ```rust // src/cache/mod.rs #[async_trait] pub trait CacheStore: Send + Sync { async fn get(&self, key: &str) -> Option; async fn set(&self, key: &str, value: &str, ttl: Option) -> AppResult<()>; async fn delete(&self, key: &str) -> AppResult<()>; async fn incr(&self, key: &str, delta: i64) -> AppResult; } // src/cache/memory.rs — 开发阶段用 pub struct MemoryCache { /* HashMap + TTL */ } // src/cache/redis.rs — 生产环境用 pub struct RedisCache { /* redis::aio::Connection */ } ``` **集成点:** - `services/post.rs` — 文章详情缓存(slug → post),列表缓存(分页参数 → 结果集) - `services/auth.rs` — Token 黑名单、用户权限缓存 - `services/comment.rs` — 评论计数缓存 - `middleware/rate_limit.rs` — 已有 `RateLimitStore` trait,补充 `RedisStore` 实现 #### 2.2 后台任务队列 **现状:** 无异步任务机制。所有操作在请求线程同步完成。 **问题:** 定时发布、邮件发送、图片处理、统计分析、Webhook 回调都需要异步执行。 **方案:** ```rust // src/worker/mod.rs #[async_trait] pub trait JobQueue: Send + Sync { async fn enqueue(&self, job: Job) -> AppResult<()>; async fn dequeue(&self) -> Option; } pub enum Job { SendEmail { to: String, subject: String, body: String }, ProcessImage { media_id: String, operations: Vec }, ScheduledPublish { post_id: String, publish_at: String }, WebhookNotify { url: String, payload: serde_json::Value }, RebuildSearchIndex { post_ids: Vec }, GenerateSitemap, } ``` **实现选择:** | 方案 | 优点 | 缺点 | |------|------|------| | `tokio::spawn` + 内存队列 | 零依赖,简单 | 进程重启丢失 | | SQLite 持久化队列 | 无额外依赖,可靠 | 性能上限低 | | Redis Stream | 成熟、支持消费者组 | 引入 Redis 依赖 | | 独立 sidekiq-rs / tower-worker | 专业方案 | 重量级 | **建议:** 先实现 SQLite 持久化队列(与现有架构一致),后期按需切换 Redis Stream。 #### 2.3 全站搜索 **现状:** `services/post.rs` 使用 `LIKE '%keyword%'` 全表扫描,无索引利用。 **问题:** 文章量上千后搜索变慢,无法支持高亮、分词、相关性排序。 **方案 A — SQLite FTS5(推荐起步):** ```sql -- migrations/003_fts5.sql CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5( title, content, content=posts, content_rowid=rowid ); -- 触发器自动同步 CREATE TRIGGER posts_ai AFTER INSERT ON posts BEGIN INSERT INTO posts_fts(rowid, title, content) VALUES (new.rowid, new.title, new.content); END; ``` ```rust // models/search.rs pub async fn search_posts(pool: &Pool, query: &str, page: i64, page_size: i64) -> AppResult<(Vec, i64)> { // SELECT * FROM posts_fts WHERE posts_fts MATCH ? ORDER BY rank LIMIT ? OFFSET ? } ``` **方案 B — Meilisearch(后期升级):** 当文章量超过 10 万或需要中文分词时切换。通过插件 Hook `OnPostCreated`/`OnPostUpdated`/`OnPostDeleted` 同步索引。 #### 2.4 数据库事务 **现状:** `sync_tags` 等多步操作缺少事务包裹,中间失败会导致数据不一致。 **修复:** ```rust // services/post.rs pub async fn create_post(pool: &Pool, /* ... */) -> AppResult { let mut tx = pool.begin().await?; let post = sqlx::query_scalar!("INSERT INTO posts ...") .fetch_one(&mut *tx) .await?; for tag_id in &tag_ids { sqlx::query!("INSERT INTO posts_tags ...", post.id, tag_id) .execute(&mut *tx) .await?; } tx.commit().await?; Ok(post) } ``` --- ### P1 — 不补就做不好 #### 2.5 实时推送(WebSocket / SSE) **现状:** 纯请求-响应模式,无服务端推送能力。 **场景:** 新评论通知作者、审核状态变更通知、在线访客计数、协作编辑。 **方案:** ```rust // src/server/mod.rs — 新增 WebSocket 路由 .route("/ws", get(ws_handler)) .route("/api/v1/events", get(sse_handler)) // src/services/event.rs pub struct EventBus { tx: broadcast::Sender, } pub enum Event { NewComment { post_slug: String, comment: CommentResponse }, PostPublished { post: PostResponse }, SystemNotification { message: String }, } ``` **推荐 SSE(Server-Sent Events)起步:** - 比 WebSocket 简单,单向推送足够 - 浏览器原生支持,无需额外库 - axum 集成简单(`axum::response::sse::Sse`) #### 2.6 通知系统 **现状:** 无任何通知机制。 **方案:** ```rust // src/services/notification.rs pub struct NotificationService { email: Option, webhook: Option, in_app: InAppNotifier, } pub trait Notifier: Send + Sync { async fn send(&self, notification: &Notification) -> AppResult<()>; } pub struct Notification { pub recipient: String, pub event: NotificationEvent, pub channels: Vec, // Email, InApp, Webhook } ``` **通知触发点(复用现有 Hook):** | 事件 | 通知对象 | 渠道 | |------|---------|------| | 新评论 | 文章作者 | InApp + Email | | 评论审核通过 | 评论者 | Email | | 文章发布 | 订阅者 | Email + Webhook | | 用户注册 | 管理员 | InApp | | 登录异常 | 用户 | Email | #### 2.7 媒体处理管线 **现状:** 文件原样存储,无压缩、缩略图、格式转换。 **方案:** ```rust // src/services/media.rs — 扩展 pub enum ImageOperation { Resize { width: u32, height: u32 }, ConvertToWebP, GenerateThumbnail { size: u32 }, Compress { quality: u8 }, } pub async fn process_upload( pool: &Pool, user_id: &str, file: Bytes, filename: &str, ) -> AppResult { // 1. 校验文件类型和大小 // 2. 图片:生成缩略图 + WebP 转换 + 压缩 // 3. 存储原始文件和处理后文件 // 4. 记录数据库 // 5. 可选:上传到 S3 } ``` #### 2.8 API 版本化策略 **现状:** `/api/v1` 硬编码前缀,无版本管理策略。 **方案:** ```rust // server/mod.rs let api_v1 = axum::Router::new() .route("/posts", get(post::list).post(post::create)) // ... let api_v2 = axum::Router::new() .route("/posts", get(post_v2::list).post(post_v2::create)) // ... breaking changes let app = axum::Router::new() .nest("/api/v1", api_v1) .nest("/api/v2", api_v2) ``` **版本兼容原则:** - 新增字段:向前兼容,不算 breaking change - 删除/重命名字段:新版本 - 新增端点:在当前版本添加 - 废弃端点:响应头 `Deprecation: true`,至少保留一个大版本 #### 2.9 审计日志 **现状:** 无操作记录。 **方案:** ```sql -- migrations/004_audit_log.sql CREATE TABLE IF NOT EXISTS audit_logs ( id TEXT PRIMARY KEY, user_id TEXT, action TEXT NOT NULL, -- 'post.create', 'user.update_role', 'comment.delete' resource_type TEXT NOT NULL, -- 'post', 'user', 'comment', 'media' resource_id TEXT, detail TEXT, -- JSON 格式的变更详情 ip_address TEXT, user_agent TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_logs(user_id); CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_logs(created_at); ``` ```rust // src/middleware/audit.rs pub async fn audit_middleware( // 记录所有写操作(POST/PUT/DELETE) // 自动提取 user_id、action、resource 信息 ) ``` --- ### P2 — 做大时才需要 #### 2.10 水平扩展 **现状:** 单实例部署,内存存储(限流器、会话)。 **需要改造:** | 组件 | 当前 | 目标 | |------|------|------| | 限流器 | `MemoryStore` | `RedisStore`(trait 已预留) | | 会话/缓存 | 无 | Redis | | 文件存储 | 本地磁盘 | S3 兼容对象存储 | | 搜索 | SQLite FTS5 | Meilisearch(独立服务) | | 任务队列 | 内存 | Redis Stream / 独立 worker | **改造后架构:** ``` ┌─────────┐ │ Nginx │ (负载均衡) └────┬────┘ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ App :3001│ │ App :3002│ │ App :3003│ (无状态实例) └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ └─────────────┼─────────────┘ │ ┌─────────┼─────────┐ ▼ ▼ ▼ ┌──────┐ ┌────────┐ ┌──────┐ │ Redis│ │ PostgreSQL│ │ S3 │ └──────┘ └────────┘ └──────┘ ``` #### 2.11 可观测性 **现状:** `tracing` 日志已有,但缺 metrics 和分布式追踪。 **方案:** ```toml # Cargo.toml 新增 [dependencies] prometheus = "0.13" metrics = "0.24" metrics-exporter-prometheus = "0.16" ``` **关键指标:** | 类别 | 指标 | |------|------| | HTTP | 请求量、延迟分布、错误率(按路由/方法/状态码) | | 数据库 | 连接池使用率、查询延迟、慢查询 | | 插件 | 加载时间、Hook 执行时间、内存占用 | | 业务 | 文章发布量、评论量、注册量、活跃用户 | | 系统 | CPU、内存、磁盘、网络 | #### 2.12 内容版本与草稿自动保存 **现状:** 只有 draft/published 两种状态,无版本历史。 **方案:** ```sql -- migrations/005_post_revisions.sql CREATE TABLE IF NOT EXISTS post_revisions ( id TEXT PRIMARY KEY, post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, revision_number INTEGER NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, excerpt TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(post_id, revision_number) ); ``` - 每次更新文章时自动创建一个 revision - 支持对比任意两个版本 - 支持回滚到指定版本 - 草稿自动保存:前端定时 POST,后端 upsert #### 2.13 CI/CD + Docker **现状:** guide.md 有 Dockerfile 模板但未实际创建。 **需要创建:** - `Dockerfile` — 多阶段构建(builder → runtime) - `docker-compose.yml` — 开发环境(app + redis + postgres) - `.github/workflows/ci.yml` — fmt + clippy + test + build - `docker-compose.prod.yml` — 生产部署模板 --- ## 3. 架构改造要点 ### 3.1 事件总线(EventBus) 当前 Plugin Hook 是同步链式调用,无法解耦。平台级系统需要事件驱动架构。 ```rust // src/eventbus/mod.rs use tokio::sync::broadcast; pub struct EventBus { tx: broadcast::Sender>, } #[derive(Debug, Clone)] pub enum Event { PostCreating { data: PostCreatingData }, PostCreated { post_id: String }, PostUpdated { post_id: String, old: PostSnapshot, new: PostSnapshot }, PostDeleted { post_id: String }, CommentCreated { comment_id: String, post_slug: String }, UserRegistered { user_id: String }, LoginAttempt { email: String, success: bool, ip: String }, } impl EventBus { pub fn new(capacity: usize) -> Self { let (tx, _) = broadcast::channel(capacity); Self { tx } } pub async fn emit(&self, event: Event) { let _ = self.tx.send(Arc::new(event)); } pub fn subscribe(&self) -> broadcast::Receiver> { self.tx.subscribe() } } ``` **订阅者:** ``` EventBus.emit(PostCreated) ├── PluginManager.dispatch_action(PostCreated) // 插件 Hook ├── NotificationService.notify(author) // 邮件/站内通知 ├── SearchIndexer.index(post) // 搜索索引同步 ├── AuditLogger.log("post.create", ...) // 审计日志 └── CacheInvalidator.invalidate("posts:*") // 缓存失效 ``` ### 3.2 Repository 抽象 当前 Model 层直接写 SQL,耦合数据库实现。复杂平台需要抽象层以便 mock、缓存、换引擎。 ```rust // src/repositories/mod.rs #[async_trait] pub trait PostRepository: Send + Sync { async fn find_by_slug(&self, slug: &str) -> AppResult>; async fn list(&self, filter: PostFilter, page: i64, page_size: i64) -> AppResult>; async fn create(&self, input: CreatePostInput) -> AppResult; async fn update(&self, id: &str, input: UpdatePostInput) -> AppResult; async fn delete(&self, id: &str) -> AppResult<()>; } // src/repositories/sqlx_post.rs pub struct SqlxPostRepository { pool: Pool } // src/repositories/cached_post.rs pub struct CachedPostRepository { inner: P, cache: Arc, } ``` **好处:** - `CachedPostRepository` 装饰器模式,为任意 Repository 添加缓存 - 测试时 mock `PostRepository`,隔离数据库 - 未来切换 ORM 或搜索引擎时只改 Repository 实现 ### 3.3 配置热更新 当前 `AppConfig` 启动时一次性加载。平台级系统需要运行时配置变更。 ```rust // src/config/runtime.rs pub struct RuntimeConfig { inner: Arc>, } impl RuntimeConfig { pub async fn reload(&self, new_config: AppConfig) { *self.inner.write().await = new_config; } pub async fn get(&self) -> tokio::sync::RwLockReadGuard<'_, AppConfig> { self.inner.read().await } } ``` **可热更新的配置项:** - 每页文章数 - 限流阈值 - 插件启用/禁用 - CORS 白名单 - 功能开关(Feature Flags) --- ## 4. 演进路线 ### 阶段 1 — 基础设施(2-3 周) ``` Week 1: ├── SQLite 事务包裹关键操作(sync_tags、create_post、delete_post) ├── FTS5 全文搜索(零依赖,SQLite 内建) └── EventBus trait + broadcast 实现 Week 2: ├── CacheStore trait + MemoryCache 实现 ├── 文章列表/详情缓存集成 └── 后台任务队列(SQLite 持久化方案) Week 3: ├── 媒体处理管线(image crate:缩略图 + WebP) ├── 审计日志表 + 中间件 └── 集成测试覆盖新功能 ``` ### 阶段 2 — 核心功能(2-3 周) ``` Week 4: ├── SSE 实时推送(新评论、通知) ├── 通知系统框架(Email + InApp) └── 邮件发送(lettre crate) Week 5: ├── 内容版本/修订历史 ├── 草稿自动保存 API └── API 版本化策略落地 Week 6: ├── Admin Dashboard API(统计、管理) ├── RedisStore 实现(限流器 + 缓存) └── 集成测试 + 性能测试 ``` ### 阶段 3 — 平台化(按需) ``` ├── Repository 抽象层 ├── 配置热更新 ├── Prometheus metrics 集成 ├── Docker + docker-compose + CI/CD ├── Meilisearch 替换 FTS5 ├── S3 对象存储支持 └── 水平扩展验证(多实例部署测试) ``` --- ## 5. 依赖新增预估 ### 阶段 1 | 依赖 | 用途 | 体积影响 | |------|------|---------| | `tokio` (broadcast) | EventBus | 已有,零增长 | | `image` | 图片处理 | +~3MB 编译 | | 无新增 | FTS5 是 SQLite 内建 | 零 | ### 阶段 2 | 依赖 | 用途 | 体积影响 | |------|------|---------| | `lettre` | 邮件发送 | +~2MB | | `redis` | 缓存/限流/队列 | +~1MB | ### 阶段 3 | 依赖 | 用途 | 体积影响 | |------|------|---------| | `prometheus` | 指标采集 | +~1MB | | `rust-s3` | 对象存储 | +~2MB | | `meilisearch-sdk` | 搜索引擎客户端 | +~0.5MB | --- ## 6. 风险与注意事项 ### 6.1 性能 - WASM 插件每次 Hook 调用有 ~10-100μs 开销,高频场景需评估 - Redis 引入网络延迟,缓存策略需权衡 TTL 和一致性 - EventBus broadcast 需控制容量,避免内存溢出 ### 6.2 兼容性 - FTS5 需要 SQLite 编译时启用(大部分发行版已默认启用) - `image` crate 的 WebP 编码需要 `webp` feature flag - Meilisearch 需要独立部署,增加运维复杂度 ### 6.3 迁移策略 - 所有新功能通过 feature flag 控制,不影响现有功能 - 缓存默认关闭(`MemoryCache`),生产环境启用 Redis - 搜索默认 FTS5,通过配置切换 Meilisearch - 每个阶段完成后确保 `cargo test` + `cargo clippy` 全部通过