diff --git a/Extensions/ServiceCollectionExtensions.cs b/Extensions/ServiceCollectionExtensions.cs index 3f987af..636422f 100644 --- a/Extensions/ServiceCollectionExtensions.cs +++ b/Extensions/ServiceCollectionExtensions.cs @@ -40,6 +40,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); } diff --git a/Foxel.csproj b/Foxel.csproj index 14b7763..5ea75da 100644 --- a/Foxel.csproj +++ b/Foxel.csproj @@ -26,7 +26,6 @@ - diff --git a/Models/DataBase/BackgroundTask.cs b/Models/DataBase/BackgroundTask.cs index 9d1884a..9d2081a 100644 --- a/Models/DataBase/BackgroundTask.cs +++ b/Models/DataBase/BackgroundTask.cs @@ -7,6 +7,7 @@ namespace Foxel.Models.DataBase public enum TaskType { PictureProcessing = 0, + VisualRecognition = 1, } public enum TaskExecutionStatus diff --git a/Models/DataBase/Picture.cs b/Models/DataBase/Picture.cs index 727fbbe..7d8930d 100644 --- a/Models/DataBase/Picture.cs +++ b/Models/DataBase/Picture.cs @@ -9,8 +9,16 @@ public class Picture : BaseModel { [StringLength(255)] public string Name { get; set; } = string.Empty; + /// + /// Path to the high-definition (possibly format-converted) image. + /// [StringLength(1024)] public string Path { get; set; } = string.Empty; + /// + /// Path to the original, untouched uploaded file. + /// + [StringLength(1024)] public string OriginalPath { get; set; } = string.Empty; + [StringLength(1024)] public string? ThumbnailPath { get; set; } = string.Empty; [StringLength(2000)] public string Description { get; set; } = string.Empty; diff --git a/Models/Response/Picture/PictureResponse.cs b/Models/Response/Picture/PictureResponse.cs index 6e0a842..1f4aa3e 100644 --- a/Models/Response/Picture/PictureResponse.cs +++ b/Models/Response/Picture/PictureResponse.cs @@ -8,6 +8,7 @@ public record PictureResponse public string Name { get; set; } = string.Empty; public string? Path { get; set; } public string? ThumbnailPath { get; set; } + public string? OriginalPath { get; set; } public string Description { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } public List? Tags { get; set; } diff --git a/Services/Background/BackgroundTaskQueue.cs b/Services/Background/BackgroundTaskQueue.cs index 631c7eb..735d072 100644 --- a/Services/Background/BackgroundTaskQueue.cs +++ b/Services/Background/BackgroundTaskQueue.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Text.Json; using System.Threading.Channels; using Foxel.Models.DataBase; @@ -25,19 +24,17 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable IConfigService configuration, ILogger logger) { - _serviceProvider = serviceProvider; // Keep IServiceProvider to resolve processors + _serviceProvider = serviceProvider; _contextFactory = contextFactory; _logger = logger; _processingTasks = new List(); _maxConcurrentTasks = configuration.GetValueAsync("BackgroundTasks:MaxConcurrentTasks", 10).Result; // 保持原有逻辑 _signal = new SemaphoreSlim(_maxConcurrentTasks); - var options = new BoundedChannelOptions(10000) // 保持原有逻辑 + var options = new BoundedChannelOptions(10000) { FullMode = BoundedChannelFullMode.Wait }; _queue = Channel.CreateBounded(options); - - // 启动处理器,确保在服务启动时就开始处理队列 StartProcessor(); } @@ -51,7 +48,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable throw new KeyNotFoundException($"找不到 PictureId: {pictureId} 的图片"); } - var payload = new PictureProcessingPayload + var payload = new Processors.PictureProcessingPayload { PictureId = pictureId, OriginalFilePath = originalFilePath, @@ -74,7 +71,39 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable await _queue.Writer.WriteAsync(backgroundTask.Id); _logger.LogInformation("图片处理任务已加入队列: TaskId={TaskId}, PictureId={PictureId}", backgroundTask.Id, pictureId); - StartProcessor(); + StartProcessor(); // Ensure processor is running or starts for new items + + return backgroundTask.Id; + } + + public async Task QueueVisualRecognitionTaskAsync(VisualRecognitionPayload payload) + { + await using var dbContext = await _contextFactory.CreateDbContextAsync(); + // Optionally, validate picture existence again, though PictureTaskProcessor should ensure it. + var picture = await dbContext.Pictures.FindAsync(payload.PictureId); + if (picture == null) + { + _logger.LogError("无法为不存在的图片 PictureId: {PictureId} 创建视觉识别任务", payload.PictureId); + throw new KeyNotFoundException($"尝试为 PictureId: {payload.PictureId} 创建视觉识别任务时找不到图片"); + } + + var backgroundTask = new BackgroundTask + { + Type = TaskType.VisualRecognition, // New TaskType + Payload = JsonSerializer.Serialize(payload), + UserId = payload.UserIdForPicture, // Comes from the payload + RelatedEntityId = payload.PictureId, + Status = TaskExecutionStatus.Pending, + CreatedAt = DateTime.UtcNow + }; + + dbContext.BackgroundTasks.Add(backgroundTask); + await dbContext.SaveChangesAsync(); + + await _queue.Writer.WriteAsync(backgroundTask.Id); + _logger.LogInformation("视觉识别任务已加入队列: TaskId={TaskId}, PictureId={PictureId}", backgroundTask.Id, payload.PictureId); + + StartProcessor(); // Ensure processor is running or starts for new items return backgroundTask.Id; } @@ -96,13 +125,29 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable var picture = await dbContext.Pictures.FindAsync(task.RelatedEntityId.Value); if (picture != null) { - taskName = picture.Name; + taskName = $"图片处理: {picture.Name}"; } else { taskName = "图片处理 (图片信息丢失)"; } } + else if (task.Type == TaskType.VisualRecognition && task.RelatedEntityId.HasValue) // Added for VisualRecognition + { + var picture = await dbContext.Pictures.FindAsync(task.RelatedEntityId.Value); + if (picture != null) + { + taskName = $"视觉识别: {picture.Name}"; + } + else + { + taskName = "视觉识别 (图片信息丢失)"; + } + } + else + { + taskName = $"任务: {task.Id} ({task.Type})"; // Generic name + } statusList.Add(new TaskDetailsDto { @@ -156,30 +201,28 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable { await using var dbContext = await _contextFactory.CreateDbContextAsync(); var unfinishedTasks = await dbContext.BackgroundTasks - .Where(bt => bt.Type == TaskType.PictureProcessing && + .Where(bt => (bt.Type == TaskType.PictureProcessing || bt.Type == TaskType.VisualRecognition) && // Added VisualRecognition (bt.Status == TaskExecutionStatus.Pending || bt.Status == TaskExecutionStatus.Processing)) .ToListAsync(); if (unfinishedTasks.Any()) { - _logger.LogInformation("正在恢复 {Count} 个未完成的图片处理任务", unfinishedTasks.Count); + _logger.LogInformation("正在恢复 {Count} 个未完成的任务", unfinishedTasks.Count); foreach (var task in unfinishedTasks) { - // 确保任务状态在数据库中被重置为 Pending,以防上次运行时停在 Processing 状态 if (task.Status == TaskExecutionStatus.Processing) { task.Status = TaskExecutionStatus.Pending; - task.StartedAt = null; // 重置开始时间 - // 保留 Progress 和 ErrorMessage 以供参考 + task.StartedAt = null; } await _queue.Writer.WriteAsync(task.Id); - _logger.LogInformation("已恢复图片处理任务到队列: TaskId={TaskId}, RelatedEntityId={RelatedEntityId}", task.Id, task.RelatedEntityId); + _logger.LogInformation("已恢复任务到队列: TaskId={TaskId}, Type={TaskType}, RelatedEntityId={RelatedEntityId}", task.Id, task.Type, task.RelatedEntityId); } - await dbContext.SaveChangesAsync(); // 保存状态更改 + await dbContext.SaveChangesAsync(); } else { - _logger.LogInformation("没有需要恢复的图片处理任务"); + _logger.LogInformation("没有需要恢复的任务"); } } catch (Exception ex) @@ -243,7 +286,9 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable case TaskType.PictureProcessing: processor = scope.ServiceProvider.GetRequiredService(); break; - // Future task types can be added here + case TaskType.VisualRecognition: // Added case for VisualRecognition + processor = scope.ServiceProvider.GetRequiredService(); + break; default: _logger.LogError("未找到任务类型 {TaskType} 的处理器: TaskId={TaskId}", taskToCheck.Type, taskToCheck.Id); await MarkTaskAsFailedByQueue(taskToCheck.Id, $"未找到任务类型 {taskToCheck.Type} 的处理器。"); diff --git a/Services/Background/IBackgroundTaskQueue.cs b/Services/Background/IBackgroundTaskQueue.cs index 18ed447..23aaffd 100644 --- a/Services/Background/IBackgroundTaskQueue.cs +++ b/Services/Background/IBackgroundTaskQueue.cs @@ -1,4 +1,5 @@ using Foxel.Models.DataBase; +using Foxel.Services.Background.Processors; // For VisualRecognitionPayload namespace Foxel.Services.Background; @@ -8,13 +9,20 @@ namespace Foxel.Services.Background; public interface IBackgroundTaskQueue { /// - /// 将图片处理任务添加到队列 + /// 将图片处理任务(元数据和缩略图)添加到队列 /// /// 图片ID /// 原始图片路径 /// 任务ID Task QueuePictureProcessingTaskAsync(int pictureId, string originalFilePath); + /// + /// 将视觉识别任务添加到队列 + /// + /// 视觉识别任务的Payload + /// 任务ID + Task QueueVisualRecognitionTaskAsync(VisualRecognitionPayload payload); + /// /// 获取用户的所有任务状态 (目前主要指图片处理任务) /// diff --git a/Services/Background/Processors/PictureTaskProcessor.cs b/Services/Background/Processors/PictureTaskProcessor.cs index 4b095fc..dc83110 100644 --- a/Services/Background/Processors/PictureTaskProcessor.cs +++ b/Services/Background/Processors/PictureTaskProcessor.cs @@ -6,9 +6,23 @@ using Foxel.Utils; using Microsoft.EntityFrameworkCore; using System.Text.Json; using Foxel.Services.Attributes; +using Foxel.Services.Background; // Added for IBackgroundTaskQueue namespace Foxel.Services.Background.Processors { + public class PictureProcessingPayload // Ensure this is defined or imported + { + public int PictureId { get; set; } + public string OriginalFilePath { get; set; } = string.Empty; + public int? UserIdForPicture { get; set; } + } + + public class VisualRecognitionPayload // Define new payload + { + public int PictureId { get; set; } + public int? UserIdForPicture { get; set; } + } + public class PictureTaskProcessor : ITaskProcessor { private readonly IDbContextFactory _contextFactory; @@ -60,18 +74,17 @@ namespace Foxel.Services.Background.Processors var originalFilePathFromPayload = payload.OriginalFilePath; string localFilePath = ""; bool isTempFile = false; - string thumbnailForAI = string.Empty; + // string thumbnailForAI = string.Empty; // No longer directly used for AI here await using var dbContext = await _contextFactory.CreateDbContextAsync(); var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id); if (currentBackgroundTaskState == null) { _logger.LogError("在 PictureTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id); - return; + return; } - - var picture = await dbContext.Pictures.Include(p => p.User).ThenInclude(u => u.Tags).FirstOrDefaultAsync(p => p.Id == pictureId); + var picture = await dbContext.Pictures.Include(p => p.User).FirstOrDefaultAsync(p => p.Id == pictureId); try { @@ -83,10 +96,9 @@ namespace Foxel.Services.Background.Processors } using var scope = _serviceProvider.CreateScope(); - var aiService = scope.ServiceProvider.GetRequiredService(); var storageService = scope.ServiceProvider.GetRequiredService(); - string contentRootPath = _environment.ContentRootPath; + string contentRootPath = _environment.ContentRootPath; if (picture.StorageType == StorageType.Local) { @@ -94,7 +106,7 @@ namespace Foxel.Services.Background.Processors } else { - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15, currentBackgroundTaskState: currentBackgroundTaskState); + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 25, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress localFilePath = await storageService.ExecuteAsync(picture.StorageType, provider => provider.DownloadFileAsync(originalFilePathFromPayload)); isTempFile = true; @@ -105,22 +117,28 @@ namespace Foxel.Services.Background.Processors throw new Exception($"找不到图片文件: {localFilePath} (源路径: {originalFilePathFromPayload})"); } - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 20, currentBackgroundTaskState: currentBackgroundTaskState); - thumbnailForAI = localFilePath; + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 50, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress if (string.IsNullOrEmpty(picture.ThumbnailPath)) { var tempThumbContainer = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(tempThumbContainer); - var thumbnailDiskPath = Path.Combine(tempThumbContainer, Path.GetFileNameWithoutExtension(Path.GetFileName(localFilePath)) + "_thumb.webp"); + + // Derive baseName from OriginalPath (which is in originalFilePathFromPayload) + // originalFilePathFromPayload is the stored path/key, not a local path. + // We need the base name (UUID part) from the picture's OriginalPath. + string baseNameFromOriginalPath = Path.GetFileNameWithoutExtension(picture.OriginalPath); + + var thumbnailDiskPath = Path.Combine(tempThumbContainer, $"{baseNameFromOriginalPath}-thumbnail-temp.webp"); await ImageHelper.CreateThumbnailAsync(localFilePath, thumbnailDiskPath, 500); - thumbnailForAI = thumbnailDiskPath; + // thumbnailForAI = thumbnailDiskPath; // This temp path is for AI, but AI is in next step - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 25, currentBackgroundTaskState: currentBackgroundTaskState); + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 65, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress await using var thumbnailFileStream = new FileStream(thumbnailDiskPath, FileMode.Open, FileAccess.Read); - var thumbnailStorageFileName = Path.GetFileNameWithoutExtension(picture.Path.Split('/').LastOrDefault() ?? picture.Name) + "_thumb.webp"; + // Use the new naming convention for storage + var thumbnailStorageFileName = $"{baseNameFromOriginalPath}-thumbnail.webp"; string storedThumbnailPath = await storageService.ExecuteAsync( picture.StorageType, @@ -129,104 +147,18 @@ namespace Foxel.Services.Background.Processors if (Directory.Exists(tempThumbContainer)) Directory.Delete(tempThumbContainer, true); } - else - { - if (picture.StorageType != StorageType.Local && !string.IsNullOrEmpty(picture.ThumbnailPath)) - { - thumbnailForAI = await storageService.ExecuteAsync(picture.StorageType, - provider => provider.DownloadFileAsync(picture.ThumbnailPath)); - } - else if (!string.IsNullOrEmpty(picture.ThumbnailPath)) - { - // 对于本地存储的缩略图,也基于 ContentRootPath 构建路径 - thumbnailForAI = Path.Combine(contentRootPath, picture.ThumbnailPath.TrimStart('/')); - } - } - - if (!File.Exists(thumbnailForAI) && isTempFile) // If thumbnailForAI was meant to be a temp file but doesn't exist, re-download or handle - { - _logger.LogWarning("AI分析所需的缩略图文件不存在: {ThumbnailPath}", thumbnailForAI); - // Fallback or error handling if thumbnailForAI is critical - if (string.IsNullOrEmpty(picture.ThumbnailPath) || picture.StorageType == StorageType.Local) - { - thumbnailForAI = localFilePath; // Fallback to original if thumbnail is missing and was supposed to be local or not generated - } - else - { - // Attempt to re-download if it was from remote storage - thumbnailForAI = await storageService.ExecuteAsync(picture.StorageType, provider => provider.DownloadFileAsync(picture.ThumbnailPath!)); - if (!File.Exists(thumbnailForAI)) throw new Exception($"无法获取用于AI分析的缩略图: {picture.ThumbnailPath}"); - } - } - - - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 30, currentBackgroundTaskState: currentBackgroundTaskState); + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 80, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress var exifInfo = await ImageHelper.ExtractExifInfoAsync(localFilePath); picture.ExifInfo = exifInfo; picture.TakenAt = ImageHelper.ParseExifDateTime(exifInfo.DateTimeOriginal); - await dbContext.SaveChangesAsync(); - - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 50, currentBackgroundTaskState: currentBackgroundTaskState); - string base64Image = await ImageHelper.ConvertImageToBase64(thumbnailForAI); - var (title, description) = await aiService.AnalyzeImageAsync(base64Image); - - string finalTitle = !string.IsNullOrWhiteSpace(title) && title != "AI生成的标题" ? title : Path.GetFileNameWithoutExtension(picture.Name); - string finalDescription = !string.IsNullOrWhiteSpace(description) && description != "AI生成的描述" ? description : picture.Description; - picture.Name = finalTitle; - picture.Description = finalDescription; - - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 60, currentBackgroundTaskState: currentBackgroundTaskState); - var combinedText = $"{finalTitle}. {finalDescription}"; - var embedding = await aiService.GetEmbeddingAsync(combinedText); - picture.Embedding = embedding; - - if (picture.UserId.HasValue && embedding != null && embedding.Length > 0) - { - var vectorDbService = scope.ServiceProvider.GetRequiredService(); - await vectorDbService.AddPictureToUserCollectionAsync(picture.UserId.Value, new Models.Vector.PictureVector - { - Id = (ulong)picture.Id, - Name = picture.Name, - Embedding = embedding - }); - } - - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 70, currentBackgroundTaskState: currentBackgroundTaskState); - var availableTagNames = await dbContext.Tags.Select(t => t.Name).ToListAsync(); - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 80, currentBackgroundTaskState: currentBackgroundTaskState); - var matchedTagNames = await aiService.GenerateTagsFromImageAsync(base64Image, availableTagNames, true); - - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 90, currentBackgroundTaskState: currentBackgroundTaskState); - if (picture.User != null && matchedTagNames.Any()) - { - picture.Tags ??= new List(); - foreach (var tagName in matchedTagNames) - { - var existingTag = await dbContext.Tags.FirstOrDefaultAsync(t => t.Name.ToLower() == tagName.ToLower()); - if (existingTag == null) - { - existingTag = new Tag { Name = tagName.Trim(), Description = tagName.Trim() }; - dbContext.Tags.Add(existingTag); - } - if (!picture.Tags.Any(t => t.Id == existingTag.Id)) picture.Tags.Add(existingTag); - - picture.User.Tags ??= new List(); - if (!picture.User.Tags.Any(t => t.Id == existingTag.Id)) picture.User.Tags.Add(existingTag); - } - } - await dbContext.SaveChangesAsync(); await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100, completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState); + } catch (Exception ex) { - _logger.LogError(ex, "图片处理任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id, pictureId); + _logger.LogError(ex, "图片元数据处理任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id, pictureId); await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Failed, currentBackgroundTaskState.Progress, ex.Message, currentBackgroundTaskState: currentBackgroundTaskState); - if (picture != null) - { - // Potentially update picture entity itself if needed, though task failure is primary - await dbContext.SaveChangesAsync(); - } } finally { @@ -234,11 +166,6 @@ namespace Foxel.Services.Background.Processors { try { File.Delete(localFilePath); } catch (Exception ex) { _logger.LogWarning(ex, "删除临时主图片文件失败: {FilePath}", localFilePath); } } - bool thumbnailIsTemp = thumbnailForAI.StartsWith(Path.GetTempPath()); - if (thumbnailIsTemp && thumbnailForAI != localFilePath && File.Exists(thumbnailForAI)) - { - try { File.Delete(thumbnailForAI); } catch (Exception ex) { _logger.LogWarning(ex, "删除临时缩略图文件失败: {FilePath}", thumbnailForAI); } - } } } diff --git a/Services/Background/Processors/VisualRecognitionTaskProcessor.cs b/Services/Background/Processors/VisualRecognitionTaskProcessor.cs new file mode 100644 index 0000000..b30ed63 --- /dev/null +++ b/Services/Background/Processors/VisualRecognitionTaskProcessor.cs @@ -0,0 +1,218 @@ +using Foxel.Models.DataBase; +using Foxel.Services.AI; +using Foxel.Services.Storage; +using Foxel.Services.VectorDB; +using Foxel.Utils; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using Foxel.Services.Attributes; + +namespace Foxel.Services.Background.Processors +{ + + public class VisualRecognitionTaskProcessor : ITaskProcessor + { + private readonly IDbContextFactory _contextFactory; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly IWebHostEnvironment _environment; + + public VisualRecognitionTaskProcessor( + IDbContextFactory contextFactory, + IServiceProvider serviceProvider, + ILogger logger, + IWebHostEnvironment environment) + { + _contextFactory = contextFactory; + _serviceProvider = serviceProvider; + _logger = logger; + _environment = environment; + } + + public async Task ProcessAsync(BackgroundTask backgroundTask) + { + if (backgroundTask.Payload == null) + { + await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "任务 Payload 为空。"); + _logger.LogError("视觉识别任务 Payload 为空: TaskId={TaskId}", backgroundTask.Id); + return; + } + + VisualRecognitionPayload? payload; + try + { + payload = JsonSerializer.Deserialize(backgroundTask.Payload); + } + catch (JsonException ex) + { + _logger.LogError(ex, "无法解析视觉识别任务的 Payload: TaskId={TaskId}", backgroundTask.Id); + await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "Payload 解析失败。"); + return; + } + + if (payload == null || payload.PictureId == 0) + { + _logger.LogError("视觉识别任务的 Payload 无效或缺少 PictureId: TaskId={TaskId}", backgroundTask.Id); + await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "Payload 无效或缺少 PictureId。"); + return; + } + + var pictureId = payload.PictureId; + string thumbnailForAIDownloadPath = string.Empty; // Path if thumbnail needs to be downloaded + bool isTempThumbnailFile = false; + + await using var dbContext = await _contextFactory.CreateDbContextAsync(); + var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id); + if (currentBackgroundTaskState == null) + { + _logger.LogError("在 VisualRecognitionTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id); + return; + } + + var picture = await dbContext.Pictures.Include(p => p.User).ThenInclude(u => u.Tags).FirstOrDefaultAsync(p => p.Id == pictureId); + + try + { + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 10, currentBackgroundTaskState: currentBackgroundTaskState); + + if (picture == null) + { + throw new Exception($"找不到ID为{pictureId}的图片。"); + } + if (string.IsNullOrEmpty(picture.ThumbnailPath)) + { + throw new Exception($"图片ID {pictureId} 的缩略图路径为空,无法进行AI分析。"); + } + + using var scope = _serviceProvider.CreateScope(); + var aiService = scope.ServiceProvider.GetRequiredService(); + var storageService = scope.ServiceProvider.GetRequiredService(); + string contentRootPath = _environment.ContentRootPath; + string actualThumbnailPathForAI; + + if (picture.StorageType == StorageType.Local) + { + actualThumbnailPathForAI = Path.Combine(contentRootPath, picture.ThumbnailPath.TrimStart('/')); + } + else // Remote storage + { + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15, currentBackgroundTaskState: currentBackgroundTaskState); + thumbnailForAIDownloadPath = await storageService.ExecuteAsync(picture.StorageType, + provider => provider.DownloadFileAsync(picture.ThumbnailPath)); + actualThumbnailPathForAI = thumbnailForAIDownloadPath; + isTempThumbnailFile = true; + } + + if (string.IsNullOrEmpty(actualThumbnailPathForAI) || !File.Exists(actualThumbnailPathForAI)) + { + throw new Exception($"找不到用于AI分析的缩略图文件: {actualThumbnailPathForAI} (源路径: {picture.ThumbnailPath})"); + } + + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 20, currentBackgroundTaskState: currentBackgroundTaskState); + string base64Image = await ImageHelper.ConvertImageToBase64(actualThumbnailPathForAI); + + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 40, currentBackgroundTaskState: currentBackgroundTaskState); + var (title, description) = await aiService.AnalyzeImageAsync(base64Image); + + string finalTitle = !string.IsNullOrWhiteSpace(title) && title != "AI生成的标题" ? title : Path.GetFileNameWithoutExtension(picture.Name); + string finalDescription = !string.IsNullOrWhiteSpace(description) && description != "AI生成的描述" ? description : picture.Description; + picture.Name = finalTitle; // Potentially overwrites name set from filename + picture.Description = finalDescription; + + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 60, currentBackgroundTaskState: currentBackgroundTaskState); + var combinedText = $"{finalTitle}. {finalDescription}"; + var embedding = await aiService.GetEmbeddingAsync(combinedText); + picture.Embedding = embedding; + + if (picture.UserId.HasValue && embedding != null && embedding.Length > 0) + { + var vectorDbService = scope.ServiceProvider.GetRequiredService(); + await vectorDbService.AddPictureToUserCollectionAsync(picture.UserId.Value, new Models.Vector.PictureVector + { + Id = (ulong)picture.Id, + Name = picture.Name, + Embedding = embedding + }); + } + + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 70, currentBackgroundTaskState: currentBackgroundTaskState); + var availableTagNames = await dbContext.Tags.Select(t => t.Name).ToListAsync(); + + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 80, currentBackgroundTaskState: currentBackgroundTaskState); + var matchedTagNames = await aiService.GenerateTagsFromImageAsync(base64Image, availableTagNames, true); + + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 90, currentBackgroundTaskState: currentBackgroundTaskState); + if (picture.User != null && matchedTagNames.Any()) + { + picture.Tags ??= new List(); + foreach (var tagName in matchedTagNames) + { + var existingTag = await dbContext.Tags.FirstOrDefaultAsync(t => t.Name.ToLower() == tagName.ToLower()); + if (existingTag == null) + { + existingTag = new Tag { Name = tagName.Trim(), Description = tagName.Trim() }; + dbContext.Tags.Add(existingTag); + } + if (!picture.Tags.Any(t => t.Id == existingTag.Id)) picture.Tags.Add(existingTag); + + picture.User.Tags ??= new List(); + if (!picture.User.Tags.Any(t => t.Id == existingTag.Id)) picture.User.Tags.Add(existingTag); + } + } + + await dbContext.SaveChangesAsync(); // Save all AI-related changes to Picture + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100, completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState); + } + catch (Exception ex) + { + _logger.LogError(ex, "视觉识别任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id, pictureId); + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Failed, currentBackgroundTaskState.Progress, ex.Message, currentBackgroundTaskState: currentBackgroundTaskState); + // dbContext.SaveChangesAsync() might be called in UpdateTaskStatusInDb or here if picture state needs saving on error + } + finally + { + if (isTempThumbnailFile && File.Exists(thumbnailForAIDownloadPath)) + { + try { File.Delete(thumbnailForAIDownloadPath); } catch (Exception ex) { _logger.LogWarning(ex, "删除临时AI缩略图文件失败: {FilePath}", thumbnailForAIDownloadPath); } + } + } + } + + private async Task UpdateTaskStatusInDb(Guid taskId, TaskExecutionStatus status, int progress, string? error = null, DateTime? startedAt = null, DateTime? completedAt = null, BackgroundTask? currentBackgroundTaskState = null) + { + await using var dbContext = await _contextFactory.CreateDbContextAsync(); + var taskToUpdate = currentBackgroundTaskState ?? await dbContext.BackgroundTasks.FindAsync(taskId); + + if (taskToUpdate != null) + { + if (currentBackgroundTaskState != null && dbContext.Entry(currentBackgroundTaskState).State == EntityState.Detached) + { + dbContext.BackgroundTasks.Attach(currentBackgroundTaskState); + } + + taskToUpdate.Status = status; + taskToUpdate.Progress = progress; + taskToUpdate.ErrorMessage = string.IsNullOrEmpty(error) ? taskToUpdate.ErrorMessage : error; // Keep existing error if new one is null/empty + if (startedAt.HasValue) taskToUpdate.StartedAt = startedAt; + if (completedAt.HasValue) taskToUpdate.CompletedAt = completedAt; + + if ((status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed) && !taskToUpdate.StartedAt.HasValue) + { + taskToUpdate.StartedAt = taskToUpdate.CreatedAt; // Ensure StartedAt is set + } + if (status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed) + { + taskToUpdate.CompletedAt ??= DateTime.UtcNow; // Ensure CompletedAt is set + } + + + await dbContext.SaveChangesAsync(); + _logger.LogInformation("任务状态更新 (VisualRecognitionProcessor): TaskId={TaskId}, Status={Status}, Progress={Progress}%", taskId, status, progress); + } + else + { + _logger.LogWarning("尝试在 VisualRecognitionProcessor 中更新不存在的任务状态: TaskId={TaskId}", taskId); + } + } + } +} diff --git a/Services/Media/PictureService.cs b/Services/Media/PictureService.cs index ac35e1e..6fb20a5 100644 --- a/Services/Media/PictureService.cs +++ b/Services/Media/PictureService.cs @@ -343,6 +343,8 @@ public class PictureService( Name = picture.Name, Path = storageService.ExecuteAsync(picture.StorageType, provider => Task.FromResult(provider.GetUrl(picture.Path ?? string.Empty))).Result, + OriginalPath = storageService.ExecuteAsync(picture.StorageType, provider => // Added OriginalPath + Task.FromResult(provider.GetUrl(picture.OriginalPath ?? string.Empty))).Result, ThumbnailPath = !string.IsNullOrEmpty(picture.ThumbnailPath) ? storageService.ExecuteAsync(picture.StorageType, provider => Task.FromResult(provider.GetUrl(picture.ThumbnailPath))).Result @@ -479,126 +481,95 @@ public class PictureService( quality = int.Parse(defaultQualityConfig); } - string originalFileName = fileName; - string finalFileName = fileName; - string finalContentType = contentType; - Stream finalStream = fileStream; + string baseName = Guid.NewGuid().ToString(); + string originalFileExtension = Path.GetExtension(fileName); + string originalStorageFileName = $"{baseName}{originalFileExtension}"; - // 如果需要格式转换 - if (convertToFormat != ImageFormat.Original) - { - // 创建临时文件保存原始上传内容 - string tempOriginalFile = Path.GetTempFileName(); - string tempConvertedFile = Path.GetTempFileName(); - - try - { - // 保存原始文件到临时位置 - await using (var tempFileStream = new FileStream(tempOriginalFile, FileMode.Create)) - { - await fileStream.CopyToAsync(tempFileStream); - } - - // 转换格式 - string convertedFilePath = await ImageHelper.ConvertImageFormatAsync( - tempOriginalFile, tempConvertedFile, convertToFormat, quality); - - // 更新文件信息 - string newExtension = ImageHelper.GetFileExtensionFromFormat(convertToFormat); - finalFileName = Path.ChangeExtension(Path.GetFileNameWithoutExtension(originalFileName), newExtension); - finalContentType = ImageHelper.GetMimeTypeFromFormat(convertToFormat); - - // 创建新的流用于上传转换后的文件 - finalStream = new FileStream(convertedFilePath, FileMode.Open, FileAccess.Read); - } - catch - { - // 清理临时文件 - if (File.Exists(tempOriginalFile)) File.Delete(tempOriginalFile); - if (File.Exists(tempConvertedFile)) File.Delete(tempConvertedFile); - throw; - } - } - - string? tempOriginalFileForThumbnail = null; - string? tempThumbnailFile = null; + string? tempOriginalLocalPath = null; + string? tempConvertedHdLocalPath = null; + string? tempThumbnailLocalPath = null; + + string storedOriginalPath; + string storedHdPath; + string? storedThumbnailPath = null; try { - // 如果是匿名用户或者需要立即生成缩略图,先保存到临时文件 - bool shouldGenerateThumbnail = userId.HasValue; // 只为注册用户生成缩略图 - - if (shouldGenerateThumbnail) + tempOriginalLocalPath = Path.GetTempFileName() + originalFileExtension; + File.Move(Path.GetTempFileName(), tempOriginalLocalPath); + await using (var tempFileStream = new FileStream(tempOriginalLocalPath, FileMode.Create)) { - 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; // 重置位置用于后续上传 - } - } + await fileStream.CopyToAsync(tempFileStream); } - // 使用存储服务保存文件 - string relativePath = await storageService.ExecuteAsync(storageType.Value, - provider => provider.SaveAsync(finalStream, finalFileName, finalContentType)); + await using (var originalLocalStream = new FileStream(tempOriginalLocalPath, FileMode.Open, FileAccess.Read)) + { + storedOriginalPath = await storageService.ExecuteAsync(storageType.Value, + provider => provider.SaveAsync(originalLocalStream, originalStorageFileName, contentType)); + } - string? thumbnailPath = null; + string hdStorageFileName; + string hdContentType; + string sourceForHdProcessing = tempOriginalLocalPath; - // 生成缩略图 - if (shouldGenerateThumbnail && !string.IsNullOrEmpty(tempOriginalFileForThumbnail)) + if (convertToFormat != ImageFormat.Original) + { + string convertedExtension = ImageHelper.GetFileExtensionFromFormat(convertToFormat); + hdStorageFileName = $"{baseName}-high-definition{convertedExtension}"; + hdContentType = ImageHelper.GetMimeTypeFromFormat(convertToFormat); + + tempConvertedHdLocalPath = Path.GetTempFileName() + convertedExtension; + File.Move(Path.GetTempFileName(), tempConvertedHdLocalPath); + + await ImageHelper.ConvertImageFormatAsync(sourceForHdProcessing, tempConvertedHdLocalPath, convertToFormat, quality); + + await using var convertedHdStream = new FileStream(tempConvertedHdLocalPath, FileMode.Open, FileAccess.Read); + storedHdPath = await storageService.ExecuteAsync(storageType.Value, + provider => provider.SaveAsync(convertedHdStream, hdStorageFileName, hdContentType)); + } + else + { + hdStorageFileName = originalStorageFileName; // Same as original + hdContentType = contentType; + // No separate upload needed if it's the same as original; Path will point to the same stored object as OriginalPath. + // However, to ensure distinctness or if storage provider handles it, we can re-upload or copy. + // For simplicity, if no conversion, Path = OriginalPath. + storedHdPath = storedOriginalPath; + } + + // 4. Generate and upload thumbnail (Picture.ThumbnailPath) + bool shouldGenerateThumbnailNow = userId.HasValue; + if (shouldGenerateThumbnailNow) { try { - tempThumbnailFile = Path.GetTempFileName(); - string thumbnailFileName = Path.ChangeExtension(tempThumbnailFile, ".webp"); - File.Move(tempThumbnailFile, thumbnailFileName); - tempThumbnailFile = thumbnailFileName; + tempThumbnailLocalPath = Path.GetTempFileName() + ".webp"; + File.Move(Path.GetTempFileName(), tempThumbnailLocalPath); - // 生成缩略图 - await ImageHelper.CreateThumbnailAsync(tempOriginalFileForThumbnail, tempThumbnailFile, 500); + await ImageHelper.CreateThumbnailAsync(tempOriginalLocalPath, tempThumbnailLocalPath, 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")); + string thumbnailUploadFileName = $"{baseName}-thumbnail.webp"; + await using var thumbnailFileStream = new FileStream(tempThumbnailLocalPath, FileMode.Open, FileAccess.Read); + storedThumbnailPath = await storageService.ExecuteAsync(storageType.Value, + provider => provider.SaveAsync(thumbnailFileStream, thumbnailUploadFileName, "image/webp")); } catch (Exception ex) { - logger.LogError(ex, "生成缩略图失败"); + logger.LogError(ex, "生成和上传缩略图失败 during initial upload"); + // Continue without thumbnail if it fails here, background task can try later if needed } } - - // 创建基本的Picture对象,使用文件名作为标题和描述 - string initialTitle = Path.GetFileNameWithoutExtension(originalFileName); + + string initialTitle = Path.GetFileNameWithoutExtension(fileName); string initialDescription = $"Uploaded on {DateTime.UtcNow}"; await using var dbContext = await contextFactory.CreateDbContextAsync(); - - // 获取用户 User? user = null; if (userId is not null) { user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId); - if (user == null) - { - throw new Exception("找不到指定的用户"); - } + if (user == null) throw new Exception("找不到指定的用户"); } - - // 检查相册是否存在并且属于当前用户 Album? album = null; if (albumId.HasValue) { @@ -616,38 +587,45 @@ public class PictureService( } } - bool isAnonymous = userId == null; - // 创建图片对象并保存到数据库 var picture = new Picture { Name = initialTitle, Description = initialDescription, - Path = relativePath, // 这是存储服务返回的路径/键 + OriginalPath = storedOriginalPath, // Store path to original uploaded file + Path = storedHdPath, // Store path to HD (possibly converted) file + ThumbnailPath = storedThumbnailPath, // Store path to thumbnail User = user, Permission = permission, AlbumId = albumId, StorageType = storageType.Value, // ProcessingStatus 等字段已移除 - ThumbnailPath = thumbnailPath // 如果生成了缩略图,则保存其存储路径/键 }; dbContext.Pictures.Add(picture); await dbContext.SaveChangesAsync(); - if (!isAnonymous) + if (userId != null) // Only queue for registered users { - // 使用 relativePath (即存储服务中的对象键/路径) 来排队任务 - await backgroundTaskQueue.QueuePictureProcessingTaskAsync(picture.Id, picture.Path); + // Pass OriginalPath for EXIF extraction and other initial processing + await backgroundTaskQueue.QueuePictureProcessingTaskAsync(picture.Id, picture.OriginalPath); + + var visualRecognitionPayload = new Background.Processors.VisualRecognitionPayload + { + PictureId = picture.Id, + UserIdForPicture = picture.UserId + }; + await backgroundTaskQueue.QueueVisualRecognitionTaskAsync(visualRecognitionPayload); } - // 返回图片基本信息 var pictureResponse = new PictureResponse { Id = picture.Id, Name = picture.Name, Path = await storageService.ExecuteAsync(picture.StorageType, provider => - Task.FromResult(provider.GetUrl(picture.Path))), // 使用 picture.Path + Task.FromResult(provider.GetUrl(picture.Path))), + OriginalPath = await storageService.ExecuteAsync(picture.StorageType, provider => // Added OriginalPath + Task.FromResult(provider.GetUrl(picture.OriginalPath))), ThumbnailPath = !string.IsNullOrEmpty(picture.ThumbnailPath) ? await storageService.ExecuteAsync(picture.StorageType, provider => Task.FromResult(provider.GetUrl(picture.ThumbnailPath))) @@ -665,27 +643,18 @@ public class PictureService( } finally { - // 清理临时文件 - if (!string.IsNullOrEmpty(tempOriginalFileForThumbnail) && File.Exists(tempOriginalFileForThumbnail)) + // Clean up temporary local files + if (!string.IsNullOrEmpty(tempOriginalLocalPath) && File.Exists(tempOriginalLocalPath)) { - try { File.Delete(tempOriginalFileForThumbnail); } catch { } + try { File.Delete(tempOriginalLocalPath); } catch { /* ignored */ } } - - if (!string.IsNullOrEmpty(tempThumbnailFile) && File.Exists(tempThumbnailFile)) + if (!string.IsNullOrEmpty(tempConvertedHdLocalPath) && File.Exists(tempConvertedHdLocalPath)) { - try { File.Delete(tempThumbnailFile); } catch { } + try { File.Delete(tempConvertedHdLocalPath); } catch { /* ignored */ } } - - // 清理转换后的临时流 - if (finalStream != fileStream && finalStream is FileStream tempFileStream) + if (!string.IsNullOrEmpty(tempThumbnailLocalPath) && File.Exists(tempThumbnailLocalPath)) { - string tempFilePath = tempFileStream.Name; - finalStream.Dispose(); - if (File.Exists(tempFilePath)) File.Delete(tempFilePath); - - // 同时清理原始临时文件 - string tempOriginalFileFromConvert = Path.ChangeExtension(tempFilePath, null); - if (File.Exists(tempOriginalFileFromConvert)) File.Delete(tempOriginalFileFromConvert); + try { File.Delete(tempThumbnailLocalPath); } catch { /* ignored */ } } } } @@ -736,10 +705,10 @@ public class PictureService( } var filesToDelete = - new List<(int PictureId, string Path, string ThumbnailPath, int? UserId, StorageType StorageType)>(); + new List<(int PictureId, string Path, string? ThumbnailPath, string OriginalPath, int? UserId, StorageType StorageType)>(); foreach (var picture in picturesToDelete) { - filesToDelete.Add((picture.Id, picture.Path, picture.ThumbnailPath ?? string.Empty, picture.User?.Id, + filesToDelete.Add((picture.Id, picture.Path, picture.ThumbnailPath, picture.OriginalPath, picture.User?.Id, picture.StorageType)); } @@ -749,7 +718,7 @@ public class PictureService( await dbContext.SaveChangesAsync(); } - foreach (var (pictureId, path, thumbnailPath, userId, storageType) in filesToDelete) + foreach (var (pictureId, path, thumbnailPath, originalPath, userId, storageType) in filesToDelete) { try { @@ -757,11 +726,21 @@ public class PictureService( try { - // 使用存储服务删除文件 - await storageService.ExecuteAsync(storageType, - provider => provider.DeleteAsync(path)); + // Delete original file + if (!string.IsNullOrEmpty(originalPath)) + { + await storageService.ExecuteAsync(storageType, + provider => provider.DeleteAsync(originalPath)); + } + + // Delete HD/processed file + if (!string.IsNullOrEmpty(path) && path != originalPath) // Avoid double delete if Path is same as OriginalPath + { + await storageService.ExecuteAsync(storageType, + provider => provider.DeleteAsync(path)); + } - // 删除缩略图 + // Delete thumbnail if (!string.IsNullOrEmpty(thumbnailPath)) { await storageService.ExecuteAsync(storageType, @@ -868,8 +847,11 @@ public class PictureService( Name = picture.Name, Path = await storageService.ExecuteAsync(picture.StorageType, provider => Task.FromResult(provider.GetUrl(picture.Path ?? string.Empty))), - ThumbnailPath = await storageService.ExecuteAsync(picture.StorageType, provider => - Task.FromResult(provider.GetUrl(picture.ThumbnailPath ?? string.Empty))), + OriginalPath = await storageService.ExecuteAsync(picture.StorageType, provider => // Added OriginalPath + Task.FromResult(provider.GetUrl(picture.OriginalPath ?? string.Empty))), + ThumbnailPath = !string.IsNullOrEmpty(picture.ThumbnailPath) ? + await storageService.ExecuteAsync(picture.StorageType, provider => Task.FromResult(provider.GetUrl(picture.ThumbnailPath))) + : null, Description = picture.Description, CreatedAt = picture.CreatedAt, Tags = picture.Tags?.Select(t => t.Name).ToList() ?? new List(), diff --git a/Services/Storage/Providers/CosStorageProvider.cs b/Services/Storage/Providers/CosStorageProvider.cs index 5cc9d70..00bf2d7 100644 --- a/Services/Storage/Providers/CosStorageProvider.cs +++ b/Services/Storage/Providers/CosStorageProvider.cs @@ -68,8 +68,10 @@ public class CosStorageProvider(IConfigService configService, ILogger logger) : IStorageProvider +public class LocalStorageProvider(IConfigService configService) : IStorageProvider { private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads"); @@ -15,13 +15,12 @@ public class LocalStorageProvider(IConfigService configService, ILogger { @@ -126,7 +127,7 @@ const BackgroundTasks: React.FC = () => { dataIndex: 'taskName', // Changed dataIndex key: 'taskName', render: (text: string, record: TaskDetailsViewModel) => ( // Updated type and logic - record.taskType === 0 && record.relatedEntityId // 修正: 使用数字 0 比较 + (record.taskType === 0 || record.taskType === 1) && record.relatedEntityId // 检查是否为图片处理或视觉识别任务 ? {text} : text ), @@ -151,6 +152,11 @@ const BackgroundTasks: React.FC = () => { key: 'taskType', render: (taskType: number | undefined) => // 接收数字类型的 taskType taskType !== undefined ? taskTypeDisplayMapping[taskType] || `未知类型 (${taskType})` : '-', + filters: [ // 可以为任务类型添加筛选器 + { text: '图片处理', value: 0 }, + { text: '视觉识别', value: 1 }, + ], + onFilter: (value, record: TaskDetailsViewModel) => record.taskType === (value as number), }, { title: '进度', diff --git a/Web/src/routes/index.tsx b/Web/src/routes/index.tsx index 5332615..6f54a3c 100644 --- a/Web/src/routes/index.tsx +++ b/Web/src/routes/index.tsx @@ -157,19 +157,20 @@ const routes: RouteConfig[] = [ hideInMenu: true, breadcrumb: { title: '用户详情', - parent: 'admin-user' // 修改: 指向父路由的 key + parent: 'admin-user' } }, { path: 'pictures', key: 'admin-picture', icon: , - label: '图片管理', + label: '图片', element: , area: 'admin', + groupLabel: '资源管理', breadcrumb: { - title: '图片管理' - } + title: '图片' + } }, { path: 'log', diff --git a/appsettings.json b/appsettings.json index dcf0f26..7076fdb 100644 --- a/appsettings.json +++ b/appsettings.json @@ -7,7 +7,9 @@ "Default": "Information", "System.Net.Http.HttpClient": "Warning", "Microsoft.AspNetCore": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + "Microsoft.EntityFrameworkCore.Database.Command": "Warning", + "Foxel.Services.Background.Processors.PictureTaskProcessor": "Warning", + "Foxel.Services.Background.Processors.VisualRecognitionTaskProcessor": "Warning" } }, "AllowedHosts": ""