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!);