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 = `
+
+ `;
+}
+
// 显示加载更多指示器
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
+}