update code level
This commit is contained in:
@@ -37,6 +37,6 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn server_address(&self) -> String {
|
||||
format!("{}:{}", self.server.host, self.server.port)
|
||||
format!("{}:{}", "localhost", self.server.port)
|
||||
}
|
||||
}
|
||||
173
src/database.rs
173
src/database.rs
@@ -33,11 +33,11 @@ impl Database {
|
||||
}
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(10) // 增加连接数
|
||||
.min_connections(2) // 保持最小连接数
|
||||
.acquire_timeout(std::time::Duration::from_secs(30)) // 增加获取连接超时时间
|
||||
.idle_timeout(std::time::Duration::from_secs(300)) // 增加空闲超时时间
|
||||
.max_lifetime(std::time::Duration::from_secs(1800)) // 增加最大生命周期
|
||||
.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)) // 增加最大生命周期
|
||||
.connect(url)
|
||||
.await?;
|
||||
|
||||
@@ -54,7 +54,11 @@ impl Database {
|
||||
// 创建表
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
|
||||
Ok(Database { pool })
|
||||
// 确保image_count字段存在并正确初始化
|
||||
let db = Database { pool };
|
||||
db.ensure_image_count_field().await?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
|
||||
@@ -106,30 +110,28 @@ impl Database {
|
||||
}
|
||||
pub async fn get_albums(&self, page: Option<usize>, limit: Option<usize>) -> Result<Vec<AlbumInfo>, sqlx::Error> {
|
||||
let page = page.unwrap_or(1);
|
||||
let limit = limit.unwrap_or(200);
|
||||
let limit = limit.unwrap_or(50); // 减少默认加载数量,提高响应速度
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let albums = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
a.id,
|
||||
a.title,
|
||||
a.document,
|
||||
a.description,
|
||||
a.cover_image_id,
|
||||
COUNT(img.id) as image_count,
|
||||
a.created_at
|
||||
FROM albums a
|
||||
LEFT JOIN images img ON img.album_id = a.id
|
||||
GROUP BY a.id, a.title, a.document, a.description, a.cover_image_id, a.created_at
|
||||
ORDER BY a.document DESC
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
document,
|
||||
description,
|
||||
cover_image_id,
|
||||
image_count,
|
||||
created_at
|
||||
FROM albums
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"#
|
||||
)
|
||||
.bind(limit as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
.bind(limit as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for row in albums {
|
||||
@@ -166,10 +168,10 @@ impl Database {
|
||||
document,
|
||||
description,
|
||||
cover_image_id,
|
||||
0 as image_count,
|
||||
image_count,
|
||||
created_at
|
||||
FROM albums
|
||||
ORDER BY document DESC
|
||||
ORDER BY created_at DESC
|
||||
"#
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
@@ -203,25 +205,22 @@ impl Database {
|
||||
pub async fn search_albums(&self, query: &str, page: Option<usize>, limit: Option<usize>) -> Result<Vec<AlbumInfo>, sqlx::Error> {
|
||||
let search_pattern = format!("%{}%", query);
|
||||
let page = page.unwrap_or(1);
|
||||
let limit = limit.unwrap_or(200);
|
||||
let limit = limit.unwrap_or(50); // 减少默认加载数量,提高响应速度
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let albums = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
a.id,
|
||||
a.title,
|
||||
a.document,
|
||||
a.description,
|
||||
a.cover_image_id,
|
||||
COUNT(img.id) as image_count,
|
||||
a.created_at
|
||||
FROM albums a
|
||||
LEFT JOIN images img ON img.album_id = a.id
|
||||
LEFT JOIN images i ON i.id = a.cover_image_id
|
||||
WHERE a.title LIKE ?
|
||||
GROUP BY a.id, a.title, a.document, a.description, a.cover_image_id, a.created_at
|
||||
ORDER BY a.created_at DESC
|
||||
id,
|
||||
title,
|
||||
document,
|
||||
description,
|
||||
cover_image_id,
|
||||
image_count,
|
||||
created_at
|
||||
FROM albums
|
||||
WHERE title LIKE ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"#
|
||||
)
|
||||
@@ -291,6 +290,7 @@ impl Database {
|
||||
title: row.get("title"),
|
||||
description: row.get("description"),
|
||||
cover_image_id,
|
||||
image_count: row.get("image_count"),
|
||||
created_at: row.get::<String, _>("created_at"),
|
||||
updated_at: row.get::<String, _>("updated_at"),
|
||||
};
|
||||
@@ -355,7 +355,7 @@ impl Database {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO albums (id, title, document, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
"INSERT INTO albums (id, title, document, description, image_count, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)"
|
||||
)
|
||||
.bind(id.to_string())
|
||||
.bind(album.title)
|
||||
@@ -391,6 +391,9 @@ impl Database {
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// 更新相册的图片数量
|
||||
self.update_album_image_count(image.album_id).await?;
|
||||
|
||||
// 如果这是相册的第一张图片,设置为封面
|
||||
let image_count = sqlx::query_scalar::<_, i64>(
|
||||
"SELECT COUNT(*) FROM images WHERE album_id = ?"
|
||||
@@ -412,6 +415,26 @@ impl Database {
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// 更新相册的图片数量
|
||||
pub async fn update_album_image_count(&self, album_id: Uuid) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE albums
|
||||
SET image_count = (
|
||||
SELECT COUNT(*) FROM images WHERE album_id = ?
|
||||
),
|
||||
updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
"#
|
||||
)
|
||||
.bind(album_id.to_string())
|
||||
.bind(album_id.to_string())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_images(&self, images: Vec<CreateImage>) -> Result<Vec<Uuid>, sqlx::Error> {
|
||||
if images.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
@@ -474,12 +497,15 @@ impl Database {
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新相册的图片数量
|
||||
self.update_album_image_count(album_id).await?;
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// 获取相册中的图片数量
|
||||
/// 获取相册中的图片数量(直接从image_count字段读取)
|
||||
pub async fn get_album_image_count(&self, album_id: Uuid) -> Result<i64, sqlx::Error> {
|
||||
let count = sqlx::query_scalar::<_, i64>(
|
||||
"SELECT COUNT(*) FROM images WHERE album_id = ?"
|
||||
@@ -491,6 +517,38 @@ impl Database {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// 确保image_count字段存在并正确初始化
|
||||
pub async fn ensure_image_count_field(&self) -> Result<(), sqlx::Error> {
|
||||
// 检查字段是否存在
|
||||
let exists: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('albums') WHERE name = 'image_count'"
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
if exists == 0 {
|
||||
// 字段不存在,添加字段
|
||||
sqlx::query("ALTER TABLE albums ADD COLUMN image_count INTEGER NOT NULL DEFAULT 0")
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// 更新现有数据的image_count
|
||||
sqlx::query(
|
||||
"UPDATE albums SET image_count = (
|
||||
SELECT COUNT(*) FROM images WHERE images.album_id = albums.id
|
||||
)"
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
println!("✅ image_count字段已成功添加并初始化");
|
||||
} else {
|
||||
println!("ℹ️ image_count字段已存在,跳过初始化");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除相册及其关联的图片
|
||||
pub async fn delete_album(&self, album_id: Uuid) -> Result<(), sqlx::Error> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
@@ -511,6 +569,39 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_image(&self, image_id: Uuid) -> Result<bool, sqlx::Error> {
|
||||
// 先获取图片所属的相册ID
|
||||
let album_id_result = sqlx::query_scalar::<_, String>(
|
||||
r#"
|
||||
SELECT album_id FROM images WHERE id = ?
|
||||
"#
|
||||
)
|
||||
.bind(image_id.to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM images
|
||||
WHERE id = ?
|
||||
"#
|
||||
)
|
||||
.bind(image_id.to_string())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// 如果删除成功且找到了相册ID,更新相册图片数量
|
||||
if result.rows_affected() > 0 {
|
||||
if let Some(album_id_str) = album_id_result {
|
||||
if let Ok(album_id) = Uuid::parse_str(&album_id_str) {
|
||||
self.update_album_image_count(album_id).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn update_album_cover(&self, album_id: Uuid, cover_image_id: Option<Uuid>) -> Result<(), sqlx::Error> {
|
||||
let cover_id_str = cover_image_id.map(|id| id.to_string());
|
||||
|
||||
|
||||
@@ -80,6 +80,44 @@ impl ImageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 快速生成缩略图(低质量但快速)
|
||||
pub async fn generate_fast_thumbnail(&self, image_data: &[u8], _filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
// 使用快速解码和缩放
|
||||
let img = image::load_from_memory(image_data)?;
|
||||
|
||||
// 快速缩放到小尺寸(200x150)
|
||||
let thumbnail = img.thumbnail(200, 150);
|
||||
|
||||
// 使用低质量JPEG压缩以加快编码速度
|
||||
let mut output = Vec::new();
|
||||
thumbnail.write_to(&mut std::io::Cursor::new(&mut output), image::ImageOutputFormat::Jpeg(50))?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// 快速调整图片尺寸(最大宽度)
|
||||
pub async fn resize_image_to_max(&self, image_data: &[u8], max_width: u32) -> Vec<u8> {
|
||||
match image::load_from_memory(image_data) {
|
||||
Ok(img) => {
|
||||
let (width, height) = img.dimensions();
|
||||
if width <= max_width {
|
||||
return image_data.to_vec(); // 无需调整
|
||||
}
|
||||
|
||||
let new_height = (height as f32 * max_width as f32 / width as f32) as u32;
|
||||
let resized = img.thumbnail(max_width, new_height);
|
||||
|
||||
let mut output = Vec::new();
|
||||
if resized.write_to(&mut std::io::Cursor::new(&mut output), image::ImageOutputFormat::Jpeg(75)).is_ok() {
|
||||
output
|
||||
} else {
|
||||
image_data.to_vec() // 失败时返回原图
|
||||
}
|
||||
}
|
||||
Err(_) => image_data.to_vec(), // 解码失败时返回原图
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_image_url(&self, filename: &str) -> String {
|
||||
format!("/images/{}", filename)
|
||||
}
|
||||
@@ -128,7 +166,7 @@ impl ImageProcessor {
|
||||
let img = image::load_from_memory(image_data)?;
|
||||
|
||||
// 生成缩略图(400x300,保持宽高比)
|
||||
let thumbnail = img.thumbnail(400, 300);
|
||||
let thumbnail = img.thumbnail(600, 400);
|
||||
|
||||
// 将缩略图数据保存到内存缓冲区
|
||||
let mut buffer = std::io::Cursor::new(Vec::new());
|
||||
|
||||
14
src/main.rs
14
src/main.rs
@@ -6,7 +6,7 @@ mod models;
|
||||
mod routes;
|
||||
|
||||
use actix_files as afs;
|
||||
use actix_web::{App, HttpResponse, HttpServer, web};
|
||||
use actix_web::{App, HttpResponse, HttpServer, web, middleware};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -74,6 +74,10 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
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(&image_processor)))
|
||||
// API路由
|
||||
@@ -92,8 +96,12 @@ async fn main() -> std::io::Result<()> {
|
||||
.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))
|
||||
// 静态文件服务
|
||||
.service(afs::Files::new("/assets", "./assets").show_files_listing())
|
||||
// 静态文件服务 - 启用缓存
|
||||
.service(afs::Files::new("/assets", "./assets")
|
||||
.show_files_listing()
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true))
|
||||
// HTML页面路由
|
||||
.route(
|
||||
"/",
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct Album {
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub cover_image_id: Option<Uuid>,
|
||||
pub image_count: i64, // 新增图片数量字段
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use actix_web::{HttpResponse, Responder, web};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use crate::image_processor;
|
||||
|
||||
pub async fn get_albums(
|
||||
db: web::Data<Arc<Database>>,
|
||||
@@ -301,7 +302,11 @@ pub async fn get_thumbnail(
|
||||
match fs::read(&thumbnail_path) {
|
||||
Ok(data) => {
|
||||
let mime_type = image_processor.detect_mime_type("jpg"); // 缩略图总是JPEG格式
|
||||
HttpResponse::Ok().content_type(mime_type).body(data)
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime_type)
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(data)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("读取缩略图失败: {}", e);
|
||||
@@ -311,36 +316,42 @@ pub async fn get_thumbnail(
|
||||
}
|
||||
}
|
||||
} else if original_path.exists() {
|
||||
// 生成缩略图并返回
|
||||
// 先返回原始图片的缩略版本(快速响应)
|
||||
match fs::read(&original_path) {
|
||||
Ok(data) => {
|
||||
// 使用ImageProcessor生成缩略图
|
||||
match image_processor.generate_thumbnail_from_memory(&data, &image.id.to_string()).await {
|
||||
// 快速生成小尺寸缩略图(不进行复杂处理)
|
||||
match image_processor.generate_fast_thumbnail(&data, &image.id.to_string()).await {
|
||||
Ok(thumbnail_data) => {
|
||||
// 异步保存缩略图到文件(不阻塞响应)
|
||||
// 异步保存高质量缩略图到文件(不阻塞响应)
|
||||
let config_clone = image_processor.config.clone();
|
||||
let filename_clone = image.id.clone();
|
||||
let thumbnail_data_clone = thumbnail_data.clone();
|
||||
let original_data_clone = data.clone();
|
||||
tokio::spawn(async move {
|
||||
let thumbnail_path = config_clone
|
||||
.thumbnails_dir
|
||||
.join(filename_clone.to_string());
|
||||
if let Err(e) =
|
||||
fs::write(&thumbnail_path, &thumbnail_data_clone)
|
||||
{
|
||||
eprintln!("保存缩略图文件失败: {}", e);
|
||||
|
||||
// 生成高质量缩略图并保存
|
||||
if let Ok(high_quality_data) = ImageProcessor::new(config_clone)
|
||||
.generate_thumbnail_from_memory(&original_data_clone, &filename_clone.to_string()).await {
|
||||
if let Err(e) = fs::write(&thumbnail_path, &high_quality_data) {
|
||||
eprintln!("保存高质量缩略图失败: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 返回缩略图数据
|
||||
// 返回快速生成的缩略图数据
|
||||
HttpResponse::Ok()
|
||||
.content_type("image/jpeg")
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(thumbnail_data)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("生成缩略图失败: {}", e);
|
||||
// 如果生成缩略图失败,返回原始图片
|
||||
HttpResponse::Ok().content_type(image.mime_type).body(data)
|
||||
eprintln!("快速缩略图生成失败: {}", e);
|
||||
// 如果快速生成失败,返回原始图片的小尺寸版本
|
||||
let small_data = image_processor.resize_image_to_max(&data, 800).await;
|
||||
HttpResponse::Ok().content_type(image.mime_type).body(small_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user