diff --git a/Services/Background/BackgroundTaskQueue.cs b/Services/Background/BackgroundTaskQueue.cs index c4fe9bc..8624689 100644 --- a/Services/Background/BackgroundTaskQueue.cs +++ b/Services/Background/BackgroundTaskQueue.cs @@ -236,38 +236,58 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable throw new Exception($"找不到图片文件: {localFilePath}"); } - // 创建缩略图 + // 检查并生成缩略图(如果不存在) await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 20); - var thumbnailPath = Path.Combine( - Path.GetDirectoryName(localFilePath)!, - Path.GetFileNameWithoutExtension(Path.GetFileName(localFilePath)) + "_thumb.webp"); - - await ImageHelper.CreateThumbnailAsync(localFilePath, thumbnailPath, 500); - - // 更新缩略图路径到数据库 - await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 25); - - if (picture.StorageType == StorageType.Local) + + string thumbnailForAI = localFilePath; // 用于AI分析的缩略图路径 + + if (string.IsNullOrEmpty(picture.ThumbnailPath)) { - // 本地存储缩略图 - var relativeThumbnailPath = - $"/Uploads/{Path.GetRelativePath("Uploads", Path.GetDirectoryName(thumbnailPath)!)}/{Path.GetFileName(thumbnailPath)}"; - picture.ThumbnailPath = relativeThumbnailPath.Replace('\\', '/'); + // 如果缩略图不存在,生成缩略图 + var thumbnailPath = Path.Combine( + Path.GetDirectoryName(localFilePath)!, + Path.GetFileNameWithoutExtension(Path.GetFileName(localFilePath)) + "_thumb.webp"); + + await ImageHelper.CreateThumbnailAsync(localFilePath, thumbnailPath, 500); + thumbnailForAI = thumbnailPath; + + // 更新缩略图路径到数据库 + await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 25); + + if (picture.StorageType == StorageType.Local) + { + // 本地存储缩略图 + var relativeThumbnailPath = + $"/Uploads/{Path.GetRelativePath("Uploads", Path.GetDirectoryName(thumbnailPath)!)}/{Path.GetFileName(thumbnailPath)}"; + picture.ThumbnailPath = relativeThumbnailPath.Replace('\\', '/'); + } + else + { + // 上传缩略图并获取存储路径或元数据 + await using var thumbnailFileStream = new FileStream(thumbnailPath, FileMode.Open, FileAccess.Read); + var thumbnailFileName = Path.GetFileName(thumbnailPath); + var thumbnailContentType = "image/webp"; + + string thumbnailStoragePath = await storageService.ExecuteAsync( + picture.StorageType, + provider => provider.SaveAsync(thumbnailFileStream, thumbnailFileName, thumbnailContentType)); + + // 将路径或元数据存储到ThumbnailPath + picture.ThumbnailPath = thumbnailStoragePath; + } } else { - // 非本地存储,上传缩略图到对应的存储服务 - await using var thumbnailFileStream = new FileStream(thumbnailPath, FileMode.Open, FileAccess.Read); - var thumbnailFileName = Path.GetFileName(thumbnailPath); - var thumbnailContentType = "image/webp"; - - // 上传缩略图并获取存储路径或元数据 - string thumbnailStoragePath = await storageService.ExecuteAsync( - picture.StorageType, - provider => provider.SaveAsync(thumbnailFileStream, thumbnailFileName, thumbnailContentType)); - - // 将路径或元数据存储到ThumbnailPath - picture.ThumbnailPath = thumbnailStoragePath; + // 如果缩略图已存在,下载用于AI分析 + if (picture.StorageType != StorageType.Local) + { + thumbnailForAI = await storageService.ExecuteAsync(picture.StorageType, + provider => provider.DownloadFileAsync(picture.ThumbnailPath)); + } + else + { + thumbnailForAI = Path.Combine(Directory.GetCurrentDirectory(), picture.ThumbnailPath.TrimStart('/')); + } } // 3. 提取EXIF信息 @@ -283,7 +303,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable // 5. 将缩略图转换为Base64并调用AI分析 await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 50); - string base64Image = await ImageHelper.ConvertImageToBase64(thumbnailPath); + string base64Image = await ImageHelper.ConvertImageToBase64(thumbnailForAI); var (title, description) = await aiService.AnalyzeImageAsync(base64Image); // 6. 确定最终标题和描述 diff --git a/Services/Media/PictureService.cs b/Services/Media/PictureService.cs index e9f1d7f..342a79c 100644 --- a/Services/Media/PictureService.cs +++ b/Services/Media/PictureService.cs @@ -516,12 +516,67 @@ public class PictureService( } } + string? tempOriginalFileForThumbnail = null; + string? tempThumbnailFile = null; + try { + // 如果是匿名用户或者需要立即生成缩略图,先保存到临时文件 + bool shouldGenerateThumbnail = userId.HasValue; // 只为注册用户生成缩略图 + + if (shouldGenerateThumbnail) + { + tempOriginalFileForThumbnail = Path.GetTempFileName(); + + // 保存原始文件到临时位置用于生成缩略图 + await using (var tempFileStream = new FileStream(tempOriginalFileForThumbnail, FileMode.Create)) + { + if (finalStream != fileStream) // 已经是转换后的流 + { + finalStream.Position = 0; + await finalStream.CopyToAsync(tempFileStream); + finalStream.Position = 0; // 重置位置用于后续上传 + } + else + { + await finalStream.CopyToAsync(tempFileStream); + finalStream.Position = 0; // 重置位置用于后续上传 + } + } + } + // 使用存储服务保存文件 string relativePath = await storageService.ExecuteAsync(storageType.Value, provider => provider.SaveAsync(finalStream, finalFileName, finalContentType)); + string? thumbnailPath = null; + + // 生成缩略图 + if (shouldGenerateThumbnail && !string.IsNullOrEmpty(tempOriginalFileForThumbnail)) + { + try + { + tempThumbnailFile = Path.GetTempFileName(); + string thumbnailFileName = Path.ChangeExtension(tempThumbnailFile, ".webp"); + File.Move(tempThumbnailFile, thumbnailFileName); + tempThumbnailFile = thumbnailFileName; + + // 生成缩略图 + await ImageHelper.CreateThumbnailAsync(tempOriginalFileForThumbnail, tempThumbnailFile, 500); + + // 上传缩略图 + await using var thumbnailFileStream = new FileStream(tempThumbnailFile, FileMode.Open, FileAccess.Read); + var thumbnailStorageFileName = Path.GetFileNameWithoutExtension(finalFileName) + "_thumb.webp"; + + thumbnailPath = await storageService.ExecuteAsync(storageType.Value, + provider => provider.SaveAsync(thumbnailFileStream, thumbnailStorageFileName, "image/webp")); + } + catch (Exception ex) + { + Console.WriteLine($"生成缩略图失败: {ex.Message}"); + } + } + // 创建基本的Picture对象,使用文件名作为标题和描述 string initialTitle = Path.GetFileNameWithoutExtension(originalFileName); string initialDescription = $"Uploaded on {DateTime.UtcNow}"; @@ -570,7 +625,7 @@ public class PictureService( AlbumId = albumId, StorageType = storageType.Value, ProcessingStatus = isAnonymous ? ProcessingStatus.Completed : ProcessingStatus.Pending, - ThumbnailPath = isAnonymous ? relativePath : null + ThumbnailPath = thumbnailPath ?? (isAnonymous ? relativePath : null) // 匿名用户使用原图作为缩略图 }; dbContext.Pictures.Add(picture); @@ -588,10 +643,13 @@ public class PictureService( Name = picture.Name, Path = await storageService.ExecuteAsync(picture.StorageType, provider => Task.FromResult(provider.GetUrl(relativePath))), - ThumbnailPath = isAnonymous + ThumbnailPath = !string.IsNullOrEmpty(thumbnailPath) ? await storageService.ExecuteAsync(picture.StorageType, provider => - Task.FromResult(provider.GetUrl(relativePath))) - : null, + Task.FromResult(provider.GetUrl(thumbnailPath))) + : (isAnonymous + ? await storageService.ExecuteAsync(picture.StorageType, provider => + Task.FromResult(provider.GetUrl(relativePath))) + : null), Description = picture.Description, CreatedAt = picture.CreatedAt, Tags = new List(), @@ -605,6 +663,17 @@ public class PictureService( } finally { + // 清理临时文件 + if (!string.IsNullOrEmpty(tempOriginalFileForThumbnail) && File.Exists(tempOriginalFileForThumbnail)) + { + try { File.Delete(tempOriginalFileForThumbnail); } catch { } + } + + if (!string.IsNullOrEmpty(tempThumbnailFile) && File.Exists(tempThumbnailFile)) + { + try { File.Delete(tempThumbnailFile); } catch { } + } + // 清理转换后的临时流 if (finalStream != fileStream && finalStream is FileStream tempFileStream) { @@ -613,8 +682,8 @@ public class PictureService( if (File.Exists(tempFilePath)) File.Delete(tempFilePath); // 同时清理原始临时文件 - string tempOriginalFile = Path.ChangeExtension(tempFilePath, null); - if (File.Exists(tempOriginalFile)) File.Delete(tempOriginalFile); + string tempOriginalFileFromConvert = Path.ChangeExtension(tempFilePath, null); + if (File.Exists(tempOriginalFileFromConvert)) File.Delete(tempOriginalFileFromConvert); } } }