From a819a0db91943a35867e3770d4d21b49ba9d37fd Mon Sep 17 00:00:00 2001 From: Milky0217 Date: Wed, 15 Apr 2026 11:51:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=9D=99=E6=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=A2=ABJWT=E6=8B=A6=E6=88=AA=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 /static/{tail:.*} 路由移到受保护 scope 之前 - 确保静态文件无需认证即可访问 --- src/handlers/admin.rs | 118 ++++++ src/handlers/auth.rs | 125 +++++++ src/handlers/health.rs | 21 ++ src/handlers/mod.rs | 26 ++ src/handlers/static_files.rs | 34 ++ src/handlers/user.rs | 77 ++++ src/handlers/weather.rs | 290 +++++++++++++++ src/main.rs | 694 +---------------------------------- src/models.rs | 21 ++ 9 files changed, 728 insertions(+), 678 deletions(-) create mode 100644 src/handlers/admin.rs create mode 100644 src/handlers/auth.rs create mode 100644 src/handlers/health.rs create mode 100644 src/handlers/mod.rs create mode 100644 src/handlers/static_files.rs create mode 100644 src/handlers/user.rs create mode 100644 src/handlers/weather.rs diff --git a/src/handlers/admin.rs b/src/handlers/admin.rs new file mode 100644 index 0000000..3f57167 --- /dev/null +++ b/src/handlers/admin.rs @@ -0,0 +1,118 @@ +use actix_web::{web, get, put, HttpResponse, Responder}; +use sqlx::postgres::PgPool; +use tracing::error; +use chrono::{DateTime, Utc}; + +use crate::db; +use crate::models::{Claims, ErrorResponse, UpdatePaymentRequest}; + +#[get("/api/admin/users/{id}")] +pub async fn admin_get_user( + path: web::Path, + pool: web::Data, + claims: web::ReqData, +) -> impl Responder { + let target_user_id = path.into_inner(); + tracing::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")] +pub 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(); + tracing::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 DateTime::parse_from_rfc3339(date_str) { + Ok(dt) => Some(dt.with_timezone(&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 + })) + } + } +} diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs new file mode 100644 index 0000000..be3e672 --- /dev/null +++ b/src/handlers/auth.rs @@ -0,0 +1,125 @@ +use actix_web::{web, HttpResponse, Responder, post}; +use reqwest::Client; +use sqlx::postgres::PgPool; +use tracing::{debug, error, info, warn}; + +use crate::auth::generate_token; +use crate::models::{ + AppState, ErrorResponse, TokenResponse, WeChatApiResponse, WeChatLoginRequest, +}; + +#[post("/api/login")] +pub 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: None, + }); + } + }; + + 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: None, + }); + } + }; + + let token_response = TokenResponse::new(token); + HttpResponse::Ok().json(token_response) +} diff --git a/src/handlers/health.rs b/src/handlers/health.rs new file mode 100644 index 0000000..1914af3 --- /dev/null +++ b/src/handlers/health.rs @@ -0,0 +1,21 @@ +use actix_web::{web, get, HttpResponse, Responder}; +use sqlx::postgres::PgPool; + +#[get("/health")] +pub async fn health_check(pool: web::Data) -> impl Responder { + // 检查数据库连接 + match sqlx::query("SELECT 1").fetch_one(pool.get_ref()).await { + Ok(_) => HttpResponse::Ok().json(serde_json::json!({ + "status": "healthy", + "database": "connected" + })), + Err(e) => { + tracing::error!("健康检查失败: {}", e); + HttpResponse::ServiceUnavailable().json(serde_json::json!({ + "status": "unhealthy", + "database": "disconnected", + "error": "数据库连接失败" + })) + } + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..57ae302 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,26 @@ +// handlers 模块 - 按功能拆分路由处理器 +pub mod admin; +pub mod auth; +pub mod health; +pub mod static_files; +pub mod user; +pub mod weather; + +// 编译时嵌入目录(供 handlers 使用) +pub use include_dir::{Dir, include_dir}; +pub static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static"); +pub static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates"); + +// re-export handlers for convenient use in main.rs +pub use admin::admin_get_user; +pub use admin::admin_update_user_payment; +pub use auth::login; +pub use health::health_check; +pub use static_files::serve_static_files; +pub use user::get_current_user_profile; +pub use user::save_user_profile; +pub use weather::delete_weather; +pub use weather::generate_temp_token_handler; +pub use weather::get_weather_brief; +pub use weather::get_weather_details; +pub use weather::post_weather_data; diff --git a/src/handlers/static_files.rs b/src/handlers/static_files.rs new file mode 100644 index 0000000..a9aa8ec --- /dev/null +++ b/src/handlers/static_files.rs @@ -0,0 +1,34 @@ +use actix_web::{web, HttpResponse, Responder}; +use tracing::{debug, error}; + +use crate::handlers::STATIC_DIR; + +pub async fn serve_static_files(path: web::Path) -> impl Responder { + let file_path = path.into_inner(); + + debug!("请求静态文件: {}", file_path); + + match STATIC_DIR.get_file(&file_path) { + Some(file) => { + 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 => { + error!("文件未找到: {}", file_path); + HttpResponse::NotFound().body("静态文件不存在") + } + } +} diff --git a/src/handlers/user.rs b/src/handlers/user.rs new file mode 100644 index 0000000..5211bdc --- /dev/null +++ b/src/handlers/user.rs @@ -0,0 +1,77 @@ +use actix_web::{web, get, put, HttpResponse, Responder}; +use sqlx::postgres::PgPool; +use tracing::error; +use serde::Deserialize; + +use crate::db; +use crate::models::{Claims, ErrorResponse}; + +#[get("/api/user/profile")] +pub async fn get_current_user_profile( + pool: web::Data, + claims: web::ReqData, +) -> impl Responder { + let user_id = claims.user_id; + tracing::info!("获取当前用户信息, 用户ID: {}", user_id); + + match db::get_user_by_id(pool.get_ref(), 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")] +pub async fn save_user_profile( + pool: web::Data, + claims: web::ReqData, + body: web::Json, +) -> impl Responder { + let user_id = claims.user_id; + tracing::info!("保存用户信息, 用户ID: {}", user_id); + + match db::update_user_profile(pool.get_ref(), 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 + })) + } + } +} diff --git a/src/handlers/weather.rs b/src/handlers/weather.rs new file mode 100644 index 0000000..d947c15 --- /dev/null +++ b/src/handlers/weather.rs @@ -0,0 +1,290 @@ +use actix_web::{web, delete, get, post, HttpResponse, Responder}; +use sqlx::postgres::PgPool; +use tracing::{debug, error, info}; + +use crate::auth; +use crate::db; +use crate::handlers::TEMPLATES_DIR; +use crate::models::{AppState, Claims, ErrorResponse}; + +#[post("/api/post-weather-data")] +pub 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 db::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}")] +pub 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")] +pub 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")] +pub 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}")] +pub 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 + })) + } + } +} diff --git a/src/main.rs b/src/main.rs index 46682c9..919b973 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,45 +1,25 @@ use actix_web::middleware::from_fn; -use actix_web::{App, HttpResponse, HttpServer, Responder, delete, get, post, put, web}; +use actix_web::{App, HttpServer, web}; use include_dir::{Dir, include_dir}; -use tracing::{debug, error, info, warn}; +use tracing::{error, info}; 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 handlers; 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())?, - }) - } -} +use auth::jwt_middleware; +use db::create_pool; +use handlers::{ + admin_get_user, admin_update_user_payment, delete_weather, generate_temp_token_handler, + get_current_user_profile, get_weather_brief, get_weather_details, health_check, login, + post_weather_data, save_user_profile, serve_static_files, +}; +use models::AppState; // 加载TLS证书和私钥 fn create_ssl_acceptor() -> Result> { @@ -54,652 +34,10 @@ fn create_ssl_acceptor() -> Result, - 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: None, - }); - } - }; - - 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: None, - }); - } - }; - - 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 - })) - } - } -} - -// 健康检查端点 -#[get("/health")] -async fn health_check(pool: web::Data) -> impl Responder { - // 检查数据库连接 - match sqlx::query("SELECT 1").fetch_one(pool.get_ref()).await { - Ok(_) => HttpResponse::Ok().json(serde_json::json!({ - "status": "healthy", - "database": "connected" - })), - Err(e) => { - tracing::error!("健康检查失败: {}", e); - HttpResponse::ServiceUnavailable().json(serde_json::json!({ - "status": "unhealthy", - "database": "disconnected", - "error": "数据库连接失败" - })) - } - } -} - fn create_server_config( pool: PgPool, http_client: Client, @@ -717,9 +55,11 @@ fn create_server_config( .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")] - .service(get_weather_details) // #[get("/api/weather/details")] + .service(get_weather_details) // #[get("/weather/details")] // 受保护接口(JWT) .service( web::scope("") @@ -733,8 +73,6 @@ fn create_server_config( .service(admin_get_user) // #[get("/api/admin/users/{id}")] .service(admin_update_user_payment) // #[put("/api/admin/users/{id}/payment")] ) - // 静态文件 - .service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files))) // 健康检查 .service(health_check) } diff --git a/src/models.rs b/src/models.rs index d06e136..6b30cb4 100644 --- a/src/models.rs +++ b/src/models.rs @@ -298,3 +298,24 @@ pub struct UpdatePaymentRequest { pub is_paid: bool, pub paid_expires_at: Option, // ISO 8601 格式 } + +// 应用状态结构体 +#[derive(Clone)] +pub struct AppState { + pub jwt_secret: String, + pub wechat_appid: String, + pub wechat_secret: String, +} + +impl AppState { + pub 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())?, + }) + } +}