diff --git a/src/RAG.Api/Endpoints/Auth/LoginEndpoint.cs b/src/RAG.Api/Endpoints/Auth/LoginEndpoint.cs index 2235b67..f4ee283 100644 --- a/src/RAG.Api/Endpoints/Auth/LoginEndpoint.cs +++ b/src/RAG.Api/Endpoints/Auth/LoginEndpoint.cs @@ -1,6 +1,5 @@ using FastEndpoints; using MediatR; -using Microsoft.Extensions.Configuration; using RAG.Application.Auth.Commands; using RAG.Application.Auth.DTOs; @@ -36,3 +35,19 @@ public class LoginEndpoint(IMediator mediator, IConfiguration config) : Endpoint await Send.OkAsync(result, ct); } } + +/// 登录请求校验。规则与 LoginCommandValidator 一致,HTTP 入口层提前拦截非法输入。 +public class LoginRequestValidator : Validator +{ + public LoginRequestValidator() + { + RuleFor(x => x.Username) + .NotEmpty().WithMessage("用户名不能为空") + .MaximumLength(50).WithMessage("用户名长度不能超过 50 个字符") + .Must(u => u == u.Trim()).WithMessage("用户名前后不能有空格"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("密码不能为空") + .MaximumLength(100).WithMessage("密码长度不能超过 100 个字符"); + } +} diff --git a/src/RAG.Api/Endpoints/Auth/RegisterEndpoint.cs b/src/RAG.Api/Endpoints/Auth/RegisterEndpoint.cs index 67530db..9fc9ca9 100644 --- a/src/RAG.Api/Endpoints/Auth/RegisterEndpoint.cs +++ b/src/RAG.Api/Endpoints/Auth/RegisterEndpoint.cs @@ -23,3 +23,22 @@ public class RegisterEndpoint(IMediator mediator) : Endpoint await Send.OkAsync(true, ct); } } + +/// 自助注册请求校验。 +public class RegisterRequestValidator : Validator +{ + public RegisterRequestValidator() + { + RuleFor(x => x.Username) + .NotEmpty().WithMessage("用户名不能为空") + .Length(3, 50).WithMessage("用户名长度 3-50 个字符"); + + RuleFor(x => x.Email) + .NotEmpty().WithMessage("邮箱不能为空") + .EmailAddress().WithMessage("邮箱格式不正确"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("密码不能为空") + .Length(6, 100).WithMessage("密码长度 6-100 个字符"); + } +} diff --git a/src/RAG.Api/Endpoints/Auth/RevokeTokenEndpoint.cs b/src/RAG.Api/Endpoints/Auth/RevokeTokenEndpoint.cs index 3fba526..bb30bf6 100644 --- a/src/RAG.Api/Endpoints/Auth/RevokeTokenEndpoint.cs +++ b/src/RAG.Api/Endpoints/Auth/RevokeTokenEndpoint.cs @@ -22,3 +22,13 @@ public class RevokeTokenEndpoint(IMediator mediator) : Endpoint吊销刷新令牌请求校验。 +public class RevokeTokenRequestValidator : Validator +{ + public RevokeTokenRequestValidator() + { + RuleFor(x => x.RefreshToken) + .NotEmpty().WithMessage("RefreshToken 不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Chat/CreateConversationEndpoint.cs b/src/RAG.Api/Endpoints/Chat/CreateConversationEndpoint.cs index 8842ae5..ced9865 100644 --- a/src/RAG.Api/Endpoints/Chat/CreateConversationEndpoint.cs +++ b/src/RAG.Api/Endpoints/Chat/CreateConversationEndpoint.cs @@ -21,3 +21,14 @@ public class CreateConversationEndpoint(IMediator mediator) : Endpoint创建会话请求校验。 +public class CreateConversationRequestValidator : Validator +{ + public CreateConversationRequestValidator() + { + RuleFor(x => x.Title) + .NotEmpty().WithMessage("会话标题不能为空") + .MaximumLength(200).WithMessage("会话标题不能超过 200 个字符"); + } +} diff --git a/src/RAG.Api/Endpoints/Chat/DeleteConversationEndpoint.cs b/src/RAG.Api/Endpoints/Chat/DeleteConversationEndpoint.cs index 06e921b..0a1bfc7 100644 --- a/src/RAG.Api/Endpoints/Chat/DeleteConversationEndpoint.cs +++ b/src/RAG.Api/Endpoints/Chat/DeleteConversationEndpoint.cs @@ -23,3 +23,12 @@ public class DeleteConversationEndpoint(IMediator mediator) : Endpoint删除会话请求校验(Id 由路由提供)。 +public class DeleteConversationEndpointRequestValidator : Validator +{ + public DeleteConversationEndpointRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("会话 ID 不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Chat/GetConversationDetailEndpoint.cs b/src/RAG.Api/Endpoints/Chat/GetConversationDetailEndpoint.cs index 1e005fc..a4e7a0a 100644 --- a/src/RAG.Api/Endpoints/Chat/GetConversationDetailEndpoint.cs +++ b/src/RAG.Api/Endpoints/Chat/GetConversationDetailEndpoint.cs @@ -21,3 +21,12 @@ public class GetConversationDetailEndpoint(IMediator mediator) : Endpoint查询会话详情请求校验(Id 由路由提供)。 +public class GetConversationDetailRequestValidator : Validator +{ + public GetConversationDetailRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("会话 ID 不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Chat/SendMessageEndpoint.cs b/src/RAG.Api/Endpoints/Chat/SendMessageEndpoint.cs index 2d9e56c..ebf58a5 100644 --- a/src/RAG.Api/Endpoints/Chat/SendMessageEndpoint.cs +++ b/src/RAG.Api/Endpoints/Chat/SendMessageEndpoint.cs @@ -22,3 +22,14 @@ public class SendMessageEndpoint(IMediator mediator) : Endpoint发送消息请求校验。 +public class SendMessageRequestValidator : Validator +{ + public SendMessageRequestValidator() + { + RuleFor(x => x.Content) + .NotEmpty().WithMessage("消息内容不能为空") + .MaximumLength(10000).WithMessage("消息内容不能超过 10000 个字符"); + } +} diff --git a/src/RAG.Api/Endpoints/Chat/StreamMessageEndpoint.cs b/src/RAG.Api/Endpoints/Chat/StreamMessageEndpoint.cs index fb6a349..4762cf7 100644 --- a/src/RAG.Api/Endpoints/Chat/StreamMessageEndpoint.cs +++ b/src/RAG.Api/Endpoints/Chat/StreamMessageEndpoint.cs @@ -121,3 +121,14 @@ public class StreamMessageEndpoint(RagDbContext db, IAIChatAgent chatAgent, ICha } public record StreamMessageRequest(string Content); + +/// 流式发送消息请求校验。 +public class StreamMessageRequestValidator : Validator +{ + public StreamMessageRequestValidator() + { + RuleFor(x => x.Content) + .NotEmpty().WithMessage("消息内容不能为空") + .MaximumLength(10000).WithMessage("消息内容不能超过 10000 个字符"); + } +} diff --git a/src/RAG.Api/Endpoints/Document/ChunkUploadEndpoint.cs b/src/RAG.Api/Endpoints/Document/ChunkUploadEndpoint.cs index 4cf9a57..6b146c5 100644 --- a/src/RAG.Api/Endpoints/Document/ChunkUploadEndpoint.cs +++ b/src/RAG.Api/Endpoints/Document/ChunkUploadEndpoint.cs @@ -42,6 +42,23 @@ public class InitChunkUploadRequest public int ChunkCount { get; set; } } +/// 分片上传初始化请求校验。 +public class InitChunkUploadRequestValidator : Validator +{ + public InitChunkUploadRequestValidator() + { + RuleFor(x => x.FileName) + .NotEmpty().WithMessage("文件名不能为空") + .MaximumLength(500).WithMessage("文件名不能超过 500 个字符"); + + RuleFor(x => x.FileSize) + .GreaterThan(0).WithMessage("文件大小必须大于 0"); + + RuleFor(x => x.ChunkCount) + .InclusiveBetween(1, 10000).WithMessage("分片数量 ChunkCount 取值范围 1-10000"); + } +} + public class InitChunkUploadEndpoint : Endpoint { public override void Configure() @@ -113,6 +130,19 @@ public class CompleteChunkUploadRequest public string? Title { get; set; } } +/// 分片上传合并完成请求校验。Title 可选(不传则用原文件名)。 +public class CompleteChunkUploadRequestValidator : Validator +{ + public CompleteChunkUploadRequestValidator() + { + When(x => x.Title is not null, () => + { + RuleFor(x => x.Title!) + .MaximumLength(500).WithMessage("文档标题不能超过 500 个字符"); + }); + } +} + public class CompleteChunkUploadEndpoint(IMediator mediator) : Endpoint { public override void Configure() diff --git a/src/RAG.Api/Endpoints/Document/UploadDocumentEndpoint.cs b/src/RAG.Api/Endpoints/Document/UploadDocumentEndpoint.cs index aeb605e..a7ded1f 100644 --- a/src/RAG.Api/Endpoints/Document/UploadDocumentEndpoint.cs +++ b/src/RAG.Api/Endpoints/Document/UploadDocumentEndpoint.cs @@ -44,3 +44,20 @@ public class UploadDocumentEndpoint(IMediator mediator) : Endpoint单文件上传请求校验。Title 可选(不传则用文件名),ChunkingMode 仅当提供时校验取值。 +public class UploadDocumentRequestValidator : Validator +{ + public UploadDocumentRequestValidator() + { + When(x => x.Title is not null, () => + { + RuleFor(x => x.Title!) + .MaximumLength(500).WithMessage("文档标题不能超过 500 个字符"); + }); + + RuleFor(x => x.ChunkingMode) + .InclusiveBetween(0, 2).When(x => x.ChunkingMode.HasValue) + .WithMessage("分块模式 ChunkingMode 取值 0=General,1=Heading,2=ParentChild"); + } +} diff --git a/src/RAG.Api/Endpoints/Embedding/EmbedBatchEndpoint.cs b/src/RAG.Api/Endpoints/Embedding/EmbedBatchEndpoint.cs index 16297ac..d846935 100644 --- a/src/RAG.Api/Endpoints/Embedding/EmbedBatchEndpoint.cs +++ b/src/RAG.Api/Endpoints/Embedding/EmbedBatchEndpoint.cs @@ -25,3 +25,14 @@ public class EmbedBatchEndpoint(IMediator mediator) : Endpoint Texts); + +/// 批量文本向量化请求校验。 +public class EmbedBatchRequestValidator : Validator +{ + public EmbedBatchRequestValidator() + { + RuleFor(x => x.Texts) + .NotEmpty().WithMessage("文本列表不能为空") + .Must(t => t.Count <= 100).WithMessage("批量文本不能超过 100 条"); + } +} diff --git a/src/RAG.Api/Endpoints/Embedding/EmbedTextEndpoint.cs b/src/RAG.Api/Endpoints/Embedding/EmbedTextEndpoint.cs index 05d5563..0084a9f 100644 --- a/src/RAG.Api/Endpoints/Embedding/EmbedTextEndpoint.cs +++ b/src/RAG.Api/Endpoints/Embedding/EmbedTextEndpoint.cs @@ -24,3 +24,14 @@ public class EmbedTextEndpoint(IMediator mediator) : Endpoint单条文本向量化请求校验。 +public class EmbedTextRequestValidator : Validator +{ + public EmbedTextRequestValidator() + { + RuleFor(x => x.Text) + .NotEmpty().WithMessage("文本内容不能为空") + .MaximumLength(10000).WithMessage("单条文本不能超过 10000 个字符"); + } +} diff --git a/src/RAG.Api/Endpoints/KnowledgeBase/CreateKBEndpoint.cs b/src/RAG.Api/Endpoints/KnowledgeBase/CreateKBEndpoint.cs index ebd395f..fb25b26 100644 --- a/src/RAG.Api/Endpoints/KnowledgeBase/CreateKBEndpoint.cs +++ b/src/RAG.Api/Endpoints/KnowledgeBase/CreateKBEndpoint.cs @@ -24,3 +24,59 @@ public class CreateKBEndpoint(IMediator mediator) : Endpoint +/// 创建知识库请求校验。 +/// 名称必填;数值参数(分块/检索配置)限定合理取值范围,避免传入非法值导致下游处理异常。 +/// +public class CreateKnowledgeBaseRequestValidator : Validator +{ + public CreateKnowledgeBaseRequestValidator() + { + RuleFor(x => x.Name) + .NotEmpty().WithMessage("知识库名称不能为空") + .MaximumLength(200).WithMessage("名称不能超过 200 个字符"); + + When(x => x.Description is not null, () => + { + RuleFor(x => x.Description!) + .MaximumLength(1000).WithMessage("描述不能超过 1000 个字符"); + }); + + RuleFor(x => x.ChunkSize) + .InclusiveBetween(100, 8000).When(x => x.ChunkSize.HasValue) + .WithMessage("分块大小 ChunkSize 取值范围 100-8000"); + + RuleFor(x => x.ChunkOverlap) + .InclusiveBetween(0, 1000).When(x => x.ChunkOverlap.HasValue) + .WithMessage("分块重叠 ChunkOverlap 取值范围 0-1000"); + + RuleFor(x => x.ChunkingMode) + .InclusiveBetween(0, 2).When(x => x.ChunkingMode.HasValue) + .WithMessage("分块模式 ChunkingMode 取值 0=General,1=Heading,2=ParentChild"); + + RuleFor(x => x.RetrievalMode) + .InclusiveBetween(0, 2).When(x => x.RetrievalMode.HasValue) + .WithMessage("检索模式 RetrievalMode 取值范围 0-2"); + + RuleFor(x => x.RerankStrategy) + .InclusiveBetween(0, 2).When(x => x.RerankStrategy.HasValue) + .WithMessage("重排策略 RerankStrategy 取值范围 0-2"); + + RuleFor(x => x.RetrievalTopK) + .InclusiveBetween(1, 50).When(x => x.RetrievalTopK.HasValue) + .WithMessage("检索数量 RetrievalTopK 取值范围 1-50"); + + RuleFor(x => x.ContextTopK) + .InclusiveBetween(1, 50).When(x => x.ContextTopK.HasValue) + .WithMessage("上下文数量 ContextTopK 取值范围 1-50"); + + RuleFor(x => x.SimilarityThreshold) + .InclusiveBetween(0, 1).When(x => x.SimilarityThreshold.HasValue) + .WithMessage("相似度阈值 SimilarityThreshold 取值范围 0-1"); + + RuleFor(x => x.VectorWeight) + .InclusiveBetween(0, 1).When(x => x.VectorWeight.HasValue) + .WithMessage("向量权重 VectorWeight 取值范围 0-1"); + } +} diff --git a/src/RAG.Api/Endpoints/KnowledgeBase/DeleteKBEndpoint.cs b/src/RAG.Api/Endpoints/KnowledgeBase/DeleteKBEndpoint.cs index 6a3b4e1..288d71e 100644 --- a/src/RAG.Api/Endpoints/KnowledgeBase/DeleteKBEndpoint.cs +++ b/src/RAG.Api/Endpoints/KnowledgeBase/DeleteKBEndpoint.cs @@ -20,3 +20,12 @@ public class DeleteKBEndpoint(IMediator mediator) : Endpoint删除知识库请求校验(Id 由路由提供)。 +public class DeleteKBEndpointRequestValidator : Validator +{ + public DeleteKBEndpointRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("知识库 ID 不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/KnowledgeBase/UpdateKBEndpoint.cs b/src/RAG.Api/Endpoints/KnowledgeBase/UpdateKBEndpoint.cs index b4e7873..05c95d3 100644 --- a/src/RAG.Api/Endpoints/KnowledgeBase/UpdateKBEndpoint.cs +++ b/src/RAG.Api/Endpoints/KnowledgeBase/UpdateKBEndpoint.cs @@ -31,3 +31,59 @@ public class UpdateKBEndpoint(IMediator mediator) await Send.OkAsync(result, ct); } } + +/// 更新知识库请求校验。仅校验请求体中提供的字段(非 null 时才校验)。 +public class UpdateKnowledgeBaseRequestValidator : Validator +{ + public UpdateKnowledgeBaseRequestValidator() + { + When(x => x.Name is not null, () => + { + RuleFor(x => x.Name!) + .NotEmpty().WithMessage("知识库名称不能为空") + .MaximumLength(200).WithMessage("名称不能超过 200 个字符"); + }); + + When(x => x.Description is not null, () => + { + RuleFor(x => x.Description!) + .MaximumLength(1000).WithMessage("描述不能超过 1000 个字符"); + }); + + RuleFor(x => x.ChunkSize) + .InclusiveBetween(100, 8000).When(x => x.ChunkSize.HasValue) + .WithMessage("分块大小 ChunkSize 取值范围 100-8000"); + + RuleFor(x => x.ChunkOverlap) + .InclusiveBetween(0, 1000).When(x => x.ChunkOverlap.HasValue) + .WithMessage("分块重叠 ChunkOverlap 取值范围 0-1000"); + + RuleFor(x => x.ChunkingMode) + .InclusiveBetween(0, 2).When(x => x.ChunkingMode.HasValue) + .WithMessage("分块模式 ChunkingMode 取值 0=General,1=Heading,2=ParentChild"); + + RuleFor(x => x.RetrievalMode) + .InclusiveBetween(0, 2).When(x => x.RetrievalMode.HasValue) + .WithMessage("检索模式 RetrievalMode 取值范围 0-2"); + + RuleFor(x => x.RerankStrategy) + .InclusiveBetween(0, 2).When(x => x.RerankStrategy.HasValue) + .WithMessage("重排策略 RerankStrategy 取值范围 0-2"); + + RuleFor(x => x.RetrievalTopK) + .InclusiveBetween(1, 50).When(x => x.RetrievalTopK.HasValue) + .WithMessage("检索数量 RetrievalTopK 取值范围 1-50"); + + RuleFor(x => x.ContextTopK) + .InclusiveBetween(1, 50).When(x => x.ContextTopK.HasValue) + .WithMessage("上下文数量 ContextTopK 取值范围 1-50"); + + RuleFor(x => x.SimilarityThreshold) + .InclusiveBetween(0, 1).When(x => x.SimilarityThreshold.HasValue) + .WithMessage("相似度阈值 SimilarityThreshold 取值范围 0-1"); + + RuleFor(x => x.VectorWeight) + .InclusiveBetween(0, 1).When(x => x.VectorWeight.HasValue) + .WithMessage("向量权重 VectorWeight 取值范围 0-1"); + } +} diff --git a/src/RAG.Api/Endpoints/Notifications/NotificationEndpoints.cs b/src/RAG.Api/Endpoints/Notifications/NotificationEndpoints.cs index 8f7b19b..3f0c6ac 100644 --- a/src/RAG.Api/Endpoints/Notifications/NotificationEndpoints.cs +++ b/src/RAG.Api/Endpoints/Notifications/NotificationEndpoints.cs @@ -34,6 +34,31 @@ public record PublishNotificationRequest public List? RecipientRoles { get; init; } } +/// +/// 发布通知请求校验。 +/// 接收人范围(RecipientUserIds / RecipientRoles)可同时为空——表示全量广播, +/// 具体扇出语义由 PublishNotificationCommand 决定,此处仅校验必填字段与长度。 +/// +public class PublishNotificationRequestValidator : Validator +{ + public PublishNotificationRequestValidator() + { + RuleFor(x => x.Title) + .NotEmpty().WithMessage("通知标题不能为空") + .MaximumLength(200).WithMessage("通知标题不能超过 200 个字符"); + + RuleFor(x => x.Content) + .NotEmpty().WithMessage("通知内容不能为空") + .MaximumLength(5000).WithMessage("通知内容不能超过 5000 个字符"); + + RuleFor(x => x.Type) + .MaximumLength(50).WithMessage("通知类型 Type 长度不能超过 50 个字符"); + + RuleFor(x => x.Source) + .MaximumLength(50).WithMessage("通知来源 Source 长度不能超过 50 个字符"); + } +} + public class PublishNotificationEndpoint(IMediator mediator) : Endpoint { public override void Configure() @@ -45,9 +70,7 @@ public class PublishNotificationEndpoint(IMediator mediator) : Endpoint查询通知列表请求校验(分页参数取值范围)。 +public class GetNotificationsRequestValidator : Validator +{ + public GetNotificationsRequestValidator() + { + RuleFor(x => x.PageIndex) + .InclusiveBetween(1, 1000).WithMessage("页码 PageIndex 取值范围 1-1000"); + + RuleFor(x => x.PageSize) + .InclusiveBetween(1, 100).WithMessage("每页数量 PageSize 取值范围 1-100"); + } +} + public class GetNotificationsEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint> { diff --git a/src/RAG.Api/Endpoints/Obsidian/SyncObsidianVaultEndpoint.cs b/src/RAG.Api/Endpoints/Obsidian/SyncObsidianVaultEndpoint.cs index 39ec68a..927f805 100644 --- a/src/RAG.Api/Endpoints/Obsidian/SyncObsidianVaultEndpoint.cs +++ b/src/RAG.Api/Endpoints/Obsidian/SyncObsidianVaultEndpoint.cs @@ -32,3 +32,22 @@ public class SyncObsidianVaultEndpoint(IMediator mediator) await Send.OkAsync(result, ct); } } + +/// +/// Obsidian vault 同步请求校验。 +/// 注意:目录存在性属于 IO 层校验,留给 handler 处理(避免 validator 内做磁盘 IO); +/// 此处仅校验非空、长度与枚举取值。 +/// +public class ObsidianSyncRequestValidator : Validator +{ + public ObsidianSyncRequestValidator() + { + RuleFor(x => x.VaultDirectory) + .NotEmpty().WithMessage("Vault 目录不能为空") + .MaximumLength(1000).WithMessage("Vault 目录路径不能超过 1000 个字符"); + + RuleFor(x => x.ChunkingMode) + .InclusiveBetween(0, 2).When(x => x.ChunkingMode.HasValue) + .WithMessage("分块模式 ChunkingMode 取值 0=General,1=Heading,2=ParentChild"); + } +} diff --git a/src/RAG.Api/Endpoints/RAG/RAGQueryEndpoint.cs b/src/RAG.Api/Endpoints/RAG/RAGQueryEndpoint.cs index 4e7a3c2..d469176 100644 --- a/src/RAG.Api/Endpoints/RAG/RAGQueryEndpoint.cs +++ b/src/RAG.Api/Endpoints/RAG/RAGQueryEndpoint.cs @@ -23,3 +23,18 @@ public class RAGQueryEndpoint(IMediator mediator) : Endpoint +/// RAG 问答请求校验。/rag/query 与 /rag/stream 共用同一请求 DTO,故此校验对两个端点同时生效。 +/// +public class RAGQueryRequestValidator : Validator +{ + public RAGQueryRequestValidator() + { + RuleFor(x => x.KnowledgeBaseId).NotEmpty().WithMessage("知识库 ID 不能为空"); + + RuleFor(x => x.Question) + .NotEmpty().WithMessage("问题不能为空") + .MaximumLength(1000).WithMessage("问题不能超过 1000 个字符"); + } +} diff --git a/src/RAG.Api/Endpoints/RAG/RAGStreamEndpoint.cs b/src/RAG.Api/Endpoints/RAG/RAGStreamEndpoint.cs index d0551e7..4524bdb 100644 --- a/src/RAG.Api/Endpoints/RAG/RAGStreamEndpoint.cs +++ b/src/RAG.Api/Endpoints/RAG/RAGStreamEndpoint.cs @@ -1,7 +1,6 @@ using System.Text; using System.Text.Json; using FastEndpoints; -using Microsoft.EntityFrameworkCore; using RAG.Application.RagQA.DTOs; using RAG.Domain.Exceptions; using RAG.Domain.Interfaces; diff --git a/src/RAG.Api/Endpoints/Roles/AssignPermissionsEndpoint.cs b/src/RAG.Api/Endpoints/Roles/AssignPermissionsEndpoint.cs index 2526ae7..6d5a36f 100644 --- a/src/RAG.Api/Endpoints/Roles/AssignPermissionsEndpoint.cs +++ b/src/RAG.Api/Endpoints/Roles/AssignPermissionsEndpoint.cs @@ -25,3 +25,14 @@ public class AssignPermissionsEndpoint(IMediator mediator) : Endpoint PermissionIds); + +/// 给角色分配权限请求校验。 +public class AssignPermissionsEndpointRequestValidator : Validator +{ + public AssignPermissionsEndpointRequestValidator() + { + RuleFor(x => x.RoleId).NotEmpty().WithMessage("角色 ID 不能为空"); + RuleFor(x => x.PermissionIds) + .NotEmpty().WithMessage("权限列表不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Roles/CreateRoleEndpoint.cs b/src/RAG.Api/Endpoints/Roles/CreateRoleEndpoint.cs index 505bc49..40afe46 100644 --- a/src/RAG.Api/Endpoints/Roles/CreateRoleEndpoint.cs +++ b/src/RAG.Api/Endpoints/Roles/CreateRoleEndpoint.cs @@ -19,3 +19,20 @@ public class CreateRoleEndpoint(IMediator mediator) : Endpoint创建角色请求校验。 +public class CreateRoleRequestValidator : Validator +{ + public CreateRoleRequestValidator() + { + RuleFor(x => x.Name) + .NotEmpty().WithMessage("角色名称不能为空") + .Length(2, 50).WithMessage("角色名称长度 2-50 个字符"); + + When(x => x.Description is not null, () => + { + RuleFor(x => x.Description!) + .MaximumLength(500).WithMessage("角色描述长度不能超过 500 个字符"); + }); + } +} diff --git a/src/RAG.Api/Endpoints/Roles/DeleteRoleEndpoint.cs b/src/RAG.Api/Endpoints/Roles/DeleteRoleEndpoint.cs index 69db69c..ff725ec 100644 --- a/src/RAG.Api/Endpoints/Roles/DeleteRoleEndpoint.cs +++ b/src/RAG.Api/Endpoints/Roles/DeleteRoleEndpoint.cs @@ -20,3 +20,12 @@ public class DeleteRoleEndpoint(IMediator mediator) : Endpoint删除角色请求校验(Id 由路由提供)。 +public class DeleteRoleEndpointRequestValidator : Validator +{ + public DeleteRoleEndpointRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("角色 ID 不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Roles/GetRoleByIdEndpoint.cs b/src/RAG.Api/Endpoints/Roles/GetRoleByIdEndpoint.cs index d83a287..4f129f4 100644 --- a/src/RAG.Api/Endpoints/Roles/GetRoleByIdEndpoint.cs +++ b/src/RAG.Api/Endpoints/Roles/GetRoleByIdEndpoint.cs @@ -21,3 +21,12 @@ public class GetRoleByIdEndpoint(IMediator mediator) : Endpoint根据角色 ID 查询请求校验(Id 由路由提供)。 +public class GetRoleByIdRequestValidator : Validator +{ + public GetRoleByIdRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("角色 ID 不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Roles/UpdateRoleEndpoint.cs b/src/RAG.Api/Endpoints/Roles/UpdateRoleEndpoint.cs index b8f259e..9341fb6 100644 --- a/src/RAG.Api/Endpoints/Roles/UpdateRoleEndpoint.cs +++ b/src/RAG.Api/Endpoints/Roles/UpdateRoleEndpoint.cs @@ -21,3 +21,24 @@ public class UpdateRoleEndpoint(IMediator mediator) : Endpoint更新角色请求校验。Name 为可选更新字段,仅当提供时校验长度。 +public class UpdateRoleEndpointRequestValidator : Validator +{ + public UpdateRoleEndpointRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("角色 ID 不能为空"); + + When(x => x.Name is not null, () => + { + RuleFor(x => x.Name!) + .Length(2, 50).WithMessage("角色名称长度 2-50 个字符"); + }); + + When(x => x.Description is not null, () => + { + RuleFor(x => x.Description!) + .MaximumLength(500).WithMessage("角色描述长度不能超过 500 个字符"); + }); + } +} diff --git a/src/RAG.Api/Endpoints/Users/AssignRolesEndpoint.cs b/src/RAG.Api/Endpoints/Users/AssignRolesEndpoint.cs index 42c4957..313cd84 100644 --- a/src/RAG.Api/Endpoints/Users/AssignRolesEndpoint.cs +++ b/src/RAG.Api/Endpoints/Users/AssignRolesEndpoint.cs @@ -25,3 +25,14 @@ public class AssignRolesEndpoint(IMediator mediator) : Endpoint RoleIds); + +/// 给用户分配角色请求校验。 +public class AssignRolesEndpointRequestValidator : Validator +{ + public AssignRolesEndpointRequestValidator() + { + RuleFor(x => x.UserId).NotEmpty().WithMessage("用户 ID 不能为空"); + RuleFor(x => x.RoleIds) + .NotEmpty().WithMessage("角色列表不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Users/CreateUserEndpoint.cs b/src/RAG.Api/Endpoints/Users/CreateUserEndpoint.cs index 9bfac01..3769553 100644 --- a/src/RAG.Api/Endpoints/Users/CreateUserEndpoint.cs +++ b/src/RAG.Api/Endpoints/Users/CreateUserEndpoint.cs @@ -23,3 +23,22 @@ public class CreateUserEndpoint(IMediator mediator) : Endpoint管理员创建用户请求校验。 +public class CreateUserRequestValidator : Validator +{ + public CreateUserRequestValidator() + { + RuleFor(x => x.Username) + .NotEmpty().WithMessage("用户名不能为空") + .Length(3, 50).WithMessage("用户名长度 3-50 个字符"); + + RuleFor(x => x.Email) + .NotEmpty().WithMessage("邮箱不能为空") + .EmailAddress().WithMessage("邮箱格式不正确"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("密码不能为空") + .Length(6, 100).WithMessage("密码长度 6-100 个字符"); + } +} diff --git a/src/RAG.Api/Endpoints/Users/DeleteUserEndpoint.cs b/src/RAG.Api/Endpoints/Users/DeleteUserEndpoint.cs index 1de026d..66c144f 100644 --- a/src/RAG.Api/Endpoints/Users/DeleteUserEndpoint.cs +++ b/src/RAG.Api/Endpoints/Users/DeleteUserEndpoint.cs @@ -20,3 +20,12 @@ public class DeleteUserEndpoint(IMediator mediator) : Endpoint删除用户请求校验(Id 由路由提供)。 +public class DeleteUserEndpointRequestValidator : Validator +{ + public DeleteUserEndpointRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("用户 ID 不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Users/GetUserByIdEndpoint.cs b/src/RAG.Api/Endpoints/Users/GetUserByIdEndpoint.cs index b3fabe1..a807d13 100644 --- a/src/RAG.Api/Endpoints/Users/GetUserByIdEndpoint.cs +++ b/src/RAG.Api/Endpoints/Users/GetUserByIdEndpoint.cs @@ -21,3 +21,12 @@ public class GetUserByIdEndpoint(IMediator mediator) : Endpoint根据用户 ID 查询请求校验(Id 由路由提供)。 +public class GetUserByIdRequestValidator : Validator +{ + public GetUserByIdRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("用户 ID 不能为空"); + } +} diff --git a/src/RAG.Api/Endpoints/Users/UpdateUserEndpoint.cs b/src/RAG.Api/Endpoints/Users/UpdateUserEndpoint.cs index 0b780ca..573cb1d 100644 --- a/src/RAG.Api/Endpoints/Users/UpdateUserEndpoint.cs +++ b/src/RAG.Api/Endpoints/Users/UpdateUserEndpoint.cs @@ -21,3 +21,24 @@ public class UpdateUserEndpoint(IMediator mediator) : Endpoint更新用户请求校验。Id 由路由提供;Email/Password 为可选更新字段,仅当提供时校验格式。 +public class UpdateUserEndpointRequestValidator : Validator +{ + public UpdateUserEndpointRequestValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("用户 ID 不能为空"); + + When(x => x.Email is not null, () => + { + RuleFor(x => x.Email!) + .EmailAddress().WithMessage("邮箱格式不正确"); + }); + + When(x => x.Password is not null, () => + { + RuleFor(x => x.Password!) + .Length(6, 100).WithMessage("密码长度 6-100 个字符"); + }); + } +} diff --git a/src/RAG.Api/GlobalUsings.cs b/src/RAG.Api/GlobalUsings.cs new file mode 100644 index 0000000..ff9613f --- /dev/null +++ b/src/RAG.Api/GlobalUsings.cs @@ -0,0 +1,4 @@ +// 全局 using:endpoint 验证器直接继承 Validator,并使用 FluentValidation 规则, +// 无需在每个 endpoint 文件重复声明 using。FastEndpoints 反射扫描自动发现 Validator 子类。 +global using FastEndpoints; +global using FluentValidation; diff --git a/src/RAG.Api/Grpc/AuthGrpcService.cs b/src/RAG.Api/Grpc/AuthGrpcService.cs index 616f37a..991711b 100644 --- a/src/RAG.Api/Grpc/AuthGrpcService.cs +++ b/src/RAG.Api/Grpc/AuthGrpcService.cs @@ -1,6 +1,5 @@ using Grpc.Core; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using RAG.Infrastructure.Persistence; using System.IdentityModel.Tokens.Jwt; diff --git a/src/RAG.Api/Hubs/NotificationHub.cs b/src/RAG.Api/Hubs/NotificationHub.cs index d0f91b8..bb1af8e 100644 --- a/src/RAG.Api/Hubs/NotificationHub.cs +++ b/src/RAG.Api/Hubs/NotificationHub.cs @@ -2,7 +2,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; -using RAG.Application.Notifications; namespace RAG.Api.Hubs; diff --git a/src/RAG.Api/Middleware/GlobalExceptionMiddleware.cs b/src/RAG.Api/Middleware/GlobalExceptionMiddleware.cs index 3d30f6f..f1e40e1 100644 --- a/src/RAG.Api/Middleware/GlobalExceptionMiddleware.cs +++ b/src/RAG.Api/Middleware/GlobalExceptionMiddleware.cs @@ -2,6 +2,7 @@ using System.Net; using System.Text; using System.Text.Json; using FluentValidation; +using Namotion.Reflection; using RAG.Domain.Exceptions; namespace RAG.Api.Middleware; diff --git a/src/RAG.Api/RAGApiModule.cs b/src/RAG.Api/RAGApiModule.cs index 501162b..a470eba 100644 --- a/src/RAG.Api/RAGApiModule.cs +++ b/src/RAG.Api/RAGApiModule.cs @@ -1,5 +1,4 @@ using FastEndpoints; -using FastEndpoints.Security; using FastEndpoints.Swagger; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -147,9 +146,14 @@ public class RAGApiModule : AbpModule // 统一数据规范:时间字段输出 UTC 毫秒时间戳 config.Serializer.Options.Converters.Add(new TimestampDateTimeConverter()); config.Serializer.Options.Converters.Add(new TimestampDateTimeOffsetConverter()); + // 参数校验失败统一输出 { code, message, data },与 GlobalExceptionMiddleware 保持一致: + // ApiResponseMiddleware 对非 2xx 响应原样透传,前端 errorMessageResponseInterceptor 读取 + // response.data.message 即可展示中文校验提示。多条错误用 "; " 拼接。 + config.Errors.StatusCode = 400; config.Errors.ResponseBuilder = (failures, ctx, statusCode) => { - return new ErrorResponse(failures.Select(f => new Error(f.ErrorMessage)).ToList()); + var message = string.Join("; ", failures.Select(f => f.ErrorMessage)); + return new ValidationErrorResponse(400, message); }; }); @@ -160,5 +164,8 @@ public class RAGApiModule : AbpModule } } -public record ErrorResponse(List Errors); -public record Error(string Message); +/// +/// 参数校验失败响应信封,与 GlobalExceptionMiddleware 的 ErrorResponse 结构一致: +/// { code: 400, message: "...", data: null }。 +/// +public record ValidationErrorResponse(int Code, string Message, object? Data = null); diff --git a/src/RAG.Application/Chat/Commands/CreateConversationCommand.cs b/src/RAG.Application/Chat/Commands/CreateConversationCommand.cs index 00fb547..f8ad649 100644 --- a/src/RAG.Application/Chat/Commands/CreateConversationCommand.cs +++ b/src/RAG.Application/Chat/Commands/CreateConversationCommand.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.EntityFrameworkCore; using RAG.Application.Chat.DTOs; using RAG.Domain.Common; using RAG.Domain.Entities; diff --git a/src/RAG.Application/Document/Commands/ProcessDocumentCommand.cs b/src/RAG.Application/Document/Commands/ProcessDocumentCommand.cs index 8b23836..a9f8d10 100644 --- a/src/RAG.Application/Document/Commands/ProcessDocumentCommand.cs +++ b/src/RAG.Application/Document/Commands/ProcessDocumentCommand.cs @@ -1,9 +1,7 @@ using System.Security.Cryptography; -using System.Text.Json; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using RAG.Application.Document.DTOs; using RAG.Domain.Interfaces; using RAG.Domain.Entities; diff --git a/src/RAG.Application/Document/Commands/UploadDocumentCommand.cs b/src/RAG.Application/Document/Commands/UploadDocumentCommand.cs index 21ef69d..b085d29 100644 --- a/src/RAG.Application/Document/Commands/UploadDocumentCommand.cs +++ b/src/RAG.Application/Document/Commands/UploadDocumentCommand.cs @@ -1,4 +1,3 @@ -using System.Security.Claims; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; diff --git a/src/RAG.Application/KnowledgeBase/Commands/CreateKnowledgeBaseCommand.cs b/src/RAG.Application/KnowledgeBase/Commands/CreateKnowledgeBaseCommand.cs index 69791d3..f7ee877 100644 --- a/src/RAG.Application/KnowledgeBase/Commands/CreateKnowledgeBaseCommand.cs +++ b/src/RAG.Application/KnowledgeBase/Commands/CreateKnowledgeBaseCommand.cs @@ -1,6 +1,5 @@ using MediatR; using RAG.Application.KnowledgeBase.DTOs; -using RAG.Domain.Entities; using RAG.Domain.Enums; using RAG.Domain.Exceptions; using RAG.Infrastructure.Persistence; diff --git a/src/RAG.Application/KnowledgeBase/Commands/UpdateKnowledgeBaseCommand.cs b/src/RAG.Application/KnowledgeBase/Commands/UpdateKnowledgeBaseCommand.cs index 34e3daa..4fb4813 100644 --- a/src/RAG.Application/KnowledgeBase/Commands/UpdateKnowledgeBaseCommand.cs +++ b/src/RAG.Application/KnowledgeBase/Commands/UpdateKnowledgeBaseCommand.cs @@ -3,7 +3,6 @@ using RAG.Application.KnowledgeBase.DTOs; using RAG.Domain.Enums; using RAG.Domain.Exceptions; using RAG.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; namespace RAG.Application.KnowledgeBase.Commands; diff --git a/src/RAG.Application/KnowledgeBase/DTOs/KnowledgeBaseDTOs.cs b/src/RAG.Application/KnowledgeBase/DTOs/KnowledgeBaseDTOs.cs index d90b0d4..371d3d1 100644 --- a/src/RAG.Application/KnowledgeBase/DTOs/KnowledgeBaseDTOs.cs +++ b/src/RAG.Application/KnowledgeBase/DTOs/KnowledgeBaseDTOs.cs @@ -1,5 +1,3 @@ -using RAG.Domain.Enums; - namespace RAG.Application.KnowledgeBase.DTOs; public record KnowledgeBaseDto( diff --git a/src/RAG.Application/Notifications/Commands/PublishNotificationCommand.cs b/src/RAG.Application/Notifications/Commands/PublishNotificationCommand.cs index 7f2b22e..568b1cc 100644 --- a/src/RAG.Application/Notifications/Commands/PublishNotificationCommand.cs +++ b/src/RAG.Application/Notifications/Commands/PublishNotificationCommand.cs @@ -1,6 +1,5 @@ using MediatR; using Microsoft.EntityFrameworkCore; -using RAG.Application.Notifications.DTOs; using RAG.Domain.Entities; using RAG.Infrastructure.Persistence; diff --git a/src/RAG.Application/Obsidian/Commands/SyncObsidianVaultCommand.cs b/src/RAG.Application/Obsidian/Commands/SyncObsidianVaultCommand.cs index fb4a87c..7a72eb3 100644 --- a/src/RAG.Application/Obsidian/Commands/SyncObsidianVaultCommand.cs +++ b/src/RAG.Application/Obsidian/Commands/SyncObsidianVaultCommand.cs @@ -2,7 +2,6 @@ using System.Security.Cryptography; using MediatR; using Microsoft.EntityFrameworkCore; using RAG.Application.Obsidian.DTOs; -using RAG.Domain.Entities; using RAG.Domain.Enums; using RAG.Domain.Exceptions; using RAG.Infrastructure.Persistence; diff --git a/src/RAG.Application/RagQA/Commands/RAGQueryCommand.cs b/src/RAG.Application/RagQA/Commands/RAGQueryCommand.cs index df43329..024439f 100644 --- a/src/RAG.Application/RagQA/Commands/RAGQueryCommand.cs +++ b/src/RAG.Application/RagQA/Commands/RAGQueryCommand.cs @@ -1,6 +1,5 @@ using System.Text; using MediatR; -using Microsoft.EntityFrameworkCore; using RAG.Application.RagQA.DTOs; using RAG.Domain.Exceptions; using RAG.Domain.Interfaces; diff --git a/src/RAG.Domain/Interfaces/IRagRetrievalService.cs b/src/RAG.Domain/Interfaces/IRagRetrievalService.cs index 0c1736a..3e7b387 100644 --- a/src/RAG.Domain/Interfaces/IRagRetrievalService.cs +++ b/src/RAG.Domain/Interfaces/IRagRetrievalService.cs @@ -1,5 +1,4 @@ using RAG.Domain.Entities; -using RAG.Domain.Enums; namespace RAG.Domain.Interfaces; diff --git a/src/RAG.Infrastructure/Extractors/DocxExtractor.cs b/src/RAG.Infrastructure/Extractors/DocxExtractor.cs index 53c434b..a0890fa 100644 --- a/src/RAG.Infrastructure/Extractors/DocxExtractor.cs +++ b/src/RAG.Infrastructure/Extractors/DocxExtractor.cs @@ -1,4 +1,3 @@ -using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using RAG.Domain.Interfaces; diff --git a/src/RAG.Infrastructure/Extractors/MarkdownExtractor.cs b/src/RAG.Infrastructure/Extractors/MarkdownExtractor.cs index b3930a9..d15424c 100644 --- a/src/RAG.Infrastructure/Extractors/MarkdownExtractor.cs +++ b/src/RAG.Infrastructure/Extractors/MarkdownExtractor.cs @@ -1,4 +1,3 @@ -using Markdig; using RAG.Domain.Interfaces; namespace RAG.Infrastructure.Extractors; diff --git a/src/RAG.Infrastructure/Extractors/PptxExtractor.cs b/src/RAG.Infrastructure/Extractors/PptxExtractor.cs index 43c5046..d8c1bda 100644 --- a/src/RAG.Infrastructure/Extractors/PptxExtractor.cs +++ b/src/RAG.Infrastructure/Extractors/PptxExtractor.cs @@ -1,6 +1,4 @@ -using System.IO; using System.Text; -using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Presentation; using RAG.Domain.Interfaces; diff --git a/src/RAG.Infrastructure/Persistence/RagDbContext.cs b/src/RAG.Infrastructure/Persistence/RagDbContext.cs index ae9af05..3dc14bb 100644 --- a/src/RAG.Infrastructure/Persistence/RagDbContext.cs +++ b/src/RAG.Infrastructure/Persistence/RagDbContext.cs @@ -1,6 +1,5 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; -using Pgvector.EntityFrameworkCore; using RAG.Domain.Common; using RAG.Domain.Entities; diff --git a/src/RAG.Infrastructure/RAGInfrastructureModule.cs b/src/RAG.Infrastructure/RAGInfrastructureModule.cs index 6e4c4fb..29325ef 100644 --- a/src/RAG.Infrastructure/RAGInfrastructureModule.cs +++ b/src/RAG.Infrastructure/RAGInfrastructureModule.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Pgvector.EntityFrameworkCore; using RAG.Domain; using RAG.Domain.Interfaces; using RAG.Infrastructure.AI;