mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-07-09 06:21:35 +08:00
refactor(services): split services into separate folders and update namespaces
This commit is contained in:
289
Services/Media/AlbumService.cs
Normal file
289
Services/Media/AlbumService.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
using System.Security.Claims;
|
||||
using Foxel.Models;
|
||||
using Foxel.Models.DataBase;
|
||||
using Foxel.Models.Response.Album;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxel.Services.Media;
|
||||
|
||||
public class AlbumService(
|
||||
IDbContextFactory<MyDbContext> contextFactory,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: IAlbumService
|
||||
{
|
||||
public async Task<PaginatedResult<AlbumResponse>> GetAlbumsAsync(int page = 1, int pageSize = 10, int? userId = null)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 10;
|
||||
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 构建查询
|
||||
IQueryable<Album> query = dbContext.Albums
|
||||
.Include(a => a.User)
|
||||
.OrderByDescending(a => a.CreatedAt);
|
||||
|
||||
// 如果指定了用户ID,则只获取该用户的相册
|
||||
if (userId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.UserId == userId.Value);
|
||||
}
|
||||
|
||||
// 获取总数和分页数据
|
||||
var totalCount = await query.CountAsync();
|
||||
var albums = await query
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// 获取每个相册中的图片数量
|
||||
var albumIds = albums.Select(a => a.Id).ToList();
|
||||
var albumPictureCounts = await dbContext.Pictures
|
||||
.Where(p => p.AlbumId != null && albumIds.Contains(p.AlbumId.Value))
|
||||
.GroupBy(p => p.AlbumId)
|
||||
.Select(g => new { AlbumId = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.AlbumId!.Value, x => x.Count);
|
||||
|
||||
// 转换为响应模型
|
||||
var albumResponses = albums.Select(a => new AlbumResponse
|
||||
{
|
||||
Id = a.Id,
|
||||
Name = a.Name,
|
||||
Description = a.Description,
|
||||
PictureCount = albumPictureCounts.GetValueOrDefault(a.Id, 0),
|
||||
UserId = a.UserId,
|
||||
Username = a.User.UserName,
|
||||
CreatedAt = a.CreatedAt,
|
||||
UpdatedAt = a.UpdatedAt
|
||||
}).ToList();
|
||||
|
||||
return new PaginatedResult<AlbumResponse>
|
||||
{
|
||||
Data = albumResponses,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AlbumResponse> GetAlbumByIdAsync(int id)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
var album = await dbContext.Albums
|
||||
.Include(a => a.User)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
if (album == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{id}的相册");
|
||||
|
||||
// 获取相册中图片的数量
|
||||
var pictureCount = await dbContext.Pictures
|
||||
.Where(p => p.AlbumId == id)
|
||||
.CountAsync();
|
||||
|
||||
// 转换为响应模型
|
||||
var response = new AlbumResponse
|
||||
{
|
||||
Id = album.Id,
|
||||
Name = album.Name,
|
||||
Description = album.Description,
|
||||
PictureCount = pictureCount,
|
||||
UserId = album.UserId,
|
||||
Username = album.User.UserName,
|
||||
CreatedAt = album.CreatedAt,
|
||||
UpdatedAt = album.UpdatedAt
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("相册名称不能为空", nameof(name));
|
||||
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 检查用户是否存在
|
||||
var user = await dbContext.Users.FindAsync(userId);
|
||||
if (user == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{userId}的用户");
|
||||
|
||||
// 创建新相册
|
||||
var album = new Album
|
||||
{
|
||||
Name = name.Trim(),
|
||||
Description = description?.Trim() ?? string.Empty,
|
||||
UserId = userId,
|
||||
User = user,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
dbContext.Albums.Add(album);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// 转换为响应模型
|
||||
return new AlbumResponse
|
||||
{
|
||||
Id = album.Id,
|
||||
Name = album.Name,
|
||||
Description = album.Description,
|
||||
PictureCount = 0,
|
||||
UserId = album.UserId,
|
||||
Username = user.UserName,
|
||||
CreatedAt = album.CreatedAt,
|
||||
UpdatedAt = album.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("相册名称不能为空", nameof(name));
|
||||
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 获取相册
|
||||
var album = await dbContext.Albums
|
||||
.Include(a => a.User)
|
||||
.Include(a => a.Pictures)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
if (album == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{id}的相册");
|
||||
|
||||
// 权限检查 - 只有相册的创建者或系统管理员可以更新
|
||||
if (userId.HasValue && album.UserId != userId.Value)
|
||||
{
|
||||
// 检查用户是否是管理员
|
||||
var user = await dbContext.Users.FindAsync(userId.Value);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException("您没有权限更新此相册");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新相册信息
|
||||
album.Name = name.Trim();
|
||||
album.Description = description?.Trim() ?? album.Description;
|
||||
album.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// 转换为响应模型
|
||||
return new AlbumResponse
|
||||
{
|
||||
Id = album.Id,
|
||||
Name = album.Name,
|
||||
Description = album.Description,
|
||||
PictureCount = album.Pictures?.Count ?? 0,
|
||||
UserId = album.UserId,
|
||||
Username = album.User.UserName,
|
||||
CreatedAt = album.CreatedAt,
|
||||
UpdatedAt = album.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAlbumAsync(int id)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
var album = await dbContext.Albums.FindAsync(id);
|
||||
if (album == null)
|
||||
return false;
|
||||
|
||||
// 注意:相册删除前,需要确保关联的图片被正确处理
|
||||
// 这里只移除相册,而不删除图片
|
||||
|
||||
dbContext.Albums.Remove(album);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> AddPictureToAlbumAsync(int albumId, int pictureId)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 获取相册和图片
|
||||
var album = await dbContext.Albums.FindAsync(albumId);
|
||||
if (album == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{albumId}的相册");
|
||||
|
||||
var picture = await dbContext.Pictures.FindAsync(pictureId);
|
||||
if (picture == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{pictureId}的图片");
|
||||
|
||||
// 将图片添加到相册
|
||||
picture.AlbumId = albumId;
|
||||
picture.Album = album;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePictureFromAlbumAsync(int albumId, int pictureId)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 获取图片
|
||||
var picture = await dbContext.Pictures
|
||||
.FirstOrDefaultAsync(p => p.Id == pictureId && p.AlbumId == albumId);
|
||||
|
||||
if (picture == null)
|
||||
throw new KeyNotFoundException($"在相册中找不到ID为{pictureId}的图片");
|
||||
|
||||
// 从相册中移除图片
|
||||
picture.AlbumId = null;
|
||||
picture.Album = null;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> AddPicturesToAlbumAsync(int albumId, List<int> pictureIds)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
var album = await dbContext.Albums.FindAsync(albumId)
|
||||
?? throw new KeyNotFoundException("相册不存在");
|
||||
|
||||
// 检查是否有权限修改此相册
|
||||
var currentUser = httpContextAccessor.HttpContext?.User;
|
||||
if (currentUser != null)
|
||||
{
|
||||
var userId = int.Parse(currentUser.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0");
|
||||
if (album.UserId != userId)
|
||||
{
|
||||
throw new UnauthorizedAccessException("您没有权限修改此相册");
|
||||
}
|
||||
}
|
||||
|
||||
var successCount = 0;
|
||||
|
||||
foreach (var pictureId in pictureIds)
|
||||
{
|
||||
var picture = await dbContext.Pictures.FindAsync(pictureId);
|
||||
if (picture == null) continue; // 跳过不存在的图片
|
||||
|
||||
// 直接更新 Picture 的 AlbumId
|
||||
if (picture.AlbumId != albumId)
|
||||
{
|
||||
picture.AlbumId = albumId;
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0)
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
16
Services/Media/IAlbumService.cs
Normal file
16
Services/Media/IAlbumService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Foxel.Models;
|
||||
using Foxel.Models.Response.Album;
|
||||
|
||||
namespace Foxel.Services.Media;
|
||||
|
||||
public interface IAlbumService
|
||||
{
|
||||
Task<PaginatedResult<AlbumResponse>> GetAlbumsAsync(int page = 1, int pageSize = 10, int? userId = null);
|
||||
Task<AlbumResponse> GetAlbumByIdAsync(int id);
|
||||
Task<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId);
|
||||
Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null);
|
||||
Task<bool> DeleteAlbumAsync(int id);
|
||||
Task<bool> AddPictureToAlbumAsync(int albumId, int pictureId);
|
||||
Task<bool> AddPicturesToAlbumAsync(int albumId, List<int> pictureIds);
|
||||
Task<bool> RemovePictureFromAlbumAsync(int albumId, int pictureId);
|
||||
}
|
||||
88
Services/Media/IPictureService.cs
Normal file
88
Services/Media/IPictureService.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Foxel.Models;
|
||||
using Foxel.Models.DataBase;
|
||||
using Foxel.Models.Response.Picture;
|
||||
using Foxel.Services.Attributes;
|
||||
|
||||
namespace Foxel.Services.Media;
|
||||
|
||||
public interface IPictureService
|
||||
{
|
||||
Task<PaginatedResult<PictureResponse>> GetPicturesAsync(
|
||||
int page = 1,
|
||||
int pageSize = 8,
|
||||
string? searchQuery = null,
|
||||
List<string>? tags = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
int? userId = null,
|
||||
string? sortBy = "newest",
|
||||
bool? onlyWithGps = false,
|
||||
bool useVectorSearch = false,
|
||||
double similarityThreshold = 0.36,
|
||||
int? excludeAlbumId = null,
|
||||
int? albumId = null,
|
||||
bool onlyFavorites = false,
|
||||
int? ownerId = null,
|
||||
bool includeAllPublic = false
|
||||
);
|
||||
|
||||
Task<(PictureResponse Picture, int Id)> UploadPictureAsync(
|
||||
string fileName,
|
||||
Stream fileStream,
|
||||
string contentType,
|
||||
int? userId,
|
||||
PermissionType permission = PermissionType.Public,
|
||||
int? albumId = null,
|
||||
StorageType? storageType = null
|
||||
);
|
||||
|
||||
Task<ExifInfo> GetPictureExifInfoAsync(int pictureId);
|
||||
|
||||
/// <summary>
|
||||
/// 批量删除多张图片
|
||||
/// </summary>
|
||||
/// <param name="pictureIds">要删除的图片ID列表</param>
|
||||
/// <returns>每个图片ID对应的删除结果、可能的错误信息和所有者ID</returns>
|
||||
Task<Dictionary<int, (bool Success, string? ErrorMessage, int? UserId)>> DeleteMultiplePicturesAsync(
|
||||
List<int> pictureIds);
|
||||
|
||||
/// <summary>
|
||||
/// 更新图片信息
|
||||
/// </summary>
|
||||
/// <param name="pictureId">图片ID</param>
|
||||
/// <param name="name">新标题(可选)</param>
|
||||
/// <param name="description">新描述(可选)</param>
|
||||
/// <param name="tags">新标签(可选)</param>
|
||||
/// <returns>更新后的图片视图模型和所有者ID</returns>
|
||||
Task<(PictureResponse Picture, int? UserId)> UpdatePictureAsync(
|
||||
int pictureId,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
List<string>? tags = null);
|
||||
|
||||
/// <summary>
|
||||
/// 收藏图片
|
||||
/// </summary>
|
||||
/// <param name="pictureId">图片ID</param>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>成功/失败</returns>
|
||||
Task<bool> FavoritePictureAsync(int pictureId, int userId);
|
||||
|
||||
/// <summary>
|
||||
/// 取消收藏图片
|
||||
/// </summary>
|
||||
/// <param name="pictureId">图片ID</param>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>成功/失败</returns>
|
||||
Task<bool> UnfavoritePictureAsync(int pictureId, int userId);
|
||||
|
||||
/// <summary>
|
||||
/// 检查图片是否被特定用户收藏
|
||||
/// </summary>
|
||||
/// <param name="pictureId">图片ID</param>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>是否收藏</returns>
|
||||
Task<bool> IsPictureFavoritedByUserAsync(int pictureId, int userId);
|
||||
|
||||
|
||||
}
|
||||
24
Services/Media/ITagService.cs
Normal file
24
Services/Media/ITagService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Foxel.Models;
|
||||
using Foxel.Models.Response.Tag;
|
||||
|
||||
namespace Foxel.Services.Media;
|
||||
|
||||
public interface ITagService
|
||||
{
|
||||
Task<PaginatedResult<TagResponse>> GetFilteredTagsAsync(
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
string? searchQuery = null,
|
||||
string? sortBy = "pictureCount",
|
||||
string? sortDirection = "desc",
|
||||
int? minPictureCount = null
|
||||
);
|
||||
|
||||
Task<TagResponse> GetTagByIdAsync(int id);
|
||||
|
||||
Task<TagResponse> CreateTagAsync(string name, string? description = null);
|
||||
|
||||
Task<TagResponse> UpdateTagAsync(int id, string? name = null, string? description = null);
|
||||
|
||||
Task<bool> DeleteTagAsync(int id);
|
||||
}
|
||||
760
Services/Media/PictureService.cs
Normal file
760
Services/Media/PictureService.cs
Normal file
@@ -0,0 +1,760 @@
|
||||
using System.Text.Json;
|
||||
using Foxel.Models;
|
||||
using Foxel.Models.DataBase;
|
||||
using Foxel.Models.Response.Picture;
|
||||
using Foxel.Services.AI;
|
||||
using Foxel.Services.Attributes;
|
||||
using Foxel.Services.Background;
|
||||
using Foxel.Services.Configuration;
|
||||
using Foxel.Services.Storage;
|
||||
using Foxel.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Pgvector;
|
||||
using Pgvector.EntityFrameworkCore;
|
||||
|
||||
namespace Foxel.Services.Media;
|
||||
|
||||
public class PictureService(
|
||||
IDbContextFactory<MyDbContext> contextFactory,
|
||||
IAiService embeddingService,
|
||||
IConfigService configuration,
|
||||
IBackgroundTaskQueue backgroundTaskQueue,
|
||||
IStorageService storageService)
|
||||
: IPictureService
|
||||
{
|
||||
public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(
|
||||
int page = 1,
|
||||
int pageSize = 8,
|
||||
string? searchQuery = null,
|
||||
List<string>? tags = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
int? userId = null,
|
||||
string? sortBy = "newest",
|
||||
bool? onlyWithGps = false,
|
||||
bool useVectorSearch = false,
|
||||
double similarityThreshold = 0.36,
|
||||
int? excludeAlbumId = null,
|
||||
int? albumId = null,
|
||||
bool onlyFavorites = false,
|
||||
int? ownerId = null,
|
||||
bool includeAllPublic = false
|
||||
)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 8;
|
||||
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 决定是使用向量搜索还是普通搜索
|
||||
if (useVectorSearch && !string.IsNullOrWhiteSpace(searchQuery))
|
||||
{
|
||||
return await PerformVectorSearchAsync(
|
||||
dbContext, page, pageSize, searchQuery, tags,
|
||||
startDate, endDate, userId, onlyWithGps, similarityThreshold,
|
||||
excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic);
|
||||
}
|
||||
else
|
||||
{
|
||||
return await PerformStandardSearchAsync(
|
||||
dbContext, page, pageSize, searchQuery, tags,
|
||||
startDate, endDate, userId, sortBy, onlyWithGps,
|
||||
excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行向量搜索
|
||||
private async Task<PaginatedResult<PictureResponse>> PerformVectorSearchAsync(
|
||||
MyDbContext dbContext,
|
||||
int page,
|
||||
int pageSize,
|
||||
string searchQuery,
|
||||
List<string>? tags,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
int? userId,
|
||||
bool? onlyWithGps,
|
||||
double similarityThreshold,
|
||||
int? excludeAlbumId,
|
||||
int? albumId,
|
||||
bool onlyFavorites,
|
||||
int? ownerId,
|
||||
bool includeAllPublic)
|
||||
{
|
||||
var queryEmbedding = await embeddingService.GetEmbeddingAsync(searchQuery);
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// 执行标准搜索
|
||||
private async Task<PaginatedResult<PictureResponse>> PerformStandardSearchAsync(
|
||||
MyDbContext dbContext,
|
||||
int page,
|
||||
int pageSize,
|
||||
string? searchQuery,
|
||||
List<string>? tags,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
int? userId,
|
||||
string? sortBy,
|
||||
bool? onlyWithGps,
|
||||
int? excludeAlbumId,
|
||||
int? albumId,
|
||||
bool onlyFavorites,
|
||||
int? ownerId,
|
||||
bool includeAllPublic)
|
||||
{
|
||||
// 构建基础查询
|
||||
IQueryable<Picture> query = dbContext.Pictures
|
||||
.Include(p => p.Tags)
|
||||
.Include(p => p.User);
|
||||
|
||||
// 应用文本搜索条件
|
||||
if (!string.IsNullOrWhiteSpace(searchQuery))
|
||||
{
|
||||
var searchTerm = searchQuery.ToLower();
|
||||
query = query.Where(p =>
|
||||
(p.Name.ToLower().Contains(searchTerm)) ||
|
||||
(p.Description.ToLower().Contains(searchTerm)));
|
||||
}
|
||||
|
||||
// 应用共通的查询条件
|
||||
query = ApplyCommonFilters(query, tags, startDate, endDate, userId, onlyWithGps,
|
||||
excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic);
|
||||
|
||||
// 应用排序
|
||||
query = ApplySorting(query, sortBy);
|
||||
|
||||
// 获取总记录数
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
// 获取分页数据
|
||||
var picturesData = await query
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// 转换为响应格式
|
||||
var pictures = picturesData
|
||||
.Select(p => MapPictureToResponse(p))
|
||||
.ToList();
|
||||
|
||||
// 处理收藏信息
|
||||
await PopulateFavoriteInfo(dbContext, pictures, userId);
|
||||
|
||||
// 为当前用户的图片添加相册信息
|
||||
if (userId.HasValue)
|
||||
{
|
||||
await PopulateAlbumInfo(dbContext, pictures, userId.Value);
|
||||
}
|
||||
|
||||
return new PaginatedResult<PictureResponse>
|
||||
{
|
||||
Data = pictures,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount
|
||||
};
|
||||
}
|
||||
|
||||
// 应用共通的过滤条件
|
||||
private IQueryable<Picture> ApplyCommonFilters(
|
||||
IQueryable<Picture> query,
|
||||
List<string>? tags,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
int? userId,
|
||||
bool? onlyWithGps,
|
||||
int? excludeAlbumId,
|
||||
int? albumId,
|
||||
bool onlyFavorites,
|
||||
int? ownerId,
|
||||
bool includeAllPublic)
|
||||
{
|
||||
// 应用标签筛选
|
||||
if (tags != null && tags.Any())
|
||||
{
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
var tagName = tag.Trim();
|
||||
if (!string.IsNullOrEmpty(tagName))
|
||||
{
|
||||
var normalizedTagName = tagName.ToLower();
|
||||
query = query.Where(p => p.Tags!.Any(t => t.Name.ToLower().Equals(normalizedTagName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用日期范围筛选
|
||||
if (startDate.HasValue)
|
||||
{
|
||||
DateTime utcStartDate = startDate.Value.ToUniversalTime();
|
||||
query = query.Where(p =>
|
||||
(p.TakenAt.HasValue && p.TakenAt >= utcStartDate) ||
|
||||
(!p.TakenAt.HasValue && p.CreatedAt >= utcStartDate));
|
||||
}
|
||||
|
||||
if (endDate.HasValue)
|
||||
{
|
||||
DateTime utcEndDate = endDate.Value.ToUniversalTime().AddDays(1).AddMilliseconds(-1);
|
||||
query = query.Where(p =>
|
||||
(p.TakenAt.HasValue && p.TakenAt <= utcEndDate) ||
|
||||
(!p.TakenAt.HasValue && p.CreatedAt <= utcEndDate));
|
||||
}
|
||||
|
||||
// 应用用户筛选和权限过滤逻辑
|
||||
if (ownerId.HasValue)
|
||||
{
|
||||
if (userId.HasValue && userId.Value == ownerId.Value)
|
||||
{
|
||||
query = query.Where(p => p.User != null && p.User.Id == ownerId.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(p =>
|
||||
p.User != null && p.User.Id == ownerId.Value && p.Permission == PermissionType.Public);
|
||||
}
|
||||
}
|
||||
else if (userId.HasValue)
|
||||
{
|
||||
if (includeAllPublic)
|
||||
{
|
||||
query = query.Where(p =>
|
||||
(p.User != null && p.User.Id == userId.Value) ||
|
||||
(p.User != null && p.User.Id != userId.Value &&
|
||||
p.Permission == PermissionType.Public)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(p => p.User != null && p.User.Id == userId.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(p => p.Permission == PermissionType.Public);
|
||||
}
|
||||
|
||||
// 筛选有GPS信息的图片
|
||||
if (onlyWithGps == true)
|
||||
{
|
||||
query = query.Where(p =>
|
||||
p.ExifInfo != null &&
|
||||
!string.IsNullOrEmpty(p.ExifInfo.GpsLatitude) &&
|
||||
!string.IsNullOrEmpty(p.ExifInfo.GpsLongitude));
|
||||
}
|
||||
|
||||
// 排除指定相册的图片
|
||||
if (excludeAlbumId.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.AlbumId != excludeAlbumId.Value || p.AlbumId == null);
|
||||
}
|
||||
|
||||
// 筛选指定相册的图片
|
||||
if (albumId.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.AlbumId == albumId.Value);
|
||||
}
|
||||
|
||||
// 筛选收藏的图片
|
||||
if (onlyFavorites && userId.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.Favorites!.Any(f => f.User.Id == userId.Value));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
private IQueryable<Picture> ApplySorting(IQueryable<Picture> query, string? sortBy)
|
||||
{
|
||||
return sortBy?.ToLower() switch
|
||||
{
|
||||
// 拍摄时间排序
|
||||
"takenat_desc" or "newest" => query.OrderByDescending(p => p.TakenAt ?? p.CreatedAt),
|
||||
"takenat_asc" or "oldest" => query.OrderBy(p => p.TakenAt ?? p.CreatedAt),
|
||||
|
||||
// 上传时间排序
|
||||
"uploaddate_desc" => query.OrderByDescending(p => p.CreatedAt),
|
||||
"uploaddate_asc" => query.OrderBy(p => p.CreatedAt),
|
||||
|
||||
// 名称排序
|
||||
"name_asc" or "name" => query.OrderBy(p => p.Name),
|
||||
"name_desc" => query.OrderByDescending(p => p.Name),
|
||||
|
||||
// 默认排序
|
||||
_ => query.OrderByDescending(p => p.TakenAt ?? p.CreatedAt)
|
||||
};
|
||||
}
|
||||
|
||||
// 将数据库实体映射到响应对象
|
||||
private PictureResponse MapPictureToResponse(Picture picture)
|
||||
{
|
||||
return new PictureResponse
|
||||
{
|
||||
Id = picture.Id,
|
||||
Name = picture.Name,
|
||||
Path = storageService.GetUrl(picture.StorageType, picture.Path),
|
||||
ThumbnailPath = storageService.GetUrl(picture.StorageType, picture.ThumbnailPath),
|
||||
Description = picture.Description,
|
||||
CreatedAt = picture.CreatedAt,
|
||||
Tags = picture.Tags != null ? picture.Tags.Select(t => t.Name).ToList() : new List<string>(),
|
||||
TakenAt = picture.TakenAt,
|
||||
ExifInfo = picture.ExifInfo ?? new ExifInfo(),
|
||||
UserId = picture.UserId,
|
||||
Username = picture.User?.UserName,
|
||||
AlbumId = picture.AlbumId,
|
||||
Permission = picture.Permission
|
||||
};
|
||||
}
|
||||
|
||||
// 填充收藏信息
|
||||
private async Task PopulateFavoriteInfo(MyDbContext dbContext, List<PictureResponse> pictures, int? userId)
|
||||
{
|
||||
if (userId.HasValue && pictures.Any())
|
||||
{
|
||||
var pictureIds = pictures.Select(p => p.Id).ToList();
|
||||
|
||||
// 获取用户收藏的图片ID
|
||||
var favoritedPictureIds = await dbContext.Favorites
|
||||
.Where(f => f.User.Id == userId.Value && pictureIds.Contains(f.PictureId))
|
||||
.Select(f => f.PictureId)
|
||||
.ToHashSetAsync(); // 使用 ToHashSetAsync 提高查找效率
|
||||
|
||||
// 一次性获取所有相关图片的收藏总数
|
||||
var favoriteCounts = await dbContext.Favorites
|
||||
.Where(f => pictureIds.Contains(f.PictureId))
|
||||
.GroupBy(f => f.PictureId)
|
||||
.Select(g => new { PictureId = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.PictureId, x => x.Count);
|
||||
|
||||
foreach (var picture in pictures)
|
||||
{
|
||||
picture.IsFavorited = favoritedPictureIds.Contains(picture.Id);
|
||||
picture.FavoriteCount = favoriteCounts.GetValueOrDefault(picture.Id, 0);
|
||||
}
|
||||
}
|
||||
else if (pictures.Any()) // 如果用户未登录,仍然需要获取收藏总数
|
||||
{
|
||||
var pictureIds = pictures.Select(p => p.Id).ToList();
|
||||
var favoriteCounts = await dbContext.Favorites
|
||||
.Where(f => pictureIds.Contains(f.PictureId))
|
||||
.GroupBy(f => f.PictureId)
|
||||
.Select(g => new { PictureId = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.PictureId, x => x.Count);
|
||||
|
||||
foreach (var picture in pictures)
|
||||
{
|
||||
picture.IsFavorited = false; // 用户未登录,不可能收藏
|
||||
picture.FavoriteCount = favoriteCounts.GetValueOrDefault(picture.Id, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 填充相册信息
|
||||
private async Task PopulateAlbumInfo(MyDbContext dbContext, List<PictureResponse> pictures, int userId)
|
||||
{
|
||||
if (!pictures.Any())
|
||||
return;
|
||||
|
||||
// 获取当前用户拥有的图片ID列表
|
||||
var userPictureIds = pictures
|
||||
.Where(p => p.UserId == userId)
|
||||
.Select(p => p.Id)
|
||||
.ToList();
|
||||
|
||||
if (!userPictureIds.Any())
|
||||
return;
|
||||
|
||||
// 获取相册信息
|
||||
var pictureAlbums = await dbContext.Pictures
|
||||
.Where(p => userPictureIds.Contains(p.Id) && p.AlbumId.HasValue)
|
||||
.Select(p => new { p.Id, p.AlbumId, AlbumName = p.Album!.Name })
|
||||
.ToDictionaryAsync(p => p.Id, p => new { p.AlbumId, p.AlbumName });
|
||||
|
||||
// 填充相册信息到图片响应中
|
||||
foreach (var picture in pictures)
|
||||
{
|
||||
if (picture.UserId == userId && pictureAlbums.TryGetValue(picture.Id, out var albumInfo))
|
||||
{
|
||||
picture.AlbumId = albumInfo.AlbumId;
|
||||
picture.AlbumName = albumInfo.AlbumName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(PictureResponse Picture, int Id)> UploadPictureAsync(
|
||||
string fileName,
|
||||
Stream fileStream,
|
||||
string contentType,
|
||||
int? userId,
|
||||
PermissionType permission = PermissionType.Public,
|
||||
int? albumId = null,
|
||||
StorageType? storageType = null)
|
||||
{
|
||||
// 如果未指定存储类型,则从配置中获取默认存储类型
|
||||
if (storageType == null)
|
||||
{
|
||||
var defaultStorageTypeStr = configuration["Storage:DefaultStorage"];
|
||||
if (string.IsNullOrEmpty(defaultStorageTypeStr) || !Enum.TryParse<StorageType>(defaultStorageTypeStr, out var defaultStorageType))
|
||||
{
|
||||
defaultStorageType = StorageType.Local; // 如果配置中没有或解析失败,使用本地存储作为默认
|
||||
}
|
||||
storageType = defaultStorageType;
|
||||
}
|
||||
|
||||
string fileExtension = Path.GetExtension(fileName);
|
||||
_ = $"{Guid.NewGuid()}{fileExtension}";
|
||||
|
||||
// 使用存储服务保存文件
|
||||
string relativePath = await storageService.SaveAsync(storageType.Value, fileStream, fileName, contentType);
|
||||
|
||||
// 创建基本的Picture对象,使用文件名作为标题和描述
|
||||
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("找不到指定的用户");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查相册是否存在并且属于当前用户
|
||||
Album? album = null;
|
||||
if (albumId.HasValue)
|
||||
{
|
||||
album = await dbContext.Albums.Include(a => a.User)
|
||||
.FirstOrDefaultAsync(a => a.Id == albumId.Value);
|
||||
|
||||
if (album == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"找不到ID为{albumId.Value}的相册");
|
||||
}
|
||||
|
||||
if (album.User.Id != userId)
|
||||
{
|
||||
throw new Exception("您无权将图片添加到此相册");
|
||||
}
|
||||
}
|
||||
|
||||
bool isAnonymous = userId == null;
|
||||
|
||||
// 创建图片对象并保存到数据库
|
||||
var picture = new Picture
|
||||
{
|
||||
Name = initialTitle,
|
||||
Description = initialDescription,
|
||||
Path = relativePath,
|
||||
User = user,
|
||||
Permission = permission,
|
||||
AlbumId = albumId,
|
||||
StorageType = storageType.Value,
|
||||
ProcessingStatus = isAnonymous ? ProcessingStatus.Completed : ProcessingStatus.Pending,
|
||||
ThumbnailPath = isAnonymous ? relativePath : null
|
||||
};
|
||||
|
||||
dbContext.Pictures.Add(picture);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
if (!isAnonymous)
|
||||
{
|
||||
await backgroundTaskQueue.QueuePictureProcessingTaskAsync(picture.Id, relativePath);
|
||||
}
|
||||
|
||||
// 返回图片基本信息
|
||||
var pictureResponse = new PictureResponse
|
||||
{
|
||||
Id = picture.Id,
|
||||
Name = picture.Name,
|
||||
Path = storageService.GetUrl(picture.StorageType, relativePath),
|
||||
ThumbnailPath = isAnonymous ? storageService.GetUrl(picture.StorageType, relativePath) : null,
|
||||
Description = picture.Description,
|
||||
CreatedAt = picture.CreatedAt,
|
||||
Tags = new List<string>(),
|
||||
Permission = permission,
|
||||
AlbumId = albumId,
|
||||
AlbumName = album?.Name,
|
||||
ProcessingStatus = picture.ProcessingStatus
|
||||
};
|
||||
|
||||
return (pictureResponse, picture.Id);
|
||||
}
|
||||
|
||||
public async Task<ExifInfo> GetPictureExifInfoAsync(int pictureId)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
var picture = await dbContext.Pictures.FindAsync(pictureId);
|
||||
|
||||
if (picture == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{pictureId}的图片");
|
||||
|
||||
// 如果已有保存的EXIF信息,则直接返回
|
||||
if (!string.IsNullOrEmpty(picture.ExifInfoJson))
|
||||
{
|
||||
var exifInfo = JsonSerializer.Deserialize<ExifInfo>(picture.ExifInfoJson);
|
||||
return exifInfo ?? new ExifInfo { ErrorMessage = "无法解析EXIF信息" };
|
||||
}
|
||||
|
||||
// 否则从文件中提取
|
||||
string fullPath = Path.Combine(Directory.GetCurrentDirectory(), picture.Path.TrimStart('/'));
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
return new ExifInfo { ErrorMessage = "找不到图片文件" };
|
||||
}
|
||||
|
||||
return await ImageHelper.ExtractExifInfoAsync(fullPath);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<int, (bool Success, string? ErrorMessage, int? UserId)>> DeleteMultiplePicturesAsync(
|
||||
List<int> pictureIds)
|
||||
{
|
||||
var results = new Dictionary<int, (bool Success, string? ErrorMessage, int? UserId)>();
|
||||
if (pictureIds.Count == 0)
|
||||
return results;
|
||||
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
var picturesToDelete = await dbContext.Pictures
|
||||
.Include(p => p.User)
|
||||
.Where(p => pictureIds.Contains(p.Id))
|
||||
.ToListAsync();
|
||||
|
||||
var foundPictureIds = picturesToDelete.Select(p => p.Id).ToHashSet();
|
||||
foreach (var id in pictureIds.Where(id => !foundPictureIds.Contains(id)))
|
||||
{
|
||||
results[id] = (false, "找不到此图片", null);
|
||||
}
|
||||
|
||||
var filesToDelete =
|
||||
new List<(int PictureId, string Path, string ThumbnailPath, int? UserId, StorageType StorageType)>();
|
||||
foreach (var picture in picturesToDelete)
|
||||
{
|
||||
filesToDelete.Add((picture.Id, picture.Path, picture.ThumbnailPath, picture.User?.Id, picture.StorageType));
|
||||
}
|
||||
|
||||
if (picturesToDelete.Any())
|
||||
{
|
||||
dbContext.Pictures.RemoveRange(picturesToDelete);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
foreach (var (pictureId, path, thumbnailPath, userId, storageType) in filesToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? errorMsg = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 使用存储服务删除文件
|
||||
await storageService.DeleteAsync(storageType, path);
|
||||
|
||||
// 删除缩略图
|
||||
if (!string.IsNullOrEmpty(thumbnailPath))
|
||||
{
|
||||
await storageService.DeleteAsync(storageType, thumbnailPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMsg = $"数据库记录已删除,但删除文件失败: {ex.Message}";
|
||||
Console.WriteLine($"删除图片文件时出错:{ex.Message}");
|
||||
}
|
||||
|
||||
results[pictureId] = (true, errorMsg, userId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results[pictureId] = (false, $"处理图片删除时出错: {ex.Message}", userId);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<(PictureResponse Picture, int? UserId)> UpdatePictureAsync(
|
||||
int pictureId,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
List<string>? tags = null)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
var picture = await dbContext.Pictures
|
||||
.Include(p => p.User)
|
||||
.Include(p => p.Tags)
|
||||
.FirstOrDefaultAsync(p => p.Id == pictureId);
|
||||
|
||||
if (picture == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{pictureId}的图片");
|
||||
|
||||
var userId = picture.User?.Id;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
picture.Name = name.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
picture.Description = description.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(name) || !string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
var combinedText = $"{picture.Name}. {picture.Description}";
|
||||
var embedding = await embeddingService.GetEmbeddingAsync(combinedText);
|
||||
picture.Embedding = new Vector(embedding);
|
||||
}
|
||||
|
||||
if (tags != null)
|
||||
{
|
||||
picture.Tags?.Clear();
|
||||
|
||||
foreach (var tagName in tags.Where(t => !string.IsNullOrWhiteSpace(t)))
|
||||
{
|
||||
var tag = await dbContext.Tags.FirstOrDefaultAsync(t => t.Name.ToLower() == tagName.ToLower().Trim());
|
||||
|
||||
if (tag == null)
|
||||
{
|
||||
tag = new Tag { Name = tagName.Trim() };
|
||||
dbContext.Tags.Add(tag);
|
||||
}
|
||||
|
||||
picture.Tags?.Add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
picture.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
var pictureResponse = new PictureResponse
|
||||
{
|
||||
Id = picture.Id,
|
||||
Name = picture.Name,
|
||||
Path = storageService.GetUrl(picture.StorageType, picture.Path),
|
||||
ThumbnailPath = storageService.GetUrl(picture.StorageType, picture.ThumbnailPath),
|
||||
Description = picture.Description,
|
||||
CreatedAt = picture.CreatedAt,
|
||||
Tags = picture.Tags?.Select(t => t.Name).ToList() ?? new List<string>(),
|
||||
TakenAt = picture.TakenAt,
|
||||
ExifInfo = picture.ExifInfo
|
||||
};
|
||||
|
||||
return (pictureResponse, userId);
|
||||
}
|
||||
|
||||
public async Task<bool> FavoritePictureAsync(int pictureId, int userId)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 检查图片是否存在
|
||||
var picture = await dbContext.Pictures.FindAsync(pictureId);
|
||||
if (picture == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{pictureId}的图片");
|
||||
|
||||
// 检查用户是否存在
|
||||
var user = await dbContext.Users.FindAsync(userId);
|
||||
if (user == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{userId}的用户");
|
||||
|
||||
// 检查是否已经收藏
|
||||
var existingFavorite = await dbContext.Favorites
|
||||
.FirstOrDefaultAsync(f => f.PictureId == pictureId && f.User.Id == userId);
|
||||
|
||||
if (existingFavorite != null)
|
||||
throw new InvalidOperationException("您已经收藏过此图片");
|
||||
|
||||
// 创建新收藏
|
||||
var favorite = new Favorite
|
||||
{
|
||||
PictureId = pictureId,
|
||||
User = user,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
dbContext.Favorites.Add(favorite);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> UnfavoritePictureAsync(int pictureId, int userId)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 查找收藏记录
|
||||
var favorite = await dbContext.Favorites
|
||||
.FirstOrDefaultAsync(f => f.PictureId == pictureId && f.User.Id == userId);
|
||||
|
||||
if (favorite == null)
|
||||
throw new KeyNotFoundException($"未找到该图片的收藏记录");
|
||||
|
||||
// 移除收藏
|
||||
dbContext.Favorites.Remove(favorite);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> IsPictureFavoritedByUserAsync(int pictureId, int userId)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Favorites
|
||||
.AnyAsync(f => f.PictureId == pictureId && f.User.Id == userId);
|
||||
}
|
||||
}
|
||||
233
Services/Media/TagService.cs
Normal file
233
Services/Media/TagService.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Foxel.Models;
|
||||
using Foxel.Models.DataBase;
|
||||
using Foxel.Models.Response.Tag;
|
||||
using Foxel.Services.Media;
|
||||
|
||||
namespace Foxel.Services;
|
||||
|
||||
public class TagService(IDbContextFactory<MyDbContext> contextFactory) : ITagService
|
||||
{
|
||||
public async Task<PaginatedResult<TagResponse>> GetFilteredTagsAsync(
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
string? searchQuery = null,
|
||||
string? sortBy = "pictureCount",
|
||||
string? sortDirection = "desc",
|
||||
int? minPictureCount = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1 || pageSize > 100) pageSize = 20;
|
||||
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 构建基础查询,确保加载图片关联
|
||||
IQueryable<Tag> query = dbContext.Tags.Include(t => t.Pictures);
|
||||
|
||||
// 应用搜索条件
|
||||
if (!string.IsNullOrWhiteSpace(searchQuery))
|
||||
{
|
||||
var searchTerm = searchQuery.ToLower();
|
||||
query = query.Where(t =>
|
||||
t.Name.ToLower().Contains(searchTerm) ||
|
||||
(t.Description != null && t.Description.ToLower().Contains(searchTerm)));
|
||||
}
|
||||
|
||||
// 应用最小图片数量过滤(安全处理Pictures集合)
|
||||
if (minPictureCount.HasValue && minPictureCount.Value > 0)
|
||||
{
|
||||
query = query.Where(t => t.Pictures != null && t.Pictures.Count >= minPictureCount.Value);
|
||||
}
|
||||
|
||||
// 获取总记录数(先计算总数再排序和分页)
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
// 没有结果时返回空列表
|
||||
if (totalCount == 0)
|
||||
{
|
||||
return new PaginatedResult<TagResponse>
|
||||
{
|
||||
Data = new List<TagResponse>(),
|
||||
TotalCount = 0,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
query = ApplySorting(query, sortBy, sortDirection);
|
||||
|
||||
// 应用分页
|
||||
var tags = await query
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// 转换为响应格式,确保包含图片数量
|
||||
var tagResponses = new List<TagResponse>();
|
||||
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
tagResponses.Add(new TagResponse
|
||||
{
|
||||
Id = tag.Id,
|
||||
Name = tag.Name,
|
||||
Description = tag.Description,
|
||||
CreatedAt = tag.CreatedAt,
|
||||
PictureCount = tag.Pictures?.Count ?? 0 // 确保包含图片数量
|
||||
});
|
||||
}
|
||||
|
||||
return new PaginatedResult<TagResponse>
|
||||
{
|
||||
Data = tagResponses,
|
||||
TotalCount = totalCount,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录详细错误信息
|
||||
Console.WriteLine($"GetFilteredTagsAsync error: {ex.Message}");
|
||||
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加排序方法
|
||||
private static IQueryable<Tag> ApplySorting(
|
||||
IQueryable<Tag> query,
|
||||
string? sortBy,
|
||||
string? sortDirection)
|
||||
{
|
||||
var isAscending = string.Equals(sortDirection, "asc", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return sortBy?.ToLower() switch
|
||||
{
|
||||
"name" => isAscending
|
||||
? query.OrderBy(t => t.Name)
|
||||
: query.OrderByDescending(t => t.Name),
|
||||
|
||||
"createdat" => isAscending
|
||||
? query.OrderBy(t => t.CreatedAt)
|
||||
: query.OrderByDescending(t => t.CreatedAt),
|
||||
|
||||
"picturecount" => isAscending
|
||||
? query.OrderBy(t => t.Pictures.Count)
|
||||
: query.OrderByDescending(t => t.Pictures.Count),
|
||||
|
||||
_ => query.OrderByDescending(t => t.Pictures.Count) // 默认按图片数量降序排列
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<TagResponse> GetTagByIdAsync(int id)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
var tag = await dbContext.Tags
|
||||
.Include(t => t.Pictures)
|
||||
.FirstOrDefaultAsync(t => t.Id == id);
|
||||
|
||||
if (tag == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{id}的标签");
|
||||
|
||||
return new TagResponse
|
||||
{
|
||||
Id = tag.Id,
|
||||
Name = tag.Name,
|
||||
Description = tag.Description,
|
||||
CreatedAt = tag.CreatedAt,
|
||||
PictureCount = tag.Pictures?.Count ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<TagResponse> CreateTagAsync(string name, string? description = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("标签名称不能为空");
|
||||
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
// 检查是否已存在同名标签
|
||||
var existingTag = await dbContext.Tags.FirstOrDefaultAsync(t => t.Name.ToLower() == name.ToLower());
|
||||
if (existingTag != null)
|
||||
throw new InvalidOperationException("已存在相同名称的标签");
|
||||
|
||||
var tag = new Tag
|
||||
{
|
||||
Name = name.Trim(),
|
||||
Description = description?.Trim(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Pictures = new List<Picture>() // 初始化为空集合而不是null
|
||||
};
|
||||
|
||||
dbContext.Tags.Add(tag);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return new TagResponse
|
||||
{
|
||||
Id = tag.Id,
|
||||
Name = tag.Name,
|
||||
Description = tag.Description,
|
||||
CreatedAt = tag.CreatedAt,
|
||||
PictureCount = 0
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<TagResponse> UpdateTagAsync(int id, string? name = null, string? description = null)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
var tag = await dbContext.Tags.FindAsync(id);
|
||||
if (tag == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{id}的标签");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
// 检查是否已存在同名标签(不包括当前标签)
|
||||
var existingTag =
|
||||
await dbContext.Tags.FirstOrDefaultAsync(t => t.Id != id && t.Name.ToLower() == name.ToLower());
|
||||
|
||||
if (existingTag != null)
|
||||
throw new InvalidOperationException("已存在相同名称的标签");
|
||||
|
||||
tag.Name = name.Trim();
|
||||
}
|
||||
|
||||
if (description != null) // 允许设置为空字符串
|
||||
{
|
||||
tag.Description = description.Trim();
|
||||
}
|
||||
|
||||
tag.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return new TagResponse
|
||||
{
|
||||
Id = tag.Id,
|
||||
Name = tag.Name,
|
||||
Description = tag.Description,
|
||||
CreatedAt = tag.CreatedAt,
|
||||
PictureCount = tag.Pictures?.Count ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteTagAsync(int id)
|
||||
{
|
||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||
|
||||
var tag = await dbContext.Tags.FindAsync(id);
|
||||
if (tag == null)
|
||||
throw new KeyNotFoundException($"找不到ID为{id}的标签");
|
||||
|
||||
dbContext.Tags.Remove(tag);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user