后端 API 调用流程图全集
基于
blog/backend实际代码(axum + 多 crate 工作区)梳理的全量 API 调用流程图。 覆盖:全局请求生命周期、四大路由组全部端点、后台(Console)每一类 UI 交互对应的后端响应流程、媒体/上传/缩略图数据面、异步事件与定时任务。图表均为 Mermaid 格式,可在支持 Mermaid 的 Markdown 查看器中直接渲染。
配套文档:端点清单 / 鉴权分层 / 限流总表见
api-atlas.md(143 端点:游客 37 / 登录 31 / 管理 75)。本文件聚焦「流程图」视角,二者交叉印证。源码校验(2026-06-12):已逐一比对
crates/api/src/router/与middleware/。全局中间件栈顺序、鉴权链、ETag(仅application/json且 ≤512KiB)、CSRF 均与源码一致;本轮修正了幂等中间件流程的 3 处偏差:① 幂等键为POST:{path}:{actor}:{idempotency-key}(不含 body 哈希);② 并发同键 → 立即409 RESOURCE_CONFLICT(不排队等待);③ 响应体 > 4MB →500,且仅缓存状态码 ∈ [200,500) 的响应。详见 §7.1 / §11。
目录
1. 分层架构总览
flowchart TD
subgraph CLIENT["客户端"]
UI1["前台站点(Next.js 公开页)"]
UI2["后台 Console(/console 管理界面)"]
end
subgraph BIN["server(bin crate)main.rs + bootstrap"]
BOOT["bootstrap::assemble_services<br/>组装 AppState / 注册事件订阅 / 启动定时任务"]
FE["server_frontend::FrontendLauncher<br/>内嵌前端进程 + fallback 反向代理"]
end
subgraph API["crates/api(HTTP 层)"]
ROUTER["router/:public / comment / account / admin"]
MW["middleware/:auth · csrf · etag · idempotency<br/>rate_limit · concurrency · load_shed · timeout ..."]
HANDLER["handler/:article · auth · comment · page · file<br/>media · thumbnail · setting · user · admin · oauth ..."]
RESP["response::ApiResponse(统一 JSON 信封 + i18n)"]
end
subgraph SVC["crates/service(业务层,trait 接口)"]
AUTHS["auth / user / oauth / slider_captcha"]
CONTENT["article / page / comment / post_tag / post_category<br/>article_history / seo / search"]
STORE_S["file / upload / thumbnail / image_style / volume"]
MISC["setting / site_config_v2 / invalidation / revalidate<br/>cdn / email / music / geoip / operation"]
end
subgraph INFRA["crates/infra(仓储实现)"]
REPO["repo/*:article_repo · comment_repo · user_repo<br/>file_repo · page_repo · setting_repo · site_config_v2_repo ..."]
DB[("数据库<br/>SQLite / MySQL / PostgreSQL(sea-orm)")]
end
subgraph CACHE["crates/cache"]
CSVC["CacheService trait<br/>RedisCacheService / MemoryCacheService"]
end
subgraph STORAGE["crates/storage"]
PROV["StorageProvider trait<br/>本地磁盘 / AWS S3(按存储策略挂载)"]
end
subgraph ASYNC["异步设施"]
BUS["crates/event::EventBus<br/>15 个 Topic,2 worker,256 队列"]
TASK["crates/task:定时任务<br/>session 清理 · 文件回收 · 计数修复 ..."]
end
UI1 -->|"HTTP /api/v3/*"| ROUTER
UI2 -->|"HTTP /api/v3/console/*"| ROUTER
ROUTER --> MW --> HANDLER
HANDLER --> RESP
HANDLER --> SVC
SVC --> REPO --> DB
SVC --> CSVC
SVC --> PROV
HANDLER -->|"CacheEventPublisher(端口)"| BUS
BUS --> SVC
TASK --> SVC
BIN --> API
BOOT --> SVC
FE -.->|"非 /api 路由 fallback 代理"| UI1
关键依赖原则:api 层只依赖 service 的 trait(AppState 内全部是 Arc<dyn Trait>),不直接触达 infra/storage;事件通过 CacheEventPublisher 端口发布,handler 永不直接持有具体事件类型。
2. 服务启动流程
flowchart TD
A["main()"] --> B["configure mimalloc purge / tracing 初始化"]
B --> C["debug 构建且 REVALIDATE_TOKEN 为空?<br/>注入 dev 默认 token"]
C --> D["AppConfig::load()(ini + env)"]
D --> E["构建 tokio Runtime<br/>worker_threads / blocking / stack 可配"]
E --> F["i18n::init(locale)"]
F --> G["server_infra::database::connect<br/>SQLite / MySQL / PG"]
G --> H{"connect_cache"}
H -->|Redis 可用| I["RedisCacheService(跨实例)"]
H -->|否则| J["MemoryCacheService(单进程)"]
I --> K{"BACKEND_REQUIRE_REDIS=1<br/>且实际是内存缓存?"}
J --> K
K -->|是| K1["启动失败 bail!"]
K -->|否| L["init_trusted_proxies_config<br/>(信任代理 → 真实 IP 判定)"]
L --> M["EventBus::new()(2 worker / 256 队列)"]
M --> N["Bootstrapper::initialize<br/>迁移 + 种子数据(可 skip_migration)"]
N --> O["bootstrap::assemble_services<br/>组装 AppState:content/identity/storage/infra/capabilities<br/>注册事件订阅者 + 启动 housekeeping 循环"]
O --> P["build_full_router_with_config<br/>挂全局中间件栈"]
P --> Q["追加 /_next/webpack-hmr WS 代理<br/>+ fallback:/api/* → JSON 404,其余 → 内嵌前端代理"]
Q --> R["TcpListener bind 0.0.0.0:port"]
R --> S["frontend_launcher.start()(内嵌 Next.js)"]
S --> T["axum::serve + graceful_shutdown(ctrl_c)"]
T --> U["关停:stop 前端 → 通知 housekeeping → 等待 drain"]
3. 请求生命周期:全局中间件栈
所有进入 /api/v3/*、/assets/media/* 的请求都按以下顺序穿过全局层(router/mod.rs,自外向内):
flowchart TD
REQ(["客户端请求"]) --> TIF["track_in_flight<br/>在途请求计数"]
TIF --> TCI["trusted_client_ip<br/>按信任代理规范化真实 IP(x-real-ip)"]
TCI --> LS["load_shed_mw(global-load)<br/>全局并发超限 → 503"]
LS --> PIC["per_ip_concurrency_mw(global-per-ip)<br/>单 IP 并发超限 → 429<br/>白名单:/console/files/upload-sessions"]
PIC --> GRL["rate_limit_mw(global-route)<br/>全局令牌桶 RPM+burst → 429"]
GRL --> SEC["security_headers<br/>X-Frame-Options / nosniff / referrer-policy ..."]
SEC --> RID["request_id<br/>生成 / 透传 x-request-id"]
RID --> CORS["cors_layer(按配置 origin 白名单)"]
CORS --> COMP["compression_layer(gzip/br 响应压缩)"]
COMP --> PTL["normalize_payload_too_large_mw<br/>413 统一为 JSON"]
PTL --> TO["request_timeout_mw<br/>超时 → JSON 408"]
TO --> ROUTE{"路由匹配"}
ROUTE -->|public 组| PUB["etag_mw + public_cache"]
ROUTE -->|comment 组| CMT["private_cache + 各端点限流"]
ROUTE -->|account 组| ACC["require_auth → csrf_guard → private_cache"]
ROUTE -->|admin 组| ADM["require_auth → require_admin → csrf_guard → private_cache"]
ROUTE -->|"/assets/media"| MEDIA["serve_media(无 etag,自带 Cache-Control)"]
ROUTE -->|路径匹配但方法不符| M405["method_not_allowed → JSON 405"]
ROUTE -->|"/api/* 未匹配"| M404["fallback → JSON 404(RESOURCE_NOT_FOUND)"]
ROUTE -->|非 API 未匹配| FEP["内嵌前端反向代理"]
PUB --> H["Handler → Service → Repo/Cache/Storage"]
CMT --> H
ACC --> H
ADM --> H
H --> RESPONSE(["ApiResponse JSON / 二进制流"])
速率/并发控制要点:
| 限制器 | 作用域 | 后端 | 超限响应 |
|---|---|---|---|
| RateLimiter(global-route) | 全局 RPM+burst | CacheService(Redis 时跨实例) | 429 |
| RateLimiter(per-scope) | 单端点(login、comment-create…) | 同上 | 429 |
| ConcurrencyLimiter(global-per-ip) | 单 IP 在途并发 | 同上 | 429 |
| LoadShed(global-load) | 全实例总并发 | 同上 | 503 |
4. 路由总图(按路由组)
4.1 public 组(匿名可访问,etag_mw + public_cache)
flowchart LR
subgraph PUBLIC["public 路由组(匿名 + ETag + 公共缓存)"]
direction TB
P1["GET /api/v3/healthz → health_check"]
P2["GET /api/v3/main · /main/version → 410 Gone(v1 退役)<br/>GET /api/v3/site-config-v2 → get_site_config_v2"]
P3["POST /api/v3/session → login ⏱login限流<br/>POST /api/v3/admin/session → admin_login ⏱<br/>POST /api/v3/users → register ⏱<br/>GET /api/v3/auth/check-email → check_email ⏱"]
P4["POST /api/v3/auth/captcha/sliders → issue_slider_captcha ⏱<br/>POST /api/v3/auth/captcha/sliders/verifications → verify_slider_captcha ⏱"]
P5["GET /api/v3/articles → list_articles<br/>GET /api/v3/articles/home → list_home<br/>GET /api/v3/articles/archives → list_archives<br/>GET /api/v3/articles/statistics → get_statistics<br/>GET /api/v3/articles/url-resolutions → get_article_by_url<br/>GET /api/v3/articles/:id → get_article"]
P6["GET /api/v3/tags → list_tags<br/>GET /api/v3/categories → list_categories"]
P7["GET /api/v3/pages → list_pages<br/>GET /api/v3/pages/*path → get_page_by_path"]
P8["GET /api/v3/search → search"]
P9["GET /api/v3/music/playlist → get_playlist<br/>GET /api/v3/music/resource-resolutions → get_song_resources"]
P10["GET /rss.xml · /sitemap.xml · /robots.txt → seo handlers"]
end
4.2 comment 组(公开评论,逐端点限流,private_cache)
flowchart LR
subgraph COMMENT["comment 路由组"]
direction TB
C1["GET /api/v3/comments → list_comments ⏱comment-list"]
C2["POST /api/v3/comments → create_comment<br/>⏱comment-create → require_auth → csrf_guard → 幂等(idempotency_mw)"]
C3["GET /api/v3/comments/latest → list_latest_comments ⏱"]
C4["GET /api/v3/comments/ip-location → get_comment_ip_location ⏱"]
C5["POST/DELETE /api/v3/comments/:id/like → like/unlike_comment ⏱"]
C6["GET /api/v3/comments/:id/children → list_comment_children ⏱"]
end
4.3 account 组(登录用户,require_auth + csrf_guard + private_cache)
flowchart LR
subgraph ACCOUNT["account 路由组(登录用户)"]
direction TB
A1["GET/DELETE /api/v3/session → get_session / logout"]
A2["GET /api/v3/me → get_current_user<br/>PATCH /api/v3/me/profile → update_profile<br/>PUT /api/v3/me/password → update_password<br/>POST /api/v3/me/avatar → upload_avatar(≤5MB)"]
A3["GET /api/v3/me/sessions → list_my_sessions<br/>DELETE /api/v3/me/sessions/others → invalidate_other_sessions<br/>DELETE /api/v3/me/sessions/:sessionId → logout_session_by_id"]
A4["GET/POST /api/v3/me/articles → list_my_articles / create_article<br/>GET/PATCH/DELETE /api/v3/me/articles/:id<br/>POST /api/v3/me/articles/images(≤10MB)<br/>POST /api/v3/me/articles/draft-image-cleanup-jobs"]
A5["文章历史:<br/>GET /api/v3/me/articles/:id/history(list/summary/comparisons)<br/>GET /api/v3/me/articles/:id/history/:version<br/>POST .../history/:version/restorations<br/>GET /api/v3/article-histories/:id"]
A6["OAuth 绑定:<br/>GET /api/v3/me/oauth/bindings<br/>POST /api/v3/me/oauth/bind/:provider<br/>DELETE /api/v3/me/oauth/bindings/:id"]
A7["自评论:PATCH /api/v3/comments/:id/content ⏱<br/>DELETE /api/v3/comments/:id ⏱<br/>POST /api/v3/comments/images(≤5MB)⏱"]
A8["GET /api/v3/operations/:operationId → get_operation(异步任务查询)"]
A9["GET /api/v3/console/settings/by-keys → console_get_settings_by_keys"]
end
4.4 admin 组(Console 管理端,require_auth → require_admin → csrf_guard)
见 §9 后台 Console 的逐模块流程;端点清单:
| 模块 | 端点 |
|---|---|
| 设置 | PATCH /console/settings · GET /console/settings/export · POST /console/settings/import · POST /console/settings/backup/export-jobs · POST /console/settings/backup/import-jobs · POST /console/settings/test-email |
| 统计 | GET /console/statistics/summary |
| 缓存 | POST /console/cache/invalidation-jobs · POST /console/cache/revalidation-jobs |
| 存储策略 | GET/POST /console/storage-policies · GET/PATCH/DELETE /console/storage-policies/:id · POST /console/storage-policies/:id/direct-upload-tokens |
| 缩略图 | POST /console/thumbnails/regeneration-jobs · POST /console/thumbnails/directories/regeneration-jobs · GET /console/files/:file_id/thumbnail |
| 用户 | GET /console/users · PATCH /console/users/:id/status · DELETE /console/users/:id · GET/PUT /console/users/permissions/default · GET/PUT/DELETE /console/users/:id/permissions |
| 标签/分类 | GET/POST /console/tags · PATCH/DELETE /console/tags/:id · POST /console/tags/count-repair-jobs(categories 同构) |
| 文章批量 | POST /console/articles/deletion-jobs · status-jobs · export-jobs · import-jobs |
| 页面 | GET/POST /console/pages · GET/PATCH/DELETE /console/pages/:id · POST /console/pages/initialization-jobs · import-jobs · export-jobs |
| 评论 | GET /console/comments · PATCH :id/moderation-state · :id/pin-state · :id/info · :id(改内容)· DELETE :id · POST export/import/deletion-jobs |
| 文件 | GET /console/files · GET /console/files/tree · POST/PATCH/DELETE /console/files/directories · POST /console/files/system-media · POST /console/files/access-tokens · POST /console/files/deletion-jobs · GET/PATCH/DELETE /console/files/lookups |
| 上传会话 | POST /console/files/upload-sessions(幂等)· GET/DELETE .../:session_id · PUT .../:session_id/chunks/:index · POST .../:session_id/completions |
4.5 顶层独立路由
flowchart LR
subgraph TOP["顶层路由(不属于四大组)"]
direction TB
T1["GET /assets/media/*path_with_style → serve_media(§10)"]
T2["GET /api/v3/proxy/download → proxy_download ⏱proxy-download<br/>(SSRF 防护的安全外链下载代理)"]
T3["GET /api/v3/auth/:provider/connect → oauth_authorize ⏱oauth-connect"]
T4["GET /api/v3/auth/:provider/callback → oauth_callback"]
T5["ANY /_next/webpack-hmr → WS 代理(开发态 HMR)"]
T6["fallback:/api/* → JSON 404;其余 → 内嵌前端代理"]
end
5. 认证与会话流程
5.1 登录(前台 POST /api/v3/session / 后台 POST /api/v3/admin/session)
sequenceDiagram
autonumber
participant UI as 登录页 前台/Console
participant RL as rate_limit_mw (session-login / admin-session-login)
participant H as login / admin_login handler
participant CAP as SliderCaptchaService
participant AUTH as AuthService
participant DB as user / user_session 表
participant C as CacheService
UI->>RL: POST email+password (+captchaToken/fingerprint)
RL-->>UI: 超限 → 429
RL->>H: 放行
alt 滑块验证码启用
H->>CAP: consume_token(token, "login", ip, fingerprint)
CAP-->>H: 失败 → 直接返回错误状态码
end
H->>AUTH: login(email, password, ip, ua)
Note right of AUTH: admin 模式用 login_admin 校验 is_admin
AUTH->>DB: 校验密码哈希 / 用户状态
AUTH->>DB: 写入会话记录 credential + csrf_token + expires
AUTH->>C: 会话缓存
AUTH-->>H: LoginResult
alt 成功
H-->>UI: 200 + Set-Cookie: session HttpOnly + csrf_token
else 失败
H->>H: FAILED_LOGIN_COUNT++
H-->>UI: 4xx + i18n 结构化错误
end
5.2 受保护请求的鉴权链(account / admin 组每个请求都会执行)
flowchart TD
REQ["携带 Cookie 的请求"] --> EA["require_auth:解析 Cookie 中 session=..."]
EA -->|无 cookie| E401a["401 auth.not_logged_in"]
EA --> GS["auth_svc.get_current_user(credential)<br/>(缓存优先,回源 user_session 表)"]
GS -->|会话无效/过期| E401b["401 auth.session_expired"]
GS -->|有效| EXT["注入 AuthUser 扩展<br/>(id/db_id/username/is_admin/credential...)"]
EXT --> ADM{"admin 组?"}
ADM -->|是| RA["require_admin:AuthUser.is_admin?"]
RA -->|否| E403a["403 permission_denied_admin"]
RA -->|是| CS
ADM -->|否(account 组)| CS["csrf_guard"]
CS --> M{"GET/HEAD/OPTIONS?"}
M -->|是| PASS["放行(读操作免 CSRF)"]
M -->|否(写操作)| TK["读取 x-csrf-token 请求头"]
TK -->|缺失| E403b["403 CSRF_VALIDATION_FAILED"]
TK --> VAL["auth_svc.validate_csrf(credential, token)<br/>(token 必须与该会话绑定)"]
VAL -->|不匹配| E403b
VAL -->|匹配| PASS
PASS --> HANDLER["业务 handler"]
5.3 滑块验证码(注册/登录前置)
flowchart TD
A["UI 打开验证码"] --> B["POST /auth/captcha/sliders ⏱issue<br/>issue_slider_captcha"]
B --> C["SliderCaptchaService 生成拼图<br/>底图+缺口图+puzzle offset,存 CacheService(带 TTL)"]
C --> D["UI 拖动滑块"]
D --> E["POST /auth/captcha/sliders/verifications ⏱verify<br/>verify_slider_captcha"]
E --> F{"偏移误差是否在容差内?"}
F -->|否| G["失败:剩余尝试次数--,可重试"]
F -->|是| H["签发一次性 captchaToken(绑定场景/IP/指纹)"]
H --> I["登录/注册请求携带 captchaToken<br/>服务端 consume_token 一次性核销"]
5.4 注销与会话管理
flowchart TD
L1["DELETE /api/v3/session → logout"] --> L2["auth_svc.logout(credential)<br/>删除会话记录+缓存"]
L2 --> L3["Set-Cookie 清空 session 与 csrf_token(Max-Age=0)"]
S1["GET /api/v3/me/sessions"] --> S2["列出当前用户全部活跃会话(设备/IP/UA)"]
S3["DELETE /api/v3/me/sessions/others"] --> S4["除当前 credential 外全部失效"]
S5["DELETE /api/v3/me/sessions/:sessionId"] --> S6["按 ID 精确踢出单个会话"]
5.5 OAuth 第三方登录 / 绑定
sequenceDiagram
autonumber
participant UI as 浏览器
participant BE as 后端
participant IDP as OAuth Provider(github 等)
UI->>BE: GET /api/v3/auth/:provider/connect ⏱oauth-connect
BE->>BE: OAuthService 生成 state(防 CSRF,存缓存)
BE-->>UI: 302 → IdP 授权页
UI->>IDP: 用户授权
IDP-->>UI: 302 → /api/v3/auth/:provider/callback?code&state
UI->>BE: GET callback
BE->>BE: 校验 state
BE->>IDP: code 换 access_token → 拉取用户信息
BE->>BE: oauth_binding_repo 查绑定
alt 已绑定
BE->>BE: 直接建会话(同登录成功)
else 未绑定
BE->>BE: 按策略创建用户或要求绑定已有账号
end
BE-->>UI: Set-Cookie + 跳回前端
Note over UI,BE: 已登录用户管理绑定:GET /me/oauth/bindings · POST /me/oauth/bind/:provider · DELETE /me/oauth/bindings/:id
6. 公开内容 API 流程
6.1 公共读路径通用流程(ETag + 公共缓存)
flowchart TD
REQ["GET 公开端点"] --> PC["public_cache:写 Cache-Control 公共缓存头"]
PC --> ET["etag_mw:仅小 JSON 响应启用"]
ET --> H["handler → QueryService"]
H --> CK{"应用级缓存命中?<br/>(CacheService key 按资源+参数)"}
CK -->|HIT| RESP1["直接组装 ApiResponse"]
CK -->|MISS| REPO["Repo 查询 DB → 写回缓存"]
REPO --> RESP1
RESP1 --> ETC{"etag_mw:响应体哈希 == If-None-Match?"}
ETC -->|是| R304["304 Not Modified(空体)"]
ETC -->|否| R200["200 + ETag 头 + JSON"]
6.2 文章读取流程
flowchart TD
A1["GET /articles(分页/标签/分类筛选)"] --> Q["ArticleQueryService"]
A2["GET /articles/home(首页流)"] --> Q
A3["GET /articles/archives(归档)"] --> Q
A4["GET /articles/statistics(计数统计)"] --> Q
A5["GET /articles/url-resolutions?url=...<br/>(按自定义 URL 反查文章)"] --> Q
A6["GET /articles/:id(详情)"] --> Q
Q --> R["article_query_adapter / article_full_repo"]
R --> DB[("articles + 关联表<br/>tags / categories / 作者")]
Q --> C[("CacheService<br/>列表与详情分键缓存")]
A6 --> VC["浏览计数累加(异步,不阻塞响应)"]
6.3 搜索 / SEO / 音乐
flowchart TD
S1["GET /api/v3/search?q=..."] --> S2{"capabilities.searcher 可用?"}
S2 -->|是| S3["Searcher(全文索引)查询"]
S2 -->|否| S4["回退 LIKE 检索(search_repo)"]
S3 --> S5["统一搜索结果 JSON"]
S4 --> S5
SEO1["GET /rss.xml"] --> SEOSVC["SeoService:取最新已发布文章渲染 XML"]
SEO2["GET /sitemap.xml"] --> SEOSVC
SEO3["GET /robots.txt"] --> SEOSVC
M1["GET /music/playlist"] --> M2["MusicService(capability,可关闭)<br/>外部音乐平台 API + 缓存"]
M3["GET /music/resource-resolutions"] --> M2
7. 评论系统流程
7.1 发表评论(写路径全防护链)
sequenceDiagram
autonumber
participant UI as 评论框
participant RL as ⏱comment-create
participant AU as require_auth
participant CS as csrf_guard
participant ID as idempotency_mw (comment-create store)
participant H as create_comment
participant SVC as CommentService
participant DB as comment_repo
participant BUS as EventBus
UI->>RL: POST /api/v3/comments(携带幂等键)
RL->>AU: 限流放行
AU->>CS: 注入 AuthUser
CS->>ID: CSRF 校验通过
ID->>ID: 复合键 POST:path:actor:idempotency-key(actor=u:{db_id})
alt 已有缓存结果
ID-->>UI: 重放上次响应 + 头 idempotent-replayed:1
else 并发同键在途
ID-->>UI: 409 RESOURCE_CONFLICT(不等待)
else 首次(拿到锁 TTL 60s)
ID->>H: 执行
H->>SVC: create(内容清洗、审核状态判定、楼层)
SVC->>DB: INSERT comment
SVC-->>H: 新评论
H->>BUS: cache_events.comment_created()
H-->>ID: 200/201 响应
ID->>ID: 缓存响应(状态 200-499 且 ≤4MB,TTL 24h)
ID-->>UI: 返回响应
end
BUS-->>BUS: 订阅者:失效评论缓存 + 触发 SSR revalidate
7.2 评论读取与互动
flowchart TD
R1["GET /comments?path=...(按文章/页面分页)⏱"] --> CSVC["CommentService"]
R2["GET /comments/latest ⏱"] --> CSVC
R3["GET /comments/:id/children(楼中楼分页)⏱"] --> CSVC
R4["GET /comments/ip-location ⏱"] --> GEO["GeoIP 服务(IP → 属地,缓存)"]
L1["POST /comments/:id/like ⏱"] --> LK["点赞(按访客指纹/用户去重)"]
L2["DELETE /comments/:id/like ⏱"] --> LK
CSVC --> REPO["comment_repo:树形组装 + 审核状态过滤<br/>(pending 仅本人可见 → private_cache)"]
7.3 本人评论编辑/删除(account 组)
flowchart TD
E1["PATCH /comments/:id/content ⏱own-update"] --> O{"评论属于当前用户?"}
E2["DELETE /comments/:id ⏱own-delete"] --> O
O -->|否| F403["403"]
O -->|是| DO["CommentService 更新/软删"] --> EV["comment_updated / comment_deleted 事件"]
8. 用户中心(/me)流程
8.1 资料 / 密码 / 头像
flowchart TD
P1["PATCH /me/profile(昵称/简介/链接)"] --> US["UserService.update_profile"] --> UDB[("user 表")]
P2["PUT /me/password"] --> PV["校验旧密码 → 写新哈希"] --> INV["可选:失效其他会话"]
P3["POST /me/avatar(multipart ≤5MB)"] --> AV["图像安全校验(image_safety)<br/>→ 压缩/裁剪 → StorageProvider 写入"] --> AURL["更新 user.avatar URL"]
8.2 我的文章(创作流)
flowchart TD
A1["GET /me/articles(自己的列表)"] --> WS["ArticleWriteService / QueryService"]
A2["POST /me/articles(新建草稿)"] --> WS
A3["PATCH /me/articles/:id(保存/发布)"] --> WS
A4["DELETE /me/articles/:id"] --> WS
WS --> OWN{"作者 == 当前用户?"}
OWN -->|否| E403["403"]
OWN -->|是| ARErepo["article_repo 持久化"]
A3 --> HIST["ArticleHistoryService<br/>保存历史版本快照"]
A3 --> EVT["发布状态变化 → ArticlePublished/Updated 事件<br/>→ 缓存失效 + SSR revalidate"]
I1["POST /me/articles/images(≤10MB)"] --> IMG["图像校验 → 存储 → 返回媒体 URL<br/>(草稿期图片标记待清理)"]
I2["POST /me/articles/draft-image-cleanup-jobs"] --> CLEAN["清理未被正文引用的草稿图"]
8.3 文章历史版本
flowchart TD
H1["GET /me/articles/:id/history"] --> HS["ArticleHistoryService"]
H2["GET /me/articles/:id/history/summary"] --> HS
H3["GET /me/articles/:id/history/comparisons?from=&to=<br/>(两版本 diff)"] --> HS
H4["GET /me/articles/:id/history/:version"] --> HS
H5["POST /me/articles/:id/history/:version/restorations"] --> RES["回滚:以历史版本内容覆盖当前<br/>(同时再记一条新历史)"]
HS --> HREPO[("article_history_repo")]
9. 后台 Console:UI 交互 → 后端流程
所有 Console 请求统一先过:
require_auth → require_admin → csrf_guard → private_cache(见 §5.2)。 下面按后台 UI 页面/交互逐一展开。
9.0 Console 请求骨架(每个图默认省略该前缀)
flowchart LR
UI["Console UI 操作"] --> G["鉴权链:auth → admin → csrf"]
G --> H["console_* handler"] --> S["Service"] --> D[("DB / Cache / Storage")]
H --> E["EventBus(需要时)"]
H --> R["ApiResponse JSON"]
9.1 设置页(保存 / 导入导出 / 备份 / 测试邮件)
flowchart TD
U1["UI:修改站点设置 → 保存"] --> S1["PATCH /console/settings<br/>update_settings"]
S1 --> SS["SettingService:逐 key 校验+写入 setting 表(写真相源 = KV)"]
SS --> SV2["sync_site_config_v2_from_kv → 重建 V2 只读快照"]
SV2 --> SC["site_config_updated 事件 → 快照刷新<br/>+ 前台 /site-config-v2 缓存失效 + SSR revalidate"]
U3["UI:导出设置"] --> S3["GET /console/settings/export → JSON 下载"]
U4["UI:导入设置"] --> S4["POST /console/settings/import<br/>解析 JSON → 批量写入 → 同 SC 流程"]
U5["UI:导出整站备份"] --> S5["POST /console/settings/backup/export-jobs<br/>打包设置+内容 → 返回任务/文件"]
U6["UI:导入整站备份"] --> S6["POST /console/settings/backup/import-jobs"]
U7["UI:测试邮件按钮"] --> S7["POST /console/settings/test-email<br/>EmailService(capability)发送测试信"]
S7 -->|未配置 SMTP| E503["失败提示"]
9.2 仪表盘统计
flowchart TD
U["UI:打开 Console 首页仪表盘"] --> A["GET /console/statistics/summary<br/>console_statistics_summary"]
A --> S["聚合查询:文章数/评论数/用户数/附件量<br/>+ 运行时指标(内存/在途请求/登录失败计数)"]
S --> R["JSON 汇总 → 前端卡片渲染"]
9.3 缓存管理页
flowchart TD
U3["UI:清空全部缓存"] --> C3["POST /console/cache/invalidation-jobs<br/>console_clear_all_cache"]
C3 --> IC["InvalidationCoordinator:清 CacheService 全部业务键"]
U4["UI:触发前台再生成"] --> C4["POST /console/cache/revalidation-jobs<br/>console_revalidate_cache"]
C4 --> RV["RevalidateService → 调用 Next.js /revalidate<br/>(REVALIDATE_TOKEN 鉴权)按路径重建 ISR 页面"]
9.4 存储策略管理页
flowchart TD
U1["UI:策略列表"] --> P1["GET /console/storage-policies"]
U2["UI:新建策略(本地/S3)"] --> P2["POST /console/storage-policies"]
U3["UI:编辑"] --> P3["PATCH /console/storage-policies/:id"]
U4["UI:删除"] --> P4["DELETE /console/storage-policies/:id"]
U5["UI:申请前端直传凭证"] --> P5["POST /console/storage-policies/:id/direct-upload-tokens<br/>console_create_direct_upload_token"]
P1 --> SPS["StoragePolicyService / storage_policy_full_repo"]
P2 --> SPS
P3 --> SPS --> EV["StoragePolicyUpdated 事件<br/>→ StorageRuntime 重新挂载 provider"]
P4 --> SPS
P5 --> SIGN["按策略生成 S3 预签名 PUT URL(带过期)"]
9.5 用户管理页
flowchart TD
U1["UI:用户列表(搜索/分页)"] --> A1["GET /console/users → console_list_users"]
U2["UI:禁用/启用用户"] --> A2["PATCH /console/users/:id/status"]
A2 --> K["UserService:更新状态<br/>禁用时同步失效该用户全部会话"]
U3["UI:删除用户"] --> A3["DELETE /console/users/:id<br/>(级联处理其内容归属)"]
U4["UI:默认权限配置"] --> A4["GET/PUT /console/users/permissions/default"]
U5["UI:单用户权限覆盖"] --> A5["GET/PUT/DELETE /console/users/:id/permissions<br/>(DELETE = 恢复默认)"]
A1 --> UR[("user_repo / user_session_repo")]
A4 --> SET[("setting 表(默认权限键)")]
9.6 标签 / 分类管理页
flowchart TD
U1["UI:列表"] --> T1["GET /console/tags · /console/categories"]
U2["UI:新建"] --> T2["POST /console/tags · /console/categories"]
U3["UI:重命名/改 slug"] --> T3["PATCH /console/tags/:id · /console/categories/:id"]
U4["UI:删除"] --> T4["DELETE /console/tags/:id · /console/categories/:id"]
U5["UI:修复计数按钮"] --> T5["POST /console/tags/count-repair-jobs<br/>POST /console/categories/count-repair-jobs<br/>重算每个标签/分类的文章计数"]
T2 --> SVC["PostTagService / PostCategoryService"]
T3 --> SVC --> EV["TagUpdated / CategoryUpdated 事件<br/>→ 前台列表缓存失效 + revalidate"]
T4 --> SVC
T5 --> REPAIR["聚合 SQL 重写 count 字段"]
9.7 文章管理页(批量操作 / 导入导出)
flowchart TD
U1["UI:勾选多篇 → 批量删除"] --> A1["POST /console/articles/deletion-jobs<br/>console_batch_delete_articles"]
U2["UI:勾选多篇 → 批量改状态<br/>(发布/下架/草稿)"] --> A2["POST /console/articles/status-jobs<br/>console_batch_update_article_status"]
U3["UI:导出文章"] --> A3["POST /console/articles/export-jobs<br/>console_export_articles(打包 markdown/JSON)"]
U4["UI:导入文章"] --> A4["POST /console/articles/import-jobs<br/>console_import_articles(解析 → 建文章+标签+分类)"]
A1 --> WS["ArticleWriteService 逐篇处理"]
A2 --> WS
WS --> EV["ArticleDeleted / ArticleUpdated / ArticlePublished 事件<br/>→ 缓存失效 + 搜索索引更新 + SSR revalidate"]
A4 --> PARSE["parser/extraction:front-matter 解析、图片地址改写"]
9.8 页面管理页
flowchart TD
U1["UI:页面列表"] --> P1["GET /console/pages → console_list_pages"]
U2["UI:新建页面"] --> P2["POST /console/pages → console_create_page"]
U3["UI:编辑保存"] --> P3["PATCH /console/pages/:id → console_update_page"]
U4["UI:删除"] --> P4["DELETE /console/pages/:id"]
U5["UI:初始化系统页(关于/友链等)"] --> P5["POST /console/pages/initialization-jobs"]
U6["UI:导入/导出页面"] --> P6["POST /console/pages/import-jobs / export-jobs"]
P2 --> PS["PageService → page_repo"]
P3 --> PS
P4 --> PS
PS --> EV["page_created/updated/deleted(path) 事件<br/>→ 失效 /pages/*path 缓存 + revalidate 对应路径"]
9.9 评论管理页
flowchart TD
U1["UI:评论列表(按状态/关键词筛选)"] --> C1["GET /console/comments"]
U2["UI:通过/驳回审核"] --> C2["PATCH /console/comments/:id/moderation-state"]
U3["UI:置顶/取消置顶"] --> C3["PATCH /console/comments/:id/pin-state"]
U4["UI:改昵称/邮箱/网址"] --> C4["PATCH /console/comments/:id/info"]
U5["UI:编辑评论内容"] --> C5["PATCH /console/comments/:id"]
U6["UI:删除单条"] --> C6["DELETE /console/comments/:id"]
U7["UI:批量删除"] --> C7["POST /console/comments/deletion-jobs"]
U8["UI:导入/导出"] --> C8["POST /console/comments/import-jobs / export-jobs"]
C2 --> CS["CommentService → comment_repo"]
C3 --> CS
C5 --> CS
C6 --> CS
CS --> EV["comment_updated / comment_deleted 事件 → 缓存失效"]
9.10 附件 / 文件管理页
flowchart TD
U1["UI:附件网格(搜索/类型/排序/分页)"] --> F1["GET /console/files<br/>query/type/sortBy/sortDir/page/pageSize<br/>或 ?path= 精确查 / ?all=1 管理员全量"]
F1 --> FS1["FileService.list_attachments_page(DB 侧分页)"]
FS1 --> OWN["按 owner_id 批量解析上传者昵称<br/>(user_svc.display_names_by_ids,去重查询)"]
U2["UI:目录树展开"] --> F2["GET /console/files/tree?parentId="]
U3["UI:新建文件夹"] --> F3["POST /console/files/directories"]
U4["UI:移动文件夹"] --> F4["PATCH /console/files/directories<br/>(path → targetPath)"]
U5["UI:删除文件夹"] --> F5["DELETE /console/files/directories?path="]
U6["UI:上传站点素材(logo/favicon)"] --> F6["POST /console/files/system-media(multipart)<br/>→ upload_system_media → 存储+登记"]
U7["UI:取私有文件临时链接"] --> F7["POST /console/files/access-tokens<br/>→ sign_media → 签名 URL"]
U8["UI:批量删除文件"] --> F8["POST /console/files/deletion-jobs<br/>逐 path 调 file_svc.delete(存储对象+DB 记录)"]
U9["UI:按路径查/重命名/删除"] --> F9["GET/PATCH/DELETE /console/files/lookups"]
F2 --> FSVC["FileService → file_repo / file_meta_repo"]
F3 --> FSVC
F6 --> FSVC --> EVF["FileCreated 事件 → 缩略图预生成等订阅者"]
9.11 缩略图
flowchart TD
U1["UI:附件卡片需要缩略图"] --> T1["GET /console/files/:file_id/thumbnail"]
T1 --> TS["ThumbnailService"]
TS --> HIT{"缩略图缓存存在?"}
HIT -->|是| OUT["直接返回图片字节(带缓存头)"]
HIT -->|否| GEN["读源文件(源大小上限保护)<br/>→ 解码缩放(并发加权信号量)→ 写缓存"]
GEN --> OUT
U2["UI:单文件重新生成"] --> T2["POST /console/thumbnails/regeneration-jobs"]
U3["UI:目录批量重新生成"] --> T3["POST /console/thumbnails/directories/regeneration-jobs"]
T2 --> RG["失效旧缩略图 → 按上述生成流程重建"]
T3 --> RG
10. 媒体服务数据面(/assets/media)
GET /assets/media/{*path_with_style} —— 前台正文图、头像、附件的统一出口(serve_media):
flowchart TD
REQ["GET /assets/media/a/b.png!style<br/>或 ?w=&h=&format=...<br/>或 .../download"] --> SPLIT["split_media_path:<br/>解析 source / style 后缀 / download 标记"]
SPLIT -->|source 为空| E400["400 image.id_missing"]
SPLIT --> MOUNT["StorageRuntime.resolve_for_media_source<br/>→ 按路径前缀选 mount(策略+provider)"]
MOUNT --> INFO["provider.get_object_info(轻量元数据探测)"]
INFO -->|NotFound| E404["404 image.file_not_found"]
INFO -->|FeatureNotSupported / 其他错误| NOMETA["进入无元数据回退路径"]
INFO --> STYLE{"download 或 无 style?"}
STYLE -->|是(原文件直出)| STREAM["stream_media_response:<br/>tokio duplex 64KB 有界管道<br/>provider.stream → Body 流式输出<br/>Content-Length=元数据 size"]
NOMETA --> BUF["provider.get 全量读 → 魔数嗅探 MIME → 缓冲输出"]
STYLE -->|否(带样式)| BIG{"源文件 > 10MB?"}
BIG -->|是| STREAM2["跳过解码,流式直出原图"]
BIG -->|否| PARSE["ImageStyle::parse(style)"]
PARSE -->|非法| E400b["400 image.style_invalid"]
PARSE --> CACHE{"image_style 磁盘缓存启用且命中?"}
CACHE -->|HIT| OUT1["200 + x-image-style-cache: HIT"]
CACHE -->|MISS| SEM["按源大小估算内存权重<br/>acquire style_memory_budget 信号量"]
SEM --> LOAD["provider.get 读源字节"]
LOAD --> PROC["ImageStyleService::process_blocking<br/>(blocking 线程池解码/缩放/转码)"]
PROC -->|失败| FALL["回退输出原图字节"]
PROC -->|成功| PUT["写磁盘样式缓存"] --> OUT2["200 + x-image-style-cache: MISS"]
STREAM --> HDRS
STREAM2 --> HDRS
OUT1 --> HDRS
OUT2 --> HDRS
FALL --> HDRS["统一安全头:<br/>SVG/HTML/JS 等活性内容 → 强制 octet-stream + attachment + CSP sandbox<br/>nosniff + Cache-Control 7 天"]
代理下载(外链资源安全中转):
flowchart TD
P1["GET /api/v3/proxy/download?url=... ⏱proxy-download"] --> P2["safe_http:协议/内网地址/重定向校验(防 SSRF)"]
P2 -->|非法目标| PE["4xx 拒绝"]
P2 --> P3["受限下载(大小/超时上限)→ 透传响应"]
11. 分块上传与直传时序
Console 附件上传是后台最复杂的交互链(POST /console/files/upload-sessions 挂了幂等中间件,且豁免单 IP 并发限制):
sequenceDiagram
autonumber
participant UI as Console 上传器
participant ID as idempotency_mw (file-upload-session-create)
participant H as file.rs handlers
participant UP as UploadService
participant POL as StoragePolicyService
participant ST as StorageProvider
participant S3 as AWS S3(client 模式)
UI->>ID: POST /console/files/upload-sessions {uri, size, policyId?}
Note over ID: 重试相同请求 → 直接重放首个响应,不会泄漏重复会话
ID->>H: console_create_upload_session
alt policyId 非空
H->>POL: describe_upload_target(policyId)
POL-->>H: {upload_method, policy_type, chunk_size, db_id}
else policyId 为空
Note over H: 回退服务端分块(complete 时按 uri 路径解析 mount)
end
H->>UP: create_session(CreateUploadParams)
UP-->>H: sessionId + chunk 规划
alt upload_method == "client" 且 aws_s3(直传)
H->>POL: create_direct_upload_token(path,size)
POL-->>H: 预签名 upload_url
H-->>UI: 201 {sessionId, uploadUrl}
UI->>S3: PUT 文件(直传,不过后端)
UI->>H: POST .../:session_id/completions
H->>UP: complete → 校验对象存在 → 登记 file_repo
else 服务端分块
H-->>UI: 201 {sessionId, chunkSize}
loop 每个分块 i
UI->>H: PUT .../:session_id/chunks/:i(Bytes 体)
H->>UP: upload_chunk(session, i, bytes)
UP->>ST: 写临时分块
UP-->>UI: 进度状态
end
UI->>H: POST .../:session_id/completions
H->>UP: complete(session)
UP->>ST: 顺序拼装 → 流式写入最终对象
UP->>UP: 登记附件记录(file_repo)+ FileCreated 事件
UP-->>UI: 201 附件元数据
end
opt 查询 / 放弃
UI->>H: GET .../:session_id(断点续传状态)
UI->>H: DELETE .../:session_id(清理临时分块)
end
幂等中间件内部逻辑(comment-create 与 file-upload-session-create 共用实现):
flowchart TD
A["写请求进入 idempotency_mw"] --> M0{"方法 == POST?"}
M0 -->|否| PASS["放行(仅 POST 启用幂等)"]
M0 -->|是| HK{"携带 idempotency-key 头?"}
HK -->|否| PASS
HK -->|是| B["复合键 POST:{path}:{actor}:{idempotency-key}<br/>actor = 已登录 u:{db_id} / 否则 ip:{clientIp}"]
B --> C{"查存储(缓存优先→本地)已有结果?"}
C -->|是| D["重放缓存的 status+content-type+body<br/>+ 头 idempotent-replayed:1"]
C -->|否| E{"set_if_absent 拿锁(TTL 60s)成功?"}
E -->|否(并发同键在途)| F["立即 409 RESOURCE_CONFLICT<br/>不排队、不等待"]
E -->|是| G["执行内层 handler"]
G --> BL{"响应体可在 4MB 内读取?"}
BL -->|否(超 4MB)| ERR["500 response body too large"]
BL -->|是| ST{"状态码 ∈ [200,500)?"}
ST -->|是| I["写入幂等存储(TTL 24h)<br/>CacheService(Redis 时跨实例)"]
ST -->|否(≥500)| SKIP["失败响应不固化"]
I --> REL["释放锁 compare_and_delete(token)"]
SKIP --> REL
REL --> K["返回响应"]
L["5 分钟 housekeeping 循环"] --> GC["gc_all_stores():清理过期条目"]
12. 异步机制:事件总线 / 定时任务 / 缓存失效
12.1 事件总线(写操作 → 派生动作解耦)
flowchart LR
subgraph PUBLISH["发布方(handler / service)"]
W1["文章创建/更新/删除/发布"]
W2["页面增删改(带 path)"]
W3["评论增删改"]
W4["标签/分类变更"]
W5["站点配置更新"]
W6["存储策略更新"]
W7["文件创建"]
end
subgraph BUS["EventBus(mpsc 256,2 worker,panic 自恢复,满载丢弃+计数)"]
T["Topic: article:* / page:* / comment:*<br/>category|tag:updated / site-config:updated<br/>storage-policy:updated / file:created"]
end
subgraph SUBSCRIBE["订阅者(bootstrap 注册)"]
S1["InvalidationCoordinator<br/>按 Topic 精准失效 CacheService 键"]
S2["RevalidateService<br/>调 Next.js /revalidate(token 鉴权)<br/>按 slug/path 重建 ISR 页"]
S3["搜索索引更新(Searcher)"]
S4["SiteConfigV2 快照重建"]
S5["StorageRuntime 重新挂载"]
S6["缩略图预生成(file:created)"]
S7["CDN 刷新(capability,可选)"]
end
W1 --> T
W2 --> T
W3 --> T
W4 --> T
W5 --> T
W6 --> T
W7 --> T
T --> S1
T --> S2
T --> S3
T --> S4
T --> S5
T --> S6
T --> S7
12.2 定时任务(crates/task,spawn_supervised + 分布式任务锁)
flowchart TD
subgraph LOOP["spawn_supervised(每个 job 独立 interval,panic 恢复)"]
J1["session:过期会话清理"]
J2["file:孤儿/临时上传分块回收<br/>草稿图清理"]
J3["article:浏览计数落库 / 定时发布"]
J4["taxonomy:标签分类计数对账"]
J5["housekeeping(5min):<br/>idempotency gc_all_stores + 内存指标"]
end
LOCK["with_task_lock:CacheService set_if_absent<br/>(多实例只有一个执行者,token 化 compare_and_delete 释放)"]
J1 --> LOCK
J2 --> LOCK
J3 --> LOCK
J4 --> LOCK
LOCK --> RUN["执行 → Service/Repo"]
12.3 「后台改内容 → 前台更新」端到端链路
sequenceDiagram
autonumber
participant ADM as Console UI
participant BE as 后端 handler
participant DB as DB
participant BUS as EventBus
participant INV as InvalidationCoordinator
participant NX as Next.js(内嵌前端)
participant VIS as 前台访客
ADM->>BE: PATCH /console/pages/:id(保存页面)
BE->>DB: 更新 page 行
BE->>BUS: page_updated(path)
BE-->>ADM: 200 ApiResponse(UI 立即提示成功)
BUS->>INV: 失效 /api/v3/pages/*path 相关缓存键
BUS->>NX: POST /revalidate(REVALIDATE_TOKEN)
NX->>BE: 重新拉取 GET /api/v3/pages/:path(缓存 MISS → DB)
NX->>NX: 重建 ISR 静态页
VIS->>NX: 访问页面 → 看到新内容
13. 错误与响应约定
flowchart TD
H["handler 返回 ApiResult<T>"] --> OK{"Ok?"}
OK -->|是| S["ApiResponse::success(data, msg)<br/>{ code, message, data }"]
OK -->|否| ERR["ApiError → IntoResponse"]
ERR --> I18N["error_i18n_structured:<br/>i18n key + reason code + domain<br/>{ code, message, error: { reason, domain } }"]
subgraph EDGE["边界统一 JSON 化(绝不裸返回空体)"]
E1["401 未登录 / 会话过期(auth.*)"]
E2["403 CSRF_VALIDATION_FAILED / permission_denied_admin"]
E3["404 路由未命中 RESOURCE_NOT_FOUND(/api/* fallback)"]
E4["405 METHOD_NOT_ALLOWED(method_not_allowed_fallback)"]
E5["408 请求超时(request_timeout_mw)"]
E6["413 体积超限(normalize_payload_too_large_mw)"]
E7["429 限流 / 单 IP 并发(rate_limit / concurrency)"]
E8["503 全局过载(load_shed)"]
E9["409 RESOURCE_CONFLICT(幂等并发同键)"]
E10["410 GONE(v1 站点配置 /main 退役)"]
end
响应信封:所有 /api/v3/* 返回 ApiResponse 统一结构;消息走 server_core::i18n(按启动 locale)。媒体路由返回二进制流并带独立缓存/安全头。
附:阅读指引
| 想了解 | 看哪节 + 哪些源文件 |
|---|---|
| 一次请求经过哪些层 | §3;crates/api/src/router/mod.rs |
| 登录态怎么校验 | §5.2;middleware/auth.rs、middleware/csrf.rs |
| 某个 Console 按钮打到哪 | §4.4 表 + §9 对应小节;router/admin.rs |
| 图片为什么是流式/缓存的 | §10;handler/media.rs |
| 大文件上传细节 | §11;handler/file.rs、service/upload/ |
| 改了内容前台多久生效 | §12.3;service/revalidate.rs、invalidation/ |