mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 07:36:53 +08:00
feat(vector-db): integrate vector database for image search
- Replace pgvector with Microsoft Vector DB for image vector storage and search - Update Picture model to use float[] instead of Vector type - Modify PictureService to use VectorDbService for vector search - Remove vector-related code from MyDbContext - Add PictureVector model for Vector DB integration
This commit is contained in:
@@ -4,8 +4,6 @@ using Microsoft.AspNetCore.Authentication.Cookies;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Foxel.Services.Attributes;
|
|
||||||
using System.Reflection;
|
|
||||||
using Foxel.Services.AI;
|
using Foxel.Services.AI;
|
||||||
using Foxel.Services.Auth;
|
using Foxel.Services.Auth;
|
||||||
using Foxel.Services.Background;
|
using Foxel.Services.Background;
|
||||||
@@ -48,7 +46,7 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
Console.WriteLine($"数据库连接: {connectionString}");
|
Console.WriteLine($"数据库连接: {connectionString}");
|
||||||
services.AddDbContextFactory<MyDbContext>(options =>
|
services.AddDbContextFactory<MyDbContext>(options =>
|
||||||
options.UseNpgsql(connectionString, o => o.UseVector()));
|
options.UseNpgsql(connectionString));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddApplicationOpenApi(this IServiceCollection services)
|
public static void AddApplicationOpenApi(this IServiceCollection services)
|
||||||
|
|||||||
+2
-3
@@ -16,10 +16,9 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.54.0-preview" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="Pgvector" Version="0.3.1" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.4.6" />
|
||||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.2" />
|
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.3.1" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
|
||||||
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.46" />
|
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.46" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Foxel.Services.Attributes;
|
using Foxel.Services.Attributes;
|
||||||
using Vector = Pgvector.Vector;
|
|
||||||
|
|
||||||
namespace Foxel.Models.DataBase;
|
namespace Foxel.Models.DataBase;
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ public class Picture : BaseModel
|
|||||||
[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;
|
||||||
[Column(TypeName = "vector(1024)")] public Vector? Embedding { get; set; }
|
public float[]? Embedding { get; set; }
|
||||||
|
|
||||||
public DateTime? TakenAt { get; set; }
|
public DateTime? TakenAt { get; set; }
|
||||||
|
|
||||||
@@ -57,8 +56,8 @@ public enum PermissionType
|
|||||||
|
|
||||||
public enum ProcessingStatus
|
public enum ProcessingStatus
|
||||||
{
|
{
|
||||||
Pending, // 等待处理
|
Pending, // 等待处理
|
||||||
Processing, // 处理中
|
Processing, // 处理中
|
||||||
Completed, // 处理完成
|
Completed, // 处理完成
|
||||||
Failed // 处理失败
|
Failed // 处理失败
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Foxel.Models.Vector;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.VectorData;
|
||||||
|
|
||||||
|
public class PictureVector
|
||||||
|
{
|
||||||
|
[VectorStoreKey] public int Id { get; set; }
|
||||||
|
[VectorStoreData] public string? Name { get; set; }
|
||||||
|
|
||||||
|
[VectorStoreVector(Dimensions: 1024, DistanceFunction = DistanceFunction.CosineSimilarity)]
|
||||||
|
public ReadOnlyMemory<float>? Embedding { get; set; }
|
||||||
|
}
|
||||||
@@ -5,17 +5,6 @@ namespace Foxel;
|
|||||||
|
|
||||||
public class MyDbContext(DbContextOptions<MyDbContext> options) : DbContext(options)
|
public class MyDbContext(DbContextOptions<MyDbContext> options) : DbContext(options)
|
||||||
{
|
{
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.HasPostgresExtension("vector");
|
|
||||||
|
|
||||||
modelBuilder.Entity<Picture>()
|
|
||||||
.HasIndex(p => p.Embedding)
|
|
||||||
.HasMethod("ivfflat")
|
|
||||||
.HasOperators("vector_cosine_ops")
|
|
||||||
.HasStorageParameter("lists", 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DbSet<Picture> Pictures { get; set; } = null!;
|
public DbSet<Picture> Pictures { get; set; } = null!;
|
||||||
public DbSet<User> Users { get; set; } = null!;
|
public DbSet<User> Users { get; set; } = null!;
|
||||||
public DbSet<Tag> Tags { get; set; } = null!;
|
public DbSet<Tag> Tags { get; set; } = null!;
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,7 @@
|
|||||||
using Foxel;
|
|
||||||
using Foxel.Extensions;
|
using Foxel.Extensions;
|
||||||
using Foxel.Services.Initializer;
|
using Foxel.Services.Initializer;
|
||||||
|
using Foxel.Services.VectorDB;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var environment = builder.Environment;
|
var environment = builder.Environment;
|
||||||
@@ -17,6 +16,7 @@ builder.Services.AddHttpContextAccessor();
|
|||||||
builder.Services.AddApplicationAuthentication();
|
builder.Services.AddApplicationAuthentication();
|
||||||
builder.Services.AddApplicationAuthorization();
|
builder.Services.AddApplicationAuthorization();
|
||||||
builder.Services.AddApplicationCors();
|
builder.Services.AddApplicationCors();
|
||||||
|
builder.Services.AddSingleton<VectorDbService>();
|
||||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
{
|
{
|
||||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
|
|
||||||
// 1. 获取图片信息
|
// 1. 获取图片信息
|
||||||
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 10);
|
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 10);
|
||||||
|
|
||||||
if (picture == null)
|
if (picture == null)
|
||||||
{
|
{
|
||||||
throw new Exception($"找不到ID为{task.PictureId}的图片");
|
throw new Exception($"找不到ID为{task.PictureId}的图片");
|
||||||
@@ -225,7 +225,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
{
|
{
|
||||||
// 非本地存储需要先下载文件
|
// 非本地存储需要先下载文件
|
||||||
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 15);
|
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 15);
|
||||||
localFilePath = await storageService.ExecuteAsync(picture.StorageType,
|
localFilePath = await storageService.ExecuteAsync(picture.StorageType,
|
||||||
provider => provider.DownloadFileAsync(picture.Path));
|
provider => provider.DownloadFileAsync(picture.Path));
|
||||||
isTempFile = true;
|
isTempFile = true;
|
||||||
}
|
}
|
||||||
@@ -245,11 +245,12 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
|
|
||||||
// 更新缩略图路径到数据库
|
// 更新缩略图路径到数据库
|
||||||
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 25);
|
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 25);
|
||||||
|
|
||||||
if (picture.StorageType == StorageType.Local)
|
if (picture.StorageType == StorageType.Local)
|
||||||
{
|
{
|
||||||
// 本地存储缩略图
|
// 本地存储缩略图
|
||||||
var relativeThumbnailPath = $"/Uploads/{Path.GetRelativePath("Uploads", Path.GetDirectoryName(thumbnailPath)!)}/{Path.GetFileName(thumbnailPath)}";
|
var relativeThumbnailPath =
|
||||||
|
$"/Uploads/{Path.GetRelativePath("Uploads", Path.GetDirectoryName(thumbnailPath)!)}/{Path.GetFileName(thumbnailPath)}";
|
||||||
picture.ThumbnailPath = relativeThumbnailPath.Replace('\\', '/');
|
picture.ThumbnailPath = relativeThumbnailPath.Replace('\\', '/');
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -275,7 +276,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
|
|
||||||
// 4. 从EXIF中提取拍摄时间并确保是UTC格式
|
// 4. 从EXIF中提取拍摄时间并确保是UTC格式
|
||||||
picture.TakenAt = ImageHelper.ParseExifDateTime(exifInfo.DateTimeOriginal);
|
picture.TakenAt = ImageHelper.ParseExifDateTime(exifInfo.DateTimeOriginal);
|
||||||
|
|
||||||
// 保存缩略图和EXIF信息的更改,确保这些基本信息即使在后续步骤失败时也能保存
|
// 保存缩略图和EXIF信息的更改,确保这些基本信息即使在后续步骤失败时也能保存
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
@@ -300,7 +301,18 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 60);
|
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 60);
|
||||||
var combinedText = $"{finalTitle}. {finalDescription}";
|
var combinedText = $"{finalTitle}. {finalDescription}";
|
||||||
var embedding = await aiService.GetEmbeddingAsync(combinedText);
|
var embedding = await aiService.GetEmbeddingAsync(combinedText);
|
||||||
picture.Embedding = new Pgvector.Vector(embedding);
|
picture.Embedding = embedding;
|
||||||
|
if (picture.UserId.HasValue && embedding.Length > 0)
|
||||||
|
{
|
||||||
|
var vectorDbService = scope.ServiceProvider.GetRequiredService<VectorDB.VectorDbService>();
|
||||||
|
var pictureVector = new Foxel.Models.Vector.PictureVector
|
||||||
|
{
|
||||||
|
Id = picture.Id,
|
||||||
|
Name = picture.Name,
|
||||||
|
Embedding = embedding
|
||||||
|
};
|
||||||
|
await vectorDbService.AddPictureToUserCollectionAsync(picture.UserId.Value, pictureVector);
|
||||||
|
}
|
||||||
|
|
||||||
// 8. 获取所有可用标签名称
|
// 8. 获取所有可用标签名称
|
||||||
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 70);
|
await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 70);
|
||||||
@@ -367,7 +379,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
{
|
{
|
||||||
picture.ProcessingStatus = ProcessingStatus.Failed;
|
picture.ProcessingStatus = ProcessingStatus.Failed;
|
||||||
picture.ProcessingError = ex.Message;
|
picture.ProcessingError = ex.Message;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ using Foxel.Services.Attributes;
|
|||||||
using Foxel.Services.Background;
|
using Foxel.Services.Background;
|
||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
using Foxel.Services.Storage;
|
using Foxel.Services.Storage;
|
||||||
|
using Foxel.Services.VectorDB;
|
||||||
using Foxel.Utils;
|
using Foxel.Utils;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Pgvector;
|
|
||||||
using Pgvector.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Foxel.Services.Media;
|
namespace Foxel.Services.Media;
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ public class PictureService(
|
|||||||
IAiService embeddingService,
|
IAiService embeddingService,
|
||||||
IConfigService configuration,
|
IConfigService configuration,
|
||||||
IBackgroundTaskQueue backgroundTaskQueue,
|
IBackgroundTaskQueue backgroundTaskQueue,
|
||||||
|
VectorDbService vectorDbService,
|
||||||
IStorageService storageService)
|
IStorageService storageService)
|
||||||
: IPictureService
|
: IPictureService
|
||||||
{
|
{
|
||||||
@@ -61,7 +61,7 @@ public class PictureService(
|
|||||||
{
|
{
|
||||||
// 如果向量搜索失败,记录错误并回退到标准搜索
|
// 如果向量搜索失败,记录错误并回退到标准搜索
|
||||||
Console.WriteLine($"向量搜索失败,回退到标准搜索: {ex.Message}");
|
Console.WriteLine($"向量搜索失败,回退到标准搜索: {ex.Message}");
|
||||||
|
|
||||||
// 如果是明确的配置错误,则向上抛出异常
|
// 如果是明确的配置错误,则向上抛出异常
|
||||||
if (ex.Message.Contains("请检查嵌入模型配置"))
|
if (ex.Message.Contains("请检查嵌入模型配置"))
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ public class PictureService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行标准搜索(作为默认方法或向量搜索的回退选项)
|
// 执行标准搜索(作为默认方法或向量搜索的回退选项)
|
||||||
return await PerformStandardSearchAsync(
|
return await PerformStandardSearchAsync(
|
||||||
dbContext, page, pageSize, searchQuery, tags,
|
dbContext, page, pageSize, searchQuery, tags,
|
||||||
@@ -95,78 +95,41 @@ public class PictureService(
|
|||||||
int? ownerId,
|
int? ownerId,
|
||||||
bool includeAllPublic)
|
bool includeAllPublic)
|
||||||
{
|
{
|
||||||
try
|
var queryEmbedding = await embeddingService.GetEmbeddingAsync(searchQuery);
|
||||||
|
var res = await vectorDbService.SearchAsync(queryEmbedding, userId);
|
||||||
|
|
||||||
|
var ids = res.Select(r => r.Id).ToList();
|
||||||
|
var picturesData = await dbContext.Pictures
|
||||||
|
.Include(p => p.Tags)
|
||||||
|
.Include(p => p.User)
|
||||||
|
.Where(p => ids.Contains(p.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var picturesOrdered = ids
|
||||||
|
.Select(id => picturesData.FirstOrDefault(p => p.Id == id))
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList();
|
||||||
|
var paginatedResults = picturesOrdered
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(p => MapPictureToResponse(p!))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var totalCount = picturesOrdered.Count;
|
||||||
|
|
||||||
|
await PopulateFavoriteInfo(dbContext, paginatedResults, userId);
|
||||||
|
|
||||||
|
if (userId.HasValue)
|
||||||
{
|
{
|
||||||
float[]? queryEmbedding = null;
|
await PopulateAlbumInfo(dbContext, paginatedResults, userId.Value);
|
||||||
try
|
|
||||||
{
|
|
||||||
queryEmbedding = await embeddingService.GetEmbeddingAsync(searchQuery);
|
|
||||||
|
|
||||||
// 检查嵌入向量是否有效
|
|
||||||
if (queryEmbedding == null || queryEmbedding.Length == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("嵌入模型返回了空向量");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"向量搜索失败,请检查嵌入模型配置: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryVector = new Vector(queryEmbedding);
|
|
||||||
|
|
||||||
// 构建基础查询
|
|
||||||
var query = dbContext.Pictures
|
|
||||||
.Include(p => p.Tags)
|
|
||||||
.Include(p => p.User)
|
|
||||||
.Where(p => p.Embedding != null);
|
|
||||||
|
|
||||||
// 应用共通的查询条件
|
|
||||||
query = ApplyCommonFilters(query, tags, startDate, endDate, userId, onlyWithGps,
|
|
||||||
excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic);
|
|
||||||
|
|
||||||
// 执行向量搜索
|
|
||||||
var allResults = await query
|
|
||||||
.Select(p => new
|
|
||||||
{
|
|
||||||
Picture = p,
|
|
||||||
Similarity = 1.0 - p.Embedding!.CosineDistance(queryVector)
|
|
||||||
})
|
|
||||||
.Where(p => p.Similarity >= similarityThreshold)
|
|
||||||
.OrderByDescending(p => p.Similarity)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
// 计算总数并分页
|
|
||||||
var totalCount = allResults.Count;
|
|
||||||
|
|
||||||
var paginatedResults = allResults
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.Select(r => MapPictureToResponse(r.Picture))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// 处理收藏信息
|
|
||||||
await PopulateFavoriteInfo(dbContext, paginatedResults, userId);
|
|
||||||
|
|
||||||
// 为当前用户的图片添加相册信息
|
|
||||||
if (userId.HasValue)
|
|
||||||
{
|
|
||||||
await PopulateAlbumInfo(dbContext, paginatedResults, userId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PaginatedResult<PictureResponse>
|
|
||||||
{
|
|
||||||
Data = paginatedResults,
|
|
||||||
Page = page,
|
|
||||||
PageSize = pageSize,
|
|
||||||
TotalCount = totalCount
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
return new PaginatedResult<PictureResponse>
|
||||||
{
|
{
|
||||||
Console.WriteLine($"向量搜索失败: {ex.Message}");
|
Data = paginatedResults,
|
||||||
throw new InvalidOperationException($"向量搜索失败: {ex.Message}", ex);
|
Page = page,
|
||||||
}
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行标准搜索
|
// 执行标准搜索
|
||||||
@@ -377,9 +340,9 @@ public class PictureService(
|
|||||||
{
|
{
|
||||||
Id = picture.Id,
|
Id = picture.Id,
|
||||||
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,
|
||||||
ThumbnailPath = storageService.ExecuteAsync(picture.StorageType, provider =>
|
ThumbnailPath = storageService.ExecuteAsync(picture.StorageType, provider =>
|
||||||
Task.FromResult(provider.GetUrl(picture.ThumbnailPath ?? string.Empty))).Result,
|
Task.FromResult(provider.GetUrl(picture.ThumbnailPath ?? string.Empty))).Result,
|
||||||
Description = picture.Description,
|
Description = picture.Description,
|
||||||
CreatedAt = picture.CreatedAt,
|
CreatedAt = picture.CreatedAt,
|
||||||
@@ -482,8 +445,8 @@ public class PictureService(
|
|||||||
string? configValue = configuration[configKey];
|
string? configValue = configuration[configKey];
|
||||||
return !string.IsNullOrEmpty(configValue) &&
|
return !string.IsNullOrEmpty(configValue) &&
|
||||||
Enum.TryParse<StorageType>(configValue, out var configStorageType)
|
Enum.TryParse<StorageType>(configValue, out var configStorageType)
|
||||||
? configStorageType
|
? configStorageType
|
||||||
: StorageType.Local;
|
: StorageType.Local;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId == null)
|
if (userId == null)
|
||||||
@@ -494,6 +457,7 @@ public class PictureService(
|
|||||||
{
|
{
|
||||||
storageType = GetConfigStorageType("Storage:DefaultStorage");
|
storageType = GetConfigStorageType("Storage:DefaultStorage");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageFormat convertToFormat = ImageFormat.Original;
|
ImageFormat convertToFormat = ImageFormat.Original;
|
||||||
string defaultFormatConfig = configuration["Upload:DefaultImageFormat"];
|
string defaultFormatConfig = configuration["Upload:DefaultImageFormat"];
|
||||||
if (!string.IsNullOrEmpty(defaultFormatConfig))
|
if (!string.IsNullOrEmpty(defaultFormatConfig))
|
||||||
@@ -503,12 +467,14 @@ public class PictureService(
|
|||||||
convertToFormat = parsedFormat;
|
convertToFormat = parsedFormat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int quality = 100;
|
int quality = 100;
|
||||||
string defaultQualityConfig = configuration["Upload:DefaultImageQuality"];
|
string defaultQualityConfig = configuration["Upload:DefaultImageQuality"];
|
||||||
if (!string.IsNullOrEmpty(defaultQualityConfig))
|
if (!string.IsNullOrEmpty(defaultQualityConfig))
|
||||||
{
|
{
|
||||||
quality = int.Parse(defaultQualityConfig);
|
quality = int.Parse(defaultQualityConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
string originalFileName = fileName;
|
string originalFileName = fileName;
|
||||||
string finalFileName = fileName;
|
string finalFileName = fileName;
|
||||||
string finalContentType = contentType;
|
string finalContentType = contentType;
|
||||||
@@ -553,7 +519,7 @@ public class PictureService(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 使用存储服务保存文件
|
// 使用存储服务保存文件
|
||||||
string relativePath = await storageService.ExecuteAsync(storageType.Value,
|
string relativePath = await storageService.ExecuteAsync(storageType.Value,
|
||||||
provider => provider.SaveAsync(finalStream, finalFileName, finalContentType));
|
provider => provider.SaveAsync(finalStream, finalFileName, finalContentType));
|
||||||
|
|
||||||
// 创建基本的Picture对象,使用文件名作为标题和描述
|
// 创建基本的Picture对象,使用文件名作为标题和描述
|
||||||
@@ -620,10 +586,12 @@ public class PictureService(
|
|||||||
{
|
{
|
||||||
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(relativePath))),
|
Task.FromResult(provider.GetUrl(relativePath))),
|
||||||
ThumbnailPath = isAnonymous ? await storageService.ExecuteAsync(picture.StorageType, provider =>
|
ThumbnailPath = isAnonymous
|
||||||
Task.FromResult(provider.GetUrl(relativePath))) : null,
|
? await storageService.ExecuteAsync(picture.StorageType, provider =>
|
||||||
|
Task.FromResult(provider.GetUrl(relativePath)))
|
||||||
|
: null,
|
||||||
Description = picture.Description,
|
Description = picture.Description,
|
||||||
CreatedAt = picture.CreatedAt,
|
CreatedAt = picture.CreatedAt,
|
||||||
Tags = new List<string>(),
|
Tags = new List<string>(),
|
||||||
@@ -700,7 +668,8 @@ public class PictureService(
|
|||||||
new List<(int PictureId, string Path, string ThumbnailPath, int? UserId, StorageType StorageType)>();
|
new List<(int PictureId, string Path, string ThumbnailPath, 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, picture.StorageType));
|
filesToDelete.Add((picture.Id, picture.Path, picture.ThumbnailPath ?? string.Empty, picture.User?.Id,
|
||||||
|
picture.StorageType));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (picturesToDelete.Any())
|
if (picturesToDelete.Any())
|
||||||
@@ -718,13 +687,13 @@ public class PictureService(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 使用存储服务删除文件
|
// 使用存储服务删除文件
|
||||||
await storageService.ExecuteAsync(storageType,
|
await storageService.ExecuteAsync(storageType,
|
||||||
provider => provider.DeleteAsync(path));
|
provider => provider.DeleteAsync(path));
|
||||||
|
|
||||||
// 删除缩略图
|
// 删除缩略图
|
||||||
if (!string.IsNullOrEmpty(thumbnailPath))
|
if (!string.IsNullOrEmpty(thumbnailPath))
|
||||||
{
|
{
|
||||||
await storageService.ExecuteAsync(storageType,
|
await storageService.ExecuteAsync(storageType,
|
||||||
provider => provider.DeleteAsync(thumbnailPath));
|
provider => provider.DeleteAsync(thumbnailPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -780,11 +749,11 @@ public class PictureService(
|
|||||||
{
|
{
|
||||||
var combinedText = $"{picture.Name}. {picture.Description}";
|
var combinedText = $"{picture.Name}. {picture.Description}";
|
||||||
var embedding = await embeddingService.GetEmbeddingAsync(combinedText);
|
var embedding = await embeddingService.GetEmbeddingAsync(combinedText);
|
||||||
|
|
||||||
// 只有在成功获取到非空嵌入向量时才更新
|
// 只有在成功获取到非空嵌入向量时才更新
|
||||||
if (embedding != null && embedding.Length > 0)
|
if (embedding != null && embedding.Length > 0)
|
||||||
{
|
{
|
||||||
picture.Embedding = new Vector(embedding);
|
picture.Embedding = embedding;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -826,9 +795,9 @@ public class PictureService(
|
|||||||
{
|
{
|
||||||
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 ?? string.Empty))),
|
Task.FromResult(provider.GetUrl(picture.Path ?? string.Empty))),
|
||||||
ThumbnailPath = await storageService.ExecuteAsync(picture.StorageType, provider =>
|
ThumbnailPath = await storageService.ExecuteAsync(picture.StorageType, provider =>
|
||||||
Task.FromResult(provider.GetUrl(picture.ThumbnailPath ?? string.Empty))),
|
Task.FromResult(provider.GetUrl(picture.ThumbnailPath ?? string.Empty))),
|
||||||
Description = picture.Description,
|
Description = picture.Description,
|
||||||
CreatedAt = picture.CreatedAt,
|
CreatedAt = picture.CreatedAt,
|
||||||
|
|||||||
Reference in New Issue
Block a user