use actix_web::middleware::{from_fn, DefaultHeaders}; use actix_web::{App, HttpServer, web}; use tracing::{error, info, warn}; use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod}; use reqwest::Client; use sqlx::postgres::PgPool; use std::pin::Pin; mod auth; mod config; mod db; mod error; mod handlers; mod models; mod rate_limiter; use auth::jwt_middleware; use config::AppConfig; use db::create_pool; use handlers::{ admin_force_confirm_order, admin_get_user, admin_refund_order, admin_update_user_payment, add_favorite, alipay_notify, admin_create_notification, admin_delete_notification, alipay_pay_page, alipay_refund_notify, cancel_order, create_order, delete_weather, generate_code, generate_temp_token_handler, get_current_user_profile, get_favorites, get_user_quota, get_weather_brief, get_weather_details, get_user_orders, health_check, list_notifications, login, mark_all_read, mark_notification_read, unread_count, mock_login, mock_confirm, sync_order, payment_index, payment_login_status, payment_page, payment_success, post_weather_data, report_frontend_error, refresh_token, remove_favorite, root, save_user_profile, serve_static_files, web_generate_login_code, web_login_confirm, web_login_auto_confirm, }; use models::AppState; // 加载TLS证书和私钥 fn create_ssl_acceptor() -> Result> { let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; // 从环境变量获取证书和私钥路径 let key_path = std::env::var("SSL_KEY_PATH").unwrap_or_else(|_| "path/to/private.key".to_string()); let cert_path = std::env::var("SSL_CERT_PATH").unwrap_or_else(|_| "path/to/certificate.crt".to_string()); builder.set_private_key_file(&key_path, SslFiletype::PEM)?; builder.set_certificate_chain_file(&cert_path)?; // 返回构建器 Ok(builder) } fn create_server_config( pool: PgPool, http_client: Client, app_state: AppState, ) -> App< impl actix_web::dev::ServiceFactory< actix_web::dev::ServiceRequest, Config = (), Response = actix_web::dev::ServiceResponse, Error = actix_web::Error, InitError = (), >, > { App::new() .app_data(web::Data::new(pool)) .app_data(web::Data::new(http_client)) .app_data(web::Data::new(app_state)) // 安全响应头(全局中间件) .wrap( DefaultHeaders::new() .add(("X-Content-Type-Options", "nosniff")) .add(("X-Frame-Options", "DENY")) .add(("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self'")) .add(("Referrer-Policy", "no-referrer-when-downgrade")) .add(("Permissions-Policy", "geolocation=(), microphone=(), camera=()")) ) // Sentry 请求监控(在 DSN 未配置时无操作) .wrap(sentry_actix::Sentry::new()) // 根路径(无需认证) .service(root) // #[get("/")] - 返回服务信息 // 支付页面(无需认证,外部浏览器访问) .service(payment_index) // GET /payment — 套餐选择页 .service(generate_code) // GET /payment/generate-code — 网页生成登录码 .service(payment_page) // GET /payment/page(需 JWT) .service(payment_success) .service(alipay_pay_page) // GET /payment/pay(需 JWT) .service(alipay_notify) // POST /payment/notify(支付宝异步回调) .service(alipay_refund_notify) // POST /payment/refund-notify(支付宝退款回调) .service(payment_login_status) // GET /payment/login-status(网页轮询) // 静态文件(无需认证) .service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files))) // API 接口 .service(login) // #[post("/api/login")] .service(mock_login) // #[get("/api/mock-login")](沙箱测试用) .service(refresh_token) // #[post("/api/refresh-token")](公开接口,无需认证) .service(get_weather_details) // #[get("/weather/details")](支持 JWT 或 temp_token,公开接口) .service(health_check) .service(web_generate_login_code) .service(web_login_confirm) .service(web_login_auto_confirm) .service(report_frontend_error) // POST /api/sentry/events .service( web::scope("") .wrap(from_fn(jwt_middleware)) .service(post_weather_data) .service(get_weather_brief) .service(generate_temp_token_handler) .service(delete_weather) .service(get_current_user_profile) .service(save_user_profile) .service(admin_get_user) .service(admin_update_user_payment) .service(admin_force_confirm_order) .service(admin_refund_order) .service(admin_create_notification) .service(admin_delete_notification) .service(create_order) .service(mock_confirm) .service(sync_order) .service(cancel_order) .service(get_user_orders) .service(get_user_quota) .service(get_favorites) .service(add_favorite) .service(list_notifications) .service(mark_notification_read) .service(mark_all_read) .service(unread_count) .service(remove_favorite) ) } #[actix_web::main] async fn main() -> std::io::Result<()> { // 加载配置文件(支持多环境) let app_config = match AppConfig::load() { Ok(cfg) => cfg, Err(e) => { eprintln!("配置加载失败: {}", e); std::process::exit(1); } }; // 设置环境变量(向后兼容依赖 env var 的组件) unsafe { std::env::set_var("DATABASE_URL", &app_config.database_url); std::env::set_var("JWT_SECRET", &app_config.jwt_secret); std::env::set_var("WECHAT_APPID", &app_config.wechat_appid); std::env::set_var("WECHAT_SECRET", &app_config.wechat_secret); std::env::set_var("SSL_KEY_PATH", &app_config.ssl_key_path); std::env::set_var("SSL_CERT_PATH", &app_config.ssl_cert_path); std::env::set_var("RUST_LOG", &app_config.rust_log); std::env::set_var("FREE_USER_DATA_LIMIT", app_config.free_user_data_limit.to_string()); } // Sentry 初始化(仅在 SENTRY_DSN 环境变量已设置时启用) let _sentry_guard = if let Ok(dsn) = std::env::var("SENTRY_DSN") { if !dsn.is_empty() { eprintln!("[SENTRY] Sentry 已初始化 (DSN: {})", dsn); Some(sentry::init(( dsn, sentry::ClientOptions { release: sentry::release_name!(), traces_sample_rate: std::env::var("SENTRY_TRACES_SAMPLE_RATE") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(0.2), ..Default::default() }, ))) } else { eprintln!("[SENTRY] SENTRY_DSN 为空,跳过 Sentry 初始化"); None } } else { eprintln!("[SENTRY] 未设置 SENTRY_DSN,跳过 Sentry 初始化"); None }; // 初始化文件日志(JSON 格式,带轮转) let log_dir = std::path::Path::new("./logs"); std::fs::create_dir_all(log_dir).ok(); // 确保日志目录存在 let file_appender = tracing_appender::rolling::Builder::new() .rotation(tracing_appender::rolling::Rotation::DAILY) .filename_prefix("rust-backend") .filename_suffix("log") .build(log_dir) .expect("无法创建日志文件"); let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); // 保持文件 guard 存活(使用 Box 泄漏) std::mem::forget(_guard); use std::str::FromStr; let default_directive = tracing_subscriber::filter::Directive::from_str("info").unwrap(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() .add_directive( format!("rust_backend={}", app_config.rust_log) .parse() .unwrap_or(default_directive.clone()) ) .add_directive( format!("actix_web={}", app_config.rust_log) .parse() .unwrap_or(default_directive) ) .add_directive( "sqlx=warn".parse() .expect("sqlx=warn 是合法的日志指令") ) ) .with_target(true) .with_thread_ids(false) // 生产环境可开启 .with_file(true) .with_line_number(true) .with_writer(non_blocking) .with_ansi(false) // 文件中不使用 ANSI 颜色 .json() // JSON 格式便于 ELK 解析 .init(); info!("日志系统初始化成功,JSON格式,输出到 ./logs/"); let app_state = match AppState::load() { Ok(state) => state, Err(e) => { error!("环境变量配置错误: {}", e); std::process::exit(1); } }; let pool = match create_pool().await { Ok(pool) => pool, Err(e) => { error!("Failed to create database pool: {}", e); error!("Please check your database connection configuration in .env file"); std::process::exit(1); } }; let http_client = Client::builder() .timeout(std::time::Duration::from_secs(10)) .connect_timeout(std::time::Duration::from_secs(5)) .build() .expect("创建 HTTP 客户端失败"); // 启动时清理:过期订单 + 过期登录码 match db::cleanup_expired_pending_orders(&pool, None).await { Ok(n) => info!("已清理 {} 个过期待支付订单(启动时)", n), Err(e) => warn!("启动时清理过期待支付订单失败: {}", e), } match db::cleanup_expired_login_codes(&pool).await { Ok(n) => info!("已清理 {} 个过期的 web 登录码(启动时)", n), Err(e) => warn!("启动时清理 web 登录码失败: {}", e), } // 后台定时扫描待支付订单(每 5 分钟) let pool_clone = pool.clone(); actix_web::rt::spawn(async move { let mut interval = actix_web::rt::time::interval(std::time::Duration::from_secs(300)); interval.tick().await; // 跳过立即执行 loop { interval.tick().await; tracing::info!("[定时任务] 开始扫描待支付订单..."); match db::check_and_retry_pending_orders(&pool_clone).await { Ok(confirmed) => { if confirmed > 0 { tracing::info!("[定时任务] 自动确认了 {} 个待支付订单", confirmed); } } Err(e) => tracing::warn!("[定时任务] 扫描待支付订单失败: {}", e), } // 会员到期前 7 天提醒 let _ = db::check_member_expiry_soon(&pool_clone).await; // 清理超过 24 小时的过期待支付订单 let _ = db::cleanup_expired_pending_orders(&pool_clone, None).await; // 清理过期的 refresh_token let _ = db::cleanup_expired_refresh_tokens(&pool_clone).await; } }); info!("Attempting to start server..."); let is_production = std::env::var("APP_ENV").unwrap_or_else(|_| "development".into()) == "production"; // 端口优先级:SERVER_PORT 环境变量 > 环境默认 let ports: Vec = if let Ok(port_str) = std::env::var("SERVER_PORT") { if let Ok(port) = port_str.parse::() { vec![port] } else { vec![] } } else if is_production { vec![4433] } else { vec![8080, 3000] }; let mut server: Option< Pin> + Unpin>>, > = None; let mut bound_port = 0; for port in &ports { let addr = format!("127.0.0.1:{}", port); info!("Trying to bind to {}", addr); let pool_clone = pool.clone(); let http_client_clone = http_client.clone(); let app_state_clone = app_state.clone(); if *port == 443 || *port == 8443 || *port == 4433 || *port == 4434 { let ssl_builder = match create_ssl_acceptor() { Ok(builder) => builder, Err(e) => { error!("Failed to create SSL acceptor: {}", e); continue; } }; match HttpServer::new(move || { create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone()) }) .keep_alive(std::time::Duration::from_secs(30)) .shutdown_timeout(10) .backlog(1024) .bind_openssl(&addr, ssl_builder) { Ok(s) => { info!("Successfully bound to {} with HTTPS", addr); let server_future = s.run(); server = Some(Box::pin(server_future)); bound_port = *port; break; } Err(e) => { error!("Failed to bind to {} with HTTPS: {}", addr, e); if e.kind() == std::io::ErrorKind::PermissionDenied { error!(" -> Permission denied. Try running with sudo or use a port > 1024"); } continue; } } } else { match HttpServer::new(move || { create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone()) }) .keep_alive(std::time::Duration::from_secs(30)) .shutdown_timeout(10) .backlog(1024) .bind(&addr) { Ok(s) => { info!("Successfully bound to {} with HTTP", addr); let server_future = s.run(); server = Some(Box::pin(server_future)); bound_port = *port; break; } Err(e) => { error!("Failed to bind to {} with HTTP: {}", addr, e); if e.kind() == std::io::ErrorKind::PermissionDenied { error!( " -> Permission denied. Try running with sudo or use a port > 1024" ); } continue; } } } } match server { Some(s) => { if bound_port == 443 || bound_port == 8443 { info!("Server started successfully with HTTPS"); } else { info!("Server started successfully with HTTP"); } s.await } None => { error!("Failed to bind to any port. Please check your system configuration."); std::process::exit(1); } } }