架构与模块划分

源码结构

docbit/src/
├── main.rs              # 组合根:仅 Host 配置
├── startup.rs           # DbInitService(IHostedService)
├── common/
│   ├── bootstrap.rs     # AppPaths + DbContext(唯一手动 DI)
│   ├── paths.rs         # 数据目录解析
│   └── mod.rs           # 拦截器、授权器(#[inject] 自动注册)
├── contracts/           # Request/Response/enum/I…Service trait
├── handlers/            # Handler + Service 实现(#[inject] + #[handler(inject)])
│   ├── doc_service.rs   # DocService(impl IDocumentService)
│   └── blog_service.rs  # BlogService(impl IBlogService)
└── domain/              # 实体 + EF 迁移

main.rs

let host = Host::builder()
    .mode(AppMode::Development)
    .register(common::bootstrap::configure)
    .use_spa(wwwroot)
    .add_authentication()
    .add_memory_cache()
    .build();

host.run().await?;

`main.rs` 不做业务注册。Handler、`IHostedService`、业务 Service、`IDynamicAuthorizer` 均由 `ServiceCollection::from_injected()` 自动收集。

bootstrap.rs

仅注册框架无法自动构造的基础设施:

- `AppPaths` — docs / blog-data / wwwroot / 数据库路径 - `Mutex<DbContext>` — rust-ef SQLite 上下文

业务 Service 在 `handlers/` 通过 `#[inject] (implements I…Service)` 自注册。

startup.rs

`DbInitService` 注入 `Arc<dyn IDocumentService>`,在 `start()` 中:

1. 运行 EF 迁移(m001–m004) 2. 生成缺失的文档 `INDEX.json` 3. 同步作品集 logo 到 `wwwroot`

请求数据流

GET /api/docs/{work}/index
    → contracts/docs.rs (GetDocIndexRequest)
    → handlers/docs.rs (GetDocIndexHandler)
        → Arc<dyn IDocumentService>::index()
    → DocIndex JSON (contracts DTO)

Handler 不感知 `DocService` 具体类型,测试时可替换 mock 实现。

小结

Docbit 演示 rust-webx 推荐的完整分层:**契约(含接口)→ Handler 履约 → 领域/基础设施**。面向接口,组合根最小化。

下一节:[可复用的模式提炼](docbit-patterns.md)