Skip to main content

tdm_server_rust/web/
mod.rs

1//! HTTP 路由与控制器层 (Web Layer)
2//!
3//! 定义所有 API 端点的路由注册和 Handler 函数。
4//!
5//! ## 路由表 (`create_router()`)
6//!
7//! | 路由前缀 | Controller | 端点数 | 说明 |
8//! |---------|-----------|--------|------|
9//! | `/api/login`, `/api/reg` | [`login_controller`] | 5 | 登录/注册/邀请码 |
10//! | `/api/members` | [`member_controller`] | 12 | 组员 CRUD/岗位/常驻 |
11//! | `/api/authors` | [`author_controller`] | 7 | 作者管理 |
12//! | `/api/magazines` | [`magazine_controller`] | 7 | 杂志管理 |
13//! | `/api/evaluations` | [`evaluation_controller`] | 5 | 评价管理 |
14//! | `/api/gray` | [`gray_release_controller`] | 1 | 灰度命中 |
15//! | `/api/questionnaires` | [`questionnaire_controller`] | 3 | 问卷提交查询 |
16//! | `/api/mangas` | [`manga_controller`] | 27 | 漫画/收藏/术语表 |
17//! | `/api/episodes` | [`episode_controller`] | 14 | 话数/发布/上传 |
18//! | `/api/manga/benefits` | [`manga_benefit_controller`] | 4 | 特典管理 |
19//! | `/api/reward` | [`reward_controller`] | 10 | 悬赏/兑换/抽奖 |
20//! | `/api/oss` | [`oss_controller`] | 4 | 文件上传下载 |
21//! | `/api/admin/taskTracking` | [`task_tracking_controller`] | 3 | 任务看板 |
22//! | `/api/admin/gray` | [`gray_release_controller`] | 4 | 灰度管理 |
23//!
24//! ## 中间件栈
25//!
26//! 所有 `/api/*` 路由经过:`error_log → auth → handler`
27//! dev 环境下额外添加:`debug_log` 中间件
28
29/// 作者管理 API 路由
30pub mod author_controller;
31/// 在线翻译协作 Hub(WebSocket)路由
32pub mod collaboration_hub_controller;
33/// dev 环境 API(SkyWalking 302 + health + errors)
34/// OpenAPI / Swagger UI 静态资源路由
35pub mod doc_controller;
36/// 在线翻译编辑器 API 路由
37pub mod editor_controller;
38/// 话数管理 API 路由
39pub mod episode_controller;
40/// 评价管理 API 路由
41pub mod evaluation_controller;
42/// 灰度发布 API 路由
43pub mod gray_release_controller;
44/// 登录/注册 API 路由
45pub mod login_controller;
46/// 杂志管理 API 路由
47pub mod magazine_controller;
48/// 漫画特典 API 路由
49pub mod manga_benefit_controller;
50/// 漫画管理 API 路由
51pub mod manga_controller;
52/// 组员管理 API 路由
53pub mod member_controller;
54/// OSS 文件上传/下载 API 路由
55pub mod oss_controller;
56/// 问卷管理 API 路由
57pub mod questionnaire_controller;
58/// 悬赏/兑换/抽奖 API 路由
59pub mod reward_controller;
60/// 全局搜索 API 路由
61pub mod search_controller;
62/// 任务看板 API 路由
63pub mod task_tracking_controller;
64
65use crate::{
66    app::AppState,
67    dev,
68    middleware::{auth_middleware, debug_log_middleware, error_log_middleware},
69    telemetry::telemetry_http_span_middleware,
70};
71use axum::extract::State;
72use axum::http::header;
73use axum::http::{HeaderName, StatusCode};
74use axum::middleware;
75use axum::response::IntoResponse;
76use axum::{routing::get, Json, Router};
77use serde::Serialize;
78use tower_http::cors::CorsLayer;
79
80/// 顶层健康检查响应
81#[derive(Debug, Serialize)]
82struct HealthzResponse {
83    /// 服务是否已完成基础依赖探活
84    ok: bool,
85    /// 当前运行 profile
86    profile: String,
87    /// 数据库连接是否可用
88    database: bool,
89    /// 进程已运行秒数
90    uptime_seconds: u64,
91}
92
93/// 创建带 `/api` 前缀的完整路由
94pub fn create_router(state: AppState) -> Router {
95    let api = Router::new()
96        .merge(login_controller::routes())
97        .nest("/members", member_controller::routes())
98        .nest("/authors", author_controller::routes())
99        .nest("/magazines", magazine_controller::routes())
100        .nest("/evaluations", evaluation_controller::routes())
101        .nest("/gray", gray_release_controller::routes())
102        .nest("/questionnaires", questionnaire_controller::routes())
103        .nest("/mangas", manga_controller::routes())
104        .nest("/episodes", episode_controller::routes())
105        .nest("/editor", editor_controller::routes())
106        .nest("/manga/benefits", manga_benefit_controller::routes())
107        .nest("/reward", reward_controller::routes())
108        .nest("/oss", oss_controller::routes())
109        .route("/search", get(search_controller::global_search))
110        .route("/search/", get(search_controller::global_search))
111        .nest("/admin/gray", gray_release_controller::admin_routes())
112        .nest("/admin/taskTracking", task_tracking_controller::routes());
113
114    let api = api
115        .fallback(api_not_found)
116        .layer(middleware::from_fn(error_log_middleware))
117        .layer(middleware::from_fn_with_state(
118            state.clone(),
119            auth_middleware,
120        ))
121        .with_state(state.clone());
122
123    let cors = CorsLayer::permissive().expose_headers([
124        HeaderName::from_static("server-timing"),
125        header::LOCATION,
126        HeaderName::from_static("x-download-filename"),
127        header::CONTENT_DISPOSITION,
128    ]);
129
130    let mut app = Router::new()
131        .route("/healthz", get(healthz))
132        .nest("/api", api)
133        // 协作 Hub 挂载于顶层(/hubs/*),不经过 /api 的 auth 中间件,
134        // WS 鉴权由控制器自身从查询参数 access_token 完成
135        .merge(collaboration_hub_controller::routes())
136        .layer(cors)
137        .with_state(state.clone());
138
139    if state.config.telemetry.enabled {
140        app = app.layer(middleware::from_fn(telemetry_http_span_middleware));
141    }
142
143    if matches!(state.config.profile.as_str(), "dev" | "dev-h2") {
144        let springdoc = state.config.springdoc.clone();
145        app = app.merge(doc_controller::routes(springdoc));
146        app = app.merge(dev::routes(state.clone()));
147        app = app.layer(middleware::from_fn(debug_log_middleware));
148    }
149
150    app
151}
152
153/// 顶层健康检查,不经过 `/api` 鉴权,供部署脚本和负载均衡探活使用
154#[tracing::instrument(skip_all, level = "info")]
155async fn healthz(State(state): State<AppState>) -> impl IntoResponse {
156    let database = sqlx::query_scalar::<_, i32>("SELECT 1")
157        .fetch_one(&state.db)
158        .await
159        .is_ok();
160    let response = HealthzResponse {
161        ok: database,
162        profile: state.config.profile.clone(),
163        database,
164        uptime_seconds: state.started_at.elapsed().as_secs(),
165    };
166    let status = if response.ok {
167        StatusCode::OK
168    } else {
169        StatusCode::SERVICE_UNAVAILABLE
170    };
171
172    (status, Json(response))
173}
174
175/// 未匹配的 `/api/*` 路由
176#[tracing::instrument(skip_all, level = "info")]
177async fn api_not_found() -> impl IntoResponse {
178    (
179        StatusCode::NOT_FOUND,
180        r#"{"code":404,"msg":"接口不存在","data":null}"#,
181    )
182}