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 39c40d2746
17 changed files with 473 additions and 269 deletions

View File

@@ -40,6 +40,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<WebDavStorageProvider>();
services.AddSingleton<IStorageService, StorageService>();
services.AddSingleton<PictureTaskProcessor>();
services.AddSingleton<VisualRecognitionTaskProcessor>();
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
}

View File

@@ -26,7 +26,6 @@
<ItemGroup>
<Folder Include="Uploads\" />
<Folder Include="Uploads\2025\" />
</ItemGroup>
<ItemGroup>

View File

@@ -7,6 +7,7 @@ namespace Foxel.Models.DataBase
public enum TaskType
{
PictureProcessing = 0,
VisualRecognition = 1,
}
public enum TaskExecutionStatus

View File

@@ -9,8 +9,16 @@ public class Picture : BaseModel
{
[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;
/// <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(2000)] public string Description { get; set; } = string.Empty;

View File

@@ -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<string>? Tags { get; set; }

View File

@@ -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<BackgroundTaskQueue> logger)
{
_serviceProvider = serviceProvider; // Keep IServiceProvider to resolve processors
_serviceProvider = serviceProvider;
_contextFactory = contextFactory;
_logger = logger;
_processingTasks = new List<Task>();
_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<Guid>(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<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;
}
@@ -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<PictureTaskProcessor>();
break;
// Future task types can be added here
case TaskType.VisualRecognition: // Added case for VisualRecognition
processor = scope.ServiceProvider.GetRequiredService<VisualRecognitionTaskProcessor>();
break;
default:
_logger.LogError("未找到任务类型 {TaskType} 的处理器: TaskId={TaskId}", taskToCheck.Type, taskToCheck.Id);
await MarkTaskAsFailedByQueue(taskToCheck.Id, $"未找到任务类型 {taskToCheck.Type} 的处理器。");

View File

@@ -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
{
/// <summary>
/// 将图片处理任务添加到队列
/// 将图片处理任务(元数据和缩略图)添加到队列
/// </summary>
/// <param name="pictureId">图片ID</param>
/// <param name="originalFilePath">原始图片路径</param>
/// <returns>任务ID</returns>
Task<Guid> QueuePictureProcessingTaskAsync(int pictureId, string originalFilePath);
/// <summary>
/// 将视觉识别任务添加到队列
/// </summary>
/// <param name="payload">视觉识别任务的Payload</param>
/// <returns>任务ID</returns>
Task<Guid> QueueVisualRecognitionTaskAsync(VisualRecognitionPayload payload);
/// <summary>
/// 获取用户的所有任务状态 (目前主要指图片处理任务)
/// </summary>

View File

@@ -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<MyDbContext> _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<IAiService>();
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
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<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 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); }
}
}
}

View File

@@ -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<MyDbContext> _contextFactory;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<VisualRecognitionTaskProcessor> _logger;
private readonly IWebHostEnvironment _environment;
public VisualRecognitionTaskProcessor(
IDbContextFactory<MyDbContext> contextFactory,
IServiceProvider serviceProvider,
ILogger<VisualRecognitionTaskProcessor> 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<VisualRecognitionPayload>(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<IAiService>();
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
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<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(); // 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);
}
}
}
}

View File

@@ -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<string>(),

View File

@@ -68,8 +68,10 @@ public class CosStorageProvider(IConfigService configService, ILogger<CosStorage
{
// 创建唯一的文件存储路径
string currentDate = DateTime.Now.ToString("yyyy/MM");
string ext = Path.GetExtension(fileName);
string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}";
// fileName 参数现在是期望的最终文件名部分,例如 "guid.ext"
// string ext = Path.GetExtension(fileName); // 旧的 fileName 是原始文件名,现在 fileName 是目标文件名
// string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}"; // 旧逻辑
string objectKey = $"{currentDate}/{fileName}"; // 新逻辑
// 创建临时文件
string tempPath = Path.GetTempFileName();

View File

@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging;
namespace Foxel.Services.Storage.Providers;
[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");
@@ -15,13 +15,12 @@ public class LocalStorageProvider(IConfigService configService, ILogger<LocalSto
string folder = Path.Combine(_baseDirectory, currentDate);
Directory.CreateDirectory(folder);
string ext = Path.GetExtension(fileName);
string newFileName = $"{Guid.NewGuid()}{ext}";
string newFileName = fileName;
string filePath = Path.Combine(folder, newFileName);
await using var output = new FileStream(filePath, FileMode.Create);
await fileStream.CopyToAsync(output);
return $"/Uploads/{currentDate}/{newFileName}";
return $"/Uploads/{currentDate}/{newFileName}";
}
public Task DeleteAsync(string storagePath)

View File

@@ -43,8 +43,10 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
{
// 创建唯一的文件存储路径
string currentDate = DateTime.Now.ToString("yyyy/MM");
string ext = Path.GetExtension(fileName);
string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}";
// fileName 参数现在是期望的最终文件名部分,例如 "guid.ext"
// string ext = Path.GetExtension(fileName); // 旧的 fileName 是原始文件名,现在 fileName 是目标文件名
// string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}"; // 旧逻辑
string objectKey = $"{currentDate}/{fileName}"; // 新逻辑
using var client = CreateClient();
using var transferUtility = new TransferUtility(client);

View File

@@ -33,8 +33,10 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
// 创建唯一的文件存储路径
string currentDate = DateTime.Now.ToString("yyyy/MM");
string ext = Path.GetExtension(fileName);
string newFileName = $"{Guid.NewGuid()}{ext}";
// fileName 参数现在是期望的最终文件名部分,例如 "guid.ext"
// string ext = Path.GetExtension(fileName); // 旧的 fileName 是原始文件名,现在 fileName 是目标文件名
// string newFileName = $"{Guid.NewGuid()}{ext}"; // 旧逻辑
string newFileName = fileName; // 新逻辑
string relativePath = $"{basePath}/{currentDate}/{newFileName}";
// 确保目录存在

View File

@@ -13,6 +13,7 @@ const { Title, Text } = Typography;
// 定义任务类型映射
const taskTypeDisplayMapping: { [key: number]: string } = {
0: '图片处理',
1: '视觉识别', // 新增视觉识别任务类型
};
const BackgroundTasks: React.FC = () => {
@@ -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 // 检查是否为图片处理或视觉识别任务
? <Link to={`/pictures/${record.relatedEntityId}`}>{text}</Link>
: 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: '进度',

View File

@@ -157,19 +157,20 @@ const routes: RouteConfig[] = [
hideInMenu: true,
breadcrumb: {
title: '用户详情',
parent: 'admin-user' // 修改: 指向父路由的 key
parent: 'admin-user'
}
},
{
path: 'pictures',
key: 'admin-picture',
icon: <PictureOutlined />,
label: '图片管理',
label: '图片',
element: <PictureManagement />,
area: 'admin',
groupLabel: '资源管理',
breadcrumb: {
title: '图片管理'
}
title: '图片'
}
},
{
path: 'log',

View File

@@ -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": ""