diff --git a/src/modules/rag/Application/Chat/Handlers/DeleteConversationCommandHandler.cs b/src/modules/rag/Application/Chat/Handlers/DeleteConversationCommandHandler.cs index 8cd8437..1b9f98a 100644 --- a/src/modules/rag/Application/Chat/Handlers/DeleteConversationCommandHandler.cs +++ b/src/modules/rag/Application/Chat/Handlers/DeleteConversationCommandHandler.cs @@ -1,5 +1,6 @@ using MediatR; using BuildFlow.Shared.Domain.Exceptions; +using BuildFlow.Shared.Domain.Common; using BuildFlow.Domain.Interfaces; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Application.Chat.Commands; @@ -9,12 +10,18 @@ namespace BuildFlow.Application.Chat.Handlers; /// /// Delete会话Command处理器。执行业务校验后持久化。 /// -public class DeleteConversationCommandHandler(RagDbContext db, IChatMessageCache cache) +public class DeleteConversationCommandHandler( + RagDbContext db, + IChatMessageCache cache, + ICurrentUserContext userContext) : IRequestHandler { public async Task Handle(DeleteConversationCommand request, CancellationToken ct) { - var conversation = await db.Conversations.FindAsync([request.Id], ct) + var userId = Guid.Parse(userContext.GetRequiredUserId()); + var conversation = await db.Conversations.FirstOrDefaultAsync( + x => x.Id == request.Id && x.UserId == userId, + ct) ?? throw new NotFoundException(ErrorCode.NotFound, "会话不存在"); db.Conversations.Remove(conversation); diff --git a/src/modules/rag/Application/Chat/Handlers/GetConversationDetailQueryHandler.cs b/src/modules/rag/Application/Chat/Handlers/GetConversationDetailQueryHandler.cs index 84f52d6..a9d67e0 100644 --- a/src/modules/rag/Application/Chat/Handlers/GetConversationDetailQueryHandler.cs +++ b/src/modules/rag/Application/Chat/Handlers/GetConversationDetailQueryHandler.cs @@ -1,6 +1,7 @@ using MediatR; using BuildFlow.Application.Chat.DTOs; using BuildFlow.Shared.Domain.Exceptions; +using BuildFlow.Shared.Domain.Common; using BuildFlow.Domain.Interfaces; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Application.Chat.Queries; @@ -10,12 +11,18 @@ namespace BuildFlow.Application.Chat.Handlers; /// /// Get会话Detail查询处理器。执行业务校验后持久化。 /// -public class GetConversationDetailQueryHandler(RagDbContext db, IChatMessageCache cache) +public class GetConversationDetailQueryHandler( + RagDbContext db, + IChatMessageCache cache, + ICurrentUserContext userContext) : IRequestHandler { public async Task Handle(GetConversationDetailQuery request, CancellationToken ct) { - var conversation = await db.Conversations.FindAsync([request.Id], ct) + var userId = Guid.Parse(userContext.GetRequiredUserId()); + var conversation = await db.Conversations.FirstOrDefaultAsync( + x => x.Id == request.Id && x.UserId == userId, + ct) ?? throw new NotFoundException(ErrorCode.NotFound, "会话不存在"); // 先查 Redis 缓存 diff --git a/src/modules/rag/Application/Chat/Handlers/SendMessageCommandHandler.cs b/src/modules/rag/Application/Chat/Handlers/SendMessageCommandHandler.cs index e060b8e..35c22d5 100644 --- a/src/modules/rag/Application/Chat/Handlers/SendMessageCommandHandler.cs +++ b/src/modules/rag/Application/Chat/Handlers/SendMessageCommandHandler.cs @@ -3,6 +3,7 @@ using BuildFlow.Application.Chat.DTOs; using BuildFlow.Domain.Entities.ChatMessage; using BuildFlow.Domain.Enums; using BuildFlow.Shared.Domain.Exceptions; +using BuildFlow.Shared.Domain.Common; using BuildFlow.Domain.Interfaces; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Application.Chat.Commands; @@ -12,28 +13,38 @@ using AiChatRole = Microsoft.Extensions.AI.ChatRole; namespace BuildFlow.Application.Chat.Handlers; -public class SendMessageCommandHandler(RagDbContext db, IAIChatAgent chatAgent, IChatMessageCache cache) +/// +/// 向当前用户拥有的会话发送消息,并持久化用户消息和 AI 回复。 +/// +public class SendMessageCommandHandler( + RagDbContext db, + IAIChatAgent chatAgent, + IChatMessageCache cache, + ICurrentUserContext userContext) : IRequestHandler { public async Task Handle(SendMessageCommand request, CancellationToken ct) { - var conversation = await db.Conversations.FindAsync([request.ConversationId], ct) + var userId = Guid.Parse(userContext.GetRequiredUserId()); + var conversation = await db.Conversations.FirstOrDefaultAsync( + x => x.Id == request.ConversationId && x.UserId == userId, + ct) ?? throw new NotFoundException(ErrorCode.NotFound, "会话不存在"); var userMessage = new ChatMessageEntity { - ConversationId = request.ConversationId, + ConversationId = conversation.Id, Role = ChatRoleEnum.User, Content = request.Content }; await db.ChatMessages.AddAsync(userMessage, ct); await db.SaveChangesAsync(ct); - await cache.AppendMessageAsync(request.ConversationId, + await cache.AppendMessageAsync(conversation.Id, new CachedChatMessage(userMessage.Id, ChatRoleEnum.User.ToString(), userMessage.Content, null, userMessage.CreatedAt), ct); var history = await db.ChatMessages - .Where(m => m.ConversationId == request.ConversationId) + .Where(m => m.ConversationId == conversation.Id) .OrderBy(m => m.CreatedAt) .ToListAsync(ct); @@ -45,14 +56,14 @@ public class SendMessageCommandHandler(RagDbContext db, IAIChatAgent chatAgent, var assistantMessage = new ChatMessageEntity { - ConversationId = request.ConversationId, + ConversationId = conversation.Id, Role = ChatRoleEnum.Assistant, Content = reply }; await db.ChatMessages.AddAsync(assistantMessage, ct); await db.SaveChangesAsync(ct); - await cache.AppendMessageAsync(request.ConversationId, + await cache.AppendMessageAsync(conversation.Id, new CachedChatMessage(assistantMessage.Id, ChatRoleEnum.Assistant.ToString(), assistantMessage.Content, null, assistantMessage.CreatedAt), ct); return new SendMessageResponse(assistantMessage.Id, reply); diff --git a/src/modules/rag/Application/Chat/Handlers/UpdateConversationCommandHandler.cs b/src/modules/rag/Application/Chat/Handlers/UpdateConversationCommandHandler.cs index 711343f..995be1f 100644 --- a/src/modules/rag/Application/Chat/Handlers/UpdateConversationCommandHandler.cs +++ b/src/modules/rag/Application/Chat/Handlers/UpdateConversationCommandHandler.cs @@ -1,19 +1,28 @@ using BuildFlow.Application.Chat.Commands; using BuildFlow.Application.Chat.DTOs; -using BuildFlow.Domain.Entities.Conversation; using BuildFlow.Infrastructure.Persistence; +using BuildFlow.Shared.Domain.Common; using BuildFlow.Shared.Domain.Errors; using BuildFlow.Shared.Domain.Exceptions; using MediatR; namespace BuildFlow.Application.Chat.Handlers; -public class UpdateConversationCommandHandler(RagDbContext db) : IRequestHandler +/// +/// 更新当前用户拥有的 RAG 会话基本信息。 +/// +public class UpdateConversationCommandHandler( + RagDbContext db, + ICurrentUserContext userContext) + : IRequestHandler { public async Task Handle(UpdateConversationCommand request, CancellationToken ct) { - var conversation = await db.Conversations.FindAsync([request.Id], ct) - ?? throw new BusinessException(ErrorCode.NotFound, "会话不存在"); + var userId = Guid.Parse(userContext.GetRequiredUserId()); + var conversation = await db.Conversations.FirstOrDefaultAsync( + x => x.Id == request.Id && x.UserId == userId, + ct) + ?? throw new NotFoundException(ErrorCode.NotFound, "会话不存在"); if (!string.IsNullOrWhiteSpace(request.Title)) conversation.Title = request.Title; await db.SaveChangesAsync(ct); diff --git a/tests/BuildFlow.Rag.Tests/ChatEndToEndTests.cs b/tests/BuildFlow.Rag.Tests/ChatEndToEndTests.cs index bc6cdc5..216f96b 100644 --- a/tests/BuildFlow.Rag.Tests/ChatEndToEndTests.cs +++ b/tests/BuildFlow.Rag.Tests/ChatEndToEndTests.cs @@ -4,6 +4,7 @@ using BuildFlow.Domain.Entities.Conversation; using BuildFlow.Domain.Enums; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Rag.Tests.Stub; +using BuildFlow.Shared.Domain.Errors; using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Infrastructure; using FluentAssertions; @@ -67,13 +68,14 @@ public class ChatEndToEndTests { // Given: 已创建会话 + 两轮对话 await using var db = CreateDb("multi-turn"); - var userId = Guid.Parse(new TestCurrentUserContext().UserId); + var userContext = new TestCurrentUserContext(); + var userId = Guid.Parse(userContext.UserId); var conversationId = Guid.NewGuid(); SeedConversation(db, conversationId, userId); var cache = new StubChatMessageCache(); var chatAgent = new StubAIChatAgent { ReplyText = "你好!我是 AI 助手。" }; - var handler = new SendMessageCommandHandler(db, chatAgent, cache); + var handler = new SendMessageCommandHandler(db, chatAgent, cache, userContext); // When: 第一轮 var round1 = await handler.Handle( @@ -113,7 +115,11 @@ public class ChatEndToEndTests await using var db = CreateDb("not-found"); var cache = new StubChatMessageCache(); var chatAgent = new StubAIChatAgent(); - var handler = new SendMessageCommandHandler(db, chatAgent, cache); + var handler = new SendMessageCommandHandler( + db, + chatAgent, + cache, + new TestCurrentUserContext()); // When/Then: 发消息到不存在的会话 var act = async () => await handler.Handle( @@ -123,6 +129,29 @@ public class ChatEndToEndTests await act.Should().ThrowAsync(); } + [Fact] + public async Task SendMessage_OtherUsersConversation_ThrowsNotFoundWithoutWriting() + { + await using var db = CreateDb("send-other-user"); + var userContext = new TestCurrentUserContext(); + var conversationId = Guid.NewGuid(); + SeedConversation(db, conversationId, Guid.NewGuid()); + var cache = new StubChatMessageCache(); + var handler = new SendMessageCommandHandler( + db, + new StubAIChatAgent(), + cache, + userContext); + var request = new SendMessageCommand(conversationId, "越权消息"); + + var act = () => handler.Handle(request, CancellationToken.None); + + await act.Should().ThrowAsync() + .Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在"); + db.ChatMessages.Should().BeEmpty(); + cache.Count(conversationId).Should().Be(0); + } + [Fact] public async Task CreateConversation_UnauthorizedUser_Throws() { diff --git a/tests/BuildFlow.Rag.Tests/EmbeddingAndQueryTests.cs b/tests/BuildFlow.Rag.Tests/EmbeddingAndQueryTests.cs index 41d8fcf..2f1a67e 100644 --- a/tests/BuildFlow.Rag.Tests/EmbeddingAndQueryTests.cs +++ b/tests/BuildFlow.Rag.Tests/EmbeddingAndQueryTests.cs @@ -12,6 +12,8 @@ using BuildFlow.Domain.Enums; using BuildFlow.Domain.Interfaces; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Rag.Tests.Stub; +using BuildFlow.Shared.Domain.Errors; +using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Infrastructure; using FluentAssertions; using Microsoft.EntityFrameworkCore; @@ -107,14 +109,15 @@ public class EmbeddingAndQueryTests { var db = CreateDb(); var convId = Guid.NewGuid(); - var userId = Guid.Parse(new TestCurrentUserContext().UserId); + var userContext = new TestCurrentUserContext(); + var userId = Guid.Parse(userContext.UserId); db.Conversations.Add(new ConversationEntity { Id = convId, Title = "详情", UserId = userId }); db.ChatMessages.Add(new ChatMessageEntity { ConversationId = convId, Role = ChatRoleEnum.User, Content = "你好" }); db.ChatMessages.Add(new ChatMessageEntity { ConversationId = convId, Role = ChatRoleEnum.Assistant, Content = "回复" }); await db.SaveChangesAsync(); var cache = new StubChatMessageCache(); - var handler = new GetConversationDetailQueryHandler(db, cache); + var handler = new GetConversationDetailQueryHandler(db, cache, userContext); var result = await handler.Handle(new GetConversationDetailQuery(convId), CancellationToken.None); result.Title.Should().Be("详情"); @@ -126,17 +129,93 @@ public class EmbeddingAndQueryTests { var db = CreateDb(); var convId = Guid.NewGuid(); - var userId = Guid.Parse(new TestCurrentUserContext().UserId); + var userContext = new TestCurrentUserContext(); + var userId = Guid.Parse(userContext.UserId); db.Conversations.Add(new ConversationEntity { Id = convId, Title = "待删", UserId = userId }); await db.SaveChangesAsync(); var cache = new StubChatMessageCache(); await cache.SetMessagesAsync(convId, [new CachedChatMessage(Guid.NewGuid(), "user", "hi", null, DateTime.UtcNow)]); - var handler = new DeleteConversationCommandHandler(db, cache); + var handler = new DeleteConversationCommandHandler(db, cache, userContext); await handler.Handle(new DeleteConversationCommand(convId), CancellationToken.None); (await db.Conversations.AnyAsync(c => c.Id == convId)).Should().BeFalse(); cache.Count(convId).Should().Be(0, "cache 也应清除"); } + + [Fact] + public async Task GetConversationDetail_OtherUsersConversation_ThrowsNotFound() + { + await using var db = CreateDb(); + var userContext = new TestCurrentUserContext(); + var conversationId = Guid.NewGuid(); + db.Conversations.Add(new ConversationEntity + { + Id = conversationId, + Title = "他人的详情", + UserId = Guid.NewGuid(), + }); + await db.SaveChangesAsync(); + var handler = new GetConversationDetailQueryHandler( + db, + new StubChatMessageCache(), + userContext); + var request = new GetConversationDetailQuery(conversationId); + + var act = () => handler.Handle(request, CancellationToken.None); + + await act.Should().ThrowAsync() + .Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在"); + } + + [Fact] + public async Task UpdateConversation_OtherUsersConversation_DoesNotChangeTitle() + { + await using var db = CreateDb(); + var userContext = new TestCurrentUserContext(); + var conversation = new ConversationEntity + { + Id = Guid.NewGuid(), + Title = "原始标题", + UserId = Guid.NewGuid(), + }; + db.Conversations.Add(conversation); + await db.SaveChangesAsync(); + var handler = new UpdateConversationCommandHandler(db, userContext); + var request = new UpdateConversationCommand(conversation.Id, "越权更新", null); + + var act = () => handler.Handle(request, CancellationToken.None); + + await act.Should().ThrowAsync() + .Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在"); + conversation.Title.Should().Be("原始标题"); + } + + [Fact] + public async Task DeleteConversation_OtherUsersConversation_LeavesDbAndCacheUntouched() + { + await using var db = CreateDb(); + var userContext = new TestCurrentUserContext(); + var conversationId = Guid.NewGuid(); + db.Conversations.Add(new ConversationEntity + { + Id = conversationId, + Title = "不可删除", + UserId = Guid.NewGuid(), + }); + await db.SaveChangesAsync(); + var cache = new StubChatMessageCache(); + await cache.SetMessagesAsync(conversationId, + [new CachedChatMessage(Guid.NewGuid(), "user", "保留", null, DateTime.UtcNow)]); + var handler = new DeleteConversationCommandHandler(db, cache, userContext); + var request = new DeleteConversationCommand(conversationId); + + var act = () => handler.Handle(request, CancellationToken.None); + + await act.Should().ThrowAsync() + .Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在"); + (await db.Conversations.AnyAsync(x => x.Id == conversationId)).Should().BeTrue(); + cache.Count(conversationId).Should().Be(1); + } }