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);
+ }
}