use actix_web::middleware::from_fn; use actix_web::{App, HttpResponse, HttpServer, Responder, delete, get, post, put, web}; use include_dir::{Dir, include_dir}; use log::{debug, error, info, warn}; use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod}; use reqwest::Client; use serde::Deserialize; use sqlx::postgres::PgPool; use std::pin::Pin; mod auth; mod db; mod models; use auth::{generate_token, jwt_middleware}; use db::{create_pool, insert_weather_data}; use models::{ErrorResponse, WeChatApiResponse, WeChatLoginRequest, WeatherData}; use crate::models::{Claims, TokenResponse, UpdatePaymentRequest}; // 编译时嵌入整个 static 目录 static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static"); static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates"); #[derive(Clone)] struct AppState { jwt_secret: String, wechat_appid: String, wechat_secret: String, } impl AppState { fn load() -> Result { Ok(Self { jwt_secret: std::env::var("JWT_SECRET") .map_err(|_| "环境变量JWT_SECRET未设置".to_string())?, wechat_appid: std::env::var("WECHAT_APPID") .map_err(|_| "环境变量WECHAT_APPID未设置".to_string())?, wechat_secret: std::env::var("WECHAT_SECRET") .map_err(|_| "环境变量WECHAT_SECRET未设置".to_string())?, }) } } // 加载TLS证书和私钥 fn create_ssl_acceptor() -> Result> { 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) } #[post("/api/login")] async fn login( pool: web::Data, req: web::Json, http_client: web::Data, app_state: web::Data, ) -> impl Responder { let url = format!( "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code", app_state.wechat_appid, app_state.wechat_secret, req.code ); debug!("准备调用微信API: {}", url.replace(&app_state.wechat_secret, "***")); let wechat_response = match http_client.get(&url).send().await { Ok(response) => { debug!("微信API请求成功,状态码: {}", response.status()); response } Err(e) => { error!("微信API请求失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse { error: "请求微信服务失败".to_string(), errcode: None, errmsg: None, }); } }; let wechat_data: WeChatApiResponse = match wechat_response.json().await { Ok(data) => { debug!("微信API响应解析成功"); data } Err(e) => { error!("微信API响应解析失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse { error: "解析微信响应失败".to_string(), errcode: None, errmsg: None, }); } }; if let Some(errcode) = wechat_data.errcode { warn!( "微信登录验证失败 - 错误码: {}, 错误信息: {:?}", errcode, wechat_data.errmsg ); return HttpResponse::BadRequest().json(ErrorResponse { error: "微信登录失败".to_string(), errcode: Some(errcode), errmsg: wechat_data.errmsg, }); } let openid = match wechat_data.openid { Some(id) => { let masked_openid = format!("{}***{}", &id[0..4], &id[id.len() - 4..]); debug!("获取到用户openid: {}", masked_openid); id } None => { error!("微信API响应中未包含openid"); return HttpResponse::InternalServerError().json(ErrorResponse { error: "微信API未返回openid".to_string(), errcode: None, errmsg: None, }); } }; let upsert_query = r#" INSERT INTO users (openid, name, type) VALUES ($1, left($1, 8), 2) ON CONFLICT (openid) DO UPDATE SET id = users.id RETURNING id "#; let user_id = match sqlx::query_as::<_, (i32,)>(upsert_query) .bind(&openid) .fetch_one(pool.get_ref()) .await { Ok((id,)) => { info!("用户登录/注册成功,用户ID: {}", id); id } Err(e) => { error!("用户信息处理失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse { error: "用户信息处理失败".to_string(), errcode: Some(500), errmsg: Some(e.to_string()), }); } }; let token = match generate_token(user_id, &openid, 2, &app_state.jwt_secret) { Ok(t) => { debug!("JWT生成成功: {}", &t[0..8]); t } Err(e) => { error!("JWT生成失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse { error: "生成身份令牌失败".to_string(), errcode: Some(500), errmsg: Some(e.to_string()), }); } }; let token_response = TokenResponse::new(token); HttpResponse::Ok().json(token_response) } #[post("/api/post-weather-data")] async fn post_weather_data( data: web::Json, pool: web::Data, claims: web::ReqData, ) -> impl Responder { info!("Received weather data, preparing to insert into DB..."); let user_id = claims.user_id; // 调用 db.rs 中的函数来处理数据库逻辑 match insert_weather_data(pool.get_ref(), &data, user_id).await { Ok(inserted_id) => { info!( "Successfully inserted weather data with id: {}", inserted_id ); HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": "Weather data inserted successfully", "inserted_id": inserted_id, "received_assignment": data.assignment_number })) } Err(error_msg) => { error!("Database operation failed: {}", error_msg); HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": 500, "errmsg": error_msg })) } } } #[post("/api/generate-temp-token/{resource_id}")] async fn generate_temp_token_handler( path: web::Path, pool: web::Data, claims: web::ReqData, app_state: web::Data, ) -> impl Responder { let resource_id = path.into_inner(); let openid: &String = &claims.openid; match db::get_weather_details(pool.get_ref(), resource_id).await { Ok(weather_data) => { if weather_data.openid != *openid { return HttpResponse::Forbidden().json(ErrorResponse { error: "无权限为该资源生成临时token".to_string(), errcode: Some(403), errmsg: None, }); } } Err(e) => { return HttpResponse::BadRequest().json(ErrorResponse { error: format!("资源不存在: {}", e), errcode: Some(404), errmsg: None, }); } } let temp_token = match auth::generate_temp_token(openid, resource_id, &app_state.jwt_secret, 10) { Ok(token) => token, Err(e) => { return HttpResponse::InternalServerError().json(ErrorResponse { error: format!("生成临时token失败: {}", e), errcode: Some(500), errmsg: None, }); } }; HttpResponse::Ok().json(serde_json::json!({ "success": true, "temp_token": temp_token, "expire_minutes": 10, })) } #[get("/weather/details")] async fn get_weather_details( pool: web::Data, claims: Option>, query: web::Query, app_state: web::Data, ) -> impl Responder { let mut is_temp_token = false; let (openid, weather_id) = if let Some(temp_token) = query.get("temp_token").and_then(|v| v.as_str()) { is_temp_token = true; let temp_claims = match auth::verify_temp_token(temp_token, &app_state.jwt_secret) { Ok(c) => c, Err(e) => { return HttpResponse::Unauthorized().json(ErrorResponse { error: format!("临时token无效: {}", e), errcode: Some(401), errmsg: None, }); } }; (temp_claims.openid, temp_claims.resource_id) } else if let Some(claims) = claims { let weather_id = match query.get("id").and_then(|v| v.as_i64()) { Some(id) => id as i32, None => { return HttpResponse::BadRequest().json(ErrorResponse { error: "缺少资源ID参数(id)".to_string(), errcode: Some(400), errmsg: None, }); } }; (claims.openid.clone(), weather_id) } else { return HttpResponse::Unauthorized().json(ErrorResponse { error: "缺少token(需提供JWT或临时token)".to_string(), errcode: Some(401), errmsg: None, }); }; let weather_data = match db::get_weather_details(pool.get_ref(), weather_id).await { Ok(data) => { if data.openid != openid { return HttpResponse::Forbidden().json(ErrorResponse { error: "无权限访问该数据".to_string(), errcode: Some(403), errmsg: None, }); } data } Err(error_msg) => { error!("获取天气数据详情失败: {}", error_msg); let errcode = if error_msg.starts_with("未找到") { 404 } else { 500 }; return HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": errcode, "errmsg": error_msg })); } }; if is_temp_token { let index_html = match TEMPLATES_DIR.get_file("index.html") { Some(file) => file.contents_utf8().unwrap_or_default(), None => { return HttpResponse::InternalServerError().json(ErrorResponse { error: "无法找到模板文件".to_string(), errcode: Some(500), errmsg: None, }); } }; let weather_data_json = serde_json::to_string(&weather_data).unwrap_or_default(); let data_script = format!( r#" "#, weather_data_json ); let rendered_html = index_html .replace("", &data_script) .replace( "style=\"display: none;\"", "", ); HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(rendered_html) } else { HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": weather_data })) } } #[get("/weather")] async fn get_weather_brief( query: web::Query, pool: web::Data, claims: web::ReqData, ) -> impl Responder { let page = match query.get("page") { Some(v) => { let raw = if let Some(num) = v.as_i64() { num as i32 } else if let Some(s) = v.as_str() { s.parse().unwrap_or(1) } else { 1 }; raw.max(1) } None => 1, }; let limit = match query.get("limit") { Some(v) => { let raw = if let Some(num) = v.as_i64() { num as i32 } else if let Some(s) = v.as_str() { s.parse().unwrap_or(10) } else { 10 }; raw.clamp(1, 100) } None => 10, }; debug!("获取天气数据列表, 页码: {}, 每页条数: {}", page, limit); match db::get_weather_list(pool.get_ref(), claims.user_id, page, limit).await { Ok(response) => { debug!( "获取到的天气列表: {:?}, 总条数: {}", response.list, response.total ); HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": response.list, "page": page, "limit": limit, "total": response.total })) } Err(error_msg) => { error!("获取天气数据列表失败: {}", error_msg); HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": 500, "errmsg": error_msg })) } } } // 删除天气数据 #[delete("/weather/delete/{id}")] async fn delete_weather( path: web::Path, pool: web::Data, claims: web::ReqData, ) -> impl Responder { let weather_id = path.into_inner(); info!("删除天气数据, ID: {}", weather_id); match db::delete_weather_data(pool.get_ref(), weather_id, claims.user_id).await { Ok(_) => HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": format!("天气数据 {} 已成功删除", weather_id) })), Err(error_msg) => { error!("删除天气数据失败: {}", error_msg); let errcode = if error_msg.starts_with("未找到") { 404 } else { 500 }; HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": errcode, "errmsg": error_msg })) } } } // 管理员获取用户信息 #[get("/api/admin/users/{id}")] async fn admin_get_user( path: web::Path, pool: web::Data, claims: web::ReqData, ) -> impl Responder { let target_user_id = path.into_inner(); info!("管理员获取用户信息, 目标用户ID: {}", target_user_id); // 验证当前用户是否为管理员 let current_user = match db::get_user_by_id(pool.get_ref(), claims.user_id).await { Ok(user) => user, Err(e) => { error!("获取当前用户信息失败: {}", e); return HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": 500, "errmsg": "获取用户信息失败" })); } }; if !current_user.is_admin { return HttpResponse::Forbidden().json(serde_json::json!({ "success": false, "errcode": 403, "errmsg": "无权限执行此操作" })); } // 获取目标用户信息 match db::get_user_by_id(pool.get_ref(), target_user_id).await { Ok(user) => HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": user })), Err(e) => { error!("获取目标用户信息失败: {}", e); let errcode = if e.starts_with("未找到") { 404 } else { 500 }; HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": errcode, "errmsg": e })) } } } // 管理员更新用户付费状态 #[put("/api/admin/users/{id}/payment")] async fn admin_update_user_payment( path: web::Path, pool: web::Data, claims: web::ReqData, body: web::Json, ) -> impl Responder { let target_user_id = path.into_inner(); info!("管理员更新用户付费状态, 目标用户ID: {}", target_user_id); // 验证当前用户是否为管理员 let current_user = match db::get_user_by_id(pool.get_ref(), claims.user_id).await { Ok(user) => user, Err(e) => { error!("获取当前用户信息失败: {}", e); return HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": 500, "errmsg": "获取用户信息失败" })); } }; if !current_user.is_admin { return HttpResponse::Forbidden().json(serde_json::json!({ "success": false, "errcode": 403, "errmsg": "无权限执行此操作" })); } // 解析 paid_expires_at let paid_expires_at = match &body.paid_expires_at { Some(date_str) => match chrono::DateTime::parse_from_rfc3339(date_str) { Ok(dt) => Some(dt.with_timezone(&chrono::Utc)), Err(e) => { return HttpResponse::BadRequest().json(serde_json::json!({ "success": false, "errcode": 400, "errmsg": format!("日期格式错误: {}", e) })); } }, None => None, }; // 更新用户付费状态 match db::update_user_payment_status(pool.get_ref(), target_user_id, body.is_paid, paid_expires_at).await { Ok(_) => HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": "用户付费状态已更新" })), Err(e) => { error!("更新用户付费状态失败: {}", e); HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": 500, "errmsg": e })) } } } // 在 main.rs 中添加以下函数,用于处理静态文件请求 async fn serve_static_files(path: web::Path) -> impl Responder { // 获取请求的文件路径(例如 "css/style.css") let file_path = path.into_inner(); // 【调试日志 1】打印服务器收到的文件路径 debug!("请求静态文件: {}", file_path); // 从嵌入的 STATIC_DIR 中查找文件 match STATIC_DIR.get_file(&file_path) { Some(file) => { // 根据文件扩展名设置 Content-Type let content_type = match file_path.split('.').next_back() { Some("ico") => "image/x-icon", Some("css") => "text/css", Some("js") => "application/javascript", Some("html") => "text/html", Some("ttf") => "font/ttf", Some("woff") => "font/woff", Some("woff2") => "font/woff2", _ => "application/octet-stream", // 默认类型 }; debug!( "成功找到文件: {}, Content-Type: {}", file_path, content_type ); HttpResponse::Ok() .content_type(content_type) .body(file.contents()) } None => { // 文件不存在时返回 404 // 【调试日志 3】打印文件未找到的信息 error!("文件未找到: {}", file_path); HttpResponse::NotFound().body("静态文件不存在") } } } // 获取当前用户信息(普通用户可用) #[get("/api/user/profile")] async fn get_current_user_profile( pool: web::Data, claims: web::ReqData, ) -> impl Responder { info!("获取当前用户信息, 用户ID: {}", claims.user_id); match db::get_user_by_id(pool.get_ref(), claims.user_id).await { Ok(user) => { let is_paid_active = user.is_paid && (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > chrono::Utc::now()); HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { "id": user.id, "name": user.name, "nickname": user.nickname, "avatarUrl": user.avatar_url, "is_paid": user.is_paid, "is_paid_active": is_paid_active, "is_admin": user.is_admin, "paid_expires_at": user.paid_expires_at } })) }, Err(e) => { error!("获取用户信息失败: {}", e); HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": 500, "errmsg": e })) } } } // 保存用户信息请求体 #[derive(Debug, Deserialize)] pub struct SaveUserProfileRequest { pub nickname: Option, pub avatar_url: Option, } // 保存用户信息(普通用户可用) #[put("/api/user/profile")] async fn save_user_profile( pool: web::Data, claims: web::ReqData, body: web::Json, ) -> impl Responder { info!("保存用户信息, 用户ID: {}", claims.user_id); match db::update_user_profile(pool.get_ref(), claims.user_id, &body.nickname, &body.avatar_url).await { Ok(_) => { HttpResponse::Ok().json(serde_json::json!({ "success": true })) }, Err(e) => { error!("保存用户信息失败: {}", e); HttpResponse::Ok().json(serde_json::json!({ "success": false, "errcode": 500, "errmsg": e })) } } } 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))) .service(login) .service(get_weather_details) .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), ) } #[actix_web::main] async fn main() -> std::io::Result<()> { if let Err(e) = dotenvy::dotenv() { eprintln!("警告:加载.env文件失败,使用系统环境变量: {}", e); } env_logger::init(); info!("logger init successful"); 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> + 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); } } }