feat(validation): 添加请求参数校验器并优化依赖注入配置

- 为 AssignPermissionsEndpoint 添加请求校验器
- 为 AssignRolesEndpoint 添加请求校验器
- 为分片上传相关端点添加请求校验器
- 为聊天功能相关端点添加请求校验器
- 为知识库管理相关端点添加请求校验器
- 为用户和角色管理相关端点添加请求校验器
- 为嵌入向量化相关端点添加请求校验器
- 为认证授权相关端点添加请求校验器
- 为通知系统相关端点添加请求校验器
- 为 Obsidian 同步功能添加请求校验器
- 移除不必要的 using 语句和依赖注入配置
- 统一参数校验失败响应格式为 ValidationErrorResponse
This commit is contained in:
向宁 2026-06-14 15:59:57 +08:00
parent 5b67551fee
commit dd9c2c85bb
49 changed files with 511 additions and 29 deletions

View File

@ -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 个字符");
}
}

View File

@ -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 个字符");
}
}

View File

@ -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 不能为空");
}
}

View File

@ -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 个字符");
}
}

View File

@ -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 不能为空");
}
}

View File

@ -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 不能为空");
}
}

View File

@ -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 个字符");
}
}

View File

@ -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 个字符");
}
}

View File

@ -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()

View File

@ -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");
}
}

View File

@ -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 条");
}
}

View File

@ -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 个字符");
}
}

View File

@ -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");
}
}

View File

@ -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 不能为空");
}
}

View File

@ -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");
}
}

View File

@ -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>>
{

View File

@ -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");
}
}

View File

@ -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 个字符");
}
}

View File

@ -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;

View File

@ -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("权限列表不能为空");
}
}

View File

@ -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 个字符");
});
}
}

View File

@ -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 不能为空");
}
}

View File

@ -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 不能为空");
}
}

View File

@ -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 个字符");
});
}
}

View File

@ -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("角色列表不能为空");
}
}

View File

@ -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 个字符");
}
}

View File

@ -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 不能为空");
}
}

View File

@ -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 不能为空");
}
}

View File

@ -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 个字符");
});
}
}

View File

@ -0,0 +1,4 @@
// 全局 usingendpoint 验证器直接继承 Validator<TRequest>,并使用 FluentValidation 规则,
// 无需在每个 endpoint 文件重复声明 using。FastEndpoints 反射扫描自动发现 Validator<T> 子类。
global using FastEndpoints;
global using FluentValidation;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -1,5 +1,4 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using RAG.Application.Chat.DTOs;
using RAG.Domain.Common;
using RAG.Domain.Entities;

View File

@ -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;

View File

@ -1,4 +1,3 @@
using System.Security.Claims;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;

View File

@ -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;

View File

@ -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;

View File

@ -1,5 +1,3 @@
using RAG.Domain.Enums;
namespace RAG.Application.KnowledgeBase.DTOs;
public record KnowledgeBaseDto(

View File

@ -1,6 +1,5 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using RAG.Application.Notifications.DTOs;
using RAG.Domain.Entities;
using RAG.Infrastructure.Persistence;

View File

@ -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;

View File

@ -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;

View File

@ -1,5 +1,4 @@
using RAG.Domain.Entities;
using RAG.Domain.Enums;
namespace RAG.Domain.Interfaces;

View File

@ -1,4 +1,3 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using RAG.Domain.Interfaces;

View File

@ -1,4 +1,3 @@
using Markdig;
using RAG.Domain.Interfaces;
namespace RAG.Infrastructure.Extractors;

View File

@ -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;

View File

@ -1,6 +1,5 @@
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Pgvector.EntityFrameworkCore;
using RAG.Domain.Common;
using RAG.Domain.Entities;

View File

@ -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;