fix: 调整路由顺序,修复静态文件被JWT拦截的问题
- 将 /static/{tail:.*} 路由移到受保护 scope 之前
- 确保静态文件无需认证即可访问
This commit is contained in:
118
src/handlers/admin.rs
Normal file
118
src/handlers/admin.rs
Normal file
@@ -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<i32>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
) -> 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<i32>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
body: web::Json<UpdatePaymentRequest>,
|
||||||
|
) -> 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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/handlers/auth.rs
Normal file
125
src/handlers/auth.rs
Normal file
@@ -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<PgPool>,
|
||||||
|
req: web::Json<WeChatLoginRequest>,
|
||||||
|
http_client: web::Data<Client>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
) -> 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)
|
||||||
|
}
|
||||||
21
src/handlers/health.rs
Normal file
21
src/handlers/health.rs
Normal file
@@ -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<PgPool>) -> 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": "数据库连接失败"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/handlers/mod.rs
Normal file
26
src/handlers/mod.rs
Normal file
@@ -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;
|
||||||
34
src/handlers/static_files.rs
Normal file
34
src/handlers/static_files.rs
Normal file
@@ -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<String>) -> 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("静态文件不存在")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/handlers/user.rs
Normal file
77
src/handlers/user.rs
Normal file
@@ -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<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
) -> 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<String>,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/api/user/profile")]
|
||||||
|
pub async fn save_user_profile(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
body: web::Json<SaveUserProfileRequest>,
|
||||||
|
) -> 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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
290
src/handlers/weather.rs
Normal file
290
src/handlers/weather.rs
Normal file
@@ -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<crate::models::WeatherData>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
) -> 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<i32>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
) -> 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<PgPool>,
|
||||||
|
claims: Option<web::ReqData<Claims>>,
|
||||||
|
query: web::Query<serde_json::Value>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
) -> 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#"
|
||||||
|
<script>
|
||||||
|
window.weatherData = {};
|
||||||
|
</script>
|
||||||
|
"#,
|
||||||
|
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<serde_json::Value>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
) -> 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<i32>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
) -> 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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
694
src/main.rs
694
src/main.rs
@@ -1,45 +1,25 @@
|
|||||||
use actix_web::middleware::from_fn;
|
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 include_dir::{Dir, include_dir};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{error, info};
|
||||||
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
|
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::postgres::PgPool;
|
use sqlx::postgres::PgPool;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
mod db;
|
mod db;
|
||||||
|
mod handlers;
|
||||||
mod models;
|
mod models;
|
||||||
|
|
||||||
use auth::{generate_token, jwt_middleware};
|
use auth::jwt_middleware;
|
||||||
use db::{create_pool, insert_weather_data};
|
use db::create_pool;
|
||||||
use models::{ErrorResponse, WeChatApiResponse, WeChatLoginRequest, WeatherData};
|
use handlers::{
|
||||||
|
admin_get_user, admin_update_user_payment, delete_weather, generate_temp_token_handler,
|
||||||
use crate::models::{Claims, TokenResponse, UpdatePaymentRequest};
|
get_current_user_profile, get_weather_brief, get_weather_details, health_check, login,
|
||||||
|
post_weather_data, save_user_profile, serve_static_files,
|
||||||
// 编译时嵌入整个 static 目录
|
};
|
||||||
static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static");
|
use models::AppState;
|
||||||
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<Self, String> {
|
|
||||||
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证书和私钥
|
// 加载TLS证书和私钥
|
||||||
fn create_ssl_acceptor() -> Result<SslAcceptorBuilder, Box<dyn std::error::Error>> {
|
fn create_ssl_acceptor() -> Result<SslAcceptorBuilder, Box<dyn std::error::Error>> {
|
||||||
@@ -54,652 +34,10 @@ fn create_ssl_acceptor() -> Result<SslAcceptorBuilder, Box<dyn std::error::Error
|
|||||||
builder.set_private_key_file(&key_path, SslFiletype::PEM)?;
|
builder.set_private_key_file(&key_path, SslFiletype::PEM)?;
|
||||||
builder.set_certificate_chain_file(&cert_path)?;
|
builder.set_certificate_chain_file(&cert_path)?;
|
||||||
|
|
||||||
// 返回构建器而不是构建后的接受器
|
// 返回构建器
|
||||||
Ok(builder)
|
Ok(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/api/login")]
|
|
||||||
async fn login(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
req: web::Json<WeChatLoginRequest>,
|
|
||||||
http_client: web::Data<Client>,
|
|
||||||
app_state: web::Data<AppState>,
|
|
||||||
) -> 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<WeatherData>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
claims: web::ReqData<Claims>,
|
|
||||||
) -> 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<i32>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
claims: web::ReqData<Claims>,
|
|
||||||
app_state: web::Data<AppState>,
|
|
||||||
) -> 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<PgPool>,
|
|
||||||
claims: Option<web::ReqData<Claims>>,
|
|
||||||
query: web::Query<serde_json::Value>,
|
|
||||||
app_state: web::Data<AppState>,
|
|
||||||
) -> 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#"
|
|
||||||
<script>
|
|
||||||
window.weatherData = {};
|
|
||||||
</script>
|
|
||||||
"#,
|
|
||||||
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<serde_json::Value>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
claims: web::ReqData<Claims>,
|
|
||||||
) -> 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<i32>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
claims: web::ReqData<Claims>,
|
|
||||||
) -> 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<i32>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
claims: web::ReqData<Claims>,
|
|
||||||
) -> 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<i32>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
claims: web::ReqData<Claims>,
|
|
||||||
body: web::Json<UpdatePaymentRequest>,
|
|
||||||
) -> 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<String>) -> 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<PgPool>,
|
|
||||||
claims: web::ReqData<Claims>,
|
|
||||||
) -> 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<String>,
|
|
||||||
pub avatar_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存用户信息(普通用户可用)
|
|
||||||
#[put("/api/user/profile")]
|
|
||||||
async fn save_user_profile(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
claims: web::ReqData<Claims>,
|
|
||||||
body: web::Json<SaveUserProfileRequest>,
|
|
||||||
) -> 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<PgPool>) -> 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(
|
fn create_server_config(
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
http_client: Client,
|
http_client: Client,
|
||||||
@@ -717,9 +55,11 @@ fn create_server_config(
|
|||||||
.app_data(web::Data::new(pool))
|
.app_data(web::Data::new(pool))
|
||||||
.app_data(web::Data::new(http_client))
|
.app_data(web::Data::new(http_client))
|
||||||
.app_data(web::Data::new(app_state))
|
.app_data(web::Data::new(app_state))
|
||||||
|
// 静态文件(无需认证)
|
||||||
|
.service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
|
||||||
// API 接口
|
// API 接口
|
||||||
.service(login) // #[post("/api/login")]
|
.service(login) // #[post("/api/login")]
|
||||||
.service(get_weather_details) // #[get("/api/weather/details")]
|
.service(get_weather_details) // #[get("/weather/details")]
|
||||||
// 受保护接口(JWT)
|
// 受保护接口(JWT)
|
||||||
.service(
|
.service(
|
||||||
web::scope("")
|
web::scope("")
|
||||||
@@ -733,8 +73,6 @@ fn create_server_config(
|
|||||||
.service(admin_get_user) // #[get("/api/admin/users/{id}")]
|
.service(admin_get_user) // #[get("/api/admin/users/{id}")]
|
||||||
.service(admin_update_user_payment) // #[put("/api/admin/users/{id}/payment")]
|
.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)
|
.service(health_check)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,3 +298,24 @@ pub struct UpdatePaymentRequest {
|
|||||||
pub is_paid: bool,
|
pub is_paid: bool,
|
||||||
pub paid_expires_at: Option<String>, // ISO 8601 格式
|
pub paid_expires_at: Option<String>, // 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<Self, String> {
|
||||||
|
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())?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user