Files
asd-backend/src/main.rs
Milky0217 8c9552fbfc feat: 新增支付页面接口,为接入支付宝做准备
- GET /payment: 套餐选择页(无需认证,外部浏览器打开)
- GET /payment/page: 支付引导页(需 JWT,创建订单)
- GET /payment/pay: 支付宝电脑网站支付占位页(需 JWT)
- AppState 新增支付宝配置字段(可选)
- afterbody.js: PDF下载增加状态提示和重试按钮
2026-04-23 10:48:31 +08:00

264 lines
10 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, alipay_trade_page_pay,
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, payment_index, payment_page, 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(payment_index) // GET /payment — 套餐选择页
.service(payment_page) // GET /payment/page需 JWT
.service(alipay_trade_page_pay) // GET /payment/pay需 JWT
// 静态文件(无需认证)
.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());
}
// 初始化文件日志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);
}
}
}