feat: 添加收藏功能 API

- 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 路由
This commit is contained in:
2026-04-17 17:40:58 +08:00
parent 0ef0f71049
commit bdbba2cd60
5 changed files with 181 additions and 8 deletions

View File

@@ -34,10 +34,10 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user
atmosphericstability, suitabilitydegree, winddirection, averagewinddirection, atmosphericstability, suitabilitydegree, winddirection, averagewinddirection,
winddirectionstandarddeviation, windspeed, averagewindspeed, windspeedsuitability, winddirectionstandarddeviation, windspeed, averagewindspeed, windspeedsuitability,
winddirectionsuitability, overallsuitability, inspectiontype, assignmentnumber, winddirectionsuitability, overallsuitability, inspectiontype, assignmentnumber,
calculatedwindspeed, hasspotcheckwindspeed, version calculatedwindspeed, hasspotcheckwindspeed, version, is_favorite
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $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 ) 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.calculated_wind_speed) // 这个本来就是 Option无需修改
.bind(weather_data.has_spot_check_wind_speed) .bind(weather_data.has_spot_check_wind_speed)
.bind(&weather_data.version) .bind(&weather_data.version)
.bind(weather_data.is_favorite)
.fetch_one(pool) .fetch_one(pool)
.await .await
{ {
@@ -129,7 +130,7 @@ pub async fn get_weather_details(pool: &PgPool, weather_id: i32) -> Result<Weath
wd.winddirectionstandarddeviation, wd.windspeed, wd.averagewindspeed, wd.winddirectionstandarddeviation, wd.windspeed, wd.averagewindspeed,
wd.windspeedsuitability, wd.winddirectionsuitability, wd.overallsuitability, wd.windspeedsuitability, wd.winddirectionsuitability, wd.overallsuitability,
wd.inspectiontype, wd.assignmentnumber, wd.calculatedwindspeed, wd.hasspotcheckwindspeed, wd.inspectiontype, wd.assignmentnumber, wd.calculatedwindspeed, wd.hasspotcheckwindspeed,
u.openid, wd.version wd.is_favorite, u.openid, wd.version
FROM weather_data wd FROM weather_data wd
JOIN users u ON wd.user_id = u.id JOIN users u ON wd.user_id = u.id
WHERE wd.id = $1 WHERE wd.id = $1
@@ -175,7 +176,7 @@ pub async fn get_weather_list(
// 2. 查询当前页数据列表 // 2. 查询当前页数据列表
let list_query = r#" let list_query = r#"
SELECT SELECT
wd.id, wd.title, wd.date, wd.hour, wd.min, wd.longitude, wd.latitude wd.id, wd.title, wd.date, wd.hour, wd.min, wd.longitude, wd.latitude, wd.is_favorite
FROM weather_data wd FROM weather_data wd
WHERE wd.user_id = $1 WHERE wd.user_id = $1
ORDER BY wd.date DESC, wd.hour DESC, wd.min DESC ORDER BY wd.date DESC, wd.hour DESC, wd.min DESC
@@ -404,3 +405,73 @@ pub async fn get_user_quota(
let used = count_user_weather_data(pool, user_id).await?; let used = count_user_weather_data(pool, user_id).await?;
Ok((used, is_paid_active, user.paid_expires_at)) Ok((used, is_paid_active, user.paid_expires_at))
} }
// ===== 收藏功能 DB 函数 =====
/// 获取收藏列表(分页)
pub async fn get_favorites_list(
pool: &PgPool,
user_id: i32,
page: i32,
limit: i32,
) -> Result<WeatherListResponse, String> {
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(())
}

93
src/handlers/favorites.rs Normal file
View File

@@ -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<serde_json::Value>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> 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<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> 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<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> 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
}))
}
}
}

View File

@@ -1,6 +1,7 @@
// handlers 模块 - 按功能拆分路由处理器 // handlers 模块 - 按功能拆分路由处理器
pub mod admin; pub mod admin;
pub mod auth; pub mod auth;
pub mod favorites;
pub mod health; pub mod health;
pub mod payment; pub mod payment;
pub mod static_files; 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_get_user;
pub use admin::admin_update_user_payment; pub use admin::admin_update_user_payment;
pub use auth::login; pub use auth::login;
pub use favorites::{add_favorite, get_favorites, remove_favorite};
pub use health::health_check; pub use health::health_check;
pub use static_files::serve_static_files; pub use static_files::serve_static_files;
pub use user::get_current_user_profile; pub use user::get_current_user_profile;

View File

@@ -17,10 +17,11 @@ use auth::jwt_middleware;
use config::AppConfig; use config::AppConfig;
use db::create_pool; use db::create_pool;
use handlers::{ use handlers::{
admin_get_user, admin_update_user_payment, create_order, delete_weather, admin_get_user, admin_update_user_payment, add_favorite, create_order,
generate_temp_token_handler, get_current_user_profile, get_user_quota, delete_weather, generate_temp_token_handler, get_current_user_profile,
get_weather_brief, get_weather_details, health_check, login, get_favorites, get_user_quota, get_weather_brief, get_weather_details,
mock_confirm, post_weather_data, save_user_profile, serve_static_files, health_check, login, mock_confirm, post_weather_data, remove_favorite,
save_user_profile, serve_static_files,
}; };
use models::AppState; use models::AppState;
@@ -78,6 +79,9 @@ fn create_server_config(
.service(create_order) // #[post("/api/payment/create-order")] .service(create_order) // #[post("/api/payment/create-order")]
.service(mock_confirm) // #[post("/api/payment/mock-confirm")] .service(mock_confirm) // #[post("/api/payment/mock-confirm")]
.service(get_user_quota) // #[get("/api/user/quota")] .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) .service(health_check)

View File

@@ -113,6 +113,7 @@ pub struct WeatherDataBrief {
pub longitude: String, pub longitude: String,
pub latitude: String, pub latitude: String,
pub is_favorite: bool,
} }
// 在models.rs中添加可放在WeatherDataBrief下方 // 在models.rs中添加可放在WeatherDataBrief下方
@@ -264,6 +265,8 @@ pub struct WeatherData {
#[serde(rename = "windSpeedSuitability")] #[serde(rename = "windSpeedSuitability")]
#[sqlx(rename = "windspeedsuitability")] #[sqlx(rename = "windspeedsuitability")]
pub wind_speed_suitability: String, pub wind_speed_suitability: String,
pub is_favorite: bool,
} }
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]