Some checks failed
Deploy Backend / deploy (push) Has been cancelled
P0 - Panic 风险修复: - payment.rs: unwrap() → let-else safe handling - payment.rs: get_jwt_secret() expect → Result/AppError - auth.rs: openid 切片添加 len >= 8 守卫 - main.rs: 启动时 expect → unwrap_or_else 描述性 panic - main.rs: Directive::from_str 添加 fallback P1 - 逻辑/安全修复: - payment.rs: urlencoding() + 解码 bug 修复 (移除 had_escape) - payment.rs: Mock 支付添加 check_mock_payment_allowed 检查 - db.rs: 永久会员 NULL → 2099-12-31 一致化 - user.rs: 维护模式添加安全说明注释 - 自动清理 unused_variables 警告 (_is_mobile) P2 - 错误吞没修复: - main.rs: 3 处定时任务 let _ = → if let Err = tracing::error! - db.rs + admin.rs: 7 处通知/审计日志 let _ = → tracing::warn! - auth.rs: refresh token 保存 add warn 日志 P3 - 死代码清理: - models.rs: 移除 TokenResponse (dead) - models.rs: 移除 AppState 中 5 个未使用字段 (env var 直接读取) - error.rs: 移除 3 个 dead ErrorResponse 方法 - rate_limiter.rs: extract_client_ip_from_header → #[cfg(test)] - models.rs: 注释 typo fix (user_ytpe → user_type) - db.rs: RefreshToken 添加 deserialization 注释 Shell 脚本修复: - deploy.sh: run_migrations 移到 restart_service 之前 - test.sh: 移除 EXIT trap 覆盖; heredoc 引号修复; 维护模式添加 restart - common.sh: mock_key 添加 sed 转义 (防 / & 注入) 验证: cargo check 0 warnings, 8 tests passed
392 lines
11 KiB
Rust
392 lines
11 KiB
Rust
use chrono::NaiveDate;
|
||
use serde::{Deserialize, Serialize};
|
||
use serde_json::Value as JsonValue;
|
||
use sqlx::FromRow;
|
||
use std::convert::TryFrom;
|
||
|
||
// 默认值函数
|
||
fn default_spot_check_count() -> i32 {
|
||
5
|
||
}
|
||
|
||
// 定义JWT载荷结构体
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct Claims {
|
||
// 标准字段:过期时间(必须)
|
||
pub exp: i64, // 时间戳(秒)
|
||
// 标准字段:签发时间
|
||
pub iat: i64,
|
||
// 自定义字段:用户ID(根据业务需求添加)
|
||
pub user_id: i32,
|
||
// 自定义字段:openid(可选)
|
||
pub openid: String,
|
||
// 自定义字段:user_type
|
||
pub user_type: i32,
|
||
}
|
||
|
||
// 临时访问用 token
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct TempTokenClaims {
|
||
pub exp: i64, // 过期时间(时间戳)
|
||
pub iat: i64, // 签发时间
|
||
pub openid: String, // 用户身份标识
|
||
pub resource_id: i32, // 允许访问的资源ID(如天气数据id)
|
||
}
|
||
|
||
// 登录成功后的令牌响应结构体(双 token)
|
||
#[derive(Debug, Serialize, Clone)]
|
||
pub struct LoginResponse {
|
||
pub success: bool,
|
||
pub token: String,
|
||
pub refresh_token: String,
|
||
}
|
||
|
||
impl LoginResponse {
|
||
pub fn new(token: String, refresh_token: String) -> Self {
|
||
Self {
|
||
success: true,
|
||
token,
|
||
refresh_token,
|
||
}
|
||
}
|
||
}
|
||
|
||
// Refresh Token 请求
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct RefreshTokenRequest {
|
||
pub refresh_token: String,
|
||
}
|
||
|
||
// Token 刷新响应
|
||
#[derive(Debug, Serialize)]
|
||
pub struct TokenRefreshResponse {
|
||
pub success: bool,
|
||
pub token: String,
|
||
pub refresh_token: String,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct WeChatLoginRequest {
|
||
pub code: String,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct WeChatApiResponse {
|
||
pub openid: Option<String>,
|
||
pub errcode: Option<i32>,
|
||
pub errmsg: Option<String>,
|
||
}
|
||
|
||
// 定义新类型,包装 Vec<f64>(当前 crate 内的类型)
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct FloatVec(pub Vec<f64>);
|
||
|
||
// 为 Vec<f64> 实现从 JsonValue 的转换
|
||
impl TryFrom<JsonValue> for FloatVec {
|
||
type Error = String;
|
||
|
||
fn try_from(value: JsonValue) -> Result<Self, Self::Error> {
|
||
// 检查 JSON 值是否为数组
|
||
let arr = value
|
||
.as_array()
|
||
.ok_or_else(|| "JSON 类型错误:期望数组,但得到其他类型".to_string())?;
|
||
|
||
// 遍历数组元素,转换为 f64
|
||
let mut result = Vec::with_capacity(arr.len());
|
||
for item in arr {
|
||
let num = item
|
||
.as_f64()
|
||
.ok_or_else(|| format!("JSON 元素类型错误:期望数字,但得到 {:?}", item))?;
|
||
result.push(num);
|
||
}
|
||
|
||
Ok(FloatVec(result))
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Deserialize, Serialize, FromRow)] // 添加 FromRow 派生
|
||
pub struct WeatherDataBrief {
|
||
#[sqlx(rename = "id")] // 数据库表的 id 列(自增主键),查询映射时使用
|
||
pub id: i32,
|
||
|
||
pub title: String,
|
||
|
||
pub date: NaiveDate,
|
||
|
||
#[serde(rename = "hours")]
|
||
#[sqlx(rename = "hour")] // 数据库中是 hour 字段
|
||
pub hours: i32,
|
||
|
||
#[serde(rename = "min")]
|
||
#[sqlx(rename = "min")]
|
||
pub min: i32,
|
||
|
||
pub longitude: String,
|
||
pub latitude: String,
|
||
#[serde(rename = "isFavorite", default)]
|
||
pub is_favorite: bool,
|
||
}
|
||
|
||
// 在models.rs中添加(可放在WeatherDataBrief下方)
|
||
#[derive(Debug, Serialize, FromRow)]
|
||
pub struct WeatherListResponse {
|
||
pub list: Vec<WeatherDataBrief>, // 分页数据列表
|
||
pub total: i64, // 总数据条数
|
||
}
|
||
|
||
// 天气数据结构体(修改后)
|
||
#[derive(Debug, Deserialize, Serialize, FromRow)]
|
||
pub struct WeatherData {
|
||
#[serde(skip_deserializing)]
|
||
#[sqlx(rename = "id")]
|
||
pub id: i32,
|
||
|
||
#[serde(rename = "areaType")]
|
||
#[sqlx(rename = "areatype")]
|
||
pub area_type: String,
|
||
|
||
#[serde(rename = "assignmentNumber")]
|
||
#[sqlx(rename = "assignmentnumber")]
|
||
pub assignment_number: Option<String>,
|
||
|
||
#[serde(rename = "atmosphericStability")]
|
||
#[sqlx(rename = "atmosphericstability")]
|
||
pub atmospheric_stability: String,
|
||
|
||
#[serde(rename = "averageWindDirection")]
|
||
#[sqlx(rename = "averagewinddirection")]
|
||
pub average_wind_direction: f64,
|
||
|
||
#[serde(rename = "averageWindSpeed")]
|
||
#[sqlx(rename = "averagewindspeed")]
|
||
pub average_wind_speed: f64,
|
||
|
||
#[serde(
|
||
rename = "calculatedWindSpeed",
|
||
alias = "calculated_wind_speed",
|
||
alias = "calculatedwindspeed"
|
||
)]
|
||
#[sqlx(rename = "calculatedwindspeed")]
|
||
pub calculated_wind_speed: Option<f64>, // 这个已经是 Option,很好
|
||
|
||
#[serde(rename = "cloudIndex")]
|
||
#[sqlx(rename = "cloudindex")]
|
||
pub cloud_index: String,
|
||
|
||
// --- 以下是修改的重点 ---
|
||
// 这些字段在前端是条件性发送的,因此在 Rust 中也必须是可选的
|
||
#[serde(rename = "convertedWindSpeed")]
|
||
#[sqlx(rename = "convertedwindspeed")]
|
||
pub converted_wind_speed: Option<f64>,
|
||
|
||
pub date: NaiveDate,
|
||
|
||
#[serde(rename = "daySinceJanFirst")]
|
||
#[sqlx(rename = "daysincejanfirst")]
|
||
pub day_since_jan_first: i32,
|
||
|
||
#[serde(rename = "hasMeasuredWindSpeed")]
|
||
#[sqlx(rename = "hasmeasuredwindspeed")]
|
||
pub has_measured_wind_speed: bool,
|
||
|
||
#[serde(rename = "hasSpotCheckWindSpeed")]
|
||
#[sqlx(rename = "hasspotcheckwindspeed")]
|
||
pub has_spot_check_wind_speed: bool,
|
||
|
||
#[serde(rename = "spotCheckCount", default = "default_spot_check_count")]
|
||
#[sqlx(rename = "spotcheckcount")]
|
||
pub spot_check_count: i32,
|
||
|
||
#[serde(rename = "hours")]
|
||
#[sqlx(rename = "hour")]
|
||
pub hours: i32,
|
||
|
||
#[serde(rename = "inspectionType")]
|
||
#[sqlx(rename = "inspectiontype")]
|
||
pub inspection_type: Option<String>,
|
||
|
||
pub latitude: String,
|
||
pub longitude: String,
|
||
|
||
#[serde(rename = "lowCloudiness")]
|
||
#[sqlx(rename = "lowcloudiness")]
|
||
pub low_cloudiness: String,
|
||
|
||
#[serde(rename = "measuredWindSpeed")]
|
||
#[sqlx(rename = "measuredwindspeed")]
|
||
pub measured_wind_speed: Option<f64>,
|
||
|
||
#[serde(rename = "pointHeight")]
|
||
#[sqlx(rename = "pointheight")]
|
||
pub point_height: Option<f64>,
|
||
|
||
#[serde(rename = "min")]
|
||
#[sqlx(rename = "min")]
|
||
pub min: i32,
|
||
|
||
pub openid: String,
|
||
|
||
#[serde(rename = "overallCloudiness")]
|
||
#[sqlx(rename = "overallcloudiness")]
|
||
pub overall_cloudiness: String,
|
||
|
||
#[serde(rename = "overallSuitability")]
|
||
#[sqlx(rename = "overallsuitability")]
|
||
pub overall_suitability: String,
|
||
|
||
#[serde(rename = "pointWindSpeed")]
|
||
#[sqlx(rename = "pointwindspeed")]
|
||
pub point_wind_speed: Option<f64>, // <-- 修改: f64 -> Option<f64>
|
||
|
||
#[serde(rename = "solarDeclination")]
|
||
#[sqlx(rename = "solardeclination")]
|
||
pub solar_declination: f64,
|
||
|
||
#[serde(rename = "solarRadiationLevel")]
|
||
#[sqlx(rename = "solarradiationlevel")]
|
||
pub solar_radiation_level: i32,
|
||
|
||
#[serde(rename = "suitabilityDegree")]
|
||
#[sqlx(rename = "suitabilitydegree")]
|
||
pub suitability_degree: String,
|
||
|
||
#[serde(rename = "sunAltitude")]
|
||
#[sqlx(rename = "sunaltitude")]
|
||
pub sun_altitude: f64,
|
||
|
||
pub theta: f64,
|
||
pub title: String,
|
||
|
||
pub version: String,
|
||
|
||
#[serde(rename = "windDirection")]
|
||
#[sqlx(rename = "winddirection")]
|
||
#[sqlx(try_from = "serde_json::Value")]
|
||
pub wind_direction: FloatVec,
|
||
|
||
#[serde(rename = "windDirectionStandardDeviation")]
|
||
#[sqlx(rename = "winddirectionstandarddeviation")]
|
||
pub wind_direction_standard_deviation: f64,
|
||
|
||
#[serde(rename = "windDirectionSuitability")]
|
||
#[sqlx(rename = "winddirectionsuitability")]
|
||
pub wind_direction_suitability: String,
|
||
|
||
#[serde(rename = "windSpeed")]
|
||
#[sqlx(rename = "windspeed")]
|
||
#[sqlx(try_from = "serde_json::Value")]
|
||
pub wind_speed: FloatVec,
|
||
|
||
#[serde(rename = "windSpeedSuitability")]
|
||
#[sqlx(rename = "windspeedsuitability")]
|
||
pub wind_speed_suitability: String,
|
||
|
||
#[serde(skip_deserializing)]
|
||
#[serde(rename = "isFavorite", default)]
|
||
pub is_favorite: bool,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||
pub struct User {
|
||
pub id: i32,
|
||
#[serde(rename = "name")]
|
||
#[sqlx(rename = "name")]
|
||
pub name: String,
|
||
#[sqlx(rename = "openid")]
|
||
pub openid: Option<String>,
|
||
#[sqlx(rename = "phone")]
|
||
pub phone: Option<String>,
|
||
#[serde(rename = "type")]
|
||
#[sqlx(rename = "type")]
|
||
pub user_type: i32,
|
||
#[serde(rename = "desc")]
|
||
#[sqlx(rename = "desc")]
|
||
pub description: Option<String>,
|
||
#[sqlx(rename = "is_member")]
|
||
pub is_member: bool,
|
||
#[sqlx(rename = "is_admin")]
|
||
pub is_admin: bool,
|
||
#[sqlx(rename = "membership_expires_at")]
|
||
pub membership_expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
#[serde(rename = "avatarUrl")]
|
||
#[sqlx(rename = "avatar_url")]
|
||
pub avatar_url: Option<String>,
|
||
#[serde(rename = "nickname")]
|
||
#[sqlx(rename = "nickname")]
|
||
pub nickname: Option<String>,
|
||
}
|
||
|
||
// 管理员更新用户付费状态的请求体
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct UpdatePaymentRequest {
|
||
pub is_member: bool,
|
||
pub membership_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())?,
|
||
})
|
||
}
|
||
}
|
||
|
||
// ===== 支付系统 =====
|
||
|
||
/// 创建订单请求体
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct CreateOrderRequest {
|
||
pub package_type: String,
|
||
}
|
||
|
||
/// 模拟确认支付请求体
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct MockConfirmRequest {
|
||
pub order_id: String,
|
||
}
|
||
|
||
/// payment_orders 表行结构
|
||
#[derive(Debug, Serialize, FromRow)]
|
||
pub struct PaymentOrder {
|
||
pub id: i32,
|
||
pub user_id: i32,
|
||
pub order_no: String,
|
||
pub package_type: String,
|
||
pub amount: i32,
|
||
pub status: String,
|
||
pub paid_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||
}
|
||
|
||
// ===== 通知系统 =====
|
||
|
||
#[derive(Debug, Serialize, FromRow)]
|
||
pub struct Notification {
|
||
pub id: i32,
|
||
pub scope: String,
|
||
pub user_id: Option<i32>,
|
||
#[sqlx(rename = "type")]
|
||
pub type_: String,
|
||
pub title: String,
|
||
pub content: Option<String>,
|
||
pub priority: String,
|
||
pub link: Option<String>,
|
||
pub is_read: bool,
|
||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
}
|