Files
asd-backend/src/main.rs

377 lines
15 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,
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_guest_login, 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网页轮询
// robots.txt无需认证
.route("/robots.txt", web::get().to(|| async { serve_static_files(web::Path::from("robots.txt".to_string())).await }))
// 静态文件(无需认证)
.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(web_guest_login) // POST /api/guest-login — 访客登录(免微信)
.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() {
info!("Sentry 已初始化");
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 {
info!("SENTRY_DSN 为空,跳过 Sentry 初始化");
None
}
} else {
info!("未设置 SENTRY_DSN跳过 Sentry 初始化");
None
};
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_ansi(true)
.compact()
.init();
info!("日志系统初始化成功,输出到 systemd journal (journalctl)");
info!("日志级别: {}", app_config.rust_log);
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()
.unwrap_or_else(|e| {
panic!("创建 HTTP 客户端失败: {}", e);
});
// 启动时清理:过期订单 + 过期登录码
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),
}
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!("[定时任务] 开始...");
// 会员到期前 7 天提醒
if let Err(e) = db::check_member_expiry_soon(&pool_clone).await {
tracing::error!("检查会员到期失败: {}", e);
}
// 清理超过 24 小时的过期待支付订单
if let Err(e) = db::cleanup_expired_pending_orders(&pool_clone, None).await {
tracing::error!("清理过期订单失败: {}", e);
}
// 清理过期的 refresh_token
if let Err(e) = db::cleanup_expired_refresh_tokens(&pool_clone).await {
tracing::error!("清理过期 refresh token 失败: {}", e);
}
}
});
info!("Attempting to start server...");
let is_production = std::env::var("APP_ENV").unwrap_or_else(|_| "development".into()) == "production";
// 端口优先级SERVER_PORT 环境变量 > 环境默认
let ports: Vec<u16> = if let Ok(port_str) = std::env::var("SERVER_PORT") {
if let Ok(port) = port_str.parse::<u16>() {
vec![port]
} else {
vec![]
}
} else 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 || *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);
}
}
}