update code level

This commit is contained in:
lychang
2026-07-21 02:40:41 +08:00
parent cff2eb2886
commit 12880dd480
8 changed files with 128 additions and 76 deletions

View File

@@ -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,
};

View File

@@ -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<RecommendItem> = 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<RecommendItem> = 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<RecommendItem> = 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<String, (i64, f64)> = 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<PopularPerson> {
/// 热门人物(从缓存的人物列表过滤,top_tags 保留 search.db 查询)
pub async fn popular_persons(
search_db: &SearchDb,
persons_cache: &[PersonWithCounts],
limit: usize,
) -> Vec<PopularPerson> {
let dict = tag_dict::get_tag_dict();
let persons = search_db.get_all_persons_with_aliases().await;
// 收集所有标签分类词(非人名候选)
let all_tag_words: std::collections::HashSet<String> = 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
}
}
}

View File

@@ -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")

View File

@@ -81,22 +81,26 @@ pub async fn get_random_album(cache: web::Data<Arc<RwLock<AlbumsCache>>>) -> imp
pub async fn get_album_detail(
db: web::Data<Arc<Database>>,
search_db: web::Data<Arc<SearchDb>>,
cache: web::Data<Arc<RwLock<AlbumsCache>>>,
path: web::Path<String>,
) -> 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<String> = 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<String> = 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": "相册不存在"})),

27
src/routes/cache.rs Normal file
View File

@@ -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<Arc<Database>>,
search_db: web::Data<Arc<SearchDb>>,
albums_cache: web::Data<Arc<RwLock<AlbumsCache>>>,
persons_cache: web::Data<Arc<RwLock<PersonCache>>>,
tags_cache: web::Data<Arc<RwLock<TagCache>>>,
) -> 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": "缓存已刷新"}))
}

View File

@@ -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::*;

View File

@@ -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<Arc<SearchDb>>, query: web::Query<HashMap<String, String>>) -> impl Responder {
/// 标签推荐(自动补全)— 从 TagCache 匹配
pub async fn recommend_tags(
cache: web::Data<Arc<RwLock<TagCache>>>,
query: web::Query<HashMap<String, String>>,
) -> 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<Arc<SearchDb>>, query: web::Query<HashMap<String, String>>) -> impl Responder {
/// 人物推荐(自动补全)— 从 PersonCache 匹配
pub async fn recommend_persons(
cache: web::Data<Arc<RwLock<PersonCache>>>,
query: web::Query<HashMap<String, String>>,
) -> 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<Arc<SearchDb>>, query: web::Query<HashMap<String, String>>) -> impl Responder {
pub async fn recommend_related(
search_db: web::Data<Arc<SearchDb>>,
query: web::Query<HashMap<String, String>>,
) -> 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<Arc<SearchDb>>, query: web::
HttpResponse::Ok().json(RecommendEngine::recommend_related(&search_db, context, limit).await)
}
/// 热门人物
pub async fn popular_persons(search_db: web::Data<Arc<SearchDb>>, query: web::Query<HashMap<String, String>>) -> impl Responder {
/// 热门人物 — 从 PersonCache 过滤,top_tags 保留 search.db 查询
pub async fn popular_persons(
search_db: web::Data<Arc<SearchDb>>,
cache: web::Data<Arc<RwLock<PersonCache>>>,
query: web::Query<HashMap<String, String>>,
) -> 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)
}
let c = cache.read().await;
HttpResponse::Ok().json(RecommendEngine::popular_persons(&search_db, &c.persons, limit).await)
}

View File

@@ -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<Arc<SearchDb>>) -> 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<Arc<RwLock<AlbumsCache>>>,
persons_cache: web::Data<Arc<RwLock<PersonCache>>>,
tags_cache: web::Data<Arc<RwLock<TagCache>>>,
) -> 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 — 从缓存按标签搜索相册