diff --git a/Cargo.lock b/Cargo.lock index 649dfa8..0229627 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2387,6 +2387,12 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -3133,6 +3139,7 @@ dependencies = [ "getrandom 0.4.3", "js-sys", "serde_core", + "sha1_smol", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index b926c6c..82c5497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ serde_json = "1" tokio = { version = "1", features = ["full"] } sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "migrate"] } chrono = { version = "0.4", features = ["serde"] } -uuid = { version = "1.0", features = ["v4", "serde"] } +uuid = { version = "1.0", features = ["v4", "v5", "serde"] } image = { version = "0.24", default-features = false, features = ["jpeg", "png", "gif", "webp"] } reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json"] } config = "0.13" diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 32535e6..cd74232 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -116,9 +116,9 @@ impl TagCache { pub struct PersonCache { pub persons: Vec, /// 搜索索引:关键词小写 → person_id 集合 - pub search_index: HashMap>, + pub search_index: HashMap>, /// person_id → person 投影(用于按 id 快速访问) - pub id_map: HashMap, + pub id_map: HashMap, pub last_updated: chrono::DateTime, } @@ -132,31 +132,31 @@ impl PersonCache { let name_lower = p.name.to_lowercase(); for word in name_lower.split_whitespace() { if word.len() >= 2 { - self.search_index.entry(word.to_string()).or_default().push(p.id); + self.search_index.entry(word.to_string()).or_default().push(p.id.clone()); } } - self.search_index.entry(name_lower.clone()).or_default().push(p.id); + self.search_index.entry(name_lower.clone()).or_default().push(p.id.clone()); // 显示名 if let Some(ref dn) = p.display_name { let dn_lower = dn.to_lowercase(); for word in dn_lower.split_whitespace() { if word.len() >= 2 { - self.search_index.entry(word.to_string()).or_default().push(p.id); + self.search_index.entry(word.to_string()).or_default().push(p.id.clone()); } } - self.search_index.entry(dn_lower).or_default().push(p.id); + self.search_index.entry(dn_lower).or_default().push(p.id.clone()); } // 别名 for alias in &p.aliases { let alias_lower = alias.to_lowercase(); for word in alias_lower.split_whitespace() { if word.len() >= 2 { - self.search_index.entry(word.to_string()).or_default().push(p.id); + self.search_index.entry(word.to_string()).or_default().push(p.id.clone()); } } - self.search_index.entry(alias_lower).or_default().push(p.id); + self.search_index.entry(alias_lower).or_default().push(p.id.clone()); } - self.id_map.insert(p.id, p.clone()); + self.id_map.insert(p.id.clone(), p.clone()); } self.persons = persons; @@ -175,7 +175,7 @@ impl PersonCache { // 1. 完整匹配 if let Some(ids) = self.search_index.get(&q) { for id in ids { - if seen.insert(*id) {} + if seen.insert(id.clone()) {} } } // 2. 单词匹配 @@ -183,7 +183,7 @@ impl PersonCache { if word.len() >= 2 { if let Some(ids) = self.search_index.get(word) { for id in ids { - seen.insert(*id); + seen.insert(id.clone()); } } } @@ -195,7 +195,7 @@ impl PersonCache { p.display_name.as_deref().unwrap_or(""), p.aliases.join(" ").to_lowercase()); if full.contains(&q) { - seen.insert(p.id); + seen.insert(p.id.clone()); } } } @@ -268,7 +268,7 @@ pub async fn refresh_all_caches( for p in &mut persons { let actual: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM album_persons WHERE person_id = ?" - ).bind(p.id).fetch_one(&search_db.pool).await.unwrap_or(0); + ).bind(&p.id).fetch_one(&search_db.pool).await.unwrap_or(0); if actual > 0 { p.total_albums = actual; } @@ -285,7 +285,7 @@ pub async fn refresh_persons_cache(search_db: &SearchDb, persons_cache: &Arc 0 { p.total_albums = actual; } diff --git a/src/db/metadata.rs b/src/db/metadata.rs index dd4c88a..e1a635f 100644 --- a/src/db/metadata.rs +++ b/src/db/metadata.rs @@ -74,7 +74,7 @@ impl Database { rows.iter().map(|r| { let pid = r.get::("id"); PersonWithCounts { - id: pid, name: r.get("name"), display_name: r.get("display_name"), + id: pid.to_string(), name: r.get("name"), display_name: r.get("display_name"), total_albums: r.get("total_albums"), total_images: r.get("total_images"), aliases: aliases.get(&pid).cloned().unwrap_or_default(), } @@ -91,7 +91,7 @@ impl Database { rows.iter().map(|r| { let pid = r.get::("id"); PersonWithCounts { - id: pid, name: r.get("name"), display_name: r.get("display_name"), + id: pid.to_string(), name: r.get("name"), display_name: r.get("display_name"), total_albums: r.get("total_albums"), total_images: r.get("total_images"), aliases: aliases.get(&pid).cloned().unwrap_or_default(), } @@ -226,10 +226,16 @@ impl Database { // ── 写入 ── pub async fn insert_person(&self, name: &str, display_name: Option<&str>) -> i64 { - if let Some(p) = self.get_person_by_name(name).await { return p.id; } + if let Some(p) = self.get_person_by_name_internal(name).await { return p; } sqlx::query("INSERT OR IGNORE INTO persons (name, display_name) VALUES (?, ?)") .bind(name).bind(display_name).execute(&self.pool).await.ok(); - self.get_person_by_name(name).await.map(|p| p.id).unwrap_or(0) + self.get_person_by_name_internal(name).await.unwrap_or(0) + } + + /// 内部方法:按名称查找人员,直接返回 i64 id(避免 String 转换开销) + async fn get_person_by_name_internal(&self, name: &str) -> Option { + sqlx::query_scalar::<_, i64>("SELECT id FROM persons WHERE name = ?") + .bind(name).fetch_optional(&self.pool).await.ok().flatten() } /// 与 insert_person 相同,但使用已有的事务 @@ -349,8 +355,9 @@ impl Database { pub async fn get_person_by_id(&self, id: i64) -> Option { let r = sqlx::query("SELECT * FROM persons WHERE id = ?").bind(id).fetch_optional(&self.pool).await.ok()??; + let db_id: i64 = r.get("id"); Some(Person { - id: r.get("id"), name: r.get("name"), display_name: r.get("display_name"), + id: db_id.to_string(), name: r.get("name"), display_name: r.get("display_name"), total_albums: r.get("total_albums"), total_images: r.get("total_images"), original_names: r.get("original_names"), created_at: r.get("created_at"), updated_at: r.get("updated_at"), }) diff --git a/src/engine/pipeline.rs b/src/engine/pipeline.rs index 8452d73..b641a84 100644 --- a/src/engine/pipeline.rs +++ b/src/engine/pipeline.rs @@ -111,14 +111,20 @@ pub async fn phase_parse_dirty(db: &Database, search_db: &SearchDb, dirty_set: & let person_count = parsed.iter().filter(|r| !r.person_names.is_empty()).count(); let tag_count = parsed.iter().filter(|r| !r.tags.is_empty()).count(); - // ── Phase 3b:批量写入 ── + // ── Phase 3b:批量写入 imager.db ── let _guard = db.write_lock.lock().await; let mut tx = db.pool.begin().await.expect("begin tx"); db.batch_write_parse_results(&mut tx, &parsed).await.unwrap_or_else(|e| { - eprintln!("[管线·Phase3b] 批量写入失败: {}", e); + eprintln!("[管线·Phase3b] imager.db 批量写入失败: {}", e); }); tx.commit().await.expect("commit tx"); + // ── Phase 3c:直接写入 search.db(使用相同解析结果,无需重解析) ── + match search_db.batch_sync_parse_results(&parsed).await { + Ok(n) => println!("[管线·Phase3c] search.db 同步完成:{} 个相册", n), + Err(e) => eprintln!("[管线·Phase3c] search.db 同步失败: {}", e), + } + println!("[管线·Phase3] dict 优先解析+批量写入完成: 脏集 {} 个, 有效 {} 个 (跳过 {} 无标题), 更新人名 {} 个, 更新标签 {} 个", total, parsed.len(), skipped, person_count, tag_count); (person_count, tag_count) @@ -355,88 +361,33 @@ pub async fn run_pipeline(db: &Database, search_db: &SearchDb) -> PipelineResult // 不同步 albums 表数据(albums 由 imager.db 管理) // ════════════════════════════════════════════════ -/// 全量同步:将所有相册的 tags/persons 解析结果写入 search.db 的关联表 +/// 全量同步:将所有相册的 tags/persons 解析结果写入 search.db 的关联表(单事务批量) pub async fn sync_albums_to_search_db(db: &Database, search_db: &SearchDb) -> usize { let albums = db.get_all_albums_full().await; - let mut synced = 0usize; - let mut tag_cache: std::collections::HashMap = std::collections::HashMap::new(); - let mut person_cache: std::collections::HashMap = std::collections::HashMap::new(); - for album in &albums { - // ── 1. 写入 tags → album_tags ── - if let Some(title) = album.title.as_deref() { - if !title.is_empty() { - let parse_result = title_parser::parse_album_title(&album.id, title); - if !parse_result.tags.is_empty() { - search_db.clear_album_tags(&album.id).await.ok(); - for (tag_name, category) in &parse_result.tags { - let tag_id = match tag_cache.get(tag_name) { - Some(&id) => id, - None => { - match search_db.insert_tag(tag_name, Some(category)).await { - Ok(id) => { tag_cache.insert(tag_name.clone(), id); id } - Err(_) => continue, - } - } - }; - search_db.insert_album_tag(&album.id, tag_id).await.ok(); - } - } - } + // 并行解析所有相册 + let parsed: Vec = albums.par_iter() + .filter_map(|album| { + let title = album.title.as_deref()?; + if title.is_empty() { return None; } + Some(title_parser::parse_album_title(&album.id, title)) + }) + .collect(); + + // 单事务批量写入 search.db + match search_db.batch_sync_parse_results(&parsed).await { + Ok(n) => { + println!("[Sync->search.db] 全量同步完成:{} 个相册", n); + n } - - // ── 2. 写入 persons → album_persons ── - search_db.clear_album_persons(&album.id).await.ok(); - if let Some(title) = album.title.as_deref() { - if !title.is_empty() { - let parse_result = title_parser::parse_album_title(&album.id, title); - for person_name in &parse_result.person_names { - let person_id = match person_cache.get(person_name) { - Some(&id) => id, - None => { - match search_db.insert_person(person_name, None).await { - Ok(id) => { person_cache.insert(person_name.clone(), id); id } - Err(_) => continue, - } - } - }; - search_db.insert_album_person(&album.id, person_id).await.ok(); - // 同步别名 - let (primary, aliases) = title_parser::split_person_name_with_aliases(person_name); - if !aliases.is_empty() { - for (alias_type, alias_name) in &aliases { - if alias_name != person_name { - search_db.insert_person_alias(person_id, alias_type, alias_name).await.ok(); - } - } - if primary != person_name.as_str() && !primary.is_empty() { - search_db.insert_person_alias(person_id, "primary", &primary).await.ok(); - } - } - } - } + Err(e) => { + eprintln!("[Sync->search.db] 批量同步失败: {}", e); + 0 } - - synced += 1; } - - // ── 3. 更新 person counts(从 search.db 的 album_persons 直接计数,两库 person_id 各自独立)── - for (person_name, person_id) in &person_cache { - let albums_count: i64 = match sqlx::query_scalar::<_, i64>( - "SELECT COUNT(*) FROM album_persons WHERE person_id = ?" - ).bind(*person_id).fetch_one(&search_db.pool).await { - Ok(c) => c, - Err(_) => continue, - }; - search_db.update_person_counts(*person_id, albums_count, 0).await.ok(); - } - - println!("[Sync->search.db] 同步完成:{} 个相册,{} 个人物,{} 个标签", - synced, person_cache.len(), tag_cache.len()); - synced } -/// 增量同步:只处理脏集中的相册 ID,按需查询后写入 search.db 的关联表 +/// 增量同步:只处理脏集中的相册 ID(单事务批量) pub async fn sync_dirty_albums(db: &Database, search_db: &SearchDb, dirty: &DirtySet) -> usize { if dirty.is_empty() { println!("[Sync->search.db·增量] 脏集为空,跳过"); @@ -446,80 +397,24 @@ pub async fn sync_dirty_albums(db: &Database, search_db: &SearchDb, dirty: &Dirt let dirty_ids: Vec = dirty.clone().into_inner().into_iter().collect(); let albums = db.get_albums_by_ids(&dirty_ids).await; - let mut synced = 0usize; - let mut tag_cache: std::collections::HashMap = std::collections::HashMap::new(); - let mut person_cache: std::collections::HashMap = std::collections::HashMap::new(); + // 并行解析脏集相册 + let parsed: Vec = albums.par_iter() + .filter_map(|album| { + let title = album.title.as_deref()?; + if title.is_empty() { return None; } + Some(title_parser::parse_album_title(&album.id, title)) + }) + .collect(); - for album in &albums { - // ── 1. 写入 tags → album_tags ── - if let Some(title) = album.title.as_deref() { - if !title.is_empty() { - let parse_result = title_parser::parse_album_title(&album.id, title); - if !parse_result.tags.is_empty() { - search_db.clear_album_tags(&album.id).await.ok(); - for (tag_name, category) in &parse_result.tags { - let tag_id = match tag_cache.get(tag_name) { - Some(&id) => id, - None => { - match search_db.insert_tag(tag_name, Some(category)).await { - Ok(id) => { tag_cache.insert(tag_name.clone(), id); id } - Err(_) => continue, - } - } - }; - search_db.insert_album_tag(&album.id, tag_id).await.ok(); - } - } - } + // 单事务批量写入 search.db + match search_db.batch_sync_parse_results(&parsed).await { + Ok(n) => { + println!("[Sync->search.db·增量] 脏集同步完成:{} 个相册", n); + n } - - // ── 2. 写入 persons → album_persons ── - search_db.clear_album_persons(&album.id).await.ok(); - if let Some(title) = album.title.as_deref() { - if !title.is_empty() { - let parse_result = title_parser::parse_album_title(&album.id, title); - for person_name in &parse_result.person_names { - let person_id = match person_cache.get(person_name) { - Some(&id) => id, - None => { - match search_db.insert_person(person_name, None).await { - Ok(id) => { person_cache.insert(person_name.clone(), id); id } - Err(_) => continue, - } - } - }; - search_db.insert_album_person(&album.id, person_id).await.ok(); - // 同步别名 - let (primary, aliases) = title_parser::split_person_name_with_aliases(person_name); - if !aliases.is_empty() { - for (alias_type, alias_name) in &aliases { - if alias_name != person_name { - search_db.insert_person_alias(person_id, alias_type, alias_name).await.ok(); - } - } - if primary != person_name.as_str() && !primary.is_empty() { - search_db.insert_person_alias(person_id, "primary", &primary).await.ok(); - } - } - } - } + Err(e) => { + eprintln!("[Sync->search.db·增量] 批量同步失败: {}", e); + 0 } - - synced += 1; } - - // ── 3. 更新 person counts(从 search.db 的 album_persons 计数)── - for (person_name, person_id) in &person_cache { - let albums_count: i64 = match sqlx::query_scalar::<_, i64>( - "SELECT COUNT(*) FROM album_persons WHERE person_id = ?" - ).bind(*person_id).fetch_one(&search_db.pool).await { - Ok(c) => c, - Err(_) => continue, - }; - search_db.update_person_counts(*person_id, albums_count, 0).await.ok(); - } - - println!("[Sync->search.db·增量] 同步完成:{} 个相册,{} 个人物,{} 个标签", - synced, person_cache.len(), tag_cache.len()); - synced } diff --git a/src/engine/recommend.rs b/src/engine/recommend.rs index 4567d32..4fe61da 100644 --- a/src/engine/recommend.rs +++ b/src/engine/recommend.rs @@ -197,7 +197,7 @@ impl RecommendEngine { for rp in &mut results { if let Some(person) = search_db.get_person_by_name(&rp.name).await { - let top_tags = search_db.get_person_top_tags(person.id, 5).await; + let top_tags = search_db.get_person_top_tags(&person.id, 5).await; rp.top_tags = top_tags.into_iter().map(|(t, _)| t).collect(); } } diff --git a/src/main.rs b/src/main.rs index b86599e..3e3df36 100644 --- a/src/main.rs +++ b/src/main.rs @@ -135,9 +135,9 @@ async fn main() -> std::io::Result<()> { let dirty = engine::pipeline::phase_collect_new_albums(&boot_db).await; let new_count = dirty.len(); if new_count > 0 { - info!("[启动] 发现 {} 个新相册,同步到 search.db...", new_count); + info!("[启动] 发现 {} 个新相册,同步 tags/persons 到 search.db...", new_count); let synced = engine::pipeline::sync_dirty_albums(&boot_db, &boot_search_db, &dirty).await; - info!("[启动] 增量同步完成:{} 个相册(仅 tags/persons)", synced); + info!("[启动] 增量同步完成:{} 个相册(仅 tags/persons,不同步 albums 表)", synced); } else { info!("[启动] 没有新相册"); } @@ -169,10 +169,8 @@ async fn main() -> std::io::Result<()> { // ② 查找新相册并增量解析 let new_albums = engine::pipeline::phase_collect_new_albums(&timer_db).await; if !new_albums.is_empty() { - info!("[定时] 发现 {} 个新相册,增量解析...", new_albums.len()); + info!("[定时] 发现 {} 个新相册,增量解析(同时写入 imager.db + search.db)...", new_albums.len()); engine::pipeline::phase_parse_dirty(&timer_db, &timer_search_db, &new_albums).await; - info!("[定时] 同步到 search.db..."); - engine::pipeline::sync_dirty_albums(&timer_db, &timer_search_db, &new_albums).await; } // ③ 缓存刷新(始终执行) diff --git a/src/models/person.rs b/src/models/person.rs index 40822b2..5418b56 100644 --- a/src/models/person.rs +++ b/src/models/person.rs @@ -3,7 +3,7 @@ use super::album::AlbumInfo; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Person { - pub id: i64, + pub id: String, pub name: String, pub display_name: Option, pub total_albums: i64, @@ -16,7 +16,7 @@ pub struct Person { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PersonAlias { pub id: i64, - pub person_id: i64, + pub person_id: String, pub alias_type: String, pub alias_name: String, pub created_at: String, @@ -31,7 +31,7 @@ pub struct PersonWithAlbums { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PersonWithCounts { - pub id: i64, + pub id: String, pub name: String, pub display_name: Option, pub total_albums: i64, diff --git a/src/routes/person.rs b/src/routes/person.rs index cebd944..bae5ea3 100644 --- a/src/routes/person.rs +++ b/src/routes/person.rs @@ -54,7 +54,7 @@ pub async fn get_person_detail( match p { Some(p) => { // 从 search.db 获取相册 ID 列表 - let album_ids = search_db.get_person_album_ids(p.id).await; + let album_ids = search_db.get_person_album_ids(&p.id).await; // 从 imager.db 查询相册详情 let albums = if album_ids.is_empty() { Vec::new() @@ -106,8 +106,8 @@ pub async fn get_person_detail( } /// GET /api/people/{id}/aliases — 从 search.db 查别名 -pub async fn get_person_aliases(search_db: web::Data>, path: web::Path) -> impl Responder { - let aliases = search_db.get_person_aliases(path.into_inner()).await; +pub async fn get_person_aliases(search_db: web::Data>, path: web::Path) -> impl Responder { + let aliases = search_db.get_person_aliases(&path.into_inner()).await; HttpResponse::Ok().json(aliases) } @@ -137,8 +137,11 @@ pub async fn delete_person( }; // 2. 从 imager.db 删除(persons + person_aliases + person_albums) + // person.id 来自 imager.db,存储为 i64,模型层转为 String,这里转回 i64 if let Some(person) = db.get_person_by_name(&person_name).await { - db.delete_person_completely(person.id).await; + if let Ok(pid) = person.id.parse::() { + db.delete_person_completely(pid).await; + } } // 3. 重置受影响相册的 processed_at(触发重新解析) @@ -155,7 +158,6 @@ pub async fn delete_person( let mut dirty = engine::pipeline::DirtySet::new(); for aid in &album_ids { dirty.add(aid); } let (persons_updated, tags_updated) = engine::pipeline::phase_parse_dirty(&db, &search_db, &dirty).await; - let synced = engine::pipeline::sync_dirty_albums(&db, &search_db, &dirty).await; // 5. 重载内存词典 engine::tag_dict::reload_from_search_db(&search_db).await; @@ -169,10 +171,9 @@ pub async fn delete_person( "albums_affected": album_ids.len(), "persons_updated": persons_updated, "tags_updated": tags_updated, - "synced": synced, "message": format!( - "人物 '{}' 已删除,影响 {} 个相册,重新解析完成(更新人物 {},更新标签 {},同步 {} 个)", - person_name, album_ids.len(), persons_updated, tags_updated, synced, + "人物 '{}' 已删除,影响 {} 个相册,重新解析完成(更新人物 {},更新标签 {})", + person_name, album_ids.len(), persons_updated, tags_updated, ) })) } else { diff --git a/src/routes/tag.rs b/src/routes/tag.rs index 78c0894..4f7ac31 100644 --- a/src/routes/tag.rs +++ b/src/routes/tag.rs @@ -86,18 +86,15 @@ pub async fn delete_tag( .ok(); } - // 3. 重新解析受影响相册 + // 3. 重新解析受影响相册(同时写入 imager.db + search.db) let mut dirty = engine::pipeline::DirtySet::new(); for aid in &album_ids { dirty.add(aid); } let (persons_updated, tags_updated) = engine::pipeline::phase_parse_dirty(&db, &search_db, &dirty).await; - // 4. 同步到 search.db - let synced = engine::pipeline::sync_dirty_albums(&db, &search_db, &dirty).await; - - // 5. 重载内存词典(dict 表已更新) + // 4. 重载内存词典(dict 表已更新) engine::tag_dict::reload_from_search_db(&search_db).await; - // 6. 刷新内存缓存 + // 5. 刷新内存缓存 crate::cache::refresh_all_caches(&db, &search_db, &albums_cache, &persons_cache, &tags_cache).await; HttpResponse::Ok().json(serde_json::json!({ @@ -106,10 +103,9 @@ pub async fn delete_tag( "albums_affected": album_ids.len(), "persons_updated": persons_updated, "tags_updated": tags_updated, - "synced": synced, "message": format!( - "标签 '{}' 已删除,影响 {} 个相册,重新解析完成(更新人物 {},更新标签 {},同步 {} 个)", - tag_name, album_ids.len(), persons_updated, tags_updated, synced, + "标签 '{}' 已删除,影响 {} 个相册,重新解析完成(更新人物 {},更新标签 {})", + tag_name, album_ids.len(), persons_updated, tags_updated, ) })) } diff --git a/src/search_db/mod.rs b/src/search_db/mod.rs index 3e209f5..7753324 100644 --- a/src/search_db/mod.rs +++ b/src/search_db/mod.rs @@ -3,6 +3,25 @@ mod search; pub use search::*; use sqlx::{SqlitePool, sqlite::SqlitePoolOptions, Row}; +use uuid::Uuid; + +/// 从 name 计算确定性 tag UUID(无需查数据库即可得到 id) +pub fn tag_uuid(name: &str) -> String { + Uuid::new_v5( + &Uuid::NAMESPACE_URL, + format!("https://imager.dev/tag/{}", name).as_bytes(), + ) + .to_string() +} + +/// 从 name 计算确定性 person UUID(无需查数据库即可得到 id) +pub fn person_uuid(name: &str) -> String { + Uuid::new_v5( + &Uuid::NAMESPACE_URL, + format!("https://imager.dev/person/{}", name).as_bytes(), + ) + .to_string() +} #[derive(Debug, Clone)] pub struct DictEntry { @@ -40,14 +59,25 @@ impl SearchDb { sqlx::query("PRAGMA busy_timeout=5000;").execute(&pool).await?; sqlx::query("PRAGMA wal_autocheckpoint=1000;").execute(&pool).await?; sqlx::query("PRAGMA journal_size_limit=67108864;").execute(&pool).await?; - sqlx::query("PRAGMA cache_size=-64000;").execute(&pool).await?; + sqlx::query("PRAGMA cache_size=-256000;").execute(&pool).await?; sqlx::query("PRAGMA temp_store=MEMORY;").execute(&pool).await?; + sqlx::query("PRAGMA mmap_size=268435456;").execute(&pool).await?; Self::create_tables(&pool).await?; Ok(Self { pool }) } + /// 在批量写入前临时调低 synchronous 级别提升性能 + pub async fn set_sync_off(&self) { + let _ = sqlx::query("PRAGMA synchronous=OFF;").execute(&self.pool).await; + } + + /// 批量写入后恢复 synchronous 级别 + pub async fn set_sync_normal(&self) { + let _ = sqlx::query("PRAGMA synchronous=NORMAL;").execute(&self.pool).await; + } + /// 执行 WAL checkpoint(TRUNCATE),将 WAL 完全合并回主文件并清理 -wal / -shm 文件 /// /// 在启动时调用,确保 search.db 文件干净,避免下次启动时 WAL 恢复开销。 @@ -61,18 +91,42 @@ impl SearchDb { } async fn create_tables(pool: &SqlitePool) -> Result<(), sqlx::Error> { + // ── 迁移检测:如果 tags 表存在且 id 类型是 INTEGER(旧版),重建表 ── + let has_old_schema: bool = sqlx::query( + "SELECT type FROM pragma_table_info('tags') WHERE name = 'id'" + ).fetch_optional(pool).await.ok() + .flatten() + .map(|r| { + let t: String = r.get("type"); + // 旧版用 INTEGER,新版用 TEXT + t.contains("INTEGER") + }) + .unwrap_or(false); + + if has_old_schema { + println!("[search.db] 检测到旧版 INTEGER ID 模式,重建表为 TEXT UUID 模式..."); + sqlx::query("DROP TABLE IF EXISTS album_tags").execute(pool).await?; + sqlx::query("DROP TABLE IF EXISTS album_persons").execute(pool).await?; + sqlx::query("DROP TABLE IF EXISTS person_aliases").execute(pool).await?; + sqlx::query("DROP TABLE IF EXISTS tags").execute(pool).await?; + sqlx::query("DROP TABLE IF EXISTS persons").execute(pool).await?; + println!("[search.db] 旧表已删除,即将重建"); + } + + // ── 标签表(TEXT UUID 主键) ── sqlx::query(r#" CREATE TABLE IF NOT EXISTS tags ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, category TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) "#).execute(pool).await?; + // ── 人物表(TEXT UUID 主键) ── sqlx::query(r#" CREATE TABLE IF NOT EXISTS persons ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, display_name TEXT, total_albums INTEGER DEFAULT 0, @@ -82,10 +136,11 @@ impl SearchDb { ) "#).execute(pool).await?; + // ── 人物别名 ── sqlx::query(r#" CREATE TABLE IF NOT EXISTS person_aliases ( id INTEGER PRIMARY KEY AUTOINCREMENT, - person_id INTEGER NOT NULL, + person_id TEXT NOT NULL, alias_type TEXT NOT NULL, alias_name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -93,25 +148,27 @@ impl SearchDb { ) "#).execute(pool).await?; + // ── 相册-标签关联 ── sqlx::query(r#" CREATE TABLE IF NOT EXISTS album_tags ( album_id TEXT NOT NULL, - tag_id INTEGER NOT NULL, + tag_id TEXT NOT NULL, PRIMARY KEY (album_id, tag_id), FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE ) "#).execute(pool).await?; + // ── 相册-人物关联 ── sqlx::query(r#" CREATE TABLE IF NOT EXISTS album_persons ( album_id TEXT NOT NULL, - person_id INTEGER NOT NULL, + person_id TEXT NOT NULL, PRIMARY KEY (album_id, person_id), FOREIGN KEY (person_id) REFERENCES persons(id) ON DELETE CASCADE ) "#).execute(pool).await?; - // ── 新增 albums 表:搜索返回需要的相册基础信息 ── + // ── albums 表:搜索返回需要的相册基础信息 ── sqlx::query(r#" CREATE TABLE IF NOT EXISTS albums ( id TEXT PRIMARY KEY, @@ -122,6 +179,7 @@ impl SearchDb { ) "#).execute(pool).await?; + // ── dict 表 ── sqlx::query(r#" CREATE TABLE IF NOT EXISTS dict ( word TEXT NOT NULL PRIMARY KEY, @@ -133,6 +191,7 @@ impl SearchDb { ) "#).execute(pool).await?; + // ── 索引 ── sqlx::query("CREATE INDEX IF NOT EXISTS idx_albums_title ON albums(title)").execute(pool).await?; sqlx::query("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)").execute(pool).await?; sqlx::query("CREATE INDEX IF NOT EXISTS idx_persons_name ON persons(name)").execute(pool).await?; @@ -177,56 +236,32 @@ impl SearchDb { Ok(()) } - pub async fn insert_tag(&self, name: &str, category: Option<&str>) -> Result { - let result = sqlx::query(r#" - INSERT OR IGNORE INTO tags (name, category) VALUES (?, ?) - "#) - .bind(name) - .bind(category) - .execute(&self.pool) - .await?; - - let id = result.last_insert_rowid(); - if id == 0 { - let found_id: i64 = sqlx::query_scalar("SELECT id FROM tags WHERE name = ?") - .bind(name) - .fetch_one(&self.pool) - .await?; - Ok(found_id) - } else { - Ok(id) - } + /// 插入标签(使用确定性 UUID,无需 SELECT 回查) + pub async fn insert_tag(&self, name: &str, category: Option<&str>) -> Result { + let id = tag_uuid(name); + sqlx::query(r#"INSERT OR IGNORE INTO tags (id, name, category) VALUES (?, ?, ?)"#) + .bind(&id).bind(name).bind(category) + .execute(&self.pool).await?; + Ok(id) } - pub async fn insert_person(&self, name: &str, display_name: Option<&str>) -> Result { - let result = sqlx::query(r#" - INSERT OR IGNORE INTO persons (name, display_name) VALUES (?, ?) - "#) - .bind(name) - .bind(display_name) - .execute(&self.pool) - .await?; - - let id = result.last_insert_rowid(); - if id == 0 { - let found_id: i64 = sqlx::query_scalar("SELECT id FROM persons WHERE name = ?") - .bind(name) - .fetch_one(&self.pool) - .await?; - Ok(found_id) - } else { - Ok(id) - } + /// 插入人物(使用确定性 UUID,无需 SELECT 回查) + pub async fn insert_person(&self, name: &str, display_name: Option<&str>) -> Result { + let id = person_uuid(name); + sqlx::query(r#"INSERT OR IGNORE INTO persons (id, name, display_name) VALUES (?, ?, ?)"#) + .bind(&id).bind(name).bind(display_name) + .execute(&self.pool).await?; + Ok(id) } - pub async fn update_person_counts(&self, person_id: i64, total_albums: i64, total_images: i64) -> Result<(), sqlx::Error> { + pub async fn update_person_counts(&self, person_id: &str, total_albums: i64, total_images: i64) -> Result<(), sqlx::Error> { sqlx::query("UPDATE persons SET total_albums = ?, total_images = ?, updated_at = datetime('now') WHERE id = ?") .bind(total_albums).bind(total_images).bind(person_id) .execute(&self.pool).await?; Ok(()) } - pub async fn insert_person_alias(&self, person_id: i64, alias_type: &str, alias_name: &str) -> Result<(), sqlx::Error> { + pub async fn insert_person_alias(&self, person_id: &str, alias_type: &str, alias_name: &str) -> Result<(), sqlx::Error> { sqlx::query(r#" INSERT OR IGNORE INTO person_aliases (person_id, alias_type, alias_name) VALUES (?, ?, ?) "#) @@ -238,7 +273,7 @@ impl SearchDb { Ok(()) } - pub async fn insert_album_tag(&self, album_id: &str, tag_id: i64) -> Result<(), sqlx::Error> { + pub async fn insert_album_tag(&self, album_id: &str, tag_id: &str) -> Result<(), sqlx::Error> { sqlx::query(r#" INSERT OR IGNORE INTO album_tags (album_id, tag_id) VALUES (?, ?) "#) @@ -249,7 +284,7 @@ impl SearchDb { Ok(()) } - pub async fn insert_album_person(&self, album_id: &str, person_id: i64) -> Result<(), sqlx::Error> { + pub async fn insert_album_person(&self, album_id: &str, person_id: &str) -> Result<(), sqlx::Error> { sqlx::query(r#" INSERT OR IGNORE INTO album_persons (album_id, person_id) VALUES (?, ?) "#) @@ -317,34 +352,20 @@ impl SearchDb { /// 删除 tag:从 dict/album_tags/tags 表中删除,返回受影响的 album_id 列表 pub async fn delete_tag_and_get_albums(&self, tag_name: &str) -> Result, sqlx::Error> { - // 1. 查找 tag_id(tags 表中可能存在多个同名 tag,但 name 应唯一) - let tag_ids: Vec = sqlx::query_scalar("SELECT id FROM tags WHERE name = ?") - .bind(tag_name).fetch_all(&self.pool).await.unwrap_or_default(); + // 1. 计算确定性 tag UUID + let tag_id = tag_uuid(tag_name); // 2. 查找关联 album_ids - let album_ids: Vec = if tag_ids.is_empty() { - Vec::new() - } else { - // 用参数占位构建 IN 查询 - let placeholders: Vec = tag_ids.iter().map(|_| "?".to_string()).collect(); - let sql = format!("SELECT DISTINCT album_id FROM album_tags WHERE tag_id IN ({})", placeholders.join(",")); - let mut q = sqlx::query(&sql); - for id in &tag_ids { q = q.bind(*id); } - q.fetch_all(&self.pool).await? - .iter().map(|r| r.get::("album_id")).collect() - }; + let album_ids: Vec = sqlx::query_scalar::<_, String>( + "SELECT DISTINCT album_id FROM album_tags WHERE tag_id = ?" + ).bind(&tag_id).fetch_all(&self.pool).await.unwrap_or_default(); // 3. 删除 album_tags 关联 - if !tag_ids.is_empty() { - let placeholders: Vec = tag_ids.iter().map(|_| "?".to_string()).collect(); - let sql = format!("DELETE FROM album_tags WHERE tag_id IN ({})", placeholders.join(",")); - let mut q = sqlx::query(&sql); - for id in &tag_ids { q = q.bind(*id); } - q.execute(&self.pool).await?; - } + sqlx::query("DELETE FROM album_tags WHERE tag_id = ?") + .bind(&tag_id).execute(&self.pool).await?; // 4. 删除 tags 表条目 - sqlx::query("DELETE FROM tags WHERE name = ?").bind(tag_name).execute(&self.pool).await?; + sqlx::query("DELETE FROM tags WHERE id = ?").bind(&tag_id).execute(&self.pool).await?; // 5. 删除 dict 表条目 sqlx::query("DELETE FROM dict WHERE word = ? AND type = 'tag'").bind(tag_name).execute(&self.pool).await?; @@ -354,40 +375,21 @@ impl SearchDb { /// 删除 person:从 search.db 的 album_persons/persons/person_aliases 中删除,返回受影响的 album_id 列表 pub async fn delete_person_and_get_albums(&self, person_name: &str) -> Result, sqlx::Error> { - // 1. 查找 person_id - let person_ids: Vec = sqlx::query_scalar("SELECT id FROM persons WHERE name = ?") - .bind(person_name).fetch_all(&self.pool).await.unwrap_or_default(); + // 1. 计算确定性 person UUID + let person_id = person_uuid(person_name); // 2. 查找关联 album_ids - let album_ids: Vec = if person_ids.is_empty() { - Vec::new() - } else { - let placeholders: Vec = person_ids.iter().map(|_| "?".to_string()).collect(); - let sql = format!("SELECT DISTINCT album_id FROM album_persons WHERE person_id IN ({})", placeholders.join(",")); - let mut q = sqlx::query(&sql); - for id in &person_ids { q = q.bind(*id); } - q.fetch_all(&self.pool).await? - .iter().map(|r| r.get::("album_id")).collect() - }; + let album_ids: Vec = sqlx::query_scalar::<_, String>( + "SELECT DISTINCT album_id FROM album_persons WHERE person_id = ?" + ).bind(&person_id).fetch_all(&self.pool).await.unwrap_or_default(); // 3. 删除关联表 - if !person_ids.is_empty() { - let placeholders: Vec = person_ids.iter().map(|_| "?".to_string()).collect(); - let sql = format!("DELETE FROM album_persons WHERE person_id IN ({})", placeholders.join(",")); - let mut q = sqlx::query(&sql); - for id in &person_ids { q = q.bind(*id); } - q.execute(&self.pool).await?; - - let sql2 = format!("DELETE FROM person_aliases WHERE person_id IN ({})", placeholders.join(",")); - let mut q2 = sqlx::query(&sql2); - for id in &person_ids { q2 = q2.bind(*id); } - q2.execute(&self.pool).await?; - - let sql3 = format!("DELETE FROM persons WHERE id IN ({})", placeholders.join(",")); - let mut q3 = sqlx::query(&sql3); - for id in &person_ids { q3 = q3.bind(*id); } - q3.execute(&self.pool).await?; - } + sqlx::query("DELETE FROM album_persons WHERE person_id = ?") + .bind(&person_id).execute(&self.pool).await?; + sqlx::query("DELETE FROM person_aliases WHERE person_id = ?") + .bind(&person_id).execute(&self.pool).await?; + sqlx::query("DELETE FROM persons WHERE id = ?") + .bind(&person_id).execute(&self.pool).await?; // 4. 删除 dict 表中的对应条目(如果存在) sqlx::query("DELETE FROM dict WHERE word = ? AND type = 'person'") @@ -396,6 +398,142 @@ impl SearchDb { Ok(album_ids) } + /// 批量同步解析结果到 search.db(单事务 + 预计算 UUID + 批量操作) + /// + /// 核心优化: + /// 1. 预加载全部 tags/persons 到 HashMap(仅 2 次 SELECT) + /// 2. 预计算所有 tag/person 的 UUID(零数据库查询得到 ID) + /// 3. 全量 `DELETE WHERE album_id IN (...)` 代替逐条 DELETE + /// 4. 逐条 INSERT 但无需 SELECT 查 ID(UUID 直接写入) + /// 5. 每事务一次 person counts 刷新 + pub async fn batch_sync_parse_results(&self, results: &[crate::models::parse::ParseResult]) -> Result { + use std::collections::HashMap; + let mut tx = self.pool.begin().await?; + + // 1. 预加载现有 tags/persons(仅 2 次 SELECT) + let existing_tags: HashMap = sqlx::query_as::<_, (String, String)>( + "SELECT name, id FROM tags" + ).fetch_all(&mut *tx).await?.into_iter().collect(); + + let existing_persons: HashMap = sqlx::query_as::<_, (String, String)>( + "SELECT name, id FROM persons" + ).fetch_all(&mut *tx).await?.into_iter().collect(); + + // 2. 收集所有相册 ID,用于批量 DELETE + let album_ids: Vec<&str> = results.iter().map(|r| r.album_id.as_str()).collect(); + if !album_ids.is_empty() { + let ph: Vec = album_ids.iter().map(|_| "?".to_string()).collect(); + let sql = format!("DELETE FROM album_tags WHERE album_id IN ({})", ph.join(",")); + let mut q = sqlx::query(&sql); + for id in &album_ids { q = q.bind(id); } + q.execute(&mut *tx).await?; + + let ph2: Vec = album_ids.iter().map(|_| "?".to_string()).collect(); + let sql2 = format!("DELETE FROM album_persons WHERE album_id IN ({})", ph2.join(",")); + let mut q2 = sqlx::query(&sql2); + for id in &album_ids { q2 = q2.bind(id); } + q2.execute(&mut *tx).await?; + } + + // 3. 收集去重后的 tag 和 person 名称 + let mut unique_tags: Vec<(String, Option)> = Vec::new(); + let mut seen_tags = std::collections::HashSet::new(); + let mut unique_persons: Vec = Vec::new(); + let mut seen_persons = std::collections::HashSet::new(); + + for result in results { + for (tag_name, category) in &result.tags { + if seen_tags.insert(tag_name.clone()) { + unique_tags.push((tag_name.clone(), Some(category.clone()))); + } + } + for person_name in &result.person_names { + if seen_persons.insert(person_name.clone()) { + unique_persons.push(person_name.clone()); + } + } + } + + // 4a. 写入标签:对不存在的标签 INSERT(使用预计算 UUID) + for (tag_name, category) in &unique_tags { + if !existing_tags.contains_key(tag_name) { + let id = tag_uuid(tag_name); + sqlx::query("INSERT OR IGNORE INTO tags (id, name, category) VALUES (?, ?, ?)") + .bind(&id).bind(tag_name).bind(category) + .execute(&mut *tx).await?; + } + } + + // 4b. 写入人物:对不存在的人物 INSERT(使用预计算 UUID) + for person_name in &unique_persons { + if !existing_persons.contains_key(person_name) { + let id = person_uuid(person_name); + sqlx::query("INSERT OR IGNORE INTO persons (id, name) VALUES (?, ?)") + .bind(&id).bind(person_name) + .execute(&mut *tx).await?; + } + } + + // 5. 写入批量关联数据(无 SELECT 查找,直接使用预计算的 UUID) + let mut tag_id_cache: HashMap = existing_tags.clone(); + let mut person_id_cache: HashMap = existing_persons.clone(); + + for result in results { + let album_id = &result.album_id; + + // ── album_tags ── + for (tag_name, _category) in &result.tags { + let tid = tag_id_cache.get(tag_name.as_str()) + .cloned() + .unwrap_or_else(|| tag_uuid(tag_name)); + // 缓存新写入的 tag(即使上一个缓存中已更新,这里确保缓存完整) + tag_id_cache.entry(tag_name.clone()).or_insert_with(|| tid.clone()); + sqlx::query("INSERT OR IGNORE INTO album_tags (album_id, tag_id) VALUES (?, ?)") + .bind(album_id).bind(&tid).execute(&mut *tx).await?; + } + + // ── album_persons ── + for person_name in &result.person_names { + let pid = person_id_cache.get(person_name.as_str()) + .cloned() + .unwrap_or_else(|| person_uuid(person_name)); + person_id_cache.entry(person_name.clone()).or_insert_with(|| pid.clone()); + sqlx::query("INSERT OR IGNORE INTO album_persons (album_id, person_id) VALUES (?, ?)") + .bind(album_id).bind(&pid).execute(&mut *tx).await?; + + // 别名写入 + let (primary, aliases) = crate::engine::title_parser::split_person_name_with_aliases(person_name); + if !aliases.is_empty() { + for (alias_type, alias_name) in &aliases { + if alias_name != person_name { + sqlx::query("INSERT OR IGNORE INTO person_aliases (person_id, alias_type, alias_name) VALUES (?, ?, ?)") + .bind(&pid).bind(alias_type).bind(alias_name).execute(&mut *tx).await?; + } + } + if primary != person_name.as_str() && !primary.is_empty() { + sqlx::query("INSERT OR IGNORE INTO person_aliases (person_id, alias_type, alias_name) VALUES (?, ?, ?)") + .bind(&pid).bind("primary").bind(&primary).execute(&mut *tx).await?; + } + } + } + } + + // 6. 更新 person counts(一次批量) + for (_name, pid) in &person_id_cache { + let count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM album_persons WHERE person_id = ?" + ).bind(pid).fetch_one(&mut *tx).await.unwrap_or(0); + sqlx::query("UPDATE persons SET total_albums = ?, total_images = 0 WHERE id = ?") + .bind(count).bind(pid).execute(&mut *tx).await?; + } + + tx.commit().await?; + let total = results.len(); + println!("[batch_sync] 单事务同步完成:{} 个相册,{} 个标签(key),{} 个人物(key)", + total, tag_id_cache.len(), person_id_cache.len()); + Ok(total) + } + pub async fn clear_all(&self) -> Result<(), sqlx::Error> { let mut tx = self.pool.begin().await?; sqlx::query("DELETE FROM album_tags").execute(&mut *tx).await?; diff --git a/src/search_db/search.rs b/src/search_db/search.rs index c5d0307..753418c 100644 --- a/src/search_db/search.rs +++ b/src/search_db/search.rs @@ -212,7 +212,7 @@ impl SearchDb { // ═══════════════════════════════════════ /// 获取人物别名列表 - pub async fn get_person_aliases(&self, person_id: i64) -> Vec { + pub async fn get_person_aliases(&self, person_id: &str) -> Vec { let rows = sqlx::query( "SELECT id, person_id, alias_type, alias_name, created_at FROM person_aliases WHERE person_id = ?" ).bind(person_id).fetch_all(&self.pool).await.unwrap_or_default(); @@ -234,11 +234,11 @@ impl SearchDb { let mut results = Vec::new(); for r in &persons { - let id: i64 = r.get("id"); + let id: String = r.get("id"); let name: String = r.get("name"); let aliases: Vec = sqlx::query_scalar( "SELECT alias_name FROM person_aliases WHERE person_id = ? ORDER BY alias_type" - ).bind(id).fetch_all(&self.pool).await.unwrap_or_default(); + ).bind(&id).fetch_all(&self.pool).await.unwrap_or_default(); results.push(PersonWithCounts { id, @@ -258,10 +258,10 @@ impl SearchDb { "SELECT id, name, display_name, total_albums, total_images FROM persons WHERE name = ?" ).bind(name).fetch_optional(&self.pool).await.ok()??; - let id: i64 = row.get("id"); + let id: String = row.get("id"); let aliases: Vec = sqlx::query_scalar( "SELECT alias_name FROM person_aliases WHERE person_id = ? ORDER BY alias_type" - ).bind(id).fetch_all(&self.pool).await.unwrap_or_default(); + ).bind(&id).fetch_all(&self.pool).await.unwrap_or_default(); Some(PersonWithCounts { id, @@ -274,14 +274,14 @@ impl SearchDb { } /// 获取人物的所有相册 ID(从 search.db 的 album_persons 关联表查询) - pub async fn get_person_album_ids(&self, person_id: i64) -> Vec { + pub async fn get_person_album_ids(&self, person_id: &str) -> Vec { sqlx::query_scalar::<_, String>( "SELECT album_id FROM album_persons WHERE person_id = ?" ).bind(person_id).fetch_all(&self.pool).await.unwrap_or_default() } /// 获取人物的 top 标签(用于详情页) - pub async fn get_person_top_tags(&self, person_id: i64, limit: usize) -> Vec<(String, i64)> { + pub async fn get_person_top_tags(&self, person_id: &str, limit: usize) -> Vec<(String, i64)> { let rows = sqlx::query( r#"SELECT t.name, COUNT(*) as cnt FROM tags t @@ -313,7 +313,7 @@ impl SearchDb { || name_lower.contains(&q) || alias_matches; - if matched && seen.insert(p.id) { + if matched && seen.insert(p.id.clone()) { results.push(p.clone()); } } diff --git a/src/services/search_export.rs b/src/services/search_export.rs index 67de362..46eb86c 100644 --- a/src/services/search_export.rs +++ b/src/services/search_export.rs @@ -64,8 +64,8 @@ pub async fn export_to_search_db( let albums = source_db.get_all_albums().await.map_err(|e| format!("读取相册列表失败: {}", e))?; println!("[搜索导出] 共读取 {} 个相册(来自 imager.db)", albums.len()); - let mut tag_cache: HashMap = HashMap::new(); - let mut person_cache: HashMap = HashMap::new(); + let mut tag_cache: HashMap = HashMap::new(); + let mut person_cache: HashMap = HashMap::new(); let mut result = SearchExportResult::default(); for album in &albums { @@ -97,21 +97,22 @@ pub async fn export_to_search_db( result.dict_matched_tags += 1; } + // 使用确定性 UUID,无需查数据库得到 id let tag_id = match tag_cache.get(tag_name) { - Some(&id) => id, + Some(id) => id.clone(), None => { let id = search_db .insert_tag(tag_name, Some(category)) .await .map_err(|e| format!("插入标签 {} 失败: {}", tag_name, e))?; - tag_cache.insert(tag_name.clone(), id); + tag_cache.insert(tag_name.clone(), id.clone()); result.tags_created += 1; id } }; search_db - .insert_album_tag(&album_id, tag_id) + .insert_album_tag(&album_id, &tag_id) .await .map_err(|e| format!("关联相册标签 {} → {} 失败: {}", album_id, tag_name, e))?; result.album_tags_created += 1; @@ -127,21 +128,22 @@ pub async fn export_to_search_db( result.dict_resolved_aliases += 1; } + // 使用确定性 UUID,无需查数据库得到 id let person_id = match person_cache.get(person_name) { - Some(&id) => id, + Some(id) => id.clone(), None => { let id = search_db .insert_person(person_name, None) .await .map_err(|e| format!("插入人物 {} 失败: {}", person_name, e))?; - person_cache.insert(person_name.clone(), id); + person_cache.insert(person_name.clone(), id.clone()); result.persons_created += 1; id } }; search_db - .insert_album_person(&album_id, person_id) + .insert_album_person(&album_id, &person_id) .await .map_err(|e| format!("关联相册人物 {} → {} 失败: {}", album_id, person_name, e))?; result.album_persons_created += 1; @@ -152,7 +154,7 @@ pub async fn export_to_search_db( for (alias_type, alias_name) in &aliases { if alias_name != person_name { search_db - .insert_person_alias(person_id, alias_type, alias_name) + .insert_person_alias(&person_id, alias_type, alias_name) .await .map_err(|e| format!("插入别名 {} → {} 失败: {}", person_name, alias_name, e))?; result.aliases_created += 1; @@ -160,7 +162,7 @@ pub async fn export_to_search_db( } if primary != person_name.as_str() && !primary.is_empty() { search_db - .insert_person_alias(person_id, "primary", &primary) + .insert_person_alias(&person_id, "primary", &primary) .await .map_err(|e| format!("插入别名 {} → {} 失败: {}", person_name, primary, e))?; result.aliases_created += 1;