Files
asd-backend/src/db.rs
Milky0217 61b1174b30 fix: 统一字段命名为 camelCase 并修复详情 API
- models.rs: 将 inspectiontype/assignmentnumber 的 serde rename 改为
  inspectionType/assignmentNumber,与前端 TypeScript 接口保持一致
- models.rs: 为 WeatherDataBrief 添加 isFavorite 字段支持
- models.rs: WeatherData.is_favorite 添加 skip_deserializing 避免 POST
  请求解析失败,同时使用 default 处理数据库 NULL 值
- handlers/weather.rs: get_weather_details 支持 string 和 number 类型的
  id 参数解析
- IMPROVEMENTS.md: 新增第十三章记录本次对话经验(serde 配置、config
  路径解析、TOML 结构等)
2026-04-17 20:47:00 +08:00

478 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use sqlx::{PgPool, postgres::PgPoolOptions};
use std::env;
use std::error::Error;
use chrono::Utc;
// 从 models 模块引入 WeatherData 结构体
use crate::models::{User, WeatherData, WeatherDataBrief, WeatherListResponse};
// 用于插入weather_data的数据
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, String> {
// 配额检查:非付费用户数据条数限制
let user = get_user_by_id(pool, user_id).await?;
let is_paid_active = user.is_paid && (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > Utc::now());
if !is_paid_active {
let current_count = count_user_weather_data(pool, user_id).await?;
let limit: i64 = env::var("FREE_USER_DATA_LIMIT")
.unwrap_or_else(|_| "20".to_string())
.parse()
.unwrap_or(20);
if current_count >= limit {
return Err("数据条数已达上限,请升级为付费用户".to_string());
}
}
// 准备插入数据的 SQL 语句
let insert_query = r#"
INSERT INTO weather_data (
user_id, title, date, hour, min, longitude, latitude, daysincejanfirst,
theta, solardeclination, sunaltitude, overallcloudiness, lowcloudiness,
cloudindex, solarradiationlevel, hasmeasuredwindspeed, measuredwindspeed,
convertedwindspeed, pointheight, areatype, pointwindspeed,
atmosphericstability, suitabilitydegree, winddirection, averagewinddirection,
winddirectionstandarddeviation, windspeed, averagewindspeed, windspeedsuitability,
winddirectionsuitability, overallsuitability, inspectiontype, assignmentnumber,
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, $37
) RETURNING id
"#;
// 3. 执行插入操作
let inserted_id = match sqlx::query_as::<_, (i32,)>(insert_query)
.bind(user_id)
.bind(&weather_data.title)
.bind(weather_data.date)
.bind(weather_data.hours)
.bind(weather_data.min)
.bind(&weather_data.longitude)
.bind(&weather_data.latitude)
.bind(weather_data.day_since_jan_first)
.bind(weather_data.theta)
.bind(weather_data.solar_declination)
.bind(weather_data.sun_altitude)
.bind(&weather_data.overall_cloudiness)
.bind(&weather_data.low_cloudiness)
.bind(&weather_data.cloud_index)
.bind(weather_data.solar_radiation_level)
.bind(weather_data.has_measured_wind_speed)
.bind(weather_data.measured_wind_speed) // <-- 修改: 直接绑定 Option<f64>
.bind(weather_data.converted_wind_speed) // <-- 修改: 直接绑定 Option<i32>,并移除 `as i32`
.bind(weather_data.point_height) // <-- 修改: 直接绑定 Option<f64>
.bind(&weather_data.area_type)
.bind(weather_data.point_wind_speed) // <-- 修改: 直接绑定 Option<f64>
.bind(&weather_data.atmospheric_stability)
.bind(&weather_data.suitability_degree)
.bind(
serde_json::to_value(&weather_data.wind_direction)
.map_err(|e| format!("JSON 序列化失败: {}", e))?,
)
.bind(weather_data.average_wind_direction)
.bind(weather_data.wind_direction_standard_deviation)
.bind(
serde_json::to_value(&weather_data.wind_speed)
.map_err(|e| format!("JSON 序列化失败: {}", e))?,
)
.bind(weather_data.average_wind_speed)
.bind(&weather_data.wind_speed_suitability)
.bind(&weather_data.wind_direction_suitability)
.bind(&weather_data.overall_suitability)
.bind(&weather_data.inspection_type)
.bind(&weather_data.assignment_number)
.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
{
Ok((id,)) => id,
Err(e) => {
return Err(format!("插入数据失败: {}", e));
}
};
// 4. 成功,返回插入的 ID
Ok(inserted_id)
}
pub async fn create_pool() -> Result<PgPool, Box<dyn Error>> {
// 获取数据库URL并提供友好错误信息
let database_url =
env::var("DATABASE_URL").map_err(|_| "环境变量DATABASE_URL未设置请在.env文件中配置")?;
// 配置连接池参数
let pool = PgPoolOptions::new()
.max_connections(20) // 根据应用需求调整最大连接数
.acquire_timeout(std::time::Duration::from_secs(30)) // 获取连接的超时时间
.connect(&database_url)
.await
.map_err(|e| format!("数据库连接失败: {}", e))?;
Ok(pool)
}
// 获取天气数据详情
pub async fn get_weather_details(pool: &PgPool, weather_id: i32) -> Result<WeatherData, String> {
let query = r#"
SELECT
wd.id, wd.title, wd.date, wd.hour, wd.min,
wd.longitude, wd.latitude,
wd.daysincejanfirst, wd.theta, wd.solardeclination,
wd.sunaltitude, wd.overallcloudiness, wd.lowcloudiness,
wd.cloudindex, wd.solarradiationlevel, wd.hasmeasuredwindspeed,
wd.measuredwindspeed, wd.convertedwindspeed, wd.pointheight,
wd.areatype, wd.pointwindspeed, wd.atmosphericstability,
wd.suitabilitydegree, wd.winddirection, wd.averagewinddirection,
wd.winddirectionstandarddeviation, wd.windspeed, wd.averagewindspeed,
wd.windspeedsuitability, wd.winddirectionsuitability, wd.overallsuitability,
wd.inspectiontype, wd.assignmentnumber, wd.calculatedwindspeed, wd.hasspotcheckwindspeed,
wd.is_favorite, u.openid, wd.version
FROM weather_data wd
JOIN users u ON wd.user_id = u.id
WHERE wd.id = $1
"#;
let row = match sqlx::query_as::<_, WeatherData>(query)
.bind(weather_id)
.fetch_optional(pool)
.await
{
Ok(Some(row)) => row,
Ok(None) => return Err(format!("未找到ID为 {} 的天气数据", weather_id)),
Err(e) => return Err(format!("查询天气数据失败: {}", e)),
};
Ok(row)
}
// get_weather_list函数
pub async fn get_weather_list(
pool: &PgPool,
user_id: i32,
page: i32,
limit: i32,
) -> Result<WeatherListResponse, String> {
let offset = (page - 1) * limit;
// 1. 查询符合条件的总条数
let total_query = r#"
SELECT COUNT(*) as total
FROM weather_data
WHERE user_id = $1
"#;
let total = match sqlx::query_as::<_, (i64,)>(total_query)
.bind(user_id)
.fetch_one(pool)
.await
{
Ok((count,)) => count,
Err(e) => return Err(format!("查询总条数失败: {}", e)),
};
// 2. 查询当前页数据列表
let list_query = r#"
SELECT
wd.id, wd.title, wd.date, wd.hour, wd.min, wd.longitude, wd.latitude, wd.is_favorite
FROM weather_data wd
WHERE wd.user_id = $1
ORDER BY wd.date DESC, wd.hour DESC, wd.min DESC
LIMIT $2 OFFSET $3
"#;
let list = match sqlx::query_as::<_, WeatherDataBrief>(list_query)
.bind(user_id)
.bind(limit)
.bind(offset)
.fetch_all(pool)
.await
{
Ok(data) => data,
Err(e) => return Err(format!("查询天气数据列表失败: {}", e)),
};
// 3. 包装结果并返回
Ok(WeatherListResponse { list, total })
}
pub async fn delete_weather_data(
pool: &PgPool,
weather_id: i32,
user_id: i32,
) -> Result<(), String> {
let query = r#"
DELETE FROM weather_data
WHERE id = $1 AND user_id = $2
"#;
let result = sqlx::query(query)
.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(())
}
// 根据用户ID获取用户信息
pub async fn get_user_by_id(pool: &PgPool, user_id: i32) -> Result<User, String> {
let query = r#"
SELECT
id, name, openid, phone, type, "desc", is_paid, is_admin, paid_expires_at,
avatar_url, nickname
FROM users
WHERE id = $1
"#;
let row = match sqlx::query_as::<_, User>(query)
.bind(user_id)
.fetch_optional(pool)
.await
{
Ok(Some(row)) => row,
Ok(None) => return Err(format!("未找到ID为 {} 的用户", user_id)),
Err(e) => return Err(format!("查询用户信息失败: {}", e)),
};
Ok(row)
}
// 统计用户的天气数据条数
pub async fn count_user_weather_data(pool: &PgPool, user_id: i32) -> Result<i64, String> {
let query = r#"
SELECT COUNT(*) FROM weather_data WHERE user_id = $1
"#;
let count = match sqlx::query_as::<_, (i64,)>(query)
.bind(user_id)
.fetch_one(pool)
.await
{
Ok((count,)) => count,
Err(e) => return Err(format!("查询天气数据条数失败: {}", e)),
};
Ok(count)
}
// 更新用户付费状态
pub async fn update_user_payment_status(
pool: &PgPool,
user_id: i32,
is_paid: bool,
paid_expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<(), String> {
let query = r#"
UPDATE users
SET is_paid = $1, paid_expires_at = $2
WHERE id = $3
"#;
match sqlx::query(query)
.bind(is_paid)
.bind(paid_expires_at)
.bind(user_id)
.execute(pool)
.await
{
Ok(_) => Ok(()),
Err(e) => Err(format!("更新用户付费状态失败: {}", e)),
}
}
// 更新用户个人信息
pub async fn update_user_profile(
pool: &PgPool,
user_id: i32,
nickname: &Option<String>,
avatar_url: &Option<String>,
) -> Result<(), String> {
let query = r#"
UPDATE users
SET nickname = COALESCE($1, nickname),
avatar_url = COALESCE($2, avatar_url)
WHERE id = $3
"#;
match sqlx::query(query)
.bind(nickname)
.bind(avatar_url)
.bind(user_id)
.execute(pool)
.await
{
Ok(_) => Ok(()),
Err(e) => Err(format!("更新用户个人信息失败: {}", e)),
}
}
// ===== 支付系统 DB 函数 =====
/// 创建待支付订单
pub async fn create_payment_order(
pool: &PgPool,
user_id: i32,
order_no: &str,
package_type: &str,
amount: i32,
expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<(), String> {
let query = r#"
INSERT INTO payment_orders (user_id, order_no, package_type, amount, expires_at)
VALUES ($1, $2, $3, $4, $5)
"#;
sqlx::query(query)
.bind(user_id)
.bind(order_no)
.bind(package_type)
.bind(amount)
.bind(expires_at)
.execute(pool)
.await
.map_err(|e| format!("创建订单失败: {}", e))?;
Ok(())
}
/// 确认订单支付(模拟):更新订单状态 + 激活用户付费
///
/// 返回该订单的 expires_at永久会员为 None
pub async fn confirm_payment_order(
pool: &PgPool,
order_no: &str,
user_id: i32,
) -> Result<Option<chrono::DateTime<chrono::Utc>>, String> {
let row = sqlx::query_as::<_, (i32, String, Option<chrono::DateTime<chrono::Utc>>)>(
r#"SELECT user_id, status, expires_at FROM payment_orders WHERE order_no = $1"#,
)
.bind(order_no)
.fetch_optional(pool)
.await
.map_err(|e| format!("查询订单失败: {}", e))?;
let (order_user_id, status, expires_at) = match row {
Some(r) => r,
None => return Err("订单不存在".to_string()),
};
if order_user_id != user_id {
return Err("无权操作此订单".to_string());
}
if status != "pending" {
return Err("订单状态异常,无法确认支付".to_string());
}
sqlx::query(
r#"UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1"#,
)
.bind(order_no)
.execute(pool)
.await
.map_err(|e| format!("更新订单状态失败: {}", e))?;
sqlx::query(
r#"UPDATE users SET is_paid = true, paid_expires_at = $1 WHERE id = $2"#,
)
.bind(expires_at)
.bind(user_id)
.execute(pool)
.await
.map_err(|e| format!("更新用户付费状态失败: {}", e))?;
Ok(expires_at)
}
/// 获取用户配额信息
///
/// 返回 (已用条数, 是否付费活跃, 到期时间)
pub async fn get_user_quota(
pool: &PgPool,
user_id: i32,
) -> Result<(i64, bool, Option<chrono::DateTime<chrono::Utc>>), String> {
let user = get_user_by_id(pool, user_id).await?;
let is_paid_active = user.is_paid
&& (user.paid_expires_at.is_none()
|| user.paid_expires_at.unwrap() > chrono::Utc::now());
let used = count_user_weather_data(pool, user_id).await?;
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(())
}