Files
asd-backend/src/models.rs
milky0217 2ce97243ab
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
fix: 全量代码审计修复 — 30项
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
2026-07-23 12:40:47 +08:00

392 lines
11 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 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>>,
}