update code level

This commit is contained in:
lychang
2026-03-25 14:38:21 +08:00
parent e05cf48063
commit 6b2d44ca78
8 changed files with 253 additions and 72 deletions

61
Cargo.lock generated
View File

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

View File

@@ -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"
rand = "0.8"
tracing = "0.1"
tracing-subscriber = "0.3"

View File

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

View File

@@ -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 = `
<div class="loading-state">
<i class="fas fa-spinner fa-spin"></i>
<p>${message}</p>
<div class="progress-bar">
<div class="progress" style="width: ${progressWidth}%"></div>
</div>
</div>
`;
}
// 显示加载更多指示器
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);
});

View File

@@ -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?;

View File

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

View File

@@ -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<AlbumInfo> {

View File

@@ -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<Arc<Database>>) -> 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<dyn std::error::Error>> {
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<Arc<RwLock<AlbumsCache>>>) -> im
pub async fn search_albums(
albums_cache: web::Data<Arc<RwLock<AlbumsCache>>>,
query: web::Query<HashMap<String, String>>,
query: web::Query<std::collections::HashMap<String, String>>,
) -> 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::<usize>().ok())
@@ -305,7 +347,7 @@ pub async fn search_albums(
}
pub async fn get_random_album(
albums_cache: web::Data<Arc<RwLock<crate::models::AlbumsCache>>>,
albums_cache: web::Data<Arc<RwLock<AlbumsCache>>>,
) -> 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(
}))
}
}
}
}