From a4daa8f49c7ca4ce27cefb3ece09fc2f3d34dd7a Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 12:54:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20=E5=81=A5=E5=BA=B7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=A2=9E=E5=BC=BA=20=E2=80=94=20=E6=96=B0=E5=A2=9E=20?= =?UTF-8?q?/health/ready=20=E5=B0=B1=E7=BB=AA=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 保留 /health 轻量存活检查 - 新增 /health/ready 含 DB ping + Redis ping 并行检测 - 返回 status(ok/degraded/unavailable) + 各组件延迟和错误信息 --- crates/erp-server/src/handlers/health.rs | 108 +++++++++++++++++++++-- 1 file changed, 103 insertions(+), 5 deletions(-) diff --git a/crates/erp-server/src/handlers/health.rs b/crates/erp-server/src/handlers/health.rs index bac8079..b1e85db 100644 --- a/crates/erp-server/src/handlers/health.rs +++ b/crates/erp-server/src/handlers/health.rs @@ -13,9 +13,7 @@ pub struct HealthResponse { pub modules: Vec, } -/// GET /health -/// -/// 服务健康检查,返回运行状态和已注册模块列表 +/// GET /health — 轻量存活检查 pub async fn health_check(State(state): State) -> Json { let modules = state .module_registry @@ -31,6 +29,106 @@ pub async fn health_check(State(state): State) -> Json }) } -pub fn health_check_router() -> Router { - Router::new().route("/health", get(health_check)) +#[derive(Debug, Serialize)] +pub struct ReadyResponse { + pub status: String, + pub version: String, + pub database: ComponentStatus, + pub redis: ComponentStatus, + pub modules: Vec, +} + +#[derive(Debug, Serialize)] +pub struct ComponentStatus { + pub status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub latency_ms: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// GET /health/ready — 就绪检查(含 DB + Redis 连通性) +pub async fn readiness_check(State(state): State) -> Json { + let modules = state + .module_registry + .modules() + .iter() + .map(|m| m.name().to_string()) + .collect(); + + let (db_status, redis_status) = tokio::join!( + check_database(&state.db), + check_redis(&state.redis), + ); + + let overall = if db_status.status == "ok" && redis_status.status == "ok" { + "ok" + } else if db_status.status == "ok" { + "degraded" + } else { + "unavailable" + }; + + Json(ReadyResponse { + status: overall.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + database: db_status, + redis: redis_status, + modules, + }) +} + +async fn check_database(db: &sea_orm::DatabaseConnection) -> ComponentStatus { + use sea_orm::ConnectionTrait; + let start = std::time::Instant::now(); + let stmt = sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "SELECT 1".to_string(), + ); + match db.query_one(stmt).await { + Ok(_) => ComponentStatus { + status: "ok".to_string(), + latency_ms: Some(start.elapsed().as_millis() as u64), + error: None, + }, + Err(e) => ComponentStatus { + status: "error".to_string(), + latency_ms: Some(start.elapsed().as_millis() as u64), + error: Some(e.to_string()), + }, + } +} + +async fn check_redis(client: &redis::Client) -> ComponentStatus { + let start = std::time::Instant::now(); + match client.get_multiplexed_async_connection().await { + Ok(mut conn) => { + match redis::cmd("PING") + .query_async::(&mut conn) + .await + { + Ok(_) => ComponentStatus { + status: "ok".to_string(), + latency_ms: Some(start.elapsed().as_millis() as u64), + error: None, + }, + Err(e) => ComponentStatus { + status: "error".to_string(), + latency_ms: Some(start.elapsed().as_millis() as u64), + error: Some(e.to_string()), + }, + } + } + Err(e) => ComponentStatus { + status: "error".to_string(), + latency_ms: Some(start.elapsed().as_millis() as u64), + error: Some(e.to_string()), + }, + } +} + +pub fn health_check_router() -> Router { + Router::new() + .route("/health", get(health_check)) + .route("/health/ready", get(readiness_check)) }