Files
base/crates/erp-server/src/handlers/health.rs
iven 3772afd987 chore: 干净 ERP 基座 — 删除 health/ai/wechat 业务代码
删除内容:
- 前端: health/(67文件), ai/(2文件), Copilot, MediaPicker, 相关API/Store/Hook
- 后端: wechat_handler, wechat_service, wechat_user entity, analytics handler, ai_workflow_seed
- 配置: WechatConfig, AppConfig.wechat, AuthState wechat 字段
- 启动: 微信凭据检查块, ensure_ai_workflows() 调用
- 迁移: 新增 m20260613_000170_drop_wechat_users.rs
- 脚本: api_test_health_alert.py, api_test_mp.py, mpsync.sh/ps1
- E2E: health-data page, flows/ 目录

保留: erp-core/auth/workflow/message/config/plugin + 基座前端 + 通用组件
2026-06-13 00:32:50 +08:00

136 lines
4.0 KiB
Rust

use axum::Router;
use axum::extract::State;
use axum::response::Json;
use axum::routing::get;
use serde::Serialize;
use crate::state::AppState;
#[derive(Debug, Serialize)]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub modules: Vec<String>,
}
/// GET /health — 轻量存活检查
pub async fn health_check(State(state): State<AppState>) -> Json<HealthResponse> {
let modules = state
.module_registry
.modules()
.iter()
.map(|m| m.name().to_string())
.collect();
Json(HealthResponse {
status: "ok".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
modules,
})
}
#[derive(Debug, Serialize)]
pub struct ReadyResponse {
pub status: String,
pub version: String,
pub database: ComponentStatus,
pub redis: ComponentStatus,
pub modules: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ComponentStatus {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub latency_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
/// GET /health/ready — 就绪检查(含 DB + Redis 连通性)
pub async fn readiness_check(State(state): State<AppState>) -> Json<ReadyResponse> {
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) => {
tracing::error!(error = %e, "Database health check failed");
ComponentStatus {
status: "error".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: Some("connection failed".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::<String>(&mut conn).await {
Ok(_) => ComponentStatus {
status: "ok".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: None,
},
Err(e) => {
tracing::error!(error = %e, "Redis PING failed");
ComponentStatus {
status: "error".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: Some("connection failed".to_string()),
}
}
},
Err(e) => {
tracing::error!(error = %e, "Redis connection failed");
ComponentStatus {
status: "error".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: Some("connection failed".to_string()),
}
}
}
}
pub fn health_check_router() -> Router<AppState> {
Router::new()
.route("/health", get(health_check))
.route("/health/live", get(health_check))
.route("/health/ready", get(readiness_check))
}