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:
iven
2026-04-11 01:19:30 +08:00
parent 5901ee82f0
commit 810eef769f
9 changed files with 216 additions and 30 deletions

View File

@@ -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...");
},
}
}