diff --git a/src/BuildFlow.Host/Endpoints/file/Folders/GetRootFilesEndpoint.cs b/src/BuildFlow.Host/Endpoints/file/Folders/GetRootFilesEndpoint.cs new file mode 100644 index 0000000..2676647 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/file/Folders/GetRootFilesEndpoint.cs @@ -0,0 +1,22 @@ +using File.Application.Features.Folders.DTOs; +using File.Application.Features.Folders.Queries; +using MediatR; + +namespace BuildFlow.Host.Endpoints.file.Folders; + +/// +/// 查询根级文件端点。GET /api/files/root, +/// 返回当前用户 FolderId=null 的所有 FileMeta,使根目录与子文件夹共享同一数据源(DB)。 +/// 注意:路径用 /files/root 而非 /folders/root/files,避免与 POST /folders/{FolderId}/files 模板冲突。 +/// +[Authorize] +public class GetRootFilesEndpoint(IMediator mediator) : Endpoint> +{ + 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); + } +} diff --git a/src/modules/file/Application/Features/Folders/Handlers/GetRootFilesQueryHandler.cs b/src/modules/file/Application/Features/Folders/Handlers/GetRootFilesQueryHandler.cs new file mode 100644 index 0000000..140c025 --- /dev/null +++ b/src/modules/file/Application/Features/Folders/Handlers/GetRootFilesQueryHandler.cs @@ -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; + +/// +/// 查询根级文件处理器。返回当前用户名下 FolderId=null 的所有 FileMeta(按名称排序)。 +/// 与 GetFolderQueryHandler 配合,使根目录与子文件夹共享同一数据源(DB),而非 S3 列表。 +/// +public class GetRootFilesQueryHandler(FileDbContext db, ICurrentUserContext currentUser) + : IRequestHandler> +{ + public async Task> 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(); + } +} diff --git a/src/modules/file/Application/Features/Folders/Queries/GetRootFilesQuery.cs b/src/modules/file/Application/Features/Folders/Queries/GetRootFilesQuery.cs new file mode 100644 index 0000000..30d0552 --- /dev/null +++ b/src/modules/file/Application/Features/Folders/Queries/GetRootFilesQuery.cs @@ -0,0 +1,10 @@ +using File.Application.Features.Folders.DTOs; +using MediatR; + +namespace File.Application.Features.Folders.Queries; + +/// +/// 查询根级文件(FolderId=null 的 FileMeta)。 +/// 根目录和子文件夹统一从 DB 查,避免与 S3 扁平列表数据源冲突。 +/// +public record GetRootFilesQuery : IRequest>; diff --git a/src/modules/platform/Infrastructure/Persistence/SeedData.cs b/src/modules/platform/Infrastructure/Persistence/SeedData.cs index 7223f59..407db95 100644 --- a/src/modules/platform/Infrastructure/Persistence/SeedData.cs +++ b/src/modules/platform/Infrastructure/Persistence/SeedData.cs @@ -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", diff --git a/tests/BuildFlow.IM.Integration.Tests/ChatHubAuthAndHistoryTests.cs b/tests/BuildFlow.IM.Integration.Tests/ChatHubAuthAndHistoryTests.cs index 532835d..15f4511 100644 --- a/tests/BuildFlow.IM.Integration.Tests/ChatHubAuthAndHistoryTests.cs +++ b/tests/BuildFlow.IM.Integration.Tests/ChatHubAuthAndHistoryTests.cs @@ -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 的鉴权副作用拦截。 /// -[Collection(ImServerCollection.Name)] +[Xunit.Collection(ImServerCollection.Name)] public class ChatHubAuthAndHistoryTests : IAsyncLifetime { private readonly ImServerFixture _fixture; @@ -116,8 +117,6 @@ public class ChatHubAuthAndHistoryTests : IAsyncLifetime /// 向指定会话连续写入 N 条消息,返回按插入顺序的 ObjectId 列表。 private async Task> SeedMessagesAsync(Guid conversationId, int count) { - await using var scope = _fixture.Services.CreateAsyncScope(); - var mongo = scope.ServiceProvider.GetRequiredService(); var docs = new List(); 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 condition, TimeSpan? timeout = null) diff --git a/tests/BuildFlow.IM.Integration.Tests/ChatHubMessagingTests.cs b/tests/BuildFlow.IM.Integration.Tests/ChatHubMessagingTests.cs index 49c18a1..0d031a0 100644 --- a/tests/BuildFlow.IM.Integration.Tests/ChatHubMessagingTests.cs +++ b/tests/BuildFlow.IM.Integration.Tests/ChatHubMessagingTests.cs @@ -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; /// /// ChatHub 消息收发端到端用例。覆盖发送 → 广播 → 落库 → 未读累加的核心闭环。 /// -[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(); - 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() + .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()) diff --git a/tests/BuildFlow.IM.Integration.Tests/ChatHubReadAndRecallTests.cs b/tests/BuildFlow.IM.Integration.Tests/ChatHubReadAndRecallTests.cs index 2daf7f8..2482482 100644 --- a/tests/BuildFlow.IM.Integration.Tests/ChatHubReadAndRecallTests.cs +++ b/tests/BuildFlow.IM.Integration.Tests/ChatHubReadAndRecallTests.cs @@ -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 并广播、撤回会话摘要更新。 /// -[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(); - 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() + .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()) diff --git a/tests/BuildFlow.IM.Integration.Tests/ImServerFixture.cs b/tests/BuildFlow.IM.Integration.Tests/ImServerFixture.cs index 87ba0ea..7af83c1 100644 --- a/tests/BuildFlow.IM.Integration.Tests/ImServerFixture.cs +++ b/tests/BuildFlow.IM.Integration.Tests/ImServerFixture.cs @@ -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())); - // —— MongoDB:消息正文 —— - var mongoClient = new MongoClient(MongoConnection); + // —— MongoDB:消息正文(MongoDB.Entities 静态 DB.Default)—— var mongoDbName = MongoUrl.Create(MongoConnection).DatabaseName; - services.AddSingleton(_ => mongoClient); - services.AddScoped(_ => new MongoDbContext(mongoClient, mongoDbName)); + DB.InitAsync(mongoDbName, MongoClientSettings.FromConnectionString(MongoConnection)).GetAwaiter().GetResult(); // —— Redis:在线状态与未读计数的真实实现 —— services.AddSingleton(_ => _redis!);