Files
asd-backend/src/main.rs

357 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, 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, login, 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<SslAcceptorBuilder, Box<dyn std::error::Error>> {
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(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(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::<f32>().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::new();
// 启动时清理:过期订单 + 过期登录码
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),
}
}
});
info!("Attempting to start server...");
let is_production = std::env::var("APP_ENV").unwrap_or_else(|_| "development".into()) == "production";
let ports: Vec<u16> = if is_production {
vec![4433]
} else {
vec![8080, 3000]
};
let mut server: Option<
Pin<Box<dyn std::future::Future<Output = std::io::Result<()>> + 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 {
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())
})
.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())
})
.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);
}
}
}