// src/main.rs use std::net::SocketAddr; use std::sync::Arc; use axum::{ routing::{get, post}, Router, }; use tower_http::cors::{Any, CorsLayer}; use tower_http::services::ServeDir; use sqlx::sqlite::SqlitePoolOptions; use tracing::{info, error}; use astroresearch::Config; use astroresearch::services::translation::Dictionary; use astroresearch::clients::qiniu::QiniuClient; use astroresearch::clients::ads::AdsClient; use astroresearch::clients::arxiv::ArxivClient; use astroresearch::services::download::Downloader; use astroresearch::api::handlers::{AppState, self}; #[tokio::main] async fn main() -> anyhow::Result<()> { // 1. 初始化日志记录器并保留异步写保护 Guard let _logging_guards = astroresearch::services::logging::init_logging()?; info!("正在启动 AstroResearch 天文学文献辅助系统后端服务..."); // 2. 加载环境变量配置 let config = Config::from_env(); info!("系统配置成功载入。本地 SQLite 连接串: {}", config.database_url); // 创建本地馆藏物理文件夹分类结构 std::fs::create_dir_all(&config.library_dir).unwrap_or_default(); std::fs::create_dir_all(config.library_dir.join("PDF")).unwrap_or_default(); std::fs::create_dir_all(config.library_dir.join("HTML")).unwrap_or_default(); std::fs::create_dir_all(config.library_dir.join("Markdown")).unwrap_or_default(); std::fs::create_dir_all(config.library_dir.join("Translation")).unwrap_or_default(); // 3. 初始化本地 SQLite 数据库文件连接池 if config.database_url.starts_with("sqlite://") { let db_path = config.database_url.replace("sqlite://", ""); if !db_path.contains(":memory:") { let path = std::path::Path::new(&db_path); if !path.exists() { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).unwrap_or_default(); } std::fs::File::create(path)?; info!("初始化创建本地 SQLite 数据库文件: {:?}", path); } } } let pool = SqlitePoolOptions::new() .max_connections(5) .connect(&config.database_url) .await?; info!("SQLite 数据库连接已建立。"); // 4. 自动执行数据库迁移脚本 info!("开始执行 SQL 表结构迁移..."); sqlx::migrate!("./migrations") .run(&pool) .await?; info!("数据库迁移执行完成,主表准备就绪。"); // 5. 异步加载天文学专业名词对照词表 let mut dict = Dictionary::new(); if let Err(e) = dict.load_from_file("dictionary.txt") { error!("天文学名词词表加载失败: {}", e); } // 6. 初始化并配置全部 API 与下载客户端 let qiniu = QiniuClient::new( config.qiniu_ak.clone(), config.qiniu_sk.clone(), config.qiniu_bucket.clone(), config.qiniu_domain.clone(), ); let ads = AdsClient::new(config.ads_api_key.clone()); let arxiv = ArxivClient::new(); let downloader = Downloader::new(); let app_state = Arc::new(AppState { config: config.clone(), db: pool, dict, qiniu, ads, arxiv, downloader, harvest_status: Arc::new(tokio::sync::Mutex::new(astroresearch::services::batch_sync::MetaSyncStatus::new())), process_status: Arc::new(tokio::sync::Mutex::new(astroresearch::services::batch_sync::AssetSyncStatus::new())), }); // 7. 设置 Axum 路由、CORS 头以及 React 仪表盘静态资源托管 let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); let api_routes = Router::new() .route("/search", get(handlers::search_papers)) .route("/download", post(handlers::download_paper)) .route("/parse", post(handlers::parse_paper)) .route("/translate", post(handlers::translate_paper)) .route("/citations", get(handlers::get_citation_network)) .route("/paper", get(handlers::get_paper_detail)) .route("/library", get(handlers::get_library)) .route("/export", post(handlers::export_citations)) .route("/notes", post(handlers::create_note)) .route("/notes", get(handlers::get_notes)) .route("/notes", axum::routing::delete(handlers::delete_note)) .route("/sync/meta/count", get(handlers::get_meta_sync_count)) .route("/sync/meta/run", post(handlers::run_meta_sync)) .route("/sync/meta/status", get(handlers::get_meta_sync_status)) .route("/sync/asset/run", post(handlers::run_asset_sync)) .route("/sync/asset/stop", post(handlers::stop_asset_sync)) .route("/sync/asset/status", get(handlers::get_asset_sync_status)) .route("/sync/queries", get(handlers::get_sync_queries)) .route("/sync/queries/:id", axum::routing::delete(handlers::delete_sync_query)); // 静态文件资源代理托管(当前端打包至 dashboard/dist 后,直接挂载到主域名根路由) let serve_dir = ServeDir::new("dashboard/dist") .fallback(tower_http::services::ServeFile::new("dashboard/dist/index.html")); let app = Router::new() .nest("/api", api_routes) .fallback_service(serve_dir) .layer(cors) .layer(tower_http::trace::TraceLayer::new_for_http()) .with_state(app_state); let addr = SocketAddr::from(([0, 0, 0, 0], config.port)); info!("天文学科研服务已成功监听 http://localhost:{}", config.port); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app).await?; Ok(()) }