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;