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:
79
src/db.rs
79
src/db.rs
@@ -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
93
src/handlers/favorites.rs
Normal 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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
12
src/main.rs
12
src/main.rs
@@ -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)
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
Reference in New Issue
Block a user