diff --git a/Cargo.lock b/Cargo.lock index 972a352..7aff252 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1237,6 +1237,8 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "tracing", + "tracing-subscriber", "uuid", ] @@ -1483,6 +1485,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -2132,6 +2143,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2534,6 +2554,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.45" @@ -2697,6 +2726,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2816,6 +2871,12 @@ version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index f77feef..bc3ae18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,6 @@ uuid = { version = "1.0", features = ["v4", "serde"] } image = { version = "0.24", default-features = false, features = ["jpeg", "png", "gif", "webp"] } reqwest = { version = "0.11", default-features = false, features = ["blocking","rustls-tls"] } config = "0.13" -rand = "0.8" \ No newline at end of file +rand = "0.8" +tracing = "0.1" +tracing-subscriber = "0.3" \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css index b60958b..3ed9141 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1247,9 +1247,43 @@ body { .search-hint h3 { color: var(--text-dark); margin-bottom: 15px; - font-size: 1.5rem; } +/* 加载状态样式 */ +.loading-state { + text-align: center; + padding: 60px 20px; + color: var(--text-light); + font-size: 1.2rem; + max-width: 400px; + margin: 0 auto; +} + +.loading-state .fa-spinner { + font-size: 3rem; + margin-bottom: 20px; + color: var(--secondary-blue); +} + +.loading-state p { + margin-bottom: 20px; + color: var(--text-dark); +} + +.progress-bar { + width: 100%; + height: 6px; + background-color: var(--light-gray); + border-radius: 3px; + overflow: hidden; +} + +.progress { + height: 100%; + background: linear-gradient(135deg, var(--secondary-blue) 0%, var(--primary-blue) 100%); + border-radius: 3px; + transition: width 0.3s ease;} + .search-hint p { margin-bottom: 15px; font-weight: 500; diff --git a/assets/js/app.js b/assets/js/app.js index 5610c8b..2cd9884 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -37,7 +37,12 @@ function initSearch() { // 防抖搜索 searchInput.addEventListener('input', function () { clearTimeout(searchTimeout); - searchTimeout = setTimeout(debouncedSearch, 300); + + // 优化:根据输入长度动态调整延迟 + const inputLength = this.value.length; + const debounceDelay = inputLength > 3 ? 200 : 500; // 长文本快速搜索,短文本慢速搜索 + + searchTimeout = setTimeout(debouncedSearch, debounceDelay); }); // 移动端优化:搜索框聚焦时自动滚动到可视区域 @@ -128,6 +133,22 @@ function showSkeleton(count = 6) { `).join(''); } +// 显示详细加载状态 +function showLoadingState(message = '加载中...', progress = 0) { + const container = document.getElementById('albums-container'); + const progressWidth = Math.min(Math.max(progress, 0), 100); + + container.innerHTML = ` +
+ +

${message}

+
+
+
+
+ `; +} + // 显示加载更多指示器 function showLoadingMore() { const container = document.getElementById('albums-container'); @@ -398,8 +419,8 @@ function initLazyLoading() { } }); }, { - rootMargin: '100px 0px', // 提前100px开始加载 - threshold: 0.01 + rootMargin: '50px', // 优化:提前50px开始加载 + threshold: 0.1 // 优化:降低触发阈值 }); } @@ -456,17 +477,30 @@ async function searchAlbums() { // 性能监控 function logPerformance() { if ('performance' in window) { - window.addEventListener('load', () => { - setTimeout(() => { - const navigationTiming = performance.getEntriesByType('navigation')[0]; - if (navigationTiming && navigationTiming.loadEventEnd && navigationTiming.navigationStart) { - const loadTime = navigationTiming.loadEventEnd - navigationTiming.navigationStart; - console.log('页面加载时间:', loadTime, 'ms'); + // 使用更可靠的方式获取页面加载时间 + const calculateLoadTime = () => { + const navigationTiming = performance.getEntriesByType('navigation')[0]; + if (navigationTiming && navigationTiming.loadEventEnd > 0 && navigationTiming.navigationStart > 0) { + const loadTime = navigationTiming.loadEventEnd - navigationTiming.navigationStart; + console.log('页面加载时间:', loadTime, 'ms'); + } else { + // 备用方案:使用DOMContentLoaded时间 + const domContentLoadedTime = performance.timing?.domContentLoadedEventEnd - performance.timing?.navigationStart; + if (domContentLoadedTime && domContentLoadedTime > 0) { + console.log('DOM加载时间:', domContentLoadedTime, 'ms'); } else { console.log('页面加载时间: 无法计算'); } - }, 0); - }); + } + }; + + // 如果页面已经加载完成,直接计算 + if (document.readyState === 'complete') { + calculateLoadTime(); + } else { + // 否则等待页面加载完成 + window.addEventListener('load', calculateLoadTime); + } } } @@ -486,18 +520,18 @@ function preventDoubleTapZoom() { // 预加载关键资源 function preloadCriticalResources() { - // 预加载关键CSS和字体 - const criticalResources = [ - '/assets/css/font-awesome.css', - '/assets/css/style.css', - '/assets/webfonts/fa-solid-900.woff2' + // 预加载字体文件(CSS文件不应该预加载,因为已经在HTML中引用了) + const fontResources = [ + '/assets/webfonts/fa-solid-900.woff2', + '/assets/webfonts/fa-brands-400.woff2', + '/assets/webfonts/fa-regular-400.woff2' ]; - criticalResources.forEach(resource => { + fontResources.forEach(resource => { const link = document.createElement('link'); link.rel = 'preload'; link.href = resource; - link.as = resource.endsWith('.css') ? 'style' : 'font'; + link.as = 'font'; link.crossOrigin = 'anonymous'; document.head.appendChild(link); }); diff --git a/src/database.rs b/src/database.rs index 75333db..1ceaa1c 100644 --- a/src/database.rs +++ b/src/database.rs @@ -33,11 +33,11 @@ impl Database { } let pool = SqlitePoolOptions::new() - .max_connections(20) // 增加连接数以支持并发请求 - .min_connections(5) // 保持更多连接以减少连接建立时间 - .acquire_timeout(std::time::Duration::from_secs(10)) // 减少获取连接超时时间 - .idle_timeout(std::time::Duration::from_secs(600)) // 增加空闲超时时间 - .max_lifetime(std::time::Duration::from_secs(3600)) // 增加最大生命周期 + .max_connections(10) // 优化:减少最大连接数,避免资源浪费 + .min_connections(2) // 优化:减少最小连接数 + .acquire_timeout(std::time::Duration::from_secs(5)) // 优化:减少获取连接超时时间 + .idle_timeout(std::time::Duration::from_secs(300)) // 优化:减少空闲超时时间 + .max_lifetime(std::time::Duration::from_secs(1800)) // 优化:减少最大生命周期 .connect(url) .await?; diff --git a/src/main.rs b/src/main.rs index cc7203c..eb51eb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,30 +10,34 @@ use actix_web::{App, HttpResponse, HttpServer, web, middleware}; use std::fs; use std::sync::Arc; use tokio::time::{interval, Duration}; +use tracing::{info, error, warn, debug}; use crate::models::AlbumsCache; #[actix_web::main] async fn main() -> std::io::Result<()> { + // 初始化结构化日志 + tracing_subscriber::fmt::init(); + // 加载配置 let config = match config::Config::load() { Ok(cfg) => cfg, Err(e) => { - eprintln!("加载配置失败: {}", e); + error!("加载配置失败: {}", e); std::process::exit(1); } }; - println!("启动图片展示服务..."); - println!("服务地址: http://{}", config.server_address()); + info!("启动图片展示服务..."); + info!("服务地址: http://{}", config.server_address()); // 初始化数据库 let db = match database::Database::new(&config).await { Ok(db) => { - println!("数据库连接成功"); + info!("数据库连接成功"); Arc::new(db) } Err(e) => { - eprintln!("数据库连接失败: {}", e); + error!("数据库连接失败: {}", e); std::process::exit(1); } }; @@ -42,10 +46,10 @@ async fn main() -> std::io::Result<()> { let config_clone = config.clone(); let db_clone = Arc::clone(&db); tokio::spawn(async move { - println!("[后台任务] 开始扫描图片目录并导入新相册..."); + info!("[后台任务] 开始扫描图片目录并导入新相册..."); match import::import_existing_data(&config_clone, db_clone).await { - Ok(_) => println!("[后台任务] 相册导入完成"), - Err(e) => eprintln!("[后台任务] 相册导入失败: {}", e), + Ok(_) => info!("[后台任务] 相册导入完成"), + Err(e) => error!("[后台任务] 相册导入失败: {}", e), } }); diff --git a/src/models.rs b/src/models.rs index 757696a..30ea745 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use uuid::Uuid; +use tracing::{info, error, warn, debug}; #[derive(Debug, Serialize, Deserialize)] pub struct Album { @@ -142,8 +144,8 @@ impl AlbumsCache { .push(album.clone()); } - println!("✅ 相册缓存已更新,共 {} 个相册,{} 个搜索索引项", - self.albums.len(), self.search_index.len()); + info!("相册缓存已更新,共 {} 个相册,{} 个搜索索引项", + self.albums.len(), self.search_index.len()); } pub fn search(&self, query: &str) -> Vec { diff --git a/src/routes.rs b/src/routes.rs index 28270a8..e93ffae 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -2,11 +2,47 @@ use crate::database::Database; use crate::image_processor::ImageProcessor; use crate::models::*; use actix_web::{HttpResponse, Responder, web}; -use std::collections::HashMap; use std::fs; use std::sync::Arc; use tokio::sync::RwLock; +use tracing::error; use uuid::Uuid; +// 统一错误处理宏 +macro_rules! handle_db_error { + ($result:expr, $error_msg:expr) => { + match $result { + Ok(value) => value, + Err(e) => { + error!("{}: {}", $error_msg, e); + return HttpResponse::InternalServerError().json(serde_json::json!({ + "error": $error_msg + })); + } + } + }; +} + +// 输入验证错误类型 +#[derive(Debug)] +enum ValidationError { + QueryTooLong, + InvalidCharacters, +} + +// 搜索查询验证函数 +fn validate_search_query(query: &str) -> Result<(), ValidationError> { + if query.len() > 100 { + return Err(ValidationError::QueryTooLong); + } + + // 允许Unicode字符(包括中文)、ASCII图形字符和空白字符 + // 只阻止控制字符和其他潜在危险字符 + if query.chars().any(|c| c.is_control() || c == '\0') { + return Err(ValidationError::InvalidCharacters); + } + + Ok(()) +} // 收藏功能API路由 @@ -15,15 +51,9 @@ pub async fn get_favorites(db: web::Data>) -> impl Responder { // 这里使用固定用户ID,实际项目中应该从认证信息中获取 let user_id = "default_user".to_string(); - match db.get_user_favorites(&user_id).await { - Ok(favorites) => HttpResponse::Ok().json(favorites), - Err(e) => { - eprintln!("获取收藏列表失败: {}", e); - HttpResponse::InternalServerError().json(serde_json::json!({ - "error": "获取收藏列表失败" - })) - } - } + let favorites = handle_db_error!(db.get_user_favorites(&user_id).await, "获取收藏列表失败"); + + HttpResponse::Ok().json(favorites) } /// 检查缩略图质量并在需要时升级为高质量版本 @@ -33,39 +63,39 @@ async fn check_and_upgrade_thumbnail_quality( original_path: &std::path::Path, ) -> Result<(), Box> { let thumbnail_path = config.thumbnails_dir.join(image.id.to_string()); - + // 检查原始图片是否存在 if !original_path.exists() { return Ok(()); // 原始图片不存在,无法升级 } - + // 读取缩略图文件信息 - let thumbnail_metadata = match std::fs::metadata(&thumbnail_path) { + let thumbnail_metadata = match fs::metadata(&thumbnail_path) { Ok(meta) => meta, Err(_) => return Ok(()), // 无法读取缩略图元数据 }; - + // 检查缩略图文件大小(简单质量检测) // 如果缩略图小于20KB,可能质量较低,需要升级 if thumbnail_metadata.len() < 20 * 1024 { // 读取原始图片数据 - let original_data = match std::fs::read(original_path) { + let original_data = match fs::read(original_path) { Ok(data) => data, Err(e) => { eprintln!("读取原始图片失败,无法升级缩略图: {}", e); return Ok(()); } }; - + // 生成高质量缩略图 - let image_processor = crate::image_processor::ImageProcessor::new(config.clone()); + let image_processor = ImageProcessor::new(config.clone()); match image_processor .generate_thumbnail_from_memory(&original_data, &image.id.to_string()) .await { Ok(high_quality_data) => { // 保存高质量缩略图 - if let Err(e) = std::fs::write(&thumbnail_path, &high_quality_data) { + if let Err(e) = fs::write(&thumbnail_path, &high_quality_data) { eprintln!("保存高质量缩略图失败: {}", e); } else { println!("✅ 缩略图已升级为高质量版本 (ID: {})", image.id); @@ -76,7 +106,7 @@ async fn check_and_upgrade_thumbnail_quality( } } } - + Ok(()) } @@ -97,15 +127,12 @@ pub async fn check_favorite_status( // 这里使用固定用户ID,实际项目中应该从认证信息中获取 let user_id = "default_user".to_string(); - match db.get_favorite_status(&user_id, image_id).await { - Ok(status) => HttpResponse::Ok().json(status), - Err(e) => { - eprintln!("检查收藏状态失败: {}", e); - HttpResponse::InternalServerError().json(serde_json::json!({ - "error": "检查收藏状态失败" - })) - } - } + let status = handle_db_error!( + db.get_favorite_status(&user_id, image_id).await, + "检查收藏状态失败" + ); + + HttpResponse::Ok().json(status) } // 添加收藏 @@ -254,9 +281,24 @@ pub async fn get_albums(albums_cache: web::Data>>) -> im pub async fn search_albums( albums_cache: web::Data>>, - query: web::Query>, + query: web::Query>, ) -> impl Responder { let q = query.get("q").map(|s| s.as_str()).unwrap_or(""); + + // 输入验证 + if let Err(validation_error) = validate_search_query(q) { + return match validation_error { + ValidationError::QueryTooLong => HttpResponse::BadRequest().json(serde_json::json!({ + "error": "搜索查询过长,最多100个字符" + })), + ValidationError::InvalidCharacters => { + HttpResponse::BadRequest().json(serde_json::json!({ + "error": "搜索查询包含无效字符" + })) + } + }; + } + let page = query .get("page") .and_then(|s| s.parse::().ok()) @@ -305,7 +347,7 @@ pub async fn search_albums( } pub async fn get_random_album( - albums_cache: web::Data>>, + albums_cache: web::Data>>, ) -> impl Responder { // 从内存缓存获取数据 let cache = albums_cache.read().await; @@ -401,21 +443,23 @@ pub async fn get_thumbnail( match fs::read(&thumbnail_path) { Ok(data) => { let mime_type = image_processor.detect_mime_type("jpg"); // 缩略图总是JPEG格式 - + // 异步检查缩略图质量并可能替换为高质量版本 let config_clone = image_processor.config.clone(); let image_clone = image.clone(); let original_path_clone = original_path.clone(); tokio::spawn(async move { if let Err(e) = check_and_upgrade_thumbnail_quality( - &config_clone, - &image_clone, - &original_path_clone - ).await { + &config_clone, + &image_clone, + &original_path_clone, + ) + .await + { eprintln!("缩略图质量检查失败: {}", e); } }); - + HttpResponse::Ok() .content_type(mime_type) .insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存 @@ -505,4 +549,4 @@ pub async fn get_thumbnail( })) } } -} \ No newline at end of file +}