update code level
This commit is contained in:
45
src/main.rs
45
src/main.rs
@@ -9,6 +9,8 @@ use actix_files as afs;
|
||||
use actix_web::{App, HttpResponse, HttpServer, web, middleware};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{interval, Duration};
|
||||
use crate::models::AlbumsCache;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
@@ -50,6 +52,47 @@ async fn main() -> std::io::Result<()> {
|
||||
// 初始化图片处理器
|
||||
let image_processor = Arc::new(image_processor::ImageProcessor::new(config.storage.clone()));
|
||||
|
||||
// 初始化相册内存缓存
|
||||
let albums_cache = Arc::new(tokio::sync::RwLock::new(AlbumsCache::new()));
|
||||
|
||||
// 初始加载相册数据到缓存
|
||||
let initial_cache = Arc::clone(&albums_cache);
|
||||
let initial_db = Arc::clone(&db);
|
||||
tokio::spawn(async move {
|
||||
match initial_db.get_all_albums().await {
|
||||
Ok(albums) => {
|
||||
let mut cache = initial_cache.write().await;
|
||||
cache.update(albums);
|
||||
println!("✅ 初始相册缓存加载完成");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ 初始缓存加载失败: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 设置定时刷新缓存任务(每30分钟)
|
||||
let refresh_cache = Arc::clone(&albums_cache);
|
||||
let refresh_db = Arc::clone(&db);
|
||||
tokio::spawn(async move {
|
||||
let mut interval = interval(Duration::from_secs(30 * 60)); // 30分钟
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
match refresh_db.get_all_albums().await {
|
||||
Ok(albums) => {
|
||||
let mut cache = refresh_cache.write().await;
|
||||
cache.update(albums);
|
||||
println!("🔄 相册缓存定时刷新完成");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ 缓存刷新失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
println!("API接口:");
|
||||
println!(" - GET /api/albums - 获取所有相册");
|
||||
println!(" - GET /api/search?q=关键词 - 搜索相册");
|
||||
@@ -80,9 +123,11 @@ async fn main() -> std::io::Result<()> {
|
||||
.wrap(middleware::Logger::default())
|
||||
.app_data(web::Data::new(Arc::clone(&db)))
|
||||
.app_data(web::Data::new(Arc::clone(&image_processor)))
|
||||
.app_data(web::Data::new(Arc::clone(&albums_cache)))
|
||||
// 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/thumbnail/{id}", web::get().to(routes::get_thumbnail))
|
||||
|
||||
@@ -34,7 +34,7 @@ pub struct AlbumWithImages {
|
||||
pub image_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AlbumInfo {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
@@ -91,4 +91,86 @@ pub struct FavoriteWithImage {
|
||||
pub struct FavoriteStatus {
|
||||
pub is_favorited: bool,
|
||||
pub favorite_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
// 内存缓存结构
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AlbumsCache {
|
||||
pub albums: Vec<AlbumInfo>,
|
||||
pub search_index: std::collections::HashMap<String, Vec<AlbumInfo>>,
|
||||
pub last_updated: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl AlbumsCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
albums: Vec::new(),
|
||||
search_index: std::collections::HashMap::new(),
|
||||
last_updated: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
let now = chrono::Utc::now();
|
||||
let duration = now - self.last_updated;
|
||||
duration.num_minutes() >= 30 // 30分钟过期
|
||||
}
|
||||
|
||||
pub fn update(&mut self, albums: Vec<AlbumInfo>) {
|
||||
self.albums = albums.clone();
|
||||
self.last_updated = chrono::Utc::now();
|
||||
|
||||
// 构建搜索索引
|
||||
self.search_index.clear();
|
||||
for album in &self.albums {
|
||||
let title_lower = album.title.to_lowercase();
|
||||
|
||||
// 为每个单词创建索引
|
||||
for word in title_lower.split_whitespace() {
|
||||
if word.len() >= 2 { // 只索引长度>=2的单词
|
||||
self.search_index
|
||||
.entry(word.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(album.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// 为完整标题创建索引
|
||||
self.search_index
|
||||
.entry(title_lower.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(album.clone());
|
||||
}
|
||||
|
||||
println!("✅ 相册缓存已更新,共 {} 个相册,{} 个搜索索引项",
|
||||
self.albums.len(), self.search_index.len());
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str) -> Vec<AlbumInfo> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results = Vec::new();
|
||||
let mut seen_ids = std::collections::HashSet::new();
|
||||
|
||||
// 搜索完整匹配
|
||||
if let Some(matches) = self.search_index.get(&query_lower) {
|
||||
for album in matches {
|
||||
if seen_ids.insert(album.id) {
|
||||
results.push(album.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索单词匹配
|
||||
for word in query_lower.split_whitespace() {
|
||||
if let Some(matches) = self.search_index.get(word) {
|
||||
for album in matches {
|
||||
if seen_ids.insert(album.id) {
|
||||
results.push(album.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
173
src/routes.rs
173
src/routes.rs
@@ -2,38 +2,19 @@ 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 uuid::Uuid;
|
||||
use crate::image_processor;
|
||||
|
||||
pub async fn get_albums(
|
||||
db: web::Data<Arc<Database>>,
|
||||
query: web::Query<std::collections::HashMap<String, String>>,
|
||||
) -> impl Responder {
|
||||
let page = query.get("page").and_then(|s| s.parse::<usize>().ok());
|
||||
let limit = query.get("limit").and_then(|s| s.parse::<usize>().ok());
|
||||
|
||||
match db.get_albums(page, limit).await {
|
||||
Ok(albums) => HttpResponse::Ok().json(albums),
|
||||
Err(e) => {
|
||||
eprintln!("获取相册列表失败: {}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "获取相册列表失败"
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收藏功能API路由
|
||||
|
||||
// 获取用户的收藏列表
|
||||
pub async fn get_favorites(
|
||||
db: web::Data<Arc<Database>>,
|
||||
) -> impl Responder {
|
||||
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) => {
|
||||
@@ -58,10 +39,10 @@ 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) => {
|
||||
@@ -80,7 +61,7 @@ pub async fn add_favorite(
|
||||
) -> impl Responder {
|
||||
// 这里使用固定用户ID,实际项目中应该从认证信息中获取
|
||||
let user_id = "default_user".to_string();
|
||||
|
||||
|
||||
match db.add_favorite(&user_id, favorite_data.into_inner()).await {
|
||||
Ok(favorite_id) => HttpResponse::Created().json(serde_json::json!({
|
||||
"id": favorite_id,
|
||||
@@ -108,7 +89,7 @@ pub async fn remove_favorite(
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
match db.remove_favorite(favorite_id).await {
|
||||
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
|
||||
"message": "取消收藏成功"
|
||||
@@ -135,10 +116,10 @@ pub async fn remove_favorite_by_image(
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 这里使用固定用户ID,实际项目中应该从认证信息中获取
|
||||
let user_id = "default_user".to_string();
|
||||
|
||||
|
||||
match db.remove_favorite_by_image(&user_id, image_id).await {
|
||||
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
|
||||
"message": "取消收藏成功"
|
||||
@@ -198,35 +179,106 @@ pub async fn get_original_image(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_albums(
|
||||
albums_cache: web::Data<Arc<RwLock<crate::models::AlbumsCache>>>,
|
||||
query: web::Query<HashMap<String, String>>,
|
||||
) -> impl Responder {
|
||||
let page = query.get("page").and_then(|s| s.parse::<usize>().ok()).unwrap_or(1);
|
||||
let limit = query.get("limit").and_then(|s| s.parse::<usize>().ok()).unwrap_or(50);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
// 从内存缓存获取数据
|
||||
let cache = albums_cache.read().await;
|
||||
let total_albums = cache.albums.len();
|
||||
|
||||
// 应用分页
|
||||
let start = offset.min(total_albums);
|
||||
let end = (offset + limit).min(total_albums);
|
||||
let paginated_albums = if start < end {
|
||||
cache.albums[start..end].to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
drop(cache); // 释放读锁
|
||||
|
||||
HttpResponse::Ok().json(paginated_albums)
|
||||
}
|
||||
|
||||
pub async fn search_albums(
|
||||
db: web::Data<Arc<Database>>,
|
||||
albums_cache: web::Data<Arc<RwLock<crate::models::AlbumsCache>>>,
|
||||
query: web::Query<std::collections::HashMap<String, String>>,
|
||||
) -> impl Responder {
|
||||
let q = query.get("q").map(|s| s.as_str()).unwrap_or("");
|
||||
let page = query.get("page").and_then(|s| s.parse::<usize>().ok());
|
||||
let limit = query.get("limit").and_then(|s| s.parse::<usize>().ok());
|
||||
let page = query
|
||||
.get("page")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(1);
|
||||
let limit = query
|
||||
.get("limit")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(50);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
if q.is_empty() {
|
||||
return match db.get_albums(page, limit).await {
|
||||
Ok(albums) => HttpResponse::Ok().json(albums),
|
||||
Err(e) => {
|
||||
eprintln!("获取相册列表失败: {}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "获取相册列表失败"
|
||||
}))
|
||||
}
|
||||
// 如果没有搜索词,返回所有相册(带分页)
|
||||
let cache = albums_cache.read().await;
|
||||
let total_albums = cache.albums.len();
|
||||
|
||||
// 应用分页
|
||||
let start = offset.min(total_albums);
|
||||
let end = (offset + limit).min(total_albums);
|
||||
let paginated_albums = if start < end {
|
||||
cache.albums[start..end].to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
drop(cache);
|
||||
return HttpResponse::Ok().json(paginated_albums);
|
||||
}
|
||||
|
||||
match db.search_albums(q, page, limit).await {
|
||||
Ok(albums) => HttpResponse::Ok().json(albums),
|
||||
Err(e) => {
|
||||
eprintln!("搜索相册失败: {}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "搜索失败"
|
||||
}))
|
||||
}
|
||||
// 从内存缓存搜索
|
||||
let cache = albums_cache.read().await;
|
||||
let mut search_results = cache.search(q);
|
||||
|
||||
// 应用分页到搜索结果
|
||||
let total_results = search_results.len();
|
||||
let start = offset.min(total_results);
|
||||
let end = (offset + limit).min(total_results);
|
||||
let paginated_results = if start < end {
|
||||
search_results.drain(start..end).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
drop(cache);
|
||||
|
||||
HttpResponse::Ok().json(paginated_results)
|
||||
}
|
||||
|
||||
pub async fn get_random_album(
|
||||
albums_cache: web::Data<Arc<RwLock<crate::models::AlbumsCache>>>,
|
||||
) -> impl Responder {
|
||||
// 从内存缓存获取数据
|
||||
let cache = albums_cache.read().await;
|
||||
|
||||
if cache.albums.is_empty() {
|
||||
drop(cache);
|
||||
return HttpResponse::NotFound().json(serde_json::json!({
|
||||
"error": "没有可用的相册"
|
||||
}));
|
||||
}
|
||||
|
||||
// 使用随机数生成器选择一个随机相册
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let random_index = rng.gen_range(0..cache.albums.len());
|
||||
let random_album = cache.albums[random_index].clone();
|
||||
|
||||
drop(cache);
|
||||
|
||||
HttpResponse::Ok().json(random_album)
|
||||
}
|
||||
|
||||
pub async fn get_album_detail(
|
||||
@@ -320,7 +372,10 @@ pub async fn get_thumbnail(
|
||||
match fs::read(&original_path) {
|
||||
Ok(data) => {
|
||||
// 快速生成小尺寸缩略图(不进行复杂处理)
|
||||
match image_processor.generate_fast_thumbnail(&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();
|
||||
@@ -330,11 +385,18 @@ pub async fn get_thumbnail(
|
||||
let thumbnail_path = config_clone
|
||||
.thumbnails_dir
|
||||
.join(filename_clone.to_string());
|
||||
|
||||
|
||||
// 生成高质量缩略图并保存
|
||||
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) {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -350,8 +412,11 @@ pub async fn get_thumbnail(
|
||||
Err(e) => {
|
||||
eprintln!("快速缩略图生成失败: {}", e);
|
||||
// 如果快速生成失败,返回原始图片的小尺寸版本
|
||||
let small_data = image_processor.resize_image_to_max(&data, 800).await;
|
||||
HttpResponse::Ok().content_type(image.mime_type).body(small_data)
|
||||
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