feat(file): 添加根级文件查询功能
- 新增 GetRootFilesEndpoint 端点,支持 GET /api/files/root 请求 - 添加 GetRootFilesQuery 查询对象用于获取根级文件 - 实现 GetRootFilesQueryHandler 处理器,查询 FolderId 为 null 的文件 - 统一根目录与子文件夹的数据源,均使用数据库而非 S3 列表 - 修复集成测试中的 MongoDB 访问方式,改用 MongoDB.Entities 静态 DB.Default - 更新权限配置,使 Admin 角色拥有全部权限而非仅部分权限
This commit is contained in:
parent
8e3878b9ff
commit
e6b65b6f8f
@ -0,0 +1,22 @@
|
||||
using File.Application.Features.Folders.DTOs;
|
||||
using File.Application.Features.Folders.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.file.Folders;
|
||||
|
||||
/// <summary>
|
||||
/// 查询根级文件端点。GET /api/files/root,
|
||||
/// 返回当前用户 FolderId=null 的所有 FileMeta,使根目录与子文件夹共享同一数据源(DB)。
|
||||
/// 注意:路径用 /files/root 而非 /folders/root/files,避免与 POST /folders/{FolderId}/files 模板冲突。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class GetRootFilesEndpoint(IMediator mediator) : Endpoint<EmptyRequest, List<FileMetaDto>>
|
||||
{
|
||||
public override void Configure() => Get("/files/root");
|
||||
|
||||
public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new GetRootFilesQuery(), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using File.Application.Features.Folders.DTOs;
|
||||
using File.Application.Features.Folders.Queries;
|
||||
using File.Infrastructure.Persistence;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace File.Application.Features.Folders.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 查询根级文件处理器。返回当前用户名下 FolderId=null 的所有 FileMeta(按名称排序)。
|
||||
/// 与 GetFolderQueryHandler 配合,使根目录与子文件夹共享同一数据源(DB),而非 S3 列表。
|
||||
/// </summary>
|
||||
public class GetRootFilesQueryHandler(FileDbContext db, ICurrentUserContext currentUser)
|
||||
: IRequestHandler<GetRootFilesQuery, List<FileMetaDto>>
|
||||
{
|
||||
public async Task<List<FileMetaDto>> Handle(GetRootFilesQuery req, CancellationToken ct)
|
||||
{
|
||||
var owner = currentUser.GetRequiredUserId();
|
||||
var files = await db.Files
|
||||
.Where(f => f.FolderId == null && f.OwnerId == owner)
|
||||
.OrderBy(f => f.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return files.Select(f => new FileMetaDto(f.Id, f.FolderId, f.Name, f.S3Key, f.S3Bucket,
|
||||
f.Size, f.ContentType, f.OwnerId, f.CreatedAt, f.UpdatedAt)).ToList();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
using File.Application.Features.Folders.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace File.Application.Features.Folders.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询根级文件(FolderId=null 的 FileMeta)。
|
||||
/// 根目录和子文件夹统一从 DB 查,避免与 S3 扁平列表数据源冲突。
|
||||
/// </summary>
|
||||
public record GetRootFilesQuery : IRequest<List<FileMetaDto>>;
|
||||
@ -36,8 +36,9 @@ public static class SeedData
|
||||
var userRoleId = Guid.Parse("20000000-0000-0000-0000-000000000003");
|
||||
|
||||
var allPerms = await db.Permissions.ToListAsync();
|
||||
// SuperAdmin 与 Admin 均拥有全部权限;User 仅拥有只读权限。
|
||||
await db.RolePermissions.AddRangeAsync(allPerms.Select(p => new RolePermission { RoleId = superAdminRoleId, PermissionId = p.Id }));
|
||||
await db.RolePermissions.AddRangeAsync(allPerms.Where(p => p.Code is "user:read" or "user:create" or "user:update" or "role:read" or "permission:read").Select(p => new RolePermission { RoleId = adminRoleId, PermissionId = p.Id }));
|
||||
await db.RolePermissions.AddRangeAsync(allPerms.Select(p => new RolePermission { RoleId = adminRoleId, PermissionId = p.Id }));
|
||||
await db.RolePermissions.AddRangeAsync(allPerms.Where(p => p.Code is "user:read" or "role:read" or "permission:read").Select(p => new RolePermission { RoleId = userRoleId, PermissionId = p.Id }));
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
@ -160,16 +161,10 @@ public static class SeedData
|
||||
var adminRoleId = Guid.Parse("20000000-0000-0000-0000-000000000002");
|
||||
var userRoleId = Guid.Parse("20000000-0000-0000-0000-000000000003");
|
||||
|
||||
// SuperAdmin 与 Admin 拥有全部权限(动态取全库权限码,避免后续新增权限时遗漏)。
|
||||
var allPermissionCodes = await db.Permissions.Select(p => p.Code).ToListAsync();
|
||||
await EnsureRolePermissionsAsync(db, superAdminRoleId, allPermissionCodes);
|
||||
await EnsureRolePermissionsAsync(db, adminRoleId, [
|
||||
"user:read",
|
||||
"user:create",
|
||||
"user:update",
|
||||
"role:read",
|
||||
"permission:read",
|
||||
"menu:read"
|
||||
]);
|
||||
await EnsureRolePermissionsAsync(db, adminRoleId, allPermissionCodes);
|
||||
await EnsureRolePermissionsAsync(db, userRoleId, [
|
||||
"user:read",
|
||||
"role:read",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using IM.Application.Chat.Queries;
|
||||
using IM.Domain.Enums;
|
||||
using IM.Infrastructure.Mongo;
|
||||
using MongoDB.Entities;
|
||||
using Xunit;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
@ -13,7 +14,7 @@ namespace BuildFlow.IM.Integration.Tests;
|
||||
/// 权限隔离与消息历史分页用例。
|
||||
/// 通过 MediatR 直接驱动查询/命令处理器,并验证 ChatHub 的非法访问会被 query 的鉴权副作用拦截。
|
||||
/// </summary>
|
||||
[Collection(ImServerCollection.Name)]
|
||||
[Xunit.Collection(ImServerCollection.Name)]
|
||||
public class ChatHubAuthAndHistoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ImServerFixture _fixture;
|
||||
@ -116,8 +117,6 @@ public class ChatHubAuthAndHistoryTests : IAsyncLifetime
|
||||
/// <summary>向指定会话连续写入 N 条消息,返回按插入顺序的 ObjectId 列表。</summary>
|
||||
private async Task<List<string>> SeedMessagesAsync(Guid conversationId, int count)
|
||||
{
|
||||
await using var scope = _fixture.Services.CreateAsyncScope();
|
||||
var mongo = scope.ServiceProvider.GetRequiredService<MongoDbContext>();
|
||||
var docs = new List<MessageDocument>();
|
||||
var baseTime = DateTime.UtcNow.AddMinutes(-count);
|
||||
for (var i = 0; i < count; i++)
|
||||
@ -132,8 +131,8 @@ public class ChatHubAuthAndHistoryTests : IAsyncLifetime
|
||||
CreatedAt = baseTime.AddSeconds(i),
|
||||
});
|
||||
}
|
||||
await mongo.Messages.InsertManyAsync(docs);
|
||||
return docs.Select(d => d.Id).ToList();
|
||||
await DB.Default.SaveAsync(docs);
|
||||
return docs.Select(d => d.ID).ToList();
|
||||
}
|
||||
|
||||
private static async Task WaitForAsync(Func<bool> condition, TimeSpan? timeout = null)
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
using IM.Application.Chat.DTOs;
|
||||
using IM.Domain.Common;
|
||||
using IM.Domain.Enums;
|
||||
using IM.Infrastructure.Mongo;
|
||||
using IM.Infrastructure.Persistence;
|
||||
using MongoDB.Entities;
|
||||
using Xunit;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -12,7 +14,7 @@ namespace BuildFlow.IM.Integration.Tests;
|
||||
/// <summary>
|
||||
/// ChatHub 消息收发端到端用例。覆盖发送 → 广播 → 落库 → 未读累加的核心闭环。
|
||||
/// </summary>
|
||||
[Collection(ImServerCollection.Name)]
|
||||
[Xunit.Collection(ImServerCollection.Name)]
|
||||
public class ChatHubMessagingTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ImServerFixture _fixture;
|
||||
@ -69,15 +71,13 @@ public class ChatHubMessagingTests : IAsyncLifetime
|
||||
Assert.Equal(convId, bobReceived!.ConversationId);
|
||||
Assert.Equal(ImServerFixture.AliceId, aliceReceived.SenderId);
|
||||
|
||||
// Assert ②:消息正文已落 MongoDB,可按会话查到
|
||||
await using (var scope = _fixture.Services.CreateAsyncScope())
|
||||
{
|
||||
var mongo = scope.ServiceProvider.GetRequiredService<MongoDbContext>();
|
||||
var docs = await mongo.Messages.Find(m => m.ConversationId == convId).ToListAsync();
|
||||
var persisted = Assert.Single(docs);
|
||||
Assert.Equal("hello integration", persisted.Content);
|
||||
Assert.False(persisted.IsRecalled);
|
||||
}
|
||||
// Assert ②:消息正文已落 MongoDB,可按会话查到(MongoDB.Entities 静态 DB.Default)
|
||||
var docs = await DB.Default.Find<MessageDocument>()
|
||||
.Match(m => m.ConversationId == convId)
|
||||
.ExecuteAsync();
|
||||
var persisted = Assert.Single(docs);
|
||||
Assert.Equal("hello integration", persisted.Content);
|
||||
Assert.False(persisted.IsRecalled);
|
||||
|
||||
// Assert ③:会话列表摘要已更新(PostgreSQL 冷数据)
|
||||
await using (var scope2 = _fixture.Services.CreateAsyncScope())
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
using IM.Application.Chat.DTOs;
|
||||
using IM.Domain.Common;
|
||||
using IM.Domain.Enums;
|
||||
using IM.Infrastructure.Mongo;
|
||||
using IM.Infrastructure.Persistence;
|
||||
using MongoDB.Entities;
|
||||
using Xunit;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -13,7 +15,7 @@ namespace BuildFlow.IM.Integration.Tests;
|
||||
/// 已读回执与消息撤回的端到端用例。
|
||||
/// 覆盖:已读双写清零(PG+Redis)并广播、撤回标记 Mongo 并广播、撤回会话摘要更新。
|
||||
/// </summary>
|
||||
[Collection(ImServerCollection.Name)]
|
||||
[Xunit.Collection(ImServerCollection.Name)]
|
||||
public class ChatHubReadAndRecallTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ImServerFixture _fixture;
|
||||
@ -111,15 +113,14 @@ public class ChatHubReadAndRecallTests : IAsyncLifetime
|
||||
// Act:Alice 撤回自己的消息(在 2 分钟时间窗内)
|
||||
await alice.InvokeAsync("RecallMessage", new RecallMessageRequest(convId, messageId!));
|
||||
|
||||
// Assert ①:Mongo 标记撤回
|
||||
await using (var scope = _fixture.Services.CreateAsyncScope())
|
||||
{
|
||||
var mongo = scope.ServiceProvider.GetRequiredService<MongoDbContext>();
|
||||
var doc = await mongo.Messages.Find(m => m.Id == messageId!).FirstOrDefaultAsync();
|
||||
Assert.True(doc.IsRecalled);
|
||||
Assert.Equal(ImServerFixture.AliceId, doc.RecalledBy);
|
||||
Assert.NotNull(doc.RecalledAt);
|
||||
}
|
||||
// Assert ①:Mongo 标记撤回(MongoDB.Entities 静态 DB.Default)
|
||||
var docs = await DB.Default.Find<MessageDocument>()
|
||||
.Match(m => m.ID == messageId!)
|
||||
.ExecuteAsync();
|
||||
var doc = docs.First();
|
||||
Assert.True(doc.IsRecalled);
|
||||
Assert.Equal(ImServerFixture.AliceId, doc.RecalledBy);
|
||||
Assert.NotNull(doc.RecalledAt);
|
||||
|
||||
// Assert ②:会话列表摘要更新为撤回提示(因为撤回的恰是最后一条消息)
|
||||
await using (var scope2 = _fixture.Services.CreateAsyncScope())
|
||||
|
||||
@ -12,6 +12,7 @@ using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using FluentValidation;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Entities;
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using BuildFlow.Shared.Infrastructure;
|
||||
using IM.Application.Chat.Commands;
|
||||
@ -94,11 +95,9 @@ public sealed class ImServerFixture : IAsyncLifetime
|
||||
options.UseNpgsql(PostgresConnection)
|
||||
.AddInterceptors(sp.GetRequiredService<AuditInterceptor>()));
|
||||
|
||||
// —— MongoDB:消息正文 ——
|
||||
var mongoClient = new MongoClient(MongoConnection);
|
||||
// —— MongoDB:消息正文(MongoDB.Entities 静态 DB.Default)——
|
||||
var mongoDbName = MongoUrl.Create(MongoConnection).DatabaseName;
|
||||
services.AddSingleton<IMongoClient>(_ => mongoClient);
|
||||
services.AddScoped(_ => new MongoDbContext(mongoClient, mongoDbName));
|
||||
DB.InitAsync(mongoDbName, MongoClientSettings.FromConnectionString(MongoConnection)).GetAwaiter().GetResult();
|
||||
|
||||
// —— Redis:在线状态与未读计数的真实实现 ——
|
||||
services.AddSingleton<IConnectionMultiplexer>(_ => _redis!);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user