Files
asd-backend/src/main.rs
Milky0217 61b1174b30 fix: 统一字段命名为 camelCase 并修复详情 API
- models.rs: 将 inspectiontype/assignmentnumber 的 serde rename 改为
  inspectionType/assignmentNumber,与前端 TypeScript 接口保持一致
- models.rs: 为 WeatherDataBrief 添加 isFavorite 字段支持
- models.rs: WeatherData.is_favorite 添加 skip_deserializing 避免 POST
  请求解析失败,同时使用 default 处理数据库 NULL 值
- handlers/weather.rs: get_weather_details 支持 string 和 number 类型的
  id 参数解析
- IMPROVEMENTS.md: 新增第十三章记录本次对话经验(serde 配置、config
  路径解析、TOML 结构等)
2026-04-17 20:47:00 +08:00

251 lines
9.4 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 include_dir::{Dir, include_dir};
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 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, remove_favorite,
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(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
// API 接口
.service(login) // #[post("/api/login")]
// 受保护接口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(get_weather_details) // #[get("/weather/details")] ← 移入受保护作用域
.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 ports = vec![4433, 8443, 8080, 3000, 8000, 8888];
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 {
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);
}
}
}