feat(validation): 添加请求参数校验器并优化依赖注入配置
- 为 AssignPermissionsEndpoint 添加请求校验器 - 为 AssignRolesEndpoint 添加请求校验器 - 为分片上传相关端点添加请求校验器 - 为聊天功能相关端点添加请求校验器 - 为知识库管理相关端点添加请求校验器 - 为用户和角色管理相关端点添加请求校验器 - 为嵌入向量化相关端点添加请求校验器 - 为认证授权相关端点添加请求校验器 - 为通知系统相关端点添加请求校验器 - 为 Obsidian 同步功能添加请求校验器 - 移除不必要的 using 语句和依赖注入配置 - 统一参数校验失败响应格式为 ValidationErrorResponse
This commit is contained in:
parent
5b67551fee
commit
dd9c2c85bb
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>登录请求校验。规则与 LoginCommandValidator 一致,HTTP 入口层提前拦截非法输入。</summary>
|
||||
public class LoginRequestValidator : Validator<LoginRequest>
|
||||
{
|
||||
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 个字符");
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,3 +23,22 @@ public class RegisterEndpoint(IMediator mediator) : Endpoint<RegisterRequest>
|
||||
await Send.OkAsync(true, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>自助注册请求校验。</summary>
|
||||
public class RegisterRequestValidator : Validator<RegisterRequest>
|
||||
{
|
||||
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 个字符");
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,3 +22,13 @@ public class RevokeTokenEndpoint(IMediator mediator) : Endpoint<RevokeTokenReque
|
||||
await Send.OkAsync(true, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>吊销刷新令牌请求校验。</summary>
|
||||
public class RevokeTokenRequestValidator : Validator<RevokeTokenRequest>
|
||||
{
|
||||
public RevokeTokenRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.RefreshToken)
|
||||
.NotEmpty().WithMessage("RefreshToken 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,3 +21,14 @@ public class CreateConversationEndpoint(IMediator mediator) : Endpoint<CreateCon
|
||||
}
|
||||
|
||||
public record CreateConversationRequest(string Title);
|
||||
|
||||
/// <summary>创建会话请求校验。</summary>
|
||||
public class CreateConversationRequestValidator : Validator<CreateConversationRequest>
|
||||
{
|
||||
public CreateConversationRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("会话标题不能为空")
|
||||
.MaximumLength(200).WithMessage("会话标题不能超过 200 个字符");
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,3 +23,12 @@ public class DeleteConversationEndpoint(IMediator mediator) : Endpoint<DeleteCon
|
||||
}
|
||||
|
||||
public record DeleteConversationEndpointRequest(Guid Id);
|
||||
|
||||
/// <summary>删除会话请求校验(Id 由路由提供)。</summary>
|
||||
public class DeleteConversationEndpointRequestValidator : Validator<DeleteConversationEndpointRequest>
|
||||
{
|
||||
public DeleteConversationEndpointRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty().WithMessage("会话 ID 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,3 +21,12 @@ public class GetConversationDetailEndpoint(IMediator mediator) : Endpoint<GetCon
|
||||
}
|
||||
|
||||
public record GetConversationDetailRequest(Guid Id);
|
||||
|
||||
/// <summary>查询会话详情请求校验(Id 由路由提供)。</summary>
|
||||
public class GetConversationDetailRequestValidator : Validator<GetConversationDetailRequest>
|
||||
{
|
||||
public GetConversationDetailRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty().WithMessage("会话 ID 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,3 +22,14 @@ public class SendMessageEndpoint(IMediator mediator) : Endpoint<SendMessageReque
|
||||
}
|
||||
|
||||
public record SendMessageRequest(string Content);
|
||||
|
||||
/// <summary>发送消息请求校验。</summary>
|
||||
public class SendMessageRequestValidator : Validator<SendMessageRequest>
|
||||
{
|
||||
public SendMessageRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Content)
|
||||
.NotEmpty().WithMessage("消息内容不能为空")
|
||||
.MaximumLength(10000).WithMessage("消息内容不能超过 10000 个字符");
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,3 +121,14 @@ public class StreamMessageEndpoint(RagDbContext db, IAIChatAgent chatAgent, ICha
|
||||
}
|
||||
|
||||
public record StreamMessageRequest(string Content);
|
||||
|
||||
/// <summary>流式发送消息请求校验。</summary>
|
||||
public class StreamMessageRequestValidator : Validator<StreamMessageRequest>
|
||||
{
|
||||
public StreamMessageRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Content)
|
||||
.NotEmpty().WithMessage("消息内容不能为空")
|
||||
.MaximumLength(10000).WithMessage("消息内容不能超过 10000 个字符");
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,23 @@ public class InitChunkUploadRequest
|
||||
public int ChunkCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>分片上传初始化请求校验。</summary>
|
||||
public class InitChunkUploadRequestValidator : Validator<InitChunkUploadRequest>
|
||||
{
|
||||
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<InitChunkUploadRequest, ChunkUploadSession>
|
||||
{
|
||||
public override void Configure()
|
||||
@ -113,6 +130,19 @@ public class CompleteChunkUploadRequest
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>分片上传合并完成请求校验。Title 可选(不传则用原文件名)。</summary>
|
||||
public class CompleteChunkUploadRequestValidator : Validator<CompleteChunkUploadRequest>
|
||||
{
|
||||
public CompleteChunkUploadRequestValidator()
|
||||
{
|
||||
When(x => x.Title is not null, () =>
|
||||
{
|
||||
RuleFor(x => x.Title!)
|
||||
.MaximumLength(500).WithMessage("文档标题不能超过 500 个字符");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class CompleteChunkUploadEndpoint(IMediator mediator) : Endpoint<CompleteChunkUploadRequest, DocumentDto>
|
||||
{
|
||||
public override void Configure()
|
||||
|
||||
@ -44,3 +44,20 @@ public class UploadDocumentEndpoint(IMediator mediator) : Endpoint<UploadDocumen
|
||||
}
|
||||
|
||||
public record UploadDocumentRequest(string? Title, int? ChunkingMode = null);
|
||||
|
||||
/// <summary>单文件上传请求校验。Title 可选(不传则用文件名),ChunkingMode 仅当提供时校验取值。</summary>
|
||||
public class UploadDocumentRequestValidator : Validator<UploadDocumentRequest>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,3 +25,14 @@ public class EmbedBatchEndpoint(IMediator mediator) : Endpoint<EmbedBatchRequest
|
||||
}
|
||||
|
||||
public record EmbedBatchRequest(List<string> Texts);
|
||||
|
||||
/// <summary>批量文本向量化请求校验。</summary>
|
||||
public class EmbedBatchRequestValidator : Validator<EmbedBatchRequest>
|
||||
{
|
||||
public EmbedBatchRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Texts)
|
||||
.NotEmpty().WithMessage("文本列表不能为空")
|
||||
.Must(t => t.Count <= 100).WithMessage("批量文本不能超过 100 条");
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,3 +24,14 @@ public class EmbedTextEndpoint(IMediator mediator) : Endpoint<EmbedTextRequest,
|
||||
}
|
||||
|
||||
public record EmbedTextRequest(string Text);
|
||||
|
||||
/// <summary>单条文本向量化请求校验。</summary>
|
||||
public class EmbedTextRequestValidator : Validator<EmbedTextRequest>
|
||||
{
|
||||
public EmbedTextRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Text)
|
||||
.NotEmpty().WithMessage("文本内容不能为空")
|
||||
.MaximumLength(10000).WithMessage("单条文本不能超过 10000 个字符");
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,3 +24,59 @@ public class CreateKBEndpoint(IMediator mediator) : Endpoint<CreateKnowledgeBase
|
||||
await Send.ResponseAsync(result, 201, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建知识库请求校验。
|
||||
/// 名称必填;数值参数(分块/检索配置)限定合理取值范围,避免传入非法值导致下游处理异常。
|
||||
/// </summary>
|
||||
public class CreateKnowledgeBaseRequestValidator : Validator<CreateKnowledgeBaseRequest>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,3 +20,12 @@ public class DeleteKBEndpoint(IMediator mediator) : Endpoint<DeleteKBEndpointReq
|
||||
}
|
||||
|
||||
public record DeleteKBEndpointRequest(Guid Id);
|
||||
|
||||
/// <summary>删除知识库请求校验(Id 由路由提供)。</summary>
|
||||
public class DeleteKBEndpointRequestValidator : Validator<DeleteKBEndpointRequest>
|
||||
{
|
||||
public DeleteKBEndpointRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty().WithMessage("知识库 ID 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,3 +31,59 @@ public class UpdateKBEndpoint(IMediator mediator)
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>更新知识库请求校验。仅校验请求体中提供的字段(非 null 时才校验)。</summary>
|
||||
public class UpdateKnowledgeBaseRequestValidator : Validator<UpdateKnowledgeBaseRequest>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,31 @@ public record PublishNotificationRequest
|
||||
public List<string>? RecipientRoles { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布通知请求校验。
|
||||
/// 接收人范围(RecipientUserIds / RecipientRoles)可同时为空——表示全量广播,
|
||||
/// 具体扇出语义由 PublishNotificationCommand 决定,此处仅校验必填字段与长度。
|
||||
/// </summary>
|
||||
public class PublishNotificationRequestValidator : Validator<PublishNotificationRequest>
|
||||
{
|
||||
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<PublishNotificationRequest, int>
|
||||
{
|
||||
public override void Configure()
|
||||
@ -45,9 +70,7 @@ public class PublishNotificationEndpoint(IMediator mediator) : Endpoint<PublishN
|
||||
|
||||
public override async Task HandleAsync(PublishNotificationRequest req, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.Title) || string.IsNullOrWhiteSpace(req.Content))
|
||||
throw new BusinessException("通知标题和内容不能为空");
|
||||
|
||||
// 标题/内容非空由 PublishNotificationRequestValidator 拦截,此处直接扇出
|
||||
var count = await mediator.Send(new PublishNotificationCommand(
|
||||
req.Type, req.Title, req.Content, req.Source,
|
||||
req.RelatedId, req.RelatedType,
|
||||
@ -65,6 +88,19 @@ public record GetNotificationsRequest
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
|
||||
/// <summary>查询通知列表请求校验(分页参数取值范围)。</summary>
|
||||
public class GetNotificationsRequestValidator : Validator<GetNotificationsRequest>
|
||||
{
|
||||
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<GetNotificationsRequest, PagedResult<NotificationDto>>
|
||||
{
|
||||
|
||||
@ -32,3 +32,22 @@ public class SyncObsidianVaultEndpoint(IMediator mediator)
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obsidian vault 同步请求校验。
|
||||
/// 注意:目录存在性属于 IO 层校验,留给 handler 处理(避免 validator 内做磁盘 IO);
|
||||
/// 此处仅校验非空、长度与枚举取值。
|
||||
/// </summary>
|
||||
public class ObsidianSyncRequestValidator : Validator<ObsidianSyncRequest>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,3 +23,18 @@ public class RAGQueryEndpoint(IMediator mediator) : Endpoint<RAGQueryRequest, RA
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RAG 问答请求校验。/rag/query 与 /rag/stream 共用同一请求 DTO,故此校验对两个端点同时生效。
|
||||
/// </summary>
|
||||
public class RAGQueryRequestValidator : Validator<RAGQueryRequest>
|
||||
{
|
||||
public RAGQueryRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.KnowledgeBaseId).NotEmpty().WithMessage("知识库 ID 不能为空");
|
||||
|
||||
RuleFor(x => x.Question)
|
||||
.NotEmpty().WithMessage("问题不能为空")
|
||||
.MaximumLength(1000).WithMessage("问题不能超过 1000 个字符");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -25,3 +25,14 @@ public class AssignPermissionsEndpoint(IMediator mediator) : Endpoint<AssignPerm
|
||||
}
|
||||
|
||||
public record AssignPermissionsEndpointRequest(Guid RoleId, List<Guid> PermissionIds);
|
||||
|
||||
/// <summary>给角色分配权限请求校验。</summary>
|
||||
public class AssignPermissionsEndpointRequestValidator : Validator<AssignPermissionsEndpointRequest>
|
||||
{
|
||||
public AssignPermissionsEndpointRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.RoleId).NotEmpty().WithMessage("角色 ID 不能为空");
|
||||
RuleFor(x => x.PermissionIds)
|
||||
.NotEmpty().WithMessage("权限列表不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,3 +19,20 @@ public class CreateRoleEndpoint(IMediator mediator) : Endpoint<CreateRoleRequest
|
||||
await Send.ResponseAsync(result, 201, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>创建角色请求校验。</summary>
|
||||
public class CreateRoleRequestValidator : Validator<CreateRoleRequest>
|
||||
{
|
||||
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 个字符");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,3 +20,12 @@ public class DeleteRoleEndpoint(IMediator mediator) : Endpoint<DeleteRoleEndpoin
|
||||
}
|
||||
|
||||
public record DeleteRoleEndpointRequest(Guid Id);
|
||||
|
||||
/// <summary>删除角色请求校验(Id 由路由提供)。</summary>
|
||||
public class DeleteRoleEndpointRequestValidator : Validator<DeleteRoleEndpointRequest>
|
||||
{
|
||||
public DeleteRoleEndpointRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty().WithMessage("角色 ID 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,3 +21,12 @@ public class GetRoleByIdEndpoint(IMediator mediator) : Endpoint<GetRoleByIdReque
|
||||
}
|
||||
|
||||
public record GetRoleByIdRequest(Guid Id);
|
||||
|
||||
/// <summary>根据角色 ID 查询请求校验(Id 由路由提供)。</summary>
|
||||
public class GetRoleByIdRequestValidator : Validator<GetRoleByIdRequest>
|
||||
{
|
||||
public GetRoleByIdRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty().WithMessage("角色 ID 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,3 +21,24 @@ public class UpdateRoleEndpoint(IMediator mediator) : Endpoint<UpdateRoleEndpoin
|
||||
}
|
||||
|
||||
public record UpdateRoleEndpointRequest(Guid Id, string? Name, string? Description);
|
||||
|
||||
/// <summary>更新角色请求校验。Name 为可选更新字段,仅当提供时校验长度。</summary>
|
||||
public class UpdateRoleEndpointRequestValidator : Validator<UpdateRoleEndpointRequest>
|
||||
{
|
||||
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 个字符");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,3 +25,14 @@ public class AssignRolesEndpoint(IMediator mediator) : Endpoint<AssignRolesEndpo
|
||||
}
|
||||
|
||||
public record AssignRolesEndpointRequest(Guid UserId, List<Guid> RoleIds);
|
||||
|
||||
/// <summary>给用户分配角色请求校验。</summary>
|
||||
public class AssignRolesEndpointRequestValidator : Validator<AssignRolesEndpointRequest>
|
||||
{
|
||||
public AssignRolesEndpointRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId).NotEmpty().WithMessage("用户 ID 不能为空");
|
||||
RuleFor(x => x.RoleIds)
|
||||
.NotEmpty().WithMessage("角色列表不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,3 +23,22 @@ public class CreateUserEndpoint(IMediator mediator) : Endpoint<CreateUserRequest
|
||||
await Send.ResponseAsync(result, 201, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>管理员创建用户请求校验。</summary>
|
||||
public class CreateUserRequestValidator : Validator<CreateUserRequest>
|
||||
{
|
||||
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 个字符");
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,3 +20,12 @@ public class DeleteUserEndpoint(IMediator mediator) : Endpoint<DeleteUserEndpoin
|
||||
}
|
||||
|
||||
public record DeleteUserEndpointRequest(Guid Id);
|
||||
|
||||
/// <summary>删除用户请求校验(Id 由路由提供)。</summary>
|
||||
public class DeleteUserEndpointRequestValidator : Validator<DeleteUserEndpointRequest>
|
||||
{
|
||||
public DeleteUserEndpointRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty().WithMessage("用户 ID 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,3 +21,12 @@ public class GetUserByIdEndpoint(IMediator mediator) : Endpoint<GetUserByIdReque
|
||||
}
|
||||
|
||||
public record GetUserByIdRequest(Guid Id);
|
||||
|
||||
/// <summary>根据用户 ID 查询请求校验(Id 由路由提供)。</summary>
|
||||
public class GetUserByIdRequestValidator : Validator<GetUserByIdRequest>
|
||||
{
|
||||
public GetUserByIdRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty().WithMessage("用户 ID 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,3 +21,24 @@ public class UpdateUserEndpoint(IMediator mediator) : Endpoint<UpdateUserEndpoin
|
||||
}
|
||||
|
||||
public record UpdateUserEndpointRequest(Guid Id, string? Email, string? Password, bool? IsActive);
|
||||
|
||||
/// <summary>更新用户请求校验。Id 由路由提供;Email/Password 为可选更新字段,仅当提供时校验格式。</summary>
|
||||
public class UpdateUserEndpointRequestValidator : Validator<UpdateUserEndpointRequest>
|
||||
{
|
||||
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 个字符");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
4
src/RAG.Api/GlobalUsings.cs
Normal file
4
src/RAG.Api/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
// 全局 using:endpoint 验证器直接继承 Validator<TRequest>,并使用 FluentValidation 规则,
|
||||
// 无需在每个 endpoint 文件重复声明 using。FastEndpoints 反射扫描自动发现 Validator<T> 子类。
|
||||
global using FastEndpoints;
|
||||
global using FluentValidation;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<Error> Errors);
|
||||
public record Error(string Message);
|
||||
/// <summary>
|
||||
/// 参数校验失败响应信封,与 GlobalExceptionMiddleware 的 ErrorResponse 结构一致:
|
||||
/// { code: 400, message: "...", data: null }。
|
||||
/// </summary>
|
||||
public record ValidationErrorResponse(int Code, string Message, object? Data = null);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RAG.Application.Chat.DTOs;
|
||||
using RAG.Domain.Common;
|
||||
using RAG.Domain.Entities;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Security.Claims;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
using RAG.Domain.Enums;
|
||||
|
||||
namespace RAG.Application.KnowledgeBase.DTOs;
|
||||
|
||||
public record KnowledgeBaseDto(
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RAG.Application.Notifications.DTOs;
|
||||
using RAG.Domain.Entities;
|
||||
using RAG.Infrastructure.Persistence;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using RAG.Domain.Entities;
|
||||
using RAG.Domain.Enums;
|
||||
|
||||
namespace RAG.Domain.Interfaces;
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
using RAG.Domain.Interfaces;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using Markdig;
|
||||
using RAG.Domain.Interfaces;
|
||||
|
||||
namespace RAG.Infrastructure.Extractors;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Pgvector.EntityFrameworkCore;
|
||||
using RAG.Domain.Common;
|
||||
using RAG.Domain.Entities;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user