tdm_server_rust/web/
mod.rs1pub mod author_controller;
31pub mod collaboration_hub_controller;
33pub mod doc_controller;
36pub mod editor_controller;
38pub mod episode_controller;
40pub mod evaluation_controller;
42pub mod gray_release_controller;
44pub mod login_controller;
46pub mod magazine_controller;
48pub mod manga_benefit_controller;
50pub mod manga_controller;
52pub mod member_controller;
54pub mod oss_controller;
56pub mod questionnaire_controller;
58pub mod reward_controller;
60pub mod search_controller;
62pub 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#[derive(Debug, Serialize)]
82struct HealthzResponse {
83 ok: bool,
85 profile: String,
87 database: bool,
89 uptime_seconds: u64,
91}
92
93pub 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 .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#[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#[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}