update code level
This commit is contained in:
16
src/cache/mod.rs
vendored
16
src/cache/mod.rs
vendored
@@ -246,14 +246,20 @@ pub async fn refresh_all_caches(
|
||||
a
|
||||
}).collect();
|
||||
|
||||
let mut c = albums_cache.write().await;
|
||||
c.update(enriched);
|
||||
|
||||
// 2. 刷新标签(从 search.db)
|
||||
// 2. 刷新标签(tag→albums 倒排索引直接从已富化的 album 列表构建,零 DB 查询)
|
||||
let popular_tags = search_db.get_popular_tags(200).await;
|
||||
let tag_albums = search_db.get_all_tag_albums().await;
|
||||
let mut tag_albums: HashMap<String, Vec<AlbumInfo>> = HashMap::new();
|
||||
for album in &enriched {
|
||||
for tag in &album.tags {
|
||||
tag_albums.entry(tag.clone()).or_default().push(album.clone());
|
||||
}
|
||||
}
|
||||
let mut tc = tags_cache.write().await;
|
||||
tc.update(popular_tags, tag_albums);
|
||||
|
||||
// 3. 刷新相册缓存(依赖 enriched 数据,必须在标签刷新之后执行)
|
||||
let mut c = albums_cache.write().await;
|
||||
c.update(enriched);
|
||||
}
|
||||
|
||||
// 3. 刷新人物(从 search.db)
|
||||
|
||||
@@ -228,10 +228,66 @@ impl Database {
|
||||
|
||||
pub async fn delete_album(&self, album_id: Uuid) -> Result<(), sqlx::Error> {
|
||||
let _guard = self.write_lock.lock().await;
|
||||
sqlx::query("DELETE FROM albums WHERE id = ?").bind(album_id.to_string()).execute(&self.pool).await?;
|
||||
let aid = album_id.to_string();
|
||||
// 先删除 person_albums(无 CASCADE)
|
||||
sqlx::query("DELETE FROM person_albums WHERE album_id = ?").bind(&aid).execute(&self.pool).await?;
|
||||
// 再删除 albums(images/favorites/album_tags 由 CASCADE 自动处理)
|
||||
sqlx::query("DELETE FROM albums WHERE id = ?").bind(&aid).execute(&self.pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 批量删除相册(级联清理所有关联数据)
|
||||
pub async fn delete_albums_batch(&self, album_ids: &[Uuid]) -> usize {
|
||||
if album_ids.is_empty() { return 0; }
|
||||
let _guard = self.write_lock.lock().await;
|
||||
for id in album_ids {
|
||||
let aid = id.to_string();
|
||||
sqlx::query("DELETE FROM person_albums WHERE album_id = ?").bind(&aid).execute(&self.pool).await.ok();
|
||||
sqlx::query("DELETE FROM albums WHERE id = ?").bind(&aid).execute(&self.pool).await.ok();
|
||||
}
|
||||
album_ids.len()
|
||||
}
|
||||
|
||||
/// 查询 image_count = 0 的空相册
|
||||
pub async fn get_empty_albums(&self) -> Vec<AlbumInfo> {
|
||||
let rows = sqlx::query(
|
||||
r#"SELECT id, title, document, description, cover_image_id, image_count, created_at
|
||||
FROM albums WHERE image_count IS NULL OR image_count = 0
|
||||
ORDER BY created_at DESC"#
|
||||
).fetch_all(&self.pool).await.unwrap_or_default();
|
||||
rows.iter().map(|r| {
|
||||
let id_str = r.get::<String, _>("id");
|
||||
AlbumInfo {
|
||||
id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::nil()),
|
||||
title: r.get("title"), document: r.get("document"),
|
||||
description: r.get("description"), cover_image_id: r.get("cover_image_id"),
|
||||
image_count: r.get("image_count"), created_at: r.get("created_at"),
|
||||
tags: vec![], persons: vec![],
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// 查找重名相册,返回 (keep_id, [duplicate_ids])
|
||||
pub async fn find_duplicate_albums(&self) -> Vec<(Uuid, Vec<Uuid>)> {
|
||||
let rows = sqlx::query(
|
||||
r#"SELECT id, title FROM albums WHERE title != '' ORDER BY created_at ASC"#
|
||||
).fetch_all(&self.pool).await.unwrap_or_default();
|
||||
|
||||
let mut title_map: std::collections::HashMap<String, Vec<Uuid>> = std::collections::HashMap::new();
|
||||
for r in &rows {
|
||||
let title: String = r.get("title");
|
||||
let id_str: String = r.get("id");
|
||||
if let Ok(id) = Uuid::parse_str(&id_str) {
|
||||
title_map.entry(title).or_default().push(id);
|
||||
}
|
||||
}
|
||||
|
||||
title_map.into_iter()
|
||||
.filter(|(_, ids)| ids.len() > 1)
|
||||
.map(|(_, ids)| (ids[0], ids[1..].to_vec()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn update_album_cover(&self, album_id: Uuid, image_id: Uuid) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE albums SET cover_image_id = ? WHERE id = ?")
|
||||
.bind(image_id.to_string()).bind(album_id.to_string()).execute(&self.pool).await?;
|
||||
|
||||
@@ -244,6 +244,47 @@ async fn phase_clean_persons_combined(db: &Database) -> (DirtySet, usize, usize)
|
||||
(dirty, non_persons_deleted, stop_word_persons_removed)
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════
|
||||
// Phase Z0:删除空相册(image_count = 0)
|
||||
// ════════════════════════════════════════════════
|
||||
|
||||
/// 删除所有图片数为 0 的空相册
|
||||
pub async fn phase_delete_empty_albums(db: &Database) -> usize {
|
||||
let empty = db.get_empty_albums().await;
|
||||
let count = empty.len();
|
||||
if count == 0 {
|
||||
println!("[管线·PhaseZ0] 无空相册需要删除");
|
||||
return 0;
|
||||
}
|
||||
let ids: Vec<uuid::Uuid> = empty.iter().map(|a| a.id).collect();
|
||||
let deleted = db.delete_albums_batch(&ids).await;
|
||||
println!("[管线·PhaseZ0] 删除 {} 个空相册(无图片)", deleted);
|
||||
deleted
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════
|
||||
// Phase Z1:去重同名相册
|
||||
// ════════════════════════════════════════════════
|
||||
|
||||
/// 查找同名的相册,只保留第一个(最早创建的),删除其余
|
||||
pub async fn phase_dedup_albums(db: &Database) -> usize {
|
||||
let dups = db.find_duplicate_albums().await;
|
||||
let mut total_deleted = 0usize;
|
||||
for (keep, to_delete) in &dups {
|
||||
let deleted = db.delete_albums_batch(to_delete).await;
|
||||
total_deleted += deleted;
|
||||
for id in to_delete {
|
||||
println!("[管线·PhaseZ1] 删除重复相册 {}(保留 {})", id, keep);
|
||||
}
|
||||
}
|
||||
if total_deleted == 0 {
|
||||
println!("[管线·PhaseZ1] 无重复相册");
|
||||
} else {
|
||||
println!("[管线·PhaseZ1] 去重完成:删除 {} 个重复相册", total_deleted);
|
||||
}
|
||||
total_deleted
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════
|
||||
// Pipeline 一键执行
|
||||
// ════════════════════════════════════════════════
|
||||
@@ -262,16 +303,26 @@ pub struct PipelineResult {
|
||||
pub persons_merged: usize, // Phase ④
|
||||
pub aliases_moved: usize, // Phase ④
|
||||
pub tags_removed_person: usize, // Phase ⑤
|
||||
pub empty_albums_deleted: usize, // Phase Z0
|
||||
pub dup_albums_deleted: usize, // Phase Z1
|
||||
}
|
||||
|
||||
/// 一键执行精简管线(5 阶段,2-3 次全库扫描)
|
||||
/// 一键执行精简管线
|
||||
///
|
||||
/// Phase Z0 删除空相册
|
||||
/// Phase Z1 去重同名相册
|
||||
/// Phase ① 合并预处理(清洗+短标签+人名标签+新相册) → 1次相册扫描
|
||||
/// Phase ② 合并人物清理(非人物+停用词人物) → 1次人物扫描
|
||||
/// Phase ③ 增量解析脏集 → 只扫脏集
|
||||
/// Phase ④ 人物合并 + 重计 → 1次人物扫描
|
||||
/// Phase ⑤ 后校验标签中的人名
|
||||
pub async fn run_pipeline(db: &Database) -> PipelineResult {
|
||||
// Phase Z0:删除空相册
|
||||
let empty_albums_deleted = phase_delete_empty_albums(db).await;
|
||||
|
||||
// Phase Z1:去重同名相册
|
||||
let dup_albums_deleted = phase_dedup_albums(db).await;
|
||||
|
||||
// Phase ①:合并预处理(1 次相册扫描)
|
||||
let pre = phase_preprocess_combined(db).await;
|
||||
let tags_cleaned = pre.tags_cleaned;
|
||||
@@ -305,6 +356,7 @@ pub async fn run_pipeline(db: &Database) -> PipelineResult {
|
||||
non_persons_deleted, stop_word_persons_removed, new_albums_found,
|
||||
persons_updated, tags_updated, persons_merged,
|
||||
aliases_moved, tags_removed_person,
|
||||
empty_albums_deleted, dup_albums_deleted,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,12 @@ pub async fn pipeline_run(
|
||||
"persons_merged": result.persons_merged,
|
||||
"aliases_moved": result.aliases_moved,
|
||||
"tags_removed_person": result.tags_removed_person,
|
||||
"empty_albums_deleted": result.empty_albums_deleted,
|
||||
"dup_albums_deleted": result.dup_albums_deleted,
|
||||
"search_db_synced": synced,
|
||||
"message": format!(
|
||||
"管线执行完毕:清洗标点{}相册,移除短标签{},检测人名标签{},脏集{}相册,删除非人物{},清除停用词重叠人物{},发现新相册{},更新人名{},更新标签{},合并人物{},迁移别名{},后移除人名标签{},同步标签/人物到search.db{}",
|
||||
"管线执行完毕:删除空相册{},去重{},清洗标点{}相册,移除短标签{},检测人名标签{},脏集{}相册,删除非人物{},清除停用词重叠人物{},发现新相册{},更新人名{},更新标签{},合并人物{},迁移别名{},后移除人名标签{},同步标签/人物到search.db{}",
|
||||
result.empty_albums_deleted, result.dup_albums_deleted,
|
||||
result.tags_cleaned, result.short_tags_removed, result.person_like_tags_removed, result.dirty_albums_detected,
|
||||
result.non_persons_deleted, result.stop_word_persons_removed, result.new_albums_found,
|
||||
result.persons_updated, result.tags_updated,
|
||||
|
||||
@@ -151,41 +151,10 @@ impl SearchDb {
|
||||
rows.iter().map(|r| TagStat {
|
||||
tag: r.get("name"),
|
||||
count: r.get("cnt"),
|
||||
album_count: 0,
|
||||
album_count: r.get("cnt"),
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// 获取标签倒排索引:tag → albums(缓存烘焙用)
|
||||
pub async fn get_all_tag_albums(&self) -> HashMap<String, Vec<AlbumInfo>> {
|
||||
let rows = sqlx::query(
|
||||
r#"SELECT t.name as tag_name,
|
||||
a.id, a.title, a.cover_image_id, a.image_count, a.created_at
|
||||
FROM tags t
|
||||
JOIN album_tags at ON t.id = at.tag_id
|
||||
JOIN albums a ON a.id = at.album_id
|
||||
ORDER BY a.created_at DESC"#
|
||||
).fetch_all(&self.pool).await.unwrap_or_default();
|
||||
|
||||
let mut map: HashMap<String, Vec<AlbumInfo>> = HashMap::new();
|
||||
for r in &rows {
|
||||
let tag_name: String = r.get("tag_name");
|
||||
let id_str: String = r.get("id");
|
||||
let album = AlbumInfo {
|
||||
id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::nil()),
|
||||
title: r.get("title"),
|
||||
document: String::new(),
|
||||
description: None,
|
||||
cover_image_id: r.get("cover_image_id"),
|
||||
image_count: r.get("image_count"),
|
||||
created_at: r.get("created_at"),
|
||||
tags: vec![],
|
||||
persons: vec![],
|
||||
};
|
||||
map.entry(tag_name).or_default().push(album);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// 按标签搜索相册
|
||||
pub async fn search_albums_by_tag(&self, tag: &str) -> Vec<AlbumInfo> {
|
||||
let rows = sqlx::query(
|
||||
|
||||
Reference in New Issue
Block a user