- 添加 /api/refresh-token 接口支持 Token 续期 - 登录接口返回 access_token 和 refresh_token - 新增 refresh_tokens 表存储 refresh_token - 部署脚本添加数据库备份和迁移功能 - deploy.sh 添加 4 项 API 测试 - 更新 AGENTS.md 文档
261 lines
9.9 KiB
Rust
261 lines
9.9 KiB
Rust
use actix_web::middleware::from_fn;
|
||
use actix_web::{App, HttpServer, web};
|
||
use tracing::{error, info};
|
||
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;
|
||
|
||
use auth::jwt_middleware;
|
||
use config::AppConfig;
|
||
use db::create_pool;
|
||
use handlers::{
|
||
admin_get_user, admin_update_user_payment, add_favorite, create_order,
|
||
delete_weather, generate_temp_token_handler, get_current_user_profile,
|
||
get_favorites, get_user_quota, get_weather_brief, get_weather_details,
|
||
health_check, login, mock_confirm, post_weather_data, refresh_token,
|
||
remove_favorite, root, save_user_profile, serve_static_files,
|
||
};
|
||
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))
|
||
// 根路径(无需认证)
|
||
.service(root) // #[get("/")] - 返回服务信息
|
||
// 静态文件(无需认证)
|
||
.service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
|
||
// API 接口
|
||
.service(login) // #[post("/api/login")]
|
||
.service(refresh_token) // #[post("/api/refresh-token")](公开接口,无需认证)
|
||
.service(get_weather_details) // #[get("/weather/details")](支持 JWT 或 temp_token,公开接口)
|
||
.service(health_check) // #[get("/health")](公开接口,无需认证)
|
||
// 受保护接口(JWT)
|
||
.service(
|
||
web::scope("")
|
||
.wrap(from_fn(jwt_middleware))
|
||
.service(post_weather_data) // #[post("/api/post-weather-data")]
|
||
.service(get_weather_brief) // #[get("/api/weather")]
|
||
.service(generate_temp_token_handler) // #[post("/api/generate-temp-token/{resource_id}")]
|
||
.service(delete_weather) // #[delete("/api/weather/delete/{id}")]
|
||
.service(get_current_user_profile) // #[get("/api/user/profile")]
|
||
.service(save_user_profile) // #[put("/api/user/profile")]
|
||
.service(admin_get_user) // #[get("/api/admin/users/{id}")]
|
||
.service(admin_update_user_payment) // #[put("/api/admin/users/{id}/payment")]
|
||
.service(create_order) // #[post("/api/payment/create-order")]
|
||
.service(mock_confirm) // #[post("/api/payment/mock-confirm")]
|
||
.service(get_user_quota) // #[get("/api/user/quota")]
|
||
.service(get_favorites) // #[get("/api/favorites")]
|
||
.service(add_favorite) // #[post("/api/favorites/{id}")]
|
||
.service(remove_favorite) // #[delete("/api/favorites/{id}")]
|
||
)
|
||
// 健康检查
|
||
.service(health_check)
|
||
}
|
||
|
||
#[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());
|
||
std::env::set_var("APP_VERSION", &app_config.app_version);
|
||
}
|
||
|
||
// 初始化文件日志(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);
|
||
|
||
tracing_subscriber::fmt()
|
||
.with_env_filter(
|
||
tracing_subscriber::EnvFilter::from_default_env()
|
||
.add_directive(format!("rust_backend={}", app_config.rust_log).parse().unwrap())
|
||
.add_directive(format!("actix_web={}", app_config.rust_log).parse().unwrap())
|
||
.add_directive("sqlx=warn".parse().unwrap())
|
||
)
|
||
.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();
|
||
|
||
info!("Attempting to start server...");
|
||
|
||
let is_production = std::env::var("APP_ENV").unwrap_or_else(|_| "development".into()) == "production";
|
||
let ports = if is_production {
|
||
vec![4433, 8443, 8080, 3000, 8000, 8888]
|
||
} else {
|
||
vec![8080, 3000, 8000, 8888, 4433, 8443]
|
||
};
|
||
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);
|
||
}
|
||
}
|
||
}
|