Files
asd-backend/src/main.rs
Milky0217 e14c85436b feat(auth): 添加 Refresh Token 双 Token 机制
- 添加 /api/refresh-token 接口支持 Token 续期
- 登录接口返回 access_token 和 refresh_token
- 新增 refresh_tokens 表存储 refresh_token
- 部署脚本添加数据库备份和迁移功能
- deploy.sh 添加 4 项 API 测试
- 更新 AGENTS.md 文档
2026-04-19 16:01:41 +08:00

261 lines
9.9 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;
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);
}
}
}