feat(background): add visual recognition task and refactor picture processing

This commit is contained in:
ShiYu
2025-06-08 15:40:08 +08:00
parent 7ad8b6c826
commit cf0aa87d79
16 changed files with 255 additions and 269 deletions
@@ -40,6 +40,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<WebDavStorageProvider>(); services.AddSingleton<WebDavStorageProvider>();
services.AddSingleton<IStorageService, StorageService>(); services.AddSingleton<IStorageService, StorageService>();
services.AddSingleton<PictureTaskProcessor>(); services.AddSingleton<PictureTaskProcessor>();
services.AddSingleton<VisualRecognitionTaskProcessor>();
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>(); services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
} }
-1
View File
@@ -26,7 +26,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Uploads\" /> <Folder Include="Uploads\" />
<Folder Include="Uploads\2025\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+1
View File
@@ -7,6 +7,7 @@ namespace Foxel.Models.DataBase
public enum TaskType public enum TaskType
{ {
PictureProcessing = 0, PictureProcessing = 0,
VisualRecognition = 1,
} }
public enum TaskExecutionStatus public enum TaskExecutionStatus
+8
View File
@@ -9,8 +9,16 @@ public class Picture : BaseModel
{ {
[StringLength(255)] public string Name { get; set; } = string.Empty; [StringLength(255)] public string Name { get; set; } = string.Empty;
/// <summary>
/// Path to the high-definition (possibly format-converted) image.
/// </summary>
[StringLength(1024)] public string Path { get; set; } = string.Empty; [StringLength(1024)] public string Path { get; set; } = string.Empty;
/// <summary>
/// Path to the original, untouched uploaded file.
/// </summary>
[StringLength(1024)] public string OriginalPath { get; set; } = string.Empty;
[StringLength(1024)] public string? ThumbnailPath { get; set; } = string.Empty; [StringLength(1024)] public string? ThumbnailPath { get; set; } = string.Empty;
[StringLength(2000)] public string Description { get; set; } = string.Empty; [StringLength(2000)] public string Description { get; set; } = string.Empty;
@@ -8,6 +8,7 @@ public record PictureResponse
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string? Path { get; set; } public string? Path { get; set; }
public string? ThumbnailPath { get; set; } public string? ThumbnailPath { get; set; }
public string? OriginalPath { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public List<string>? Tags { get; set; } public List<string>? Tags { get; set; }
+62 -17
View File
@@ -1,4 +1,3 @@
using System.Collections.Concurrent;
using System.Text.Json; using System.Text.Json;
using System.Threading.Channels; using System.Threading.Channels;
using Foxel.Models.DataBase; using Foxel.Models.DataBase;
@@ -25,19 +24,17 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
IConfigService configuration, IConfigService configuration,
ILogger<BackgroundTaskQueue> logger) ILogger<BackgroundTaskQueue> logger)
{ {
_serviceProvider = serviceProvider; // Keep IServiceProvider to resolve processors _serviceProvider = serviceProvider;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
_processingTasks = new List<Task>(); _processingTasks = new List<Task>();
_maxConcurrentTasks = configuration.GetValueAsync("BackgroundTasks:MaxConcurrentTasks", 10).Result; // 保持原有逻辑 _maxConcurrentTasks = configuration.GetValueAsync("BackgroundTasks:MaxConcurrentTasks", 10).Result; // 保持原有逻辑
_signal = new SemaphoreSlim(_maxConcurrentTasks); _signal = new SemaphoreSlim(_maxConcurrentTasks);
var options = new BoundedChannelOptions(10000) // 保持原有逻辑 var options = new BoundedChannelOptions(10000)
{ {
FullMode = BoundedChannelFullMode.Wait FullMode = BoundedChannelFullMode.Wait
}; };
_queue = Channel.CreateBounded<Guid>(options); _queue = Channel.CreateBounded<Guid>(options);
// 启动处理器,确保在服务启动时就开始处理队列
StartProcessor(); StartProcessor();
} }
@@ -51,7 +48,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
throw new KeyNotFoundException($"找不到 PictureId: {pictureId} 的图片"); throw new KeyNotFoundException($"找不到 PictureId: {pictureId} 的图片");
} }
var payload = new PictureProcessingPayload var payload = new Processors.PictureProcessingPayload
{ {
PictureId = pictureId, PictureId = pictureId,
OriginalFilePath = originalFilePath, OriginalFilePath = originalFilePath,
@@ -74,7 +71,39 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
await _queue.Writer.WriteAsync(backgroundTask.Id); await _queue.Writer.WriteAsync(backgroundTask.Id);
_logger.LogInformation("图片处理任务已加入队列: TaskId={TaskId}, PictureId={PictureId}", backgroundTask.Id, pictureId); _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<Guid> 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; return backgroundTask.Id;
} }
@@ -96,13 +125,29 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
var picture = await dbContext.Pictures.FindAsync(task.RelatedEntityId.Value); var picture = await dbContext.Pictures.FindAsync(task.RelatedEntityId.Value);
if (picture != null) if (picture != null)
{ {
taskName = picture.Name; taskName = $"图片处理: {picture.Name}";
} }
else else
{ {
taskName = "图片处理 (图片信息丢失)"; 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 statusList.Add(new TaskDetailsDto
{ {
@@ -156,30 +201,28 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
{ {
await using var dbContext = await _contextFactory.CreateDbContextAsync(); await using var dbContext = await _contextFactory.CreateDbContextAsync();
var unfinishedTasks = await dbContext.BackgroundTasks 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)) (bt.Status == TaskExecutionStatus.Pending || bt.Status == TaskExecutionStatus.Processing))
.ToListAsync(); .ToListAsync();
if (unfinishedTasks.Any()) if (unfinishedTasks.Any())
{ {
_logger.LogInformation("正在恢复 {Count} 个未完成的图片处理任务", unfinishedTasks.Count); _logger.LogInformation("正在恢复 {Count} 个未完成的任务", unfinishedTasks.Count);
foreach (var task in unfinishedTasks) foreach (var task in unfinishedTasks)
{ {
// 确保任务状态在数据库中被重置为 Pending,以防上次运行时停在 Processing 状态
if (task.Status == TaskExecutionStatus.Processing) if (task.Status == TaskExecutionStatus.Processing)
{ {
task.Status = TaskExecutionStatus.Pending; task.Status = TaskExecutionStatus.Pending;
task.StartedAt = null; // 重置开始时间 task.StartedAt = null;
// 保留 Progress 和 ErrorMessage 以供参考
} }
await _queue.Writer.WriteAsync(task.Id); 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 else
{ {
_logger.LogInformation("没有需要恢复的图片处理任务"); _logger.LogInformation("没有需要恢复的任务");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -243,7 +286,9 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
case TaskType.PictureProcessing: case TaskType.PictureProcessing:
processor = scope.ServiceProvider.GetRequiredService<PictureTaskProcessor>(); processor = scope.ServiceProvider.GetRequiredService<PictureTaskProcessor>();
break; break;
// Future task types can be added here case TaskType.VisualRecognition: // Added case for VisualRecognition
processor = scope.ServiceProvider.GetRequiredService<VisualRecognitionTaskProcessor>();
break;
default: default:
_logger.LogError("未找到任务类型 {TaskType} 的处理器: TaskId={TaskId}", taskToCheck.Type, taskToCheck.Id); _logger.LogError("未找到任务类型 {TaskType} 的处理器: TaskId={TaskId}", taskToCheck.Type, taskToCheck.Id);
await MarkTaskAsFailedByQueue(taskToCheck.Id, $"未找到任务类型 {taskToCheck.Type} 的处理器。"); await MarkTaskAsFailedByQueue(taskToCheck.Id, $"未找到任务类型 {taskToCheck.Type} 的处理器。");
+9 -1
View File
@@ -1,4 +1,5 @@
using Foxel.Models.DataBase; using Foxel.Models.DataBase;
using Foxel.Services.Background.Processors; // For VisualRecognitionPayload
namespace Foxel.Services.Background; namespace Foxel.Services.Background;
@@ -8,13 +9,20 @@ namespace Foxel.Services.Background;
public interface IBackgroundTaskQueue public interface IBackgroundTaskQueue
{ {
/// <summary> /// <summary>
/// 将图片处理任务添加到队列 /// 将图片处理任务(元数据和缩略图)添加到队列
/// </summary> /// </summary>
/// <param name="pictureId">图片ID</param> /// <param name="pictureId">图片ID</param>
/// <param name="originalFilePath">原始图片路径</param> /// <param name="originalFilePath">原始图片路径</param>
/// <returns>任务ID</returns> /// <returns>任务ID</returns>
Task<Guid> QueuePictureProcessingTaskAsync(int pictureId, string originalFilePath); Task<Guid> QueuePictureProcessingTaskAsync(int pictureId, string originalFilePath);
/// <summary>
/// 将视觉识别任务添加到队列
/// </summary>
/// <param name="payload">视觉识别任务的Payload</param>
/// <returns>任务ID</returns>
Task<Guid> QueueVisualRecognitionTaskAsync(VisualRecognitionPayload payload);
/// <summary> /// <summary>
/// 获取用户的所有任务状态 (目前主要指图片处理任务) /// 获取用户的所有任务状态 (目前主要指图片处理任务)
/// </summary> /// </summary>
@@ -6,9 +6,23 @@ using Foxel.Utils;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Text.Json; using System.Text.Json;
using Foxel.Services.Attributes; using Foxel.Services.Attributes;
using Foxel.Services.Background; // Added for IBackgroundTaskQueue
namespace Foxel.Services.Background.Processors 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 public class PictureTaskProcessor : ITaskProcessor
{ {
private readonly IDbContextFactory<MyDbContext> _contextFactory; private readonly IDbContextFactory<MyDbContext> _contextFactory;
@@ -60,18 +74,17 @@ namespace Foxel.Services.Background.Processors
var originalFilePathFromPayload = payload.OriginalFilePath; var originalFilePathFromPayload = payload.OriginalFilePath;
string localFilePath = ""; string localFilePath = "";
bool isTempFile = false; 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(); await using var dbContext = await _contextFactory.CreateDbContextAsync();
var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id); var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id);
if (currentBackgroundTaskState == null) if (currentBackgroundTaskState == null)
{ {
_logger.LogError("在 PictureTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id); _logger.LogError("在 PictureTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id);
return; return;
} }
var picture = await dbContext.Pictures.Include(p => p.User).FirstOrDefaultAsync(p => p.Id == pictureId);
var picture = await dbContext.Pictures.Include(p => p.User).ThenInclude(u => u.Tags).FirstOrDefaultAsync(p => p.Id == pictureId);
try try
{ {
@@ -83,10 +96,9 @@ namespace Foxel.Services.Background.Processors
} }
using var scope = _serviceProvider.CreateScope(); using var scope = _serviceProvider.CreateScope();
var aiService = scope.ServiceProvider.GetRequiredService<IAiService>();
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>(); var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
string contentRootPath = _environment.ContentRootPath; string contentRootPath = _environment.ContentRootPath;
if (picture.StorageType == StorageType.Local) if (picture.StorageType == StorageType.Local)
{ {
@@ -94,7 +106,7 @@ namespace Foxel.Services.Background.Processors
} }
else 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, localFilePath = await storageService.ExecuteAsync(picture.StorageType,
provider => provider.DownloadFileAsync(originalFilePathFromPayload)); provider => provider.DownloadFileAsync(originalFilePathFromPayload));
isTempFile = true; isTempFile = true;
@@ -105,22 +117,28 @@ namespace Foxel.Services.Background.Processors
throw new Exception($"找不到图片文件: {localFilePath} (源路径: {originalFilePathFromPayload})"); throw new Exception($"找不到图片文件: {localFilePath} (源路径: {originalFilePathFromPayload})");
} }
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 20, currentBackgroundTaskState: currentBackgroundTaskState); await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 50, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress
thumbnailForAI = localFilePath;
if (string.IsNullOrEmpty(picture.ThumbnailPath)) if (string.IsNullOrEmpty(picture.ThumbnailPath))
{ {
var tempThumbContainer = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); var tempThumbContainer = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempThumbContainer); 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); 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); 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( string storedThumbnailPath = await storageService.ExecuteAsync(
picture.StorageType, picture.StorageType,
@@ -129,104 +147,18 @@ namespace Foxel.Services.Background.Processors
if (Directory.Exists(tempThumbContainer)) Directory.Delete(tempThumbContainer, true); if (Directory.Exists(tempThumbContainer)) Directory.Delete(tempThumbContainer, true);
} }
else await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 80, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress
{
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);
var exifInfo = await ImageHelper.ExtractExifInfoAsync(localFilePath); var exifInfo = await ImageHelper.ExtractExifInfoAsync(localFilePath);
picture.ExifInfo = exifInfo; picture.ExifInfo = exifInfo;
picture.TakenAt = ImageHelper.ParseExifDateTime(exifInfo.DateTimeOriginal); 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<IVectorDbService>();
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<Tag>();
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<Tag>();
if (!picture.User.Tags.Any(t => t.Id == existingTag.Id)) picture.User.Tags.Add(existingTag);
}
}
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100, completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState); await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100, completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState);
} }
catch (Exception ex) 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); 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 finally
{ {
@@ -234,11 +166,6 @@ namespace Foxel.Services.Background.Processors
{ {
try { File.Delete(localFilePath); } catch (Exception ex) { _logger.LogWarning(ex, "删除临时主图片文件失败: {FilePath}", localFilePath); } 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); }
}
} }
} }
+109 -127
View File
@@ -343,6 +343,8 @@ public class PictureService(
Name = picture.Name, Name = picture.Name,
Path = storageService.ExecuteAsync(picture.StorageType, provider => Path = storageService.ExecuteAsync(picture.StorageType, provider =>
Task.FromResult(provider.GetUrl(picture.Path ?? string.Empty))).Result, 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) ? ThumbnailPath = !string.IsNullOrEmpty(picture.ThumbnailPath) ?
storageService.ExecuteAsync(picture.StorageType, provider => storageService.ExecuteAsync(picture.StorageType, provider =>
Task.FromResult(provider.GetUrl(picture.ThumbnailPath))).Result Task.FromResult(provider.GetUrl(picture.ThumbnailPath))).Result
@@ -479,126 +481,95 @@ public class PictureService(
quality = int.Parse(defaultQualityConfig); quality = int.Parse(defaultQualityConfig);
} }
string originalFileName = fileName; string baseName = Guid.NewGuid().ToString();
string finalFileName = fileName; string originalFileExtension = Path.GetExtension(fileName);
string finalContentType = contentType; string originalStorageFileName = $"{baseName}{originalFileExtension}";
Stream finalStream = fileStream;
// 如果需要格式转换 string? tempOriginalLocalPath = null;
if (convertToFormat != ImageFormat.Original) string? tempConvertedHdLocalPath = null;
{ string? tempThumbnailLocalPath = null;
// 创建临时文件保存原始上传内容
string tempOriginalFile = Path.GetTempFileName(); string storedOriginalPath;
string tempConvertedFile = Path.GetTempFileName(); string storedHdPath;
string? storedThumbnailPath = null;
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;
try try
{ {
// 如果是匿名用户或者需要立即生成缩略图,先保存到临时文件 tempOriginalLocalPath = Path.GetTempFileName() + originalFileExtension;
bool shouldGenerateThumbnail = userId.HasValue; // 只为注册用户生成缩略图 File.Move(Path.GetTempFileName(), tempOriginalLocalPath);
await using (var tempFileStream = new FileStream(tempOriginalLocalPath, FileMode.Create))
if (shouldGenerateThumbnail)
{ {
tempOriginalFileForThumbnail = Path.GetTempFileName(); await fileStream.CopyToAsync(tempFileStream);
// 保存原始文件到临时位置用于生成缩略图
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 using (var originalLocalStream = new FileStream(tempOriginalLocalPath, FileMode.Open, FileAccess.Read))
string relativePath = await storageService.ExecuteAsync(storageType.Value, {
provider => provider.SaveAsync(finalStream, finalFileName, finalContentType)); storedOriginalPath = await storageService.ExecuteAsync(storageType.Value,
provider => provider.SaveAsync(originalLocalStream, originalStorageFileName, contentType));
}
string? thumbnailPath = null; string hdStorageFileName;
string hdContentType;
string sourceForHdProcessing = tempOriginalLocalPath;
// 生成缩略图 if (convertToFormat != ImageFormat.Original)
if (shouldGenerateThumbnail && !string.IsNullOrEmpty(tempOriginalFileForThumbnail)) {
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 try
{ {
tempThumbnailFile = Path.GetTempFileName(); tempThumbnailLocalPath = Path.GetTempFileName() + ".webp";
string thumbnailFileName = Path.ChangeExtension(tempThumbnailFile, ".webp"); File.Move(Path.GetTempFileName(), tempThumbnailLocalPath);
File.Move(tempThumbnailFile, thumbnailFileName);
tempThumbnailFile = thumbnailFileName;
// 生成缩略图 await ImageHelper.CreateThumbnailAsync(tempOriginalLocalPath, tempThumbnailLocalPath, 500);
await ImageHelper.CreateThumbnailAsync(tempOriginalFileForThumbnail, tempThumbnailFile, 500);
// 上传缩略图 string thumbnailUploadFileName = $"{baseName}-thumbnail.webp";
await using var thumbnailFileStream = new FileStream(tempThumbnailFile, FileMode.Open, FileAccess.Read); await using var thumbnailFileStream = new FileStream(tempThumbnailLocalPath, FileMode.Open, FileAccess.Read);
var thumbnailStorageFileName = Path.GetFileNameWithoutExtension(finalFileName) + "_thumb.webp"; storedThumbnailPath = await storageService.ExecuteAsync(storageType.Value,
provider => provider.SaveAsync(thumbnailFileStream, thumbnailUploadFileName, "image/webp"));
thumbnailPath = await storageService.ExecuteAsync(storageType.Value,
provider => provider.SaveAsync(thumbnailFileStream, thumbnailStorageFileName, "image/webp"));
} }
catch (Exception ex) 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(fileName);
string initialTitle = Path.GetFileNameWithoutExtension(originalFileName);
string initialDescription = $"Uploaded on {DateTime.UtcNow}"; string initialDescription = $"Uploaded on {DateTime.UtcNow}";
await using var dbContext = await contextFactory.CreateDbContextAsync(); await using var dbContext = await contextFactory.CreateDbContextAsync();
// 获取用户
User? user = null; User? user = null;
if (userId is not null) if (userId is not null)
{ {
user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId); user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (user == null) if (user == null) throw new Exception("找不到指定的用户");
{
throw new Exception("找不到指定的用户");
}
} }
// 检查相册是否存在并且属于当前用户
Album? album = null; Album? album = null;
if (albumId.HasValue) if (albumId.HasValue)
{ {
@@ -616,38 +587,45 @@ public class PictureService(
} }
} }
bool isAnonymous = userId == null;
// 创建图片对象并保存到数据库 // 创建图片对象并保存到数据库
var picture = new Picture var picture = new Picture
{ {
Name = initialTitle, Name = initialTitle,
Description = initialDescription, 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, User = user,
Permission = permission, Permission = permission,
AlbumId = albumId, AlbumId = albumId,
StorageType = storageType.Value, StorageType = storageType.Value,
// ProcessingStatus 等字段已移除 // ProcessingStatus 等字段已移除
ThumbnailPath = thumbnailPath // 如果生成了缩略图,则保存其存储路径/键
}; };
dbContext.Pictures.Add(picture); dbContext.Pictures.Add(picture);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
if (!isAnonymous) if (userId != null) // Only queue for registered users
{ {
// 使用 relativePath (即存储服务中的对象键/路径) 来排队任务 // Pass OriginalPath for EXIF extraction and other initial processing
await backgroundTaskQueue.QueuePictureProcessingTaskAsync(picture.Id, picture.Path); 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 var pictureResponse = new PictureResponse
{ {
Id = picture.Id, Id = picture.Id,
Name = picture.Name, Name = picture.Name,
Path = await storageService.ExecuteAsync(picture.StorageType, provider => 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) ThumbnailPath = !string.IsNullOrEmpty(picture.ThumbnailPath)
? await storageService.ExecuteAsync(picture.StorageType, provider => ? await storageService.ExecuteAsync(picture.StorageType, provider =>
Task.FromResult(provider.GetUrl(picture.ThumbnailPath))) Task.FromResult(provider.GetUrl(picture.ThumbnailPath)))
@@ -665,27 +643,18 @@ public class PictureService(
} }
finally finally
{ {
// 清理临时文件 // Clean up temporary local files
if (!string.IsNullOrEmpty(tempOriginalFileForThumbnail) && File.Exists(tempOriginalFileForThumbnail)) if (!string.IsNullOrEmpty(tempOriginalLocalPath) && File.Exists(tempOriginalLocalPath))
{ {
try { File.Delete(tempOriginalFileForThumbnail); } catch { } try { File.Delete(tempOriginalLocalPath); } catch { /* ignored */ }
} }
if (!string.IsNullOrEmpty(tempConvertedHdLocalPath) && File.Exists(tempConvertedHdLocalPath))
if (!string.IsNullOrEmpty(tempThumbnailFile) && File.Exists(tempThumbnailFile))
{ {
try { File.Delete(tempThumbnailFile); } catch { } try { File.Delete(tempConvertedHdLocalPath); } catch { /* ignored */ }
} }
if (!string.IsNullOrEmpty(tempThumbnailLocalPath) && File.Exists(tempThumbnailLocalPath))
// 清理转换后的临时流
if (finalStream != fileStream && finalStream is FileStream tempFileStream)
{ {
string tempFilePath = tempFileStream.Name; try { File.Delete(tempThumbnailLocalPath); } catch { /* ignored */ }
finalStream.Dispose();
if (File.Exists(tempFilePath)) File.Delete(tempFilePath);
// 同时清理原始临时文件
string tempOriginalFileFromConvert = Path.ChangeExtension(tempFilePath, null);
if (File.Exists(tempOriginalFileFromConvert)) File.Delete(tempOriginalFileFromConvert);
} }
} }
} }
@@ -736,10 +705,10 @@ public class PictureService(
} }
var filesToDelete = 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) 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)); picture.StorageType));
} }
@@ -749,7 +718,7 @@ public class PictureService(
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
} }
foreach (var (pictureId, path, thumbnailPath, userId, storageType) in filesToDelete) foreach (var (pictureId, path, thumbnailPath, originalPath, userId, storageType) in filesToDelete)
{ {
try try
{ {
@@ -757,11 +726,21 @@ public class PictureService(
try try
{ {
// 使用存储服务删除文件 // Delete original file
await storageService.ExecuteAsync(storageType, if (!string.IsNullOrEmpty(originalPath))
provider => provider.DeleteAsync(path)); {
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)) if (!string.IsNullOrEmpty(thumbnailPath))
{ {
await storageService.ExecuteAsync(storageType, await storageService.ExecuteAsync(storageType,
@@ -868,8 +847,11 @@ public class PictureService(
Name = picture.Name, Name = picture.Name,
Path = await storageService.ExecuteAsync(picture.StorageType, provider => Path = await storageService.ExecuteAsync(picture.StorageType, provider =>
Task.FromResult(provider.GetUrl(picture.Path ?? string.Empty))), Task.FromResult(provider.GetUrl(picture.Path ?? string.Empty))),
ThumbnailPath = await storageService.ExecuteAsync(picture.StorageType, provider => OriginalPath = await storageService.ExecuteAsync(picture.StorageType, provider => // Added OriginalPath
Task.FromResult(provider.GetUrl(picture.ThumbnailPath ?? string.Empty))), 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, Description = picture.Description,
CreatedAt = picture.CreatedAt, CreatedAt = picture.CreatedAt,
Tags = picture.Tags?.Select(t => t.Name).ToList() ?? new List<string>(), Tags = picture.Tags?.Select(t => t.Name).ToList() ?? new List<string>(),
@@ -68,8 +68,10 @@ public class CosStorageProvider(IConfigService configService, ILogger<CosStorage
{ {
// 创建唯一的文件存储路径 // 创建唯一的文件存储路径
string currentDate = DateTime.Now.ToString("yyyy/MM"); string currentDate = DateTime.Now.ToString("yyyy/MM");
string ext = Path.GetExtension(fileName); // fileName 参数现在是期望的最终文件名部分,例如 "guid.ext"
string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}"; // string ext = Path.GetExtension(fileName); // 旧的 fileName 是原始文件名,现在 fileName 是目标文件名
// string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}"; // 旧逻辑
string objectKey = $"{currentDate}/{fileName}"; // 新逻辑
// 创建临时文件 // 创建临时文件
string tempPath = Path.GetTempFileName(); string tempPath = Path.GetTempFileName();
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging;
namespace Foxel.Services.Storage.Providers; namespace Foxel.Services.Storage.Providers;
[StorageProvider(StorageType.Local)] [StorageProvider(StorageType.Local)]
public class LocalStorageProvider(IConfigService configService, ILogger<LocalStorageProvider> logger) : IStorageProvider public class LocalStorageProvider(IConfigService configService) : IStorageProvider
{ {
private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads"); private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
@@ -15,13 +15,12 @@ public class LocalStorageProvider(IConfigService configService, ILogger<LocalSto
string folder = Path.Combine(_baseDirectory, currentDate); string folder = Path.Combine(_baseDirectory, currentDate);
Directory.CreateDirectory(folder); Directory.CreateDirectory(folder);
string ext = Path.GetExtension(fileName); string newFileName = fileName;
string newFileName = $"{Guid.NewGuid()}{ext}";
string filePath = Path.Combine(folder, newFileName); string filePath = Path.Combine(folder, newFileName);
await using var output = new FileStream(filePath, FileMode.Create); await using var output = new FileStream(filePath, FileMode.Create);
await fileStream.CopyToAsync(output); await fileStream.CopyToAsync(output);
return $"/Uploads/{currentDate}/{newFileName}"; return $"/Uploads/{currentDate}/{newFileName}";
} }
public Task DeleteAsync(string storagePath) public Task DeleteAsync(string storagePath)
@@ -43,8 +43,10 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
{ {
// 创建唯一的文件存储路径 // 创建唯一的文件存储路径
string currentDate = DateTime.Now.ToString("yyyy/MM"); string currentDate = DateTime.Now.ToString("yyyy/MM");
string ext = Path.GetExtension(fileName); // fileName 参数现在是期望的最终文件名部分,例如 "guid.ext"
string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}"; // string ext = Path.GetExtension(fileName); // 旧的 fileName 是原始文件名,现在 fileName 是目标文件名
// string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}"; // 旧逻辑
string objectKey = $"{currentDate}/{fileName}"; // 新逻辑
using var client = CreateClient(); using var client = CreateClient();
using var transferUtility = new TransferUtility(client); using var transferUtility = new TransferUtility(client);
@@ -33,8 +33,10 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
// 创建唯一的文件存储路径 // 创建唯一的文件存储路径
string currentDate = DateTime.Now.ToString("yyyy/MM"); string currentDate = DateTime.Now.ToString("yyyy/MM");
string ext = Path.GetExtension(fileName); // fileName 参数现在是期望的最终文件名部分,例如 "guid.ext"
string newFileName = $"{Guid.NewGuid()}{ext}"; // string ext = Path.GetExtension(fileName); // 旧的 fileName 是原始文件名,现在 fileName 是目标文件名
// string newFileName = $"{Guid.NewGuid()}{ext}"; // 旧逻辑
string newFileName = fileName; // 新逻辑
string relativePath = $"{basePath}/{currentDate}/{newFileName}"; string relativePath = $"{basePath}/{currentDate}/{newFileName}";
// 确保目录存在 // 确保目录存在
+7 -1
View File
@@ -13,6 +13,7 @@ const { Title, Text } = Typography;
// 定义任务类型映射 // 定义任务类型映射
const taskTypeDisplayMapping: { [key: number]: string } = { const taskTypeDisplayMapping: { [key: number]: string } = {
0: '图片处理', 0: '图片处理',
1: '视觉识别', // 新增视觉识别任务类型
}; };
const BackgroundTasks: React.FC = () => { const BackgroundTasks: React.FC = () => {
@@ -126,7 +127,7 @@ const BackgroundTasks: React.FC = () => {
dataIndex: 'taskName', // Changed dataIndex dataIndex: 'taskName', // Changed dataIndex
key: 'taskName', key: 'taskName',
render: (text: string, record: TaskDetailsViewModel) => ( // Updated type and logic render: (text: string, record: TaskDetailsViewModel) => ( // Updated type and logic
record.taskType === 0 && record.relatedEntityId // 修正: 使用数字 0 比较 (record.taskType === 0 || record.taskType === 1) && record.relatedEntityId // 检查是否为图片处理或视觉识别任务
? <Link to={`/pictures/${record.relatedEntityId}`}>{text}</Link> ? <Link to={`/pictures/${record.relatedEntityId}`}>{text}</Link>
: text : text
), ),
@@ -151,6 +152,11 @@ const BackgroundTasks: React.FC = () => {
key: 'taskType', key: 'taskType',
render: (taskType: number | undefined) => // 接收数字类型的 taskType render: (taskType: number | undefined) => // 接收数字类型的 taskType
taskType !== undefined ? taskTypeDisplayMapping[taskType] || `未知类型 (${taskType})` : '-', taskType !== undefined ? taskTypeDisplayMapping[taskType] || `未知类型 (${taskType})` : '-',
filters: [ // 可以为任务类型添加筛选器
{ text: '图片处理', value: 0 },
{ text: '视觉识别', value: 1 },
],
onFilter: (value, record: TaskDetailsViewModel) => record.taskType === (value as number),
}, },
{ {
title: '进度', title: '进度',
+5 -4
View File
@@ -157,19 +157,20 @@ const routes: RouteConfig[] = [
hideInMenu: true, hideInMenu: true,
breadcrumb: { breadcrumb: {
title: '用户详情', title: '用户详情',
parent: 'admin-user' // 修改: 指向父路由的 key parent: 'admin-user'
} }
}, },
{ {
path: 'pictures', path: 'pictures',
key: 'admin-picture', key: 'admin-picture',
icon: <PictureOutlined />, icon: <PictureOutlined />,
label: '图片管理', label: '图片',
element: <PictureManagement />, element: <PictureManagement />,
area: 'admin', area: 'admin',
groupLabel: '资源管理',
breadcrumb: { breadcrumb: {
title: '图片管理' title: '图片'
} }
}, },
{ {
path: 'log', path: 'log',
+3 -1
View File
@@ -7,7 +7,9 @@
"Default": "Information", "Default": "Information",
"System.Net.Http.HttpClient": "Warning", "System.Net.Http.HttpClient": "Warning",
"Microsoft.AspNetCore": "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": "" "AllowedHosts": ""