From 12880dd480bec8fbbaf19aceab7adf395ad35bd6 Mon Sep 17 00:00:00 2001 From: lychang <632945494@qq.com> Date: Tue, 21 Jul 2026 02:40:41 +0800 Subject: [PATCH] update code level --- src/engine/pipeline.rs | 12 +++++------ src/engine/recommend.rs | 47 +++++++++++++++-------------------------- src/main.rs | 28 ++++++++++++------------ src/routes/album.rs | 24 ++++++++++++--------- src/routes/cache.rs | 27 +++++++++++++++++++++++ src/routes/mod.rs | 2 ++ src/routes/recommend.rs | 40 +++++++++++++++++++++++++---------- src/routes/tag.rs | 24 ++++++++++++++++----- 8 files changed, 128 insertions(+), 76 deletions(-) create mode 100644 src/routes/cache.rs diff --git a/src/engine/pipeline.rs b/src/engine/pipeline.rs index 7da26b0..b12e74f 100644 --- a/src/engine/pipeline.rs +++ b/src/engine/pipeline.rs @@ -449,11 +449,11 @@ pub async fn sync_albums_to_search_db(db: &Database, search_db: &SearchDb) -> us synced += 1; } - // ── 3. 更新 person counts ── + // ── 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 person_albums WHERE person_id = ?" - ).bind(*person_id).fetch_one(&db.pool).await { + "SELECT COUNT(*) FROM album_persons WHERE person_id = ?" + ).bind(*person_id).fetch_one(&search_db.pool).await { Ok(c) => c, Err(_) => continue, }; @@ -537,11 +537,11 @@ pub async fn sync_dirty_albums(db: &Database, search_db: &SearchDb, dirty: &Dirt synced += 1; } - // ── 3. 更新 person counts ── + // ── 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 person_albums WHERE person_id = ?" - ).bind(*person_id).fetch_one(&db.pool).await { + "SELECT COUNT(*) FROM album_persons WHERE person_id = ?" + ).bind(*person_id).fetch_one(&search_db.pool).await { Ok(c) => c, Err(_) => continue, }; diff --git a/src/engine/recommend.rs b/src/engine/recommend.rs index 576b447..4567d32 100644 --- a/src/engine/recommend.rs +++ b/src/engine/recommend.rs @@ -14,31 +14,26 @@ use sqlx::Row; pub struct RecommendEngine; impl RecommendEngine { - /// 标签自动补全推荐 - pub async fn recommend_tags( - search_db: &SearchDb, + /// 标签自动补全推荐(从缓存的热门标签列表匹配) + pub fn recommend_tags( + popular_tags: &[TagStat], query: &str, limit: usize, ) -> RecommendResponse { let limit = limit.min(20); let q = query.trim().to_lowercase(); - let tags = search_db.get_popular_tags(200).await; let mut items: Vec = Vec::new(); - for t in &tags { + for t in popular_tags { let tag_lower = t.tag.to_lowercase(); let score = if q.is_empty() { - // 无查询时按热门度 t.count as f64 } else if tag_lower == q { - // 精确匹配 1000.0 + t.count as f64 } else if tag_lower.starts_with(&q) { - // 前缀匹配 500.0 + t.count as f64 } else if tag_lower.contains(&q) { - // 包含匹配 100.0 + t.count as f64 } else { continue; @@ -60,19 +55,18 @@ impl RecommendEngine { RecommendResponse { items, total, category: "tags".to_string() } } - /// 人物推荐 - pub async fn recommend_persons( - search_db: &SearchDb, + /// 人物推荐(从缓存的人物列表匹配) + pub fn recommend_persons( + persons: &[PersonWithCounts], query: &str, limit: usize, ) -> RecommendResponse { let limit = limit.min(20); let q = query.trim().to_lowercase(); - let persons = search_db.get_all_persons_with_aliases().await; let mut items: Vec = Vec::new(); - for p in &persons { + for p in persons { let name_lower = p.name.to_lowercase(); let score = if q.is_empty() { p.total_albums as f64 @@ -83,7 +77,6 @@ impl RecommendEngine { } else if name_lower.contains(&q) { 100.0 + p.total_albums as f64 } else { - // 检查别名 let alias_match = p.aliases.iter().any(|a| a.to_lowercase().contains(&q)); if alias_match { 200.0 + p.total_albums as f64 } else { continue } }; @@ -113,12 +106,11 @@ impl RecommendEngine { /// 关联推荐(共现标签 + 共现人物) pub async fn recommend_related( search_db: &SearchDb, - context: &str, // 标签或人名 + context: &str, limit: usize, ) -> TagRecommendation { let limit = limit.min(15); - // 获取共现标签 let co_tags = search_db.get_tag_co_occurrences(context, limit as i64).await; let related_tags: Vec = co_tags.iter().map(|c| { RecommendItem { @@ -130,13 +122,11 @@ impl RecommendEngine { } }).collect(); - // 获取相关人物(通过标签) let albums = search_db.search_albums_by_tag(context).await; let mut person_scores: std::collections::HashMap = std::collections::HashMap::new(); let total = albums.len() as f64; for album in &albums { let aid = album.id.to_string(); - // 从 search.db 查该相册的人物 let person_rows = sqlx::query( r#"SELECT p.name FROM persons p JOIN album_persons ap ON p.id = ap.person_id @@ -169,25 +159,23 @@ impl RecommendEngine { } } - /// 热门人物 - pub async fn popular_persons(search_db: &SearchDb, limit: usize) -> Vec { + /// 热门人物(从缓存的人物列表过滤,top_tags 保留 search.db 查询) + pub async fn popular_persons( + search_db: &SearchDb, + persons_cache: &[PersonWithCounts], + limit: usize, + ) -> Vec { let dict = tag_dict::get_tag_dict(); - let persons = search_db.get_all_persons_with_aliases().await; - // 收集所有标签分类词(非人名候选) let all_tag_words: std::collections::HashSet = dict.tag_to_category.keys() .map(|k| k.to_lowercase()).collect(); - // 过滤非人物:三重检查(excluded + is_likely_person_name + 非标签词) - let filtered: Vec<_> = persons.into_iter() + let filtered: Vec<_> = persons_cache.iter() .filter(|p| { - // 1. 跳过排除名单中的人物 if dict.is_excluded_person(&p.name) { return false; } - // 2. 只保留 is_likely_person_name 返回 true 的人名 if !crate::engine::title_parser::is_likely_person_name(&p.name) { return false; } - // 3. 跳过和标签分类词重叠的非人名(如 "女模特"、"内衣秀") let name_lower = p.name.to_lowercase(); if all_tag_words.contains(&name_lower) { return false; @@ -207,7 +195,6 @@ impl RecommendEngine { } }).collect(); - // 为热门人物获取 top tags(从 search.db) 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; @@ -216,4 +203,4 @@ impl RecommendEngine { } results } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index bcff9d9..cc913f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -148,8 +148,7 @@ async fn main() -> std::io::Result<()> { info!("[启动] 启动管线执行完毕(新相册={})", new_count); }); - // ── 定时管线(每 30 分钟):导入 → 增量解析 → 缓存刷新 ── - // 不做全量 reparse/merge + // ── 定时管线(每 30 分钟):导入 → 增量解析(有新相册时)→ 缓存刷新(始终执行)── let timer_cache = Arc::clone(&albums_cache); let timer_db = Arc::clone(&db); let timer_search_db = Arc::clone(&search_db); @@ -167,19 +166,16 @@ async fn main() -> std::io::Result<()> { Err(e) => error!("[定时] 导入失败: {}", e), } - // ② 查找新相册 - let new_albums = engine::pipeline::phase_collect_new_albums(&timer_db).await; - if new_albums.is_empty() { - continue; // 无新增,跳过 - } + // ② 查找新相册并增量解析 + let new_albums = engine::pipeline::phase_collect_new_albums(&timer_db).await; + if !new_albums.is_empty() { + info!("[定时] 发现 {} 个新相册,增量解析...", new_albums.len()); + engine::pipeline::phase_parse_dirty(&timer_db, &new_albums).await; + info!("[定时] 同步到 search.db..."); + engine::pipeline::sync_dirty_albums(&timer_db, &timer_search_db, &new_albums).await; + } - info!("[定时] 发现 {} 个新相册,增量解析...", new_albums.len()); - engine::pipeline::phase_parse_dirty(&timer_db, &new_albums).await; - - info!("[定时] 同步到 search.db..."); - engine::pipeline::sync_dirty_albums(&timer_db, &timer_search_db, &new_albums).await; - - // ③ 刷新缓存 + // ③ 缓存刷新(始终执行) info!("[定时] 刷新缓存..."); cache::refresh_all_caches(&timer_db, &timer_search_db, &timer_cache, &timer_persons_cache, &timer_tags_cache).await; } @@ -223,6 +219,8 @@ async fn main() -> std::io::Result<()> { println!("词典管理:"); println!(" - GET /api/dict/stats - 词典状态"); println!(" - POST /api/dict/reload - 热重载词典"); + println!("缓存管理:"); + println!(" - POST /api/cache/refresh - 手动刷新内存缓存"); println!("按 Ctrl+C 停止服务"); fs::create_dir_all(&config.storage.images_dir).unwrap_or(()); @@ -277,6 +275,8 @@ async fn main() -> std::io::Result<()> { // 词典 API .route("/api/dict/stats", web::get().to(routes::dict_stats)) .route("/api/dict/reload", web::post().to(routes::dict_reload)) + // 缓存管理 API + .route("/api/cache/refresh", web::post().to(routes::cache_refresh)) // 静态文件 .service( afs::Files::new("/assets", "./assets") diff --git a/src/routes/album.rs b/src/routes/album.rs index 6dad693..b313024 100644 --- a/src/routes/album.rs +++ b/src/routes/album.rs @@ -81,22 +81,26 @@ pub async fn get_random_album(cache: web::Data>>) -> imp pub async fn get_album_detail( db: web::Data>, - search_db: web::Data>, + cache: web::Data>>, path: web::Path, ) -> impl Responder { let id = match Uuid::parse_str(&path.into_inner()) { Ok(id) => id, Err(_) => return HttpResponse::BadRequest().json(serde_json::json!({"error": "无效ID"})) }; let id_str = id.to_string(); + + // 从内存缓存查 tags/persons + let (tags, persons) = { + let c = cache.read().await; + let found = c.albums.iter().find(|a| a.id == id); + match found { + Some(a) => (a.tags.clone(), a.persons.clone()), + None => (vec![], vec![]), + } + }; + match db.get_album_with_images(id).await { Ok(Some(mut album)) => { - // 从 search.db 查询该相册的 tags 和 persons - let tag_rows: Vec = sqlx::query_scalar( - r#"SELECT t.name FROM album_tags at JOIN tags t ON at.tag_id = t.id WHERE at.album_id = ?"# - ).bind(&id_str).fetch_all(&search_db.pool).await.unwrap_or_default(); - let person_rows: Vec = sqlx::query_scalar( - r#"SELECT p.name FROM album_persons ap JOIN persons p ON ap.person_id = p.id WHERE ap.album_id = ?"# - ).bind(&id_str).fetch_all(&search_db.pool).await.unwrap_or_default(); - album.tags = tag_rows; - album.persons = person_rows; + album.tags = tags; + album.persons = persons; HttpResponse::Ok().json(album) } Ok(None) => HttpResponse::NotFound().json(serde_json::json!({"error": "相册不存在"})), diff --git a/src/routes/cache.rs b/src/routes/cache.rs new file mode 100644 index 0000000..5274dfa --- /dev/null +++ b/src/routes/cache.rs @@ -0,0 +1,27 @@ +use crate::cache; +use crate::db::Database; +use crate::search_db::SearchDb; +use crate::cache::{AlbumsCache, TagCache, PersonCache}; +use actix_web::{HttpResponse, Responder, web}; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::info; + +/// POST /api/cache/refresh — 手动刷新所有内存缓存 +pub async fn cache_refresh( + db: web::Data>, + search_db: web::Data>, + albums_cache: web::Data>>, + persons_cache: web::Data>>, + tags_cache: web::Data>>, +) -> impl Responder { + info!("[API] 手动触发缓存刷新..."); + cache::refresh_all_caches( + db.get_ref(), + search_db.get_ref(), + albums_cache.get_ref(), + persons_cache.get_ref(), + tags_cache.get_ref(), + ).await; + HttpResponse::Ok().json(serde_json::json!({"success": true, "message": "缓存已刷新"})) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index b07fcbb..055be78 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,6 +1,7 @@ //! API 路由层 — 按领域拆分 pub mod album; +pub mod cache; pub mod image; pub mod favorite; pub mod person; @@ -12,6 +13,7 @@ pub mod pipeline; pub mod dict; pub use album::*; +pub use cache::*; pub use image::*; pub use favorite::*; pub use person::*; diff --git a/src/routes/recommend.rs b/src/routes/recommend.rs index ff9c667..60d4f2e 100644 --- a/src/routes/recommend.rs +++ b/src/routes/recommend.rs @@ -1,26 +1,39 @@ +use crate::cache::{TagCache, PersonCache}; use crate::search_db::SearchDb; use crate::engine::RecommendEngine; use crate::models::*; use actix_web::{HttpResponse, Responder, web}; use std::collections::HashMap; use std::sync::Arc; +use tokio::sync::RwLock; -/// 标签推荐(自动补全) -pub async fn recommend_tags(search_db: web::Data>, query: web::Query>) -> impl Responder { +/// 标签推荐(自动补全)— 从 TagCache 匹配 +pub async fn recommend_tags( + cache: web::Data>>, + query: web::Query>, +) -> impl Responder { let q = query.get("q").map(|s| s.as_str()).unwrap_or(""); let limit: usize = query.get("limit").and_then(|s| s.parse().ok()).unwrap_or(10); - HttpResponse::Ok().json(RecommendEngine::recommend_tags(&search_db, q, limit).await) + let c = cache.read().await; + HttpResponse::Ok().json(RecommendEngine::recommend_tags(&c.popular_tags, q, limit)) } -/// 人物推荐(自动补全) -pub async fn recommend_persons(search_db: web::Data>, query: web::Query>) -> impl Responder { +/// 人物推荐(自动补全)— 从 PersonCache 匹配 +pub async fn recommend_persons( + cache: web::Data>>, + query: web::Query>, +) -> impl Responder { let q = query.get("q").map(|s| s.as_str()).unwrap_or(""); let limit: usize = query.get("limit").and_then(|s| s.parse().ok()).unwrap_or(10); - HttpResponse::Ok().json(RecommendEngine::recommend_persons(&search_db, q, limit).await) + let c = cache.read().await; + HttpResponse::Ok().json(RecommendEngine::recommend_persons(&c.persons, q, limit)) } /// 关联推荐(标签共现 + 共现人物) -pub async fn recommend_related(search_db: web::Data>, query: web::Query>) -> impl Responder { +pub async fn recommend_related( + search_db: web::Data>, + query: web::Query>, +) -> impl Responder { let context = match query.get("context") { Some(c) if !c.is_empty() => c, _ => return HttpResponse::BadRequest().json(serde_json::json!({"error": "请提供context参数"})), @@ -29,8 +42,13 @@ pub async fn recommend_related(search_db: web::Data>, query: web:: HttpResponse::Ok().json(RecommendEngine::recommend_related(&search_db, context, limit).await) } -/// 热门人物 -pub async fn popular_persons(search_db: web::Data>, query: web::Query>) -> impl Responder { +/// 热门人物 — 从 PersonCache 过滤,top_tags 保留 search.db 查询 +pub async fn popular_persons( + search_db: web::Data>, + cache: web::Data>>, + query: web::Query>, +) -> impl Responder { let limit: usize = query.get("limit").and_then(|s| s.parse().ok()).unwrap_or(20).min(50); - HttpResponse::Ok().json(RecommendEngine::popular_persons(&search_db, limit).await) -} \ No newline at end of file + let c = cache.read().await; + HttpResponse::Ok().json(RecommendEngine::popular_persons(&search_db, &c.persons, limit).await) +} diff --git a/src/routes/tag.rs b/src/routes/tag.rs index 158f857..7653a5e 100644 --- a/src/routes/tag.rs +++ b/src/routes/tag.rs @@ -1,14 +1,28 @@ -use crate::cache::TagCache; -use crate::search_db::SearchDb; +use crate::cache::{TagCache, AlbumsCache, PersonCache}; use crate::models::*; use actix_web::{HttpResponse, Responder, web}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -/// GET /api/metadata/stats — 从 search.db 获取统计数据 -pub async fn get_metadata_stats(search_db: web::Data>) -> impl Responder { - HttpResponse::Ok().json(search_db.get_metadata_stats().await) +/// GET /api/metadata/stats — 从内存缓存计算统计数据 +pub async fn get_metadata_stats( + albums_cache: web::Data>>, + persons_cache: web::Data>>, + tags_cache: web::Data>>, +) -> impl Responder { + let ac = albums_cache.read().await; + let pc = persons_cache.read().await; + let tc = tags_cache.read().await; + + let stats = MetadataStats { + total_persons: pc.persons.len() as i64, + total_aliases: pc.persons.iter().map(|p| p.aliases.len() as i64).sum(), + total_albums: ac.albums.len() as i64, + total_images: ac.albums.iter().map(|a| a.image_count).sum(), + total_tags: tc.tag_albums.len() as i64, + }; + HttpResponse::Ok().json(stats) } /// GET /api/tags/search?tag=xxx — 从缓存按标签搜索相册