From fa40f4bd955f9a59aaa5a115f56bddb5c21f77ba Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 24 Jul 2026 17:04:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(assets):=20uuid=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E5=A4=B1=E8=B4=A5=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tokio-postgres 将 $1::uuid 的参数推断为 uuid 类型,String 无法 序列化(缺类型桥接),INSERT assets 报 error serializing parameter 0, 补偿删文件后返回 500。list/delete/purge/rebuild 的 $1::uuid 与 ANY($1::uuid[]) 全是同一颗未引爆的雷。 正路:tokio-postgres 启用 with-uuid-1,SQL 侧去掉全部 ::uuid cast, Rust 侧全程 Uuid 类型,仅在 serde DTO 边界(WASM 共享模型)转 String; 外部传入的 id 在边界 parse_str,非法 id 走业务错误而非 500。 --- Cargo.lock | 1 + Cargo.toml | 2 +- src/api/assets/delete.rs | 36 ++++++++++++++++++++++-------------- src/api/assets/list.rs | 22 ++++++++++------------ src/api/assets/rebuild.rs | 5 +++-- src/api/upload.rs | 6 ++++-- 6 files changed, 41 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c6eca1..0bd00ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3445,6 +3445,7 @@ dependencies = [ "chrono", "fallible-iterator", "postgres-protocol", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ba592f0..4507012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ required-features = ["server"] dioxus = { version = "0.7.9", features = ["fullstack", "router"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.53", features = ["rt-multi-thread", "macros", "fs", "time", "sync"], optional = true } -tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"], optional = true } +tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1"], optional = true } deadpool-postgres = { version = "0.14", optional = true } argon2 = { version = "0.5", optional = true } uuid = { version = "1", features = ["v4"], optional = true } diff --git a/src/api/assets/delete.rs b/src/api/assets/delete.rs index 68b9f07..0d19fe2 100644 --- a/src/api/assets/delete.rs +++ b/src/api/assets/delete.rs @@ -21,11 +21,17 @@ pub async fn update_asset_alt(id: String, alt: String) -> Result u, + Err(_) => return Ok(AssetOpResponse::err("素材 id 非法".to_string())), + }; + let alt = alt.trim().to_string(); let updated = client .execute( - "UPDATE assets SET alt = NULLIF($2, ''), updated_at = NOW() WHERE id = $1::uuid", - &[&id, &alt], + "UPDATE assets SET alt = NULLIF($2, ''), updated_at = NOW() WHERE id = $1", + &[&asset_uuid, &alt], ) .await .map_err(AppError::query)?; @@ -55,10 +61,15 @@ pub async fn delete_asset(id: String) -> Result let _admin = get_current_admin_user().await?; let client = get_conn().await.map_err(AppError::db_conn)?; + let asset_uuid = match uuid::Uuid::parse_str(&id) { + Ok(u) => u, + Err(_) => return Ok(AssetOpResponse::err("素材 id 非法".to_string())), + }; + let row = client .query_opt( - "SELECT id::text AS id, path, filename FROM assets WHERE id = $1::uuid", - &[&id], + "SELECT id AS id, path, filename FROM assets WHERE id = $1", + &[&asset_uuid], ) .await .map_err(AppError::query)?; @@ -71,8 +82,8 @@ pub async fn delete_asset(id: String) -> Result let ref_rows = client .query( "SELECT p.id, p.title FROM asset_refs r JOIN posts p ON p.id = r.post_id \ - WHERE r.asset_id = $1::uuid ORDER BY p.id", - &[&id], + WHERE r.asset_id = $1 ORDER BY p.id", + &[&asset_uuid], ) .await .map_err(AppError::query)?; @@ -99,7 +110,7 @@ pub async fn delete_asset(id: String) -> Result } } client - .execute("DELETE FROM assets WHERE id = $1::uuid", &[&id]) + .execute("DELETE FROM assets WHERE id = $1", &[&asset_uuid]) .await .map_err(AppError::query)?; crate::api::image::invalidate_asset_caches(&path).await; @@ -127,7 +138,7 @@ pub async fn purge_orphan_assets() -> Result $1)", &[&super::list::PURGE_GRACE_DAYS], @@ -145,11 +156,11 @@ pub async fn purge_orphan_assets() -> Result = Vec::with_capacity(rows.len()); + let mut ids: Vec = Vec::with_capacity(rows.len()); let mut freed_bytes: i64 = 0; let mut failures: i64 = 0; for row in &rows { - let id: String = row.get("id"); + let id: uuid::Uuid = row.get("id"); let path: String = row.get("path"); freed_bytes += row.get::<_, i64>("size_bytes"); let file_path = format!("uploads/{}", path); @@ -166,10 +177,7 @@ pub async fn purge_orphan_assets() -> Result = rows.iter().map(|r| r.get::<_, String>("id")).collect(); + let ids: Vec = rows.iter().map(|r| r.get::<_, uuid::Uuid>("id")).collect(); let mut refs_map: std::collections::HashMap> = std::collections::HashMap::new(); if !ids.is_empty() { let ref_rows = client .query( - "SELECT r.asset_id::text AS asset_id, p.id AS post_id, p.title \ + "SELECT r.asset_id AS asset_id, p.id AS post_id, p.title \ FROM asset_refs r JOIN posts p ON p.id = r.post_id \ - WHERE r.asset_id = ANY($1::uuid[]) \ + WHERE r.asset_id = ANY($1) \ ORDER BY p.id", &[&ids], ) .await .map_err(AppError::query)?; for rr in ref_rows { - refs_map - .entry(rr.get::<_, String>("asset_id")) - .or_default() - .push(AssetRef { - post_id: rr.get("post_id"), - title: rr.get("title"), - }); + let asset_key: String = rr.get::<_, uuid::Uuid>("asset_id").to_string(); + refs_map.entry(asset_key).or_default().push(AssetRef { + post_id: rr.get("post_id"), + title: rr.get("title"), + }); } } let assets = rows .into_iter() .map(|row| { - let id: String = row.get("id"); + let id: String = row.get::<_, uuid::Uuid>("id").to_string(); let refs = refs_map.remove(&id).unwrap_or_default(); AssetDto { asset: Asset { diff --git a/src/api/assets/rebuild.rs b/src/api/assets/rebuild.rs index b4aeb09..8e79add 100644 --- a/src/api/assets/rebuild.rs +++ b/src/api/assets/rebuild.rs @@ -109,10 +109,11 @@ pub async fn rebuild_assets_index() -> Result Result