mod config; mod models; mod db; mod engine; mod cache; mod routes; mod services; mod image_processor; mod queue; mod search_db; use crate::cache::AlbumsCache; use crate::search_db::SearchDb; use actix_files as afs; use actix_web::{App, HttpResponse, HttpServer, middleware, web}; use std::env; use std::fs; use std::sync::Arc; use tokio::time::{Duration, interval}; use tracing::{error, info}; #[actix_web::main] async fn main() -> std::io::Result<()> { tracing_subscriber::fmt::init(); let args: Vec = env::args().collect(); let export_to_search = args.iter().any(|a| a == "--export-search"); let search_db_cli_path = args.iter().find(|a| a.starts_with("--search-db=")) .map(|s| s.strip_prefix("--search-db=").unwrap_or("search.db")) .unwrap_or("search.db"); let clear_search = args.iter().any(|a| a == "--clear-search"); let wal_checkpoint = args.iter().any(|a| a == "--wal-checkpoint"); // 加载配置 let config = match config::Config::load() { Ok(cfg) => cfg, Err(e) => { error!("加载配置失败: {}", e); std::process::exit(1); } }; info!("启动图片展示服务 v2.0..."); info!("服务地址: http://{}", config.server_address()); // 初始化数据库 let db = match db::Database::new(&config).await { Ok(db) => { info!("数据库连接成功"); let db = Arc::new(db); // 启动时先合并上次未完成的 WAL,确保数据库干净 info!("正在合并数据库 WAL..."); db.wal_checkpoint().await; db } Err(e) => { error!("数据库连接失败: {}", e); std::process::exit(1); } }; if export_to_search { info!("[搜索导出] 开始将元数据导出到 search.db..."); match services::search_export::export_to_search_db(&db, search_db_cli_path, clear_search).await { Ok(result) => { info!("[搜索导出] 导出完成: 相册{}, 标签{}, 人物{}, 别名{}, 关联{}", result.albums_processed, result.tags_created, result.persons_created, result.aliases_created, result.album_tags_created + result.album_persons_created); std::process::exit(0); } Err(e) => { error!("[搜索导出] 导出失败: {}", e); std::process::exit(1); } } } if wal_checkpoint { info!("[WAL合并] 主数据库 WAL checkpoint 已在初始化时完成"); info!("[WAL合并] 正在合并 search.db WAL..."); let search_db = SearchDb::new("data/search.db").await .expect("search.db 连接失败"); search_db.wal_checkpoint().await; info!("[WAL合并] 两个数据库均已合并完成"); std::process::exit(0); } let image_processor = Arc::new(image_processor::ImageProcessor::new(config.storage.clone())); // ── 初始化 search.db(运行时搜索专用)── let search_db_path = "data/search.db"; let search_db = Arc::new( SearchDb::new(search_db_path).await .expect("search.db 连接失败") ); info!("search.db 连接成功"); search_db.wal_checkpoint().await; // ── 从 search.db 的 dict 表加载标签词典 ── match engine::tag_dict::reload_from_search_db(&search_db).await { msg => info!("{}", msg), } // ── 内存缓存(必须在管线任务之前创建)── let albums_cache = Arc::new(tokio::sync::RwLock::new(AlbumsCache::new())); let persons_cache = Arc::new(tokio::sync::RwLock::new(cache::PersonCache::default())); let tags_cache = Arc::new(tokio::sync::RwLock::new(cache::TagCache::default())); // ── 启动管线(单次):导入 → 增量解析 → 条件全量 sync → 缓存刷新 ── // 不做全量 reparse/merge,只处理新发现的相册 let boot_config = config.clone(); let boot_db = Arc::clone(&db); let boot_search_db = Arc::clone(&search_db); let boot_albums_cache = Arc::clone(&albums_cache); let boot_persons_cache = Arc::clone(&persons_cache); let boot_tags_cache = Arc::clone(&tags_cache); tokio::spawn(async move { // ① 扫描导入新相册 info!("[启动] ① 扫描导入新相册..."); match services::import::import_existing_data(&boot_config, Arc::clone(&boot_db), None).await { Ok(_) => info!("[启动] 导入完成"), Err(e) => error!("[启动] 导入失败: {}", e), } // ② 清理:删除空相册 + 去重 info!("[启动] ② 清理空相册和重复相册..."); let empty = engine::pipeline::phase_delete_empty_albums(&boot_db).await; let dedup = engine::pipeline::phase_dedup_albums(&boot_db, &boot_search_db).await; if empty > 0 || dedup > 0 { info!("[启动] 清理完成:删除空相册 {},去重删除 {}", empty, dedup); } // ③ 查找新相册 / 半成品 info!("[启动] ③ 查找新相册..."); 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); let synced = engine::pipeline::sync_dirty_albums(&boot_db, &boot_search_db, &dirty).await; info!("[启动] 增量同步完成:{} 个相册(仅 tags/persons)", synced); } else { info!("[启动] 没有新相册"); } // ④ 刷新所有内存缓存(从 search.db 读取) info!("[启动] ④ 刷新所有内存缓存..."); cache::refresh_all_caches(&boot_db, &boot_search_db, &boot_albums_cache, &boot_persons_cache, &boot_tags_cache).await; info!("[启动] 启动管线执行完毕(新相册={})", new_count); }); // ── 定时管线(每 30 分钟):导入 → 增量解析 → 缓存刷新 ── // 不做全量 reparse/merge let timer_cache = Arc::clone(&albums_cache); let timer_db = Arc::clone(&db); let timer_search_db = Arc::clone(&search_db); let timer_persons_cache = Arc::clone(&persons_cache); let timer_tags_cache = Arc::clone(&tags_cache); tokio::spawn(async move { let mut interval = interval(Duration::from_secs(30 * 60)); loop { interval.tick().await; // ① 扫描导入新相册 info!("[定时] ① 扫描导入新相册..."); match services::import::import_existing_data(&timer_db.config, Arc::clone(&timer_db), None).await { Ok(_) => {}, Err(e) => error!("[定时] 导入失败: {}", e), } // ② 查找新相册 let new_albums = engine::pipeline::phase_collect_new_albums(&timer_db).await; if new_albums.is_empty() { continue; // 无新增,跳过 } 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; } }); // 任务队列 let task_queue = Arc::new(queue::TaskQueue::new( config.queue.max_concurrent_tasks, config.queue.queue_size_limit, config.queue.high_request_rate_threshold, config.queue.idle_request_rate_threshold, )); println!("API接口:"); println!(" - GET /api/albums - 获取所有相册"); println!(" - GET /api/search?q=关键词 - 搜索相册"); println!(" - GET /api/album/random - 随机相册"); println!(" - GET /api/album/:id - 获取相册详情"); println!(" - POST /api/albums - 创建新相册"); println!(" - GET /api/image/:id - 获取图片"); println!(" - GET /api/favorites - 收藏列表"); println!(" - POST /api/favorites - 添加收藏"); println!(" - DELETE /api/favorites/:id - 取消收藏"); println!("元数据接口:"); println!(" - GET /api/people - 人员列表"); println!(" - GET /api/people/search?q=关键词 - 搜索人员"); println!(" - GET /api/people/{{name}} - 人员详情"); println!(" - GET /api/people/{{id}}/aliases - 人员别名"); println!(" - GET /api/metadata/stats - 元数据统计"); println!(" - GET /api/tags/search?tag=标签 - 按标签搜索"); println!(" - GET /api/tags/popular?limit=50 - 热门标签"); println!("推荐接口:"); println!(" - GET /api/recommend/tags?q=xxx - 标签推荐"); println!(" - GET /api/recommend/persons?q=xxx - 人物推荐"); println!(" - GET /api/recommend/related?context=xxx - 关联推荐"); println!(" - GET /api/recommend/popular-persons - 热门人物"); println!("迁移接口:"); println!(" - GET /api/migration/stats - 迁移状态"); println!(" - POST /api/migration/run?path=persons.db - 执行迁移"); println!(" - GET /api/queue/stats - 队列状态"); println!("词典管理:"); println!(" - GET /api/dict/stats - 词典状态"); println!(" - POST /api/dict/reload - 热重载词典"); println!("按 Ctrl+C 停止服务"); fs::create_dir_all(&config.storage.images_dir).unwrap_or(()); // 在 move 闭包之前 clone 一份 db 用于关闭信号处理 let shutdown_db = Arc::clone(&db); let server = HttpServer::new(move || { App::new() .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) .app_data(web::Data::new(Arc::clone(&db))) .app_data(web::Data::new(Arc::clone(&search_db))) .app_data(web::Data::new(Arc::clone(&image_processor))) .app_data(web::Data::new(Arc::clone(&albums_cache))) .app_data(web::Data::new(Arc::clone(&persons_cache))) .app_data(web::Data::new(Arc::clone(&tags_cache))) .app_data(web::Data::new(Arc::clone(&task_queue))) // 相册 API .route("/api/albums", web::get().to(routes::get_albums)) .route("/api/search", web::get().to(routes::search_albums)) .route("/api/album/random", web::get().to(routes::get_random_album)) .route("/api/album/{id}", web::get().to(routes::get_album_detail)) .route("/api/albums", web::post().to(routes::create_album)) .route("/api/image/{id}", web::get().to(routes::get_image)) // 收藏 API .route("/api/favorites", web::get().to(routes::get_favorites)) .route("/api/favorites/status/{id}", web::get().to(routes::check_favorite_status)) .route("/api/favorites", web::post().to(routes::add_favorite)) .route("/api/favorites/{id}", web::delete().to(routes::remove_favorite)) .route("/api/favorites/image/{id}", web::delete().to(routes::remove_favorite_by_image)) // 人员 API .route("/api/people", web::get().to(routes::get_people)) .route("/api/people/search", web::get().to(routes::search_people)) .route("/api/people/{name}", web::get().to(routes::get_person_detail)) .route("/api/people/{id}/aliases", web::get().to(routes::get_person_aliases)) // 标签 API .route("/api/metadata/stats", web::get().to(routes::get_metadata_stats)) .route("/api/tags/search", web::get().to(routes::search_by_tag)) .route("/api/tags/popular", web::get().to(routes::get_popular_tags)) // 推荐 API .route("/api/recommend/tags", web::get().to(routes::recommend_tags)) .route("/api/recommend/persons", web::get().to(routes::recommend_persons)) .route("/api/recommend/related", web::get().to(routes::recommend_related)) .route("/api/recommend/popular-persons", web::get().to(routes::popular_persons)) // 迁移 API .route("/api/migration/stats", web::get().to(routes::migration_stats)) .route("/api/migration/run", web::post().to(routes::migration_run)) // 管线 API .route("/api/pipeline/run", web::post().to(routes::pipeline_run)) // 队列 API .route("/api/queue/stats", web::get().to(routes::get_queue_stats)) // 词典 API .route("/api/dict/stats", web::get().to(routes::dict_stats)) .route("/api/dict/reload", web::post().to(routes::dict_reload)) // 静态文件 .service( afs::Files::new("/assets", "./assets") .show_files_listing() .prefer_utf8(true) .use_last_modified(true) .use_etag(true), ) .route("/", web::get().to(|| async { match fs::read_to_string("./assets/index.html") { Ok(content) => HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(content), Err(_) => HttpResponse::NotFound().body("首页文件不存在"), } })) .route("/album", web::get().to(|| async { match fs::read_to_string("./assets/album.html") { Ok(content) => HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(content), Err(_) => HttpResponse::NotFound().body("相册页面不存在"), } })) }) .bind(config.server_address())? .run(); // ── 信号监听:捕获 Ctrl+C / SIGINT,优雅关闭 ── let server_handle = server.handle(); tokio::spawn(async move { tokio::signal::ctrl_c().await.expect("监听关闭信号失败"); println!("\n[关闭] 收到关闭信号,正在优雅停止服务器..."); // 停止接受新请求,等待正在处理中的请求完成(最多 10s) server_handle.stop(true).await; // 服务器已停止 → 合并 WAL println!("[关闭] 服务已停止,正在合并数据库 WAL..."); shutdown_db.wal_checkpoint().await; println!("[关闭] 数据库文件已合并,再见!"); }); server.await }