diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index bdddb80..3c498ad 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -562,7 +562,115 @@ fi --- -## 十二、付费功能系统 +## 十三、本次对话经验总结 + +### 13.1 前后端字段命名一致性 + +**问题描述**: +后端 Rust 使用 `#[serde(rename = "camelCase")]` 序列化 JSON 字段,前端 TypeScript 必须使用相同的 camelCase 命名才能正确解析。 + +**受影响字段**: +| Rust 字段 | JSON 键 | 前端错误写法 | 前端正确写法 | +|-----------|---------|-------------|-------------| +| `is_favorite` | `isFavorite` | `is_favorite` | `isFavorite` | +| `inspection_type` | `inspectionType` | `inspection_type` | `inspectionType` | +| `assignment_number` | `assignmentNumber` | `assignment_number` | `assignmentNumber` | + +**问题后果**: +- 前端使用 snake_case 命名,接口返回的 camelCase 字段会是 `undefined` +- 详情页面显示 `undefined` 而非正确值 + +**验证方法**: +```bash +# 搜索后端 serde rename 配置 +grep -n 'rename = "' src/models.rs +``` + +**经验教训**: +- API 接口字段命名应在前后端团队间统一约定 +- 或让后端提供 JSON Schema / OpenAPI 文档 + +**参考**:[LRN-20260417-015](file:///home/milky/Documents/ASD/.learnings/LEARNINGS.md#LRN-20260417-015) + +--- + +### 13.2 serde 配置冲突 + +**问题描述**: +`#[serde(skip_deserializing)]` 用于 POST 请求体解析(避免 id 字段),但在 SELECT 查询时会阻止字段被填充。 + +**错误配置**: +```rust +#[serde(skip_deserializing)] // POST 时跳过,但 SELECT 时也跳过了 +pub is_favorite: Option, +``` + +**正确配置**: +```rust +#[serde(default)] // 缺失字段使用默认值 +pub is_favorite: Option, +``` + +**经验教训**: +- `skip_deserializing` 会导致数据库查询结果无法填充字段 +- 对于需要同时支持上传和查询的字段,使用 `default` + +**参考**:[LRN-20260417-016](file:///home/milky/Documents/ASD/.learnings/LEARNINGS.md#LRN-20260417-016) + +--- + +### 13.3 config crate 路径解析 + +**问题描述**: +`File::with_name("config/default")` 查找文件相对于 `cargo run` 执行目录,而非 `CARGO_MANIFEST_DIR`。 + +**错误写法**: +```rust +config::File::with_name("config/default") // 相对于 cwd +``` + +**正确写法**: +```rust +let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .map(PathBuf::from) + .expect("CARGO_MANIFEST_DIR not set"); +let config_path = manifest_dir.join("config").join("default.toml"); +``` + +**经验教训**: +- 使用 `config` crate 的路径相关函数时注意基准目录 +- 直接使用 `std::env::var("CARGO_MANIFEST_DIR")` 更可靠 + +**参考**:[LRN-20260417-001](file:///home/milky/Documents/ASD/.learnings/LEARNINGS.md#LRN-20260417-001) + +--- + +### 13.4 TOML 配置文件结构 + +**问题描述**: +TOML 文件中的 `[development]` 等 section headers 与 `config` crate 的合并逻辑冲突。 + +**错误写法**: +```toml +[development] +database_url = "..." +``` + +**正确写法**: +```toml +database_url = "..." +environment = "development" +``` + +**经验教训**: +- 保持 TOML 文件扁平结构,不使用 section headers +- 简化配置加载逻辑 + +**参考**:[LRN-20260417-002](file:///home/milky/Documents/ASD/.learnings/LEARNINGS.md#LRN-20260417-002) + +--- + +## 十四、付费功能系统 ### 当前状态 diff --git a/deploy.sh b/deploy.sh index 11530f2..1607aeb 100755 --- a/deploy.sh +++ b/deploy.sh @@ -64,12 +64,6 @@ upload_binary() { } upload_config() { - # development 环境跳过(远程已有正确配置,使用 localhost 连接) - if [ "${APP_ENV}" = "development" ]; then - log_info "跳过配置文件上传(development 环境使用远程已有配置)..." - return 0 - fi - log_info "正在上传配置文件到 ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/config/..." rsync -avzP --progress ./config/ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/config/" log_info "配置文件上传成功。" diff --git a/src/db.rs b/src/db.rs index a76c32e..1f4144c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -118,8 +118,8 @@ pub async fn create_pool() -> Result> { // 获取天气数据详情 pub async fn get_weather_details(pool: &PgPool, weather_id: i32) -> Result { let query = r#" - SELECT - wd.id, wd.title, wd.date, wd.hour, wd.min, + 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, diff --git a/src/handlers/weather.rs b/src/handlers/weather.rs index d947c15..e37b183 100644 --- a/src/handlers/weather.rs +++ b/src/handlers/weather.rs @@ -113,9 +113,13 @@ pub async fn get_weather_details( }; (temp_claims.openid, temp_claims.resource_id) } else if let Some(claims) = claims { - let weather_id = match query.get("id").and_then(|v| v.as_i64()) { - Some(id) => id as i32, - None => { + let weather_id = match query.get("id") { + Some(v) if v.is_i64() => v.as_i64().unwrap() as i32, + Some(v) if v.is_string() => { + v.as_str().and_then(|s| s.parse::().ok()).unwrap_or(0) + } + Some(v) if v.is_number() => v.as_f64().unwrap() as i32, + _ => { return HttpResponse::BadRequest().json(ErrorResponse { error: "缺少资源ID参数(id)".to_string(), errcode: Some(400), diff --git a/src/main.rs b/src/main.rs index 2a0da89..0798d55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,13 +63,13 @@ fn create_server_config( .service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files))) // API 接口 .service(login) // #[post("/api/login")] - .service(get_weather_details) // #[get("/weather/details")] // 受保护接口(JWT) .service( web::scope("") .wrap(from_fn(jwt_middleware)) .service(post_weather_data) // #[post("/api/post-weather-data")] .service(get_weather_brief) // #[get("/api/weather")] + .service(get_weather_details) // #[get("/weather/details")] ← 移入受保护作用域 .service(generate_temp_token_handler) // #[post("/api/generate-temp-token/{resource_id}")] .service(delete_weather) // #[delete("/api/weather/delete/{id}")] .service(get_current_user_profile) // #[get("/api/user/profile")] diff --git a/src/models.rs b/src/models.rs index 6ae0c17..c412734 100644 --- a/src/models.rs +++ b/src/models.rs @@ -113,6 +113,7 @@ pub struct WeatherDataBrief { pub longitude: String, pub latitude: String, + #[serde(rename = "isFavorite", default)] pub is_favorite: bool, } @@ -134,7 +135,7 @@ pub struct WeatherData { #[sqlx(rename = "areatype")] pub area_type: String, - #[serde(rename = "assignmentnumber")] + #[serde(rename = "assignmentNumber")] #[sqlx(rename = "assignmentnumber")] pub assignment_number: Option, @@ -186,7 +187,7 @@ pub struct WeatherData { #[sqlx(rename = "hour")] pub hours: i32, - #[serde(rename = "inspectiontype")] + #[serde(rename = "inspectionType")] #[sqlx(rename = "inspectiontype")] pub inspection_type: Option, @@ -266,6 +267,8 @@ pub struct WeatherData { #[sqlx(rename = "windspeedsuitability")] pub wind_speed_suitability: String, + #[serde(skip_deserializing)] + #[serde(rename = "isFavorite", default)] pub is_favorite: bool, }