feat(server): integrate AppState, ModuleRegistry, health check, and graceful shutdown
- Add AppState with DB, Config, EventBus, ModuleRegistry via Axum State - ModuleRegistry now uses Arc for Clone support, builder-pattern register() - Add /api/v1/health endpoint returning status, version, registered modules - Add graceful shutdown on CTRL+C / SIGTERM - erp-common utils: ID generation, timestamp helpers, code generator with tests - Config structs now derive Clone for state sharing - Update wiki to reflect Phase 1 completion
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub server: ServerConfig,
|
||||
pub database: DatabaseConfig,
|
||||
@@ -9,32 +9,32 @@ pub struct AppConfig {
|
||||
pub log: LogConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DatabaseConfig {
|
||||
pub url: String,
|
||||
pub max_connections: u32,
|
||||
pub min_connections: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct RedisConfig {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct JwtConfig {
|
||||
pub secret: String,
|
||||
pub access_token_ttl: String,
|
||||
pub refresh_token_ttl: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct LogConfig {
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
36
crates/erp-server/src/handlers/health.rs
Normal file
36
crates/erp-server/src/handlers/health.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use axum::extract::State;
|
||||
use axum::response::Json;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HealthResponse {
|
||||
pub status: String,
|
||||
pub version: String,
|
||||
pub modules: Vec<String>,
|
||||
}
|
||||
|
||||
/// GET /api/v1/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,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn health_check_router() -> Router<AppState> {
|
||||
Router::new().route("/api/v1/health", get(health_check))
|
||||
}
|
||||
1
crates/erp-server/src/handlers/mod.rs
Normal file
1
crates/erp-server/src/handlers/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod health;
|
||||
@@ -1,10 +1,14 @@
|
||||
mod config;
|
||||
mod db;
|
||||
mod handlers;
|
||||
mod state;
|
||||
|
||||
use axum::Router;
|
||||
use config::AppConfig;
|
||||
use erp_server_migration::Migrator;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::module::ModuleRegistry;
|
||||
use erp_server_migration::MigratorTrait;
|
||||
use state::AppState;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -21,27 +25,85 @@ async fn main() -> anyhow::Result<()> {
|
||||
.json()
|
||||
.init();
|
||||
|
||||
tracing::info!("ERP Server starting...");
|
||||
tracing::info!(version = env!("CARGO_PKG_VERSION"), "ERP Server starting...");
|
||||
|
||||
// Connect to database
|
||||
let db = db::connect(&config.database).await?;
|
||||
|
||||
// Run migrations
|
||||
Migrator::up(&db, None).await?;
|
||||
erp_server_migration::Migrator::up(&db, None).await?;
|
||||
tracing::info!("Database migrations applied");
|
||||
|
||||
// Connect to Redis
|
||||
let _redis_client = redis::Client::open(&config.redis.url[..])?;
|
||||
tracing::info!("Redis client created");
|
||||
|
||||
// Build app
|
||||
let app = Router::new()
|
||||
.fallback(|| async { axum::Json(serde_json::json!({"error": "Not found"})) });
|
||||
// Initialize event bus (capacity 1024 events)
|
||||
let event_bus = EventBus::new(1024);
|
||||
|
||||
let addr = format!("{}:{}", config.server.host, config.server.port);
|
||||
// Initialize module registry
|
||||
let registry = ModuleRegistry::new();
|
||||
// Phase 2+ will register modules here:
|
||||
// let registry = registry.register(Box::new(erp_auth::AuthModule::new(db.clone())));
|
||||
tracing::info!(module_count = registry.modules().len(), "Modules registered");
|
||||
|
||||
// Register event handlers
|
||||
registry.register_handlers(&event_bus);
|
||||
|
||||
let host = config.server.host.clone();
|
||||
let port = config.server.port;
|
||||
|
||||
// Build shared state
|
||||
let state = AppState {
|
||||
db,
|
||||
config,
|
||||
event_bus,
|
||||
module_registry: registry,
|
||||
};
|
||||
|
||||
// Build API router with versioning
|
||||
let api_v1 = Router::new().merge(handlers::health::health_check_router());
|
||||
|
||||
// Build application router
|
||||
let app = Router::new().merge(api_v1).with_state(state);
|
||||
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
tracing::info!("Server listening on {}", addr);
|
||||
axum::serve(listener, app).await?;
|
||||
tracing::info!(addr = %addr, "Server listening");
|
||||
|
||||
// Graceful shutdown on CTRL+C
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
|
||||
tracing::info!("Server shutdown complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install CTRL+C handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {
|
||||
tracing::info!("Received CTRL+C, shutting down gracefully...");
|
||||
},
|
||||
_ = terminate => {
|
||||
tracing::info!("Received SIGTERM, shutting down gracefully...");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
23
crates/erp-server/src/state.rs
Normal file
23
crates/erp-server/src/state.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use axum::extract::FromRef;
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
use crate::config::AppConfig;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::module::ModuleRegistry;
|
||||
|
||||
/// Axum 共享应用状态
|
||||
/// 所有 handler 通过 State<AppState> 获取数据库连接、配置等
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: DatabaseConnection,
|
||||
pub config: AppConfig,
|
||||
pub event_bus: EventBus,
|
||||
pub module_registry: ModuleRegistry,
|
||||
}
|
||||
|
||||
/// 允许 handler 直接提取子字段
|
||||
impl FromRef<AppState> for DatabaseConnection {
|
||||
fn from_ref(state: &AppState) -> Self {
|
||||
state.db.clone()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user