update code level
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
src/main.rs
28
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")
|
||||
|
||||
@@ -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
27
src/routes/cache.rs
Normal 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": "缓存已刷新"}))
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 — 从缓存按标签搜索相册
|
||||
|
||||
Reference in New Issue
Block a user