feat(album): add cover picture functionality to albums and enhance album management API

This commit is contained in:
shiyu
2025-06-09 15:12:50 +08:00
parent 9d9393f9ce
commit e55f311c04
24 changed files with 1496 additions and 251 deletions

View File

@@ -3,60 +3,66 @@ using Foxel.Models;
using Foxel.Models.DataBase;
using Foxel.Models.Response.Album;
using Microsoft.EntityFrameworkCore;
using Foxel.Services.Mapping; // 添加 using
namespace Foxel.Services.Media;
public class AlbumService(
IDbContextFactory<MyDbContext> contextFactory,
IHttpContextAccessor httpContextAccessor)
IHttpContextAccessor httpContextAccessor,
IMappingService mappingService) // 注入新的映射服务
: 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)
.Include(a => a.CoverPicture) // 确保包含封面图片
.Include(a => a.Pictures) // 确保包含图片集合以计算 PictureCount
.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
// .Include(a => a.CoverPicture) // 已在上面包含
.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);
// 获取每个相册中的图片数量 - 不再需要MapAlbumToResponse 会处理
// 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();
var albumResponses = albums.Select(mappingService.MapAlbumToResponse).ToList();
// var albumResponses = albums.Select(a => new AlbumResponse
// {
// Id = a.Id,
// Name = a.Name,
// Description = a.Description,
// PictureCount = albumPictureCounts.GetValueOrDefault(a.Id, 0), // 使用 MapAlbumToResponse 后不再需要
// UserId = a.UserId,
// Username = a.User.UserName,
// CreatedAt = a.CreatedAt,
// UpdatedAt = a.UpdatedAt
// }).ToList();
return new PaginatedResult<AlbumResponse>
{
Data = albumResponses,
@@ -65,206 +71,222 @@ public class AlbumService(
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)
.Include(a => a.CoverPicture) // 确保包含封面图片
.Include(a => a.Pictures) // 确保包含图片集合以计算 PictureCount
.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 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;
return mappingService.MapAlbumToResponse(album);
// var response = new AlbumResponse
// {
// Id = album.Id,
// Name = album.Name,
// Description = album.Description,
// PictureCount = pictureCount, // 使用 MapAlbumToResponse 后不再需要
// 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)
public async Task<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId, int? coverPictureId)
{
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,
// User = user, // EF Core 会自动关联
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
UpdatedAt = DateTime.UtcNow,
CoverPictureId = coverPictureId
};
dbContext.Albums.Add(album);
await dbContext.SaveChangesAsync();
// 重新加载创建的相册以包含导航属性,用于 MapAlbumToResponse
var createdAlbum = await dbContext.Albums
.Include(a => a.User)
.Include(a => a.CoverPicture)
.Include(a => a.Pictures)
.FirstAsync(a => a.Id == album.Id);
// 转换为响应模型
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
};
return mappingService.MapAlbumToResponse(createdAlbum);
// return new AlbumResponse
// {
// Id = album.Id,
// Name = album.Name,
// Description = album.Description,
// PictureCount = 0, // MapAlbumToResponse 会处理
// UserId = album.UserId,
// Username = user.UserName, // MapAlbumToResponse 会处理
// CreatedAt = album.CreatedAt,
// UpdatedAt = album.UpdatedAt
// };
}
public async Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null)
public async Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null, int? coverPictureId = 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)
// .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)
if (!userId.HasValue) // userId 仍然需要用于权限检查
throw new ArgumentException("无效的用户ID", nameof(userId));
if (album.UserId != userId.Value)
{
// 检查用户是否是管理员
var user = await dbContext.Users.FindAsync(userId.Value);
if (user == null)
{
throw new UnauthorizedAccessException("您没有权限更新此相册");
}
throw new UnauthorizedAccessException("您没有权限更新此相册");
}
// 更新相册信息
album.Name = name.Trim();
album.Description = description?.Trim() ?? album.Description;
album.UpdatedAt = DateTime.UtcNow;
album.CoverPictureId = coverPictureId;
await dbContext.SaveChangesAsync();
// 重新加载更新后的相册以包含导航属性
var updatedAlbum = await dbContext.Albums
.Include(a => a.User)
.Include(a => a.CoverPicture)
.Include(a => a.Pictures)
.FirstAsync(a => a.Id == album.Id);
// 转换为响应模型
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
};
return mappingService.MapAlbumToResponse(updatedAlbum);
// return new AlbumResponse
// {
// Id = album.Id,
// Name = album.Name,
// Description = album.Description,
// PictureCount = album.Pictures?.Count ?? 0, // MapAlbumToResponse 会处理
// UserId = album.UserId,
// Username = album.User.UserName, // MapAlbumToResponse 会处理
// 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;
// 先找出所有属于这个相册的图片
var pictures = await dbContext.Pictures
.Where(p => p.AlbumId == id)
.ToListAsync();
// 将这些图片的AlbumId设置为null
foreach (var picture in pictures)
{
picture.AlbumId = null;
picture.Album = null;
}
// 保存图片更改
await dbContext.SaveChangesAsync();
// 然后删除相册
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)
var album = await dbContext.Albums.FindAsync(albumId)
?? throw new KeyNotFoundException("相册不存在");
// 检查是否有权限修改此相册
var currentUser = httpContextAccessor.HttpContext?.User;
if (currentUser != null)
@@ -275,14 +297,14 @@ public class AlbumService(
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)
{
@@ -290,13 +312,40 @@ public class AlbumService(
successCount++;
}
}
if (successCount > 0)
{
await dbContext.SaveChangesAsync();
return true;
}
return false;
}
public async Task<bool> SetAlbumCoverAsync(int albumId, int pictureId, int userId)
{
await using var dbContext = await contextFactory.CreateDbContextAsync();
var album = await dbContext.Albums.FirstOrDefaultAsync(a => a.Id == albumId);
if (album == null)
throw new KeyNotFoundException($"找不到ID为 {albumId} 的相册");
// 权限检查:只有相册所有者可以设置封面
if (album.UserId != userId)
throw new UnauthorizedAccessException("您没有权限修改此相册的封面");
var picture = await dbContext.Pictures.FirstOrDefaultAsync(p => p.Id == pictureId);
if (picture == null)
throw new KeyNotFoundException($"找不到ID为 {pictureId} 的图片");
// 确保图片属于该相册
if (picture.AlbumId != albumId)
throw new InvalidOperationException($"图片 {pictureId} 不属于相册 {albumId}");
album.CoverPictureId = pictureId;
album.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync();
return true;
}
}

View File

@@ -7,10 +7,11 @@ 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<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId,int? coverPictureId = null);
Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null, int? coverPictureId = 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);
Task<bool> SetAlbumCoverAsync(int albumId, int pictureId, int userId);
}

View File

@@ -8,6 +8,7 @@ using Foxel.Services.Background;
using Foxel.Services.Configuration;
using Foxel.Services.Storage;
using Foxel.Services.VectorDB;
using Foxel.Services.Mapping;
using Foxel.Utils;
using Microsoft.EntityFrameworkCore;
@@ -20,50 +21,10 @@ public class PictureService(
IBackgroundTaskQueue backgroundTaskQueue,
IVectorDbService vectorDbService,
IStorageService storageService,
IMappingService mappingService, // 添加 IMappingService
ILogger<PictureService> logger)
: IPictureService
{
private async Task<PictureResponse> MapPictureToResponseAsync(Picture picture)
{
string pathUrl = string.Empty;
if (!string.IsNullOrEmpty(picture.Path))
{
pathUrl = await storageService.ExecuteAsync(picture.StorageModeId, provider =>
Task.FromResult(provider.GetUrl(picture.Id,picture.Path)));
}
string originalPathUrl = string.Empty;
if (!string.IsNullOrEmpty(picture.OriginalPath))
{
originalPathUrl = await storageService.ExecuteAsync(picture.StorageModeId, provider =>
Task.FromResult(provider.GetUrl(picture.Id,picture.OriginalPath)));
}
string? thumbnailPathUrl = null;
if (!string.IsNullOrEmpty(picture.ThumbnailPath))
{
thumbnailPathUrl = await storageService.ExecuteAsync(picture.StorageModeId, provider =>
Task.FromResult(provider.GetUrl(picture.Id,picture.ThumbnailPath)));
}
return new PictureResponse
{
Id = picture.Id,
Name = picture.Name,
Path = pathUrl,
OriginalPath = originalPathUrl,
ThumbnailPath = thumbnailPathUrl,
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,
StorageModeName = picture.StorageMode?.Name
};
}
public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(
int page = 1,
@@ -143,20 +104,19 @@ public class PictureService(
var picturesData = await dbContext.Pictures
.Include(p => p.Tags)
.Include(p => p.User)
.Include(p=>p.StorageMode)
.Include(p => p.StorageMode)
.Where(p => ids.Contains((ulong)p.Id))
.ToListAsync();
var picturesOrdered = ids
.Select(id => picturesData.FirstOrDefault(p => p.Id == (int)id))
.Where(p => p != null)
.ToList();
var paginatedResultsTasks = picturesOrdered
var paginatedResults = picturesOrdered
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(async p => await MapPictureToResponseAsync(p!))
.Select(p => mappingService.MapPictureToResponse(p!))
.ToList();
var paginatedResults = (await Task.WhenAll(paginatedResultsTasks)).ToList();
var totalCount = picturesOrdered.Count;
@@ -220,16 +180,15 @@ public class PictureService(
// 获取分页数据
var picturesData = await query
.Include(x=>x.StorageMode)
.Include(x => x.StorageMode)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// 转换为响应格式
var picturesTasks = picturesData
.Select(async p => await MapPictureToResponseAsync(p))
var pictures = picturesData
.Select(p => mappingService.MapPictureToResponse(p))
.ToList();
var pictures = (await Task.WhenAll(picturesTasks)).ToList();
// 处理收藏信息
await PopulateFavoriteInfo(dbContext, pictures, userId);
@@ -663,7 +622,7 @@ public class PictureService(
await backgroundTaskQueue.QueueVisualRecognitionTaskAsync(visualRecognitionPayload);
}
var pictureResponse = await MapPictureToResponseAsync(picture);
var pictureResponse = mappingService.MapPictureToResponse(picture);
return (pictureResponse, picture.Id);
}
finally
@@ -906,7 +865,7 @@ public class PictureService(
picture.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync();
var pictureResponse = await MapPictureToResponseAsync(picture);
var pictureResponse = mappingService.MapPictureToResponse(picture);
return (pictureResponse, userId);
}
@@ -986,7 +945,7 @@ public class PictureService(
return null;
}
var pictureResponse = await MapPictureToResponseAsync(picture);
var pictureResponse = mappingService.MapPictureToResponse(picture);
return picture;
}