From bdbba2cd6056bc40d21a51439de6a8b26727a8b4 Mon Sep 17 00:00:00 2001 From: Milky0217 Date: Fri, 17 Apr 2026 17:40:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=94=B6=E8=97=8F?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - models.rs: 添加 is_favorite 字段到 WeatherData 和 WeatherDataBrief - db.rs: 添加 get_favorites_list 和 set_weather_favorite 函数 - handlers/favorites.rs: 新建收藏 API 处理器 - handlers/mod.rs: 导出 favorites 模块 - main.rs: 注册收藏 API 路由 --- src/db.rs | 79 +++++++++++++++++++++++++++++++-- src/handlers/favorites.rs | 93 +++++++++++++++++++++++++++++++++++++++ src/handlers/mod.rs | 2 + src/main.rs | 12 +++-- src/models.rs | 3 ++ 5 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 src/handlers/favorites.rs diff --git a/src/db.rs b/src/db.rs index 0c9d38d..a76c32e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -34,10 +34,10 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user atmosphericstability, suitabilitydegree, winddirection, averagewinddirection, winddirectionstandarddeviation, windspeed, averagewindspeed, windspeedsuitability, winddirectionsuitability, overallsuitability, inspectiontype, assignmentnumber, - calculatedwindspeed, hasspotcheckwindspeed, version + calculatedwindspeed, hasspotcheckwindspeed, version, is_favorite ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, - $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36 + $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37 ) RETURNING id "#; @@ -85,6 +85,7 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user .bind(weather_data.calculated_wind_speed) // 这个本来就是 Option,无需修改 .bind(weather_data.has_spot_check_wind_speed) .bind(&weather_data.version) + .bind(weather_data.is_favorite) .fetch_one(pool) .await { @@ -129,7 +130,7 @@ pub async fn get_weather_details(pool: &PgPool, weather_id: i32) -> Result Result { + let offset = (page - 1) * limit; + + let total_query = r#" + SELECT COUNT(*) as total + FROM weather_data + WHERE user_id = $1 AND is_favorite = true + "#; + let total = sqlx::query_as::<_, (i64,)>(total_query) + .bind(user_id) + .fetch_one(pool) + .await + .map_err(|e| format!("查询收藏总数失败: {}", e))? + .0; + + let list_query = r#" + SELECT + id, title, date, hour, min, longitude, latitude, is_favorite + FROM weather_data + WHERE user_id = $1 AND is_favorite = true + ORDER BY date DESC, hour DESC, min DESC + LIMIT $2 OFFSET $3 + "#; + let list = sqlx::query_as::<_, WeatherDataBrief>(list_query) + .bind(user_id) + .bind(limit) + .bind(offset) + .fetch_all(pool) + .await + .map_err(|e| format!("查询收藏列表失败: {}", e))?; + + Ok(WeatherListResponse { list, total }) +} + +/// 设置天气数据的收藏状态 +pub async fn set_weather_favorite( + pool: &PgPool, + weather_id: i32, + user_id: i32, + is_favorite: bool, +) -> Result<(), String> { + let query = r#" + UPDATE weather_data + SET is_favorite = $1 + WHERE id = $2 AND user_id = $3 + "#; + + let result = sqlx::query(query) + .bind(is_favorite) + .bind(weather_id) + .bind(user_id) + .execute(pool) + .await + .map_err(|e| format!("更新收藏状态失败: {}", e))?; + + if result.rows_affected() == 0 { + return Err(format!("未找到ID为 {} 的天气数据或无权限修改", weather_id)); + } + + Ok(()) +} diff --git a/src/handlers/favorites.rs b/src/handlers/favorites.rs new file mode 100644 index 0000000..a47ada8 --- /dev/null +++ b/src/handlers/favorites.rs @@ -0,0 +1,93 @@ +use actix_web::{web, delete, get, post, HttpResponse, Responder}; +use sqlx::postgres::PgPool; +use tracing::{debug, error, info}; + +use crate::db; +use crate::models::Claims; + +// GET /api/favorites - 获取收藏列表 +#[get("/api/favorites")] +pub async fn get_favorites( + query: web::Query, + pool: web::Data, + claims: web::ReqData, +) -> impl Responder { + let page = query.get("page").and_then(|v| v.as_i64()).unwrap_or(1).max(1) as i32; + let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(10).clamp(1, 100) as i32; + + debug!("获取收藏列表, 页码: {}, 每页条数: {}", page, limit); + + match db::get_favorites_list(pool.get_ref(), claims.user_id, page, limit).await { + Ok(response) => { + 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 + })) + } + } +} + +// POST /api/favorites/{id} - 添加收藏 +#[post("/api/favorites/{id}")] +pub async fn add_favorite( + path: web::Path, + pool: web::Data, + claims: web::ReqData, +) -> impl Responder { + let weather_id = path.into_inner(); + info!("添加收藏, weather_id: {}", weather_id); + + match db::set_weather_favorite(pool.get_ref(), weather_id, claims.user_id, true).await { + Ok(_) => HttpResponse::Ok().json(serde_json::json!({ + "success": true, + "message": format!("已添加收藏") + })), + 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 + })) + } + } +} + +// DELETE /api/favorites/{id} - 取消收藏 +#[delete("/api/favorites/{id}")] +pub async fn remove_favorite( + path: web::Path, + pool: web::Data, + claims: web::ReqData, +) -> impl Responder { + let weather_id = path.into_inner(); + info!("取消收藏, weather_id: {}", weather_id); + + match db::set_weather_favorite(pool.get_ref(), weather_id, claims.user_id, false).await { + Ok(_) => HttpResponse::Ok().json(serde_json::json!({ + "success": true, + "message": format!("已取消收藏") + })), + 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 + })) + } + } +} \ No newline at end of file diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index d63e5c4..bcd4673 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,6 +1,7 @@ // handlers 模块 - 按功能拆分路由处理器 pub mod admin; pub mod auth; +pub mod favorites; pub mod health; pub mod payment; pub mod static_files; @@ -16,6 +17,7 @@ pub static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates"); pub use admin::admin_get_user; pub use admin::admin_update_user_payment; pub use auth::login; +pub use favorites::{add_favorite, get_favorites, remove_favorite}; pub use health::health_check; pub use static_files::serve_static_files; pub use user::get_current_user_profile; diff --git a/src/main.rs b/src/main.rs index 82fd3d3..2a0da89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,10 +17,11 @@ use auth::jwt_middleware; use config::AppConfig; use db::create_pool; use handlers::{ - admin_get_user, admin_update_user_payment, create_order, delete_weather, - generate_temp_token_handler, get_current_user_profile, get_user_quota, - get_weather_brief, get_weather_details, health_check, login, - mock_confirm, post_weather_data, save_user_profile, serve_static_files, + admin_get_user, admin_update_user_payment, add_favorite, create_order, + delete_weather, generate_temp_token_handler, get_current_user_profile, + get_favorites, get_user_quota, get_weather_brief, get_weather_details, + health_check, login, mock_confirm, post_weather_data, remove_favorite, + save_user_profile, serve_static_files, }; use models::AppState; @@ -78,6 +79,9 @@ fn create_server_config( .service(create_order) // #[post("/api/payment/create-order")] .service(mock_confirm) // #[post("/api/payment/mock-confirm")] .service(get_user_quota) // #[get("/api/user/quota")] + .service(get_favorites) // #[get("/api/favorites")] + .service(add_favorite) // #[post("/api/favorites/{id}")] + .service(remove_favorite) // #[delete("/api/favorites/{id}")] ) // 健康检查 .service(health_check) diff --git a/src/models.rs b/src/models.rs index 3f480df..6ae0c17 100644 --- a/src/models.rs +++ b/src/models.rs @@ -113,6 +113,7 @@ pub struct WeatherDataBrief { pub longitude: String, pub latitude: String, + pub is_favorite: bool, } // 在models.rs中添加(可放在WeatherDataBrief下方) @@ -264,6 +265,8 @@ pub struct WeatherData { #[serde(rename = "windSpeedSuitability")] #[sqlx(rename = "windspeedsuitability")] pub wind_speed_suitability: String, + + pub is_favorite: bool, } #[derive(Debug, Serialize, Deserialize, FromRow)]