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 结构等)
This commit is contained in:
110
IMPROVEMENTS.md
110
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<bool>,
|
||||
```
|
||||
|
||||
**正确配置**:
|
||||
```rust
|
||||
#[serde(default)] // 缺失字段使用默认值
|
||||
pub is_favorite: Option<bool>,
|
||||
```
|
||||
|
||||
**经验教训**:
|
||||
- `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)
|
||||
|
||||
---
|
||||
|
||||
## 十四、付费功能系统
|
||||
|
||||
### 当前状态
|
||||
|
||||
|
||||
@@ -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 "配置文件上传成功。"
|
||||
|
||||
@@ -118,8 +118,8 @@ pub async fn create_pool() -> Result<PgPool, Box<dyn Error>> {
|
||||
// 获取天气数据详情
|
||||
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,
|
||||
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,
|
||||
|
||||
@@ -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::<i32>().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),
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<String>,
|
||||
|
||||
@@ -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<String>,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user