diff --git a/src/BuildFlow.Host/Endpoints/File/Buckets/CreateBucketEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Buckets/CreateBucketEndpoint.cs
new file mode 100644
index 0000000..c46222d
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Buckets/CreateBucketEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Buckets.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Buckets;
+
+
+/// 创建存储桶端点。POST /api/buckets,按名称创建存储桶。
+[Authorize]
+///
+/// undefined
+///
+public class CreateBucketEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/buckets");
+ }
+
+ public override async Task HandleAsync(CreateBucketRequest req, CancellationToken ct)
+ {
+ var message = await mediator.Send(new CreateBucketCommand(req.Name), ct);
+ await Send.OkAsync(new { message }, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Buckets/CreateBucketRequest.cs b/src/BuildFlow.Host/Endpoints/File/Buckets/CreateBucketRequest.cs
new file mode 100644
index 0000000..b5843ca
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Buckets/CreateBucketRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Buckets;
+
+/// 创建存储桶请求。携带待创建存储桶的名称。
+public class CreateBucketRequest
+{
+ public string Name { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Buckets/DeleteBucketEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Buckets/DeleteBucketEndpoint.cs
new file mode 100644
index 0000000..131539b
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Buckets/DeleteBucketEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Buckets.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Buckets;
+
+
+/// 删除存储桶端点。DELETE /api/buckets,按名称删除存储桶。
+[Authorize]
+///
+/// undefined
+///
+public class DeleteBucketEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/buckets");
+ }
+
+ public override async Task HandleAsync(DeleteBucketRequest req, CancellationToken ct)
+ {
+ var message = await mediator.Send(new DeleteBucketCommand(req.Name), ct);
+ await Send.OkAsync(new { message }, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Buckets/DeleteBucketRequest.cs b/src/BuildFlow.Host/Endpoints/File/Buckets/DeleteBucketRequest.cs
new file mode 100644
index 0000000..4300b64
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Buckets/DeleteBucketRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Buckets;
+
+/// 删除存储桶请求。以查询参数传入存储桶名称。
+public class DeleteBucketRequest
+{
+ [QueryParam] public string Name { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Buckets/ListBucketsEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Buckets/ListBucketsEndpoint.cs
new file mode 100644
index 0000000..8a0e864
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Buckets/ListBucketsEndpoint.cs
@@ -0,0 +1,23 @@
+using File.Application.Features.Buckets.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Buckets;
+
+/// 列出存储桶端点。GET /api/buckets,返回全部存储桶名称。
+[Authorize]
+///
+/// undefined
+///
+public class ListBucketsEndpoint(IMediator mediator) : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/buckets");
+ }
+
+ public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new ListBucketsQuery(), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/CheckFileEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Files/CheckFileEndpoint.cs
new file mode 100644
index 0000000..b52b3e9
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/CheckFileEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Files.DTOs;
+using File.Application.Features.Files.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+/// 秒传预检端点。POST /api/files/check,按哈希查同用户已存在文件。
+[Authorize]
+///
+/// undefined
+///
+public class CheckFileEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/files/check");
+ }
+
+ public override async Task HandleAsync(CheckFileRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new CheckFileQuery(req.ContentHash, req.Size), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/CheckFileRequest.cs b/src/BuildFlow.Host/Endpoints/File/Files/CheckFileRequest.cs
new file mode 100644
index 0000000..34ad267
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/CheckFileRequest.cs
@@ -0,0 +1,10 @@
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+/// 秒传预检请求。前端上传前带 content_hash 与 size 查重。
+public class CheckFileRequest
+{
+ /// 文件内容 SHA-256 哈希(前端计算)。
+ public string ContentHash { get; init; } = default!;
+ /// 文件大小(字节),防哈希碰撞。
+ public long Size { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/DeleteFileEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Files/DeleteFileEndpoint.cs
new file mode 100644
index 0000000..f8cbeaa
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/DeleteFileEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Files.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+
+/// 删除文件端点。DELETE /api/files/delete,按对象键删除文件。
+[Authorize]
+///
+/// undefined
+///
+public class DeleteFileEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/files/delete");
+ }
+
+ public override async Task HandleAsync(DeleteFileRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new DeleteFileCommand(req.ObjectKey), ct);
+ await Send.OkAsync(new { message = "deleted" }, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/DeleteFileRequest.cs b/src/BuildFlow.Host/Endpoints/File/Files/DeleteFileRequest.cs
new file mode 100644
index 0000000..1134c41
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/DeleteFileRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+/// 删除文件请求。以查询参数指定对象键。
+public class DeleteFileRequest
+{
+ [QueryParam] public string ObjectKey { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/DownloadFileEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Files/DownloadFileEndpoint.cs
new file mode 100644
index 0000000..b02bbe6
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/DownloadFileEndpoint.cs
@@ -0,0 +1,41 @@
+using System.Net.Mime;
+using File.Application.Features.Files.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+///
+/// 下载文件端点。GET /api/files/download,按对象键流式返回文件内容。
+/// 直接把 S3 响应流透传到 HTTP 响应体,整文件不进内存;Content-Disposition 触发浏览器下载。
+/// 该路径已在 ApiResponseMiddlewareOptions.PassthroughPrefixes 注册,绕过响应包裹中间件的缓冲。
+///
+[Authorize]
+///
+/// undefined
+///
+public class DownloadFileEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure() => Get("/files/download");
+
+ public override async Task HandleAsync(DownloadFileRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new DownloadFileQuery(req.ObjectKey), ct);
+
+ // 设置响应头:真实 ContentType + 触发下载的文件名
+ HttpContext.Response.StatusCode = 200;
+ HttpContext.Response.ContentType = result.ContentType;
+ var disposition = new ContentDisposition { FileName = result.FileName, Inline = false };
+ HttpContext.Response.Headers.ContentDisposition = disposition.ToString();
+
+ // 流式透传:S3 流 → HTTP 响应体,零内存拷贝
+ try
+ {
+ await result.Stream.CopyToAsync(HttpContext.Response.Body, ct);
+ await HttpContext.Response.Body.FlushAsync(ct);
+ }
+ finally
+ {
+ await result.Stream.DisposeAsync();
+ }
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/DownloadFileRequest.cs b/src/BuildFlow.Host/Endpoints/File/Files/DownloadFileRequest.cs
new file mode 100644
index 0000000..ff1e7bd
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/DownloadFileRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+/// 下载文件请求。以查询参数指定对象键。
+public class DownloadFileRequest
+{
+ [QueryParam] public string ObjectKey { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/GetFileContentEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Files/GetFileContentEndpoint.cs
new file mode 100644
index 0000000..3358adc
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/GetFileContentEndpoint.cs
@@ -0,0 +1,25 @@
+using File.Application.Features.Files.DTOs;
+using File.Application.Features.Files.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+
+/// 文件内容端点。GET /api/files/content,返回文件文本内容。
+[Authorize]
+///
+/// undefined
+///
+public class GetFileContentEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/files/content");
+ }
+
+ public override async Task HandleAsync(GetFileContentRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetFileContentQuery(req.ObjectKey), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/GetFileContentRequest.cs b/src/BuildFlow.Host/Endpoints/File/Files/GetFileContentRequest.cs
new file mode 100644
index 0000000..c62cc6d
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/GetFileContentRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+/// 文件内容请求。以查询参数指定对象键(bucket 由系统设置统一管理)。
+public class GetFileContentRequest
+{
+ [QueryParam] public string ObjectKey { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/GetFilePreviewEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Files/GetFilePreviewEndpoint.cs
new file mode 100644
index 0000000..4635f2a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/GetFilePreviewEndpoint.cs
@@ -0,0 +1,42 @@
+using System.Net.Mime;
+using File.Application.Features.Files.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+///
+/// 文件预览端点。GET /api/files/preview,流式返回文件内容供内联展示(非下载)。
+/// 后端从 S3 拉流透传,前端永远不接触 S3 URL 和凭据。
+/// 鉴权:[Authorize] + JWT 中间件从 token query 参数读 JWT(img src / window.open 无法带 header)。
+/// 该路径在 ApiResponseMiddlewareOptions.PassthroughPrefixes 注册,绕过响应包裹中间件的缓冲。
+///
+[Authorize]
+public class GetFilePreviewEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/files/preview");
+ }
+
+ public override async Task HandleAsync(GetFilePreviewRequest req, CancellationToken ct)
+ {
+ // 复用下载查询(返回 Stream + ContentType + FileName)
+ var result = await mediator.Send(new DownloadFileQuery(req.ObjectKey), ct);
+
+ // inline = 浏览器内联展示(图片/PDF 直接渲染),而非触发下载
+ HttpContext.Response.ContentType = result.ContentType;
+ var disposition = new ContentDisposition { FileName = result.FileName, Inline = true };
+ HttpContext.Response.Headers.ContentDisposition = disposition.ToString();
+
+ // 流式透传:S3 流 → HTTP 响应体
+ try
+ {
+ await result.Stream.CopyToAsync(HttpContext.Response.Body, ct);
+ await HttpContext.Response.Body.FlushAsync(ct);
+ }
+ finally
+ {
+ await result.Stream.DisposeAsync();
+ }
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/GetFilePreviewRequest.cs b/src/BuildFlow.Host/Endpoints/File/Files/GetFilePreviewRequest.cs
new file mode 100644
index 0000000..860500b
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/GetFilePreviewRequest.cs
@@ -0,0 +1,19 @@
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+///
+/// 文件预览请求。以查询参数指定对象键。
+/// 鉴权三选一:Authorization header / Token 查询参数(JWT) / InternalToken+Expires(KKFileView 内部回拉)。
+///
+public class GetFilePreviewRequest
+{
+ [QueryParam] public string ObjectKey { get; init; } = default!;
+
+ /// JWT token(用于 img src / window.open 等无法带 Authorization header 的场景)。
+ [QueryParam] public string? Token { get; init; }
+
+ /// KKFileView 内部回拉签名(HMAC),配合 Expires 使用。
+ [QueryParam] public string? InternalToken { get; init; }
+
+ /// 内部签名过期时间戳(Unix 秒)。
+ [QueryParam] public long? Expires { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/GetOfficePreviewEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Files/GetOfficePreviewEndpoint.cs
new file mode 100644
index 0000000..8d39236
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/GetOfficePreviewEndpoint.cs
@@ -0,0 +1,27 @@
+using File.Application.Features.Files.DTOs;
+using File.Application.Features.Files.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+///
+/// Office/PDF 文档在线预览端点。GET /api/files/office-preview,
+/// 返回 KKFileView 预览地址,前端 window.open 打开即可渲染 Office 文档。
+///
+[Authorize]
+///
+/// undefined
+///
+public class GetOfficePreviewEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/files/office-preview");
+ }
+
+ public override async Task HandleAsync(GetOfficePreviewRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetOfficePreviewQuery(req.ObjectKey), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/GetOfficePreviewRequest.cs b/src/BuildFlow.Host/Endpoints/File/Files/GetOfficePreviewRequest.cs
new file mode 100644
index 0000000..7e0341c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/GetOfficePreviewRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+/// Office/PDF 文档预览请求。以查询参数指定对象键。
+public class GetOfficePreviewRequest
+{
+ [QueryParam] public string ObjectKey { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/ListFilesEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Files/ListFilesEndpoint.cs
new file mode 100644
index 0000000..47d6010
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/ListFilesEndpoint.cs
@@ -0,0 +1,25 @@
+using File.Application.Features.Files.DTOs;
+using File.Application.Features.Files.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+
+/// 列举文件端点。GET /api/files/list,支持前缀与分页查询。
+[Authorize]
+///
+/// undefined
+///
+public class ListFilesEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/files/list");
+ }
+
+ public override async Task HandleAsync(ListFilesRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new ListFilesQuery(req.Prefix, req.MaxKeys, req.ContinuationToken), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/ListFilesRequest.cs b/src/BuildFlow.Host/Endpoints/File/Files/ListFilesRequest.cs
new file mode 100644
index 0000000..2ef89f7
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/ListFilesRequest.cs
@@ -0,0 +1,9 @@
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+/// 列举文件请求。包含前缀、最大键数与续传令牌。
+public class ListFilesRequest
+{
+ [QueryParam] public string? Prefix { get; init; }
+ [QueryParam] public int? MaxKeys { get; init; }
+ [QueryParam] public string? ContinuationToken { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/UploadFileEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Files/UploadFileEndpoint.cs
new file mode 100644
index 0000000..76a1f89
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/UploadFileEndpoint.cs
@@ -0,0 +1,28 @@
+using File.Application.Features.Files.Commands;
+using File.Application.Features.Files.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+
+/// 上传文件端点。POST /api/files/upload,支持 multipart 文件上传。
+[Authorize]
+///
+/// undefined
+///
+public class UploadFileEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/files/upload");
+ AllowFileUploads();
+ }
+
+ public override async Task HandleAsync(UploadFileRequest req, CancellationToken ct)
+ {
+ await using var stream = req.File.OpenReadStream();
+ var result = await mediator.Send(new UploadFileCommand(req.ObjectKey,
+ stream, req.File.ContentType, req.File.Length, req.ContentHash), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Files/UploadFileRequest.cs b/src/BuildFlow.Host/Endpoints/File/Files/UploadFileRequest.cs
new file mode 100644
index 0000000..f00c7b8
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Files/UploadFileRequest.cs
@@ -0,0 +1,10 @@
+namespace BuildFlow.Host.Endpoints.File.Files;
+
+/// 上传文件请求。包含 object_key 与文件数据。
+public class UploadFileRequest
+{
+ public string ObjectKey { get; init; } = default!;
+ public IFormFile File { get; init; } = default!;
+ /// 文件内容哈希(可选),秒传查重用。
+ public string? ContentHash { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/CreateFolderEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Folders/CreateFolderEndpoint.cs
new file mode 100644
index 0000000..c289114
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/CreateFolderEndpoint.cs
@@ -0,0 +1,26 @@
+using File.Application.Features.Folders.Commands;
+using File.Application.Features.Folders.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+
+/// 创建文件夹端点。POST /api/folders。
+[Authorize]
+///
+/// undefined
+///
+public class CreateFolderEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/folders");
+ }
+
+ public override async Task HandleAsync(CreateFolderRequest req, CancellationToken ct)
+ {
+ var parentId = string.IsNullOrEmpty(req.ParentId) ? (Guid?)null : Guid.Parse(req.ParentId);
+ var result = await mediator.Send(new CreateFolderCommand(parentId, req.Name), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/CreateFolderRequest.cs b/src/BuildFlow.Host/Endpoints/File/Folders/CreateFolderRequest.cs
new file mode 100644
index 0000000..9300bb3
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/CreateFolderRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+/// 创建文件夹请求体(父目录 Id 与名称)。
+public class CreateFolderRequest
+{
+ public string? ParentId { get; init; }
+ public string Name { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/DeleteFolderEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Folders/DeleteFolderEndpoint.cs
new file mode 100644
index 0000000..5efa8ad
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/DeleteFolderEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Folders.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+
+/// 删除文件夹端点。DELETE /api/folders/{Id}。
+[Authorize]
+///
+/// undefined
+///
+public class DeleteFolderEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/folders/{Id}");
+ }
+
+ public override async Task HandleAsync(DeleteFolderRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new DeleteFolderCommand(Guid.Parse(req.Id)), ct);
+ await Send.OkAsync(new { message = "deleted" }, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/DeleteFolderRequest.cs b/src/BuildFlow.Host/Endpoints/File/Folders/DeleteFolderRequest.cs
new file mode 100644
index 0000000..fb71d2c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/DeleteFolderRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+/// 删除文件夹请求(路由取 Id)。
+public class DeleteFolderRequest
+{
+ [RouteParam] public string Id { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderEndpoint.cs
new file mode 100644
index 0000000..373ef7c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Folders.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+
+/// 获取文件夹详情端点。GET /api/folders/{Id}。
+[Authorize]
+///
+/// undefined
+///
+public class GetFolderEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/folders/{Id}");
+ }
+
+ public override async Task HandleAsync(GetFolderRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetFolderQuery(Guid.Parse(req.Id)), ct);
+ await Send.OkAsync(result!, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderRequest.cs b/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderRequest.cs
new file mode 100644
index 0000000..3b9efe7
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+/// 获取文件夹请求(路由取 Id)。
+public class GetFolderRequest
+{
+ [RouteParam] public string Id { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderTreeEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderTreeEndpoint.cs
new file mode 100644
index 0000000..6965032
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/GetFolderTreeEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Folders.DTOs;
+using File.Application.Features.Folders.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+/// 获取整棵文件夹树端点。GET /api/folders/tree。
+[Authorize]
+///
+/// undefined
+///
+public class GetFolderTreeEndpoint(IMediator mediator) : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/folders/tree");
+ }
+
+ public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetFolderTreeQuery(), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/GetRootFilesEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Folders/GetRootFilesEndpoint.cs
new file mode 100644
index 0000000..4ca7fbf
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/GetRootFilesEndpoint.cs
@@ -0,0 +1,25 @@
+using File.Application.Features.Folders.DTOs;
+using File.Application.Features.Folders.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+///
+/// 查询根级文件端点。GET /api/files/root,
+/// 返回当前用户 FolderId=null 的所有 FileMeta,使根目录与子文件夹共享同一数据源(DB)。
+/// 注意:路径用 /files/root 而非 /folders/root/files,避免与 POST /folders/{FolderId}/files 模板冲突。
+///
+[Authorize]
+public class GetRootFilesEndpoint(IMediator mediator) : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/files/root");
+ }
+
+ public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetRootFilesQuery(), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/MoveFileEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Folders/MoveFileEndpoint.cs
new file mode 100644
index 0000000..b98f973
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/MoveFileEndpoint.cs
@@ -0,0 +1,25 @@
+using File.Application.Features.Folders.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+
+/// 移动文件到目标文件夹端点。POST /api/files/{Id}/move。
+[Authorize]
+///
+/// undefined
+///
+public class MoveFileEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/files/{Id}/move");
+ }
+
+ public override async Task HandleAsync(MoveFileRequest req, CancellationToken ct)
+ {
+ var targetId = string.IsNullOrEmpty(req.TargetFolderId) ? (Guid?)null : Guid.Parse(req.TargetFolderId);
+ await mediator.Send(new MoveFileCommand(Guid.Parse(req.Id), targetId), ct);
+ await Send.OkAsync(new { message = "moved" }, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/MoveFileRequest.cs b/src/BuildFlow.Host/Endpoints/File/Folders/MoveFileRequest.cs
new file mode 100644
index 0000000..a46c75d
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/MoveFileRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+/// 移动文件请求(文件 Id 与目标目录)。
+public class MoveFileRequest
+{
+ [RouteParam] public string Id { get; init; } = default!;
+ public string? TargetFolderId { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/RenameFolderEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Folders/RenameFolderEndpoint.cs
new file mode 100644
index 0000000..5f217d4
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/RenameFolderEndpoint.cs
@@ -0,0 +1,25 @@
+using File.Application.Features.Folders.Commands;
+using File.Application.Features.Folders.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+
+/// 重命名文件夹端点。PUT /api/folders/{Id}。
+[Authorize]
+///
+/// undefined
+///
+public class RenameFolderEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/folders/{Id}");
+ }
+
+ public override async Task HandleAsync(RenameFolderRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new RenameFolderCommand(Guid.Parse(req.Id), req.Name), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/RenameFolderRequest.cs b/src/BuildFlow.Host/Endpoints/File/Folders/RenameFolderRequest.cs
new file mode 100644
index 0000000..3e389a5
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/RenameFolderRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+/// 重命名文件夹请求(路由 Id 与新名称)。
+public class RenameFolderRequest
+{
+ [RouteParam] public string Id { get; init; } = default!;
+ public string Name { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/UploadToFolderEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Folders/UploadToFolderEndpoint.cs
new file mode 100644
index 0000000..d72b114
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/UploadToFolderEndpoint.cs
@@ -0,0 +1,28 @@
+using File.Application.Features.Folders.Commands;
+using File.Application.Features.Folders.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+
+/// 上传文件到指定文件夹端点。POST /api/folders/{FolderId}/files。
+[Authorize]
+///
+/// undefined
+///
+public class UploadToFolderEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/folders/{FolderId}/files");
+ AllowFileUploads();
+ }
+
+ public override async Task HandleAsync(UploadToFolderRequest req, CancellationToken ct)
+ {
+ await using var stream = req.File.OpenReadStream();
+ var result = await mediator.Send(new UploadToFolderCommand(Guid.Parse(req.FolderId),
+ req.FileName, stream, req.File.ContentType, req.File.Length, req.ContentHash), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Folders/UploadToFolderRequest.cs b/src/BuildFlow.Host/Endpoints/File/Folders/UploadToFolderRequest.cs
new file mode 100644
index 0000000..9ef1f43
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Folders/UploadToFolderRequest.cs
@@ -0,0 +1,11 @@
+namespace BuildFlow.Host.Endpoints.File.Folders;
+
+/// 上传文件到文件夹请求(含 multipart 文件流)。
+public class UploadToFolderRequest
+{
+ [RouteParam] public string FolderId { get; init; } = default!;
+ public string FileName { get; init; } = default!;
+ public IFormFile File { get; init; } = default!;
+ /// 文件内容哈希(可选),秒传查重用。
+ public string? ContentHash { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/AbortMultipartEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/AbortMultipartEndpoint.cs
new file mode 100644
index 0000000..b5573bc
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/AbortMultipartEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Multipart.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+
+/// 中止分片上传端点。POST /api/files/multipart/abort。
+[Authorize]
+///
+/// undefined
+///
+public class AbortMultipartEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/files/multipart/abort");
+ }
+
+ public override async Task HandleAsync(AbortMultipartRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new AbortMultipartCommand(req.ObjectKey, req.UploadId), ct);
+ await Send.OkAsync(new { message = "aborted" }, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/AbortMultipartRequest.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/AbortMultipartRequest.cs
new file mode 100644
index 0000000..d7300e9
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/AbortMultipartRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+/// 中止分片上传请求(对象 key、UploadId)。
+public class AbortMultipartRequest
+{
+ public string ObjectKey { get; init; } = default!;
+ public string UploadId { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/CompleteMultipartEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/CompleteMultipartEndpoint.cs
new file mode 100644
index 0000000..164600e
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/CompleteMultipartEndpoint.cs
@@ -0,0 +1,26 @@
+using File.Application.Features.Multipart.Commands;
+using File.Application.Features.Multipart.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+
+/// 完成分片上传端点。POST /api/files/multipart/complete。
+[Authorize]
+///
+/// undefined
+///
+public class CompleteMultipartEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/files/multipart/complete");
+ }
+
+ public override async Task HandleAsync(CompleteMultipartRequest req, CancellationToken ct)
+ {
+ var parts = req.Parts.Select(p => new CompletedPartDto(p.PartNumber, p.ETag)).ToList();
+ var result = await mediator.Send(new CompleteMultipartCommand(req.ObjectKey, req.UploadId, parts, req.FileName, req.Size), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/CompleteMultipartRequest.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/CompleteMultipartRequest.cs
new file mode 100644
index 0000000..18d93f8
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/CompleteMultipartRequest.cs
@@ -0,0 +1,13 @@
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+/// 完成分片上传请求(含各分片信息列表)。
+public class CompleteMultipartRequest
+{
+ public string ObjectKey { get; init; } = default!;
+ public string UploadId { get; init; } = default!;
+ public List Parts { get; init; } = new();
+ /// 文件名(可选),完成后落 FileMeta 用,供秒传查重。
+ public string? FileName { get; init; }
+ /// 文件总大小(可选),落 FileMeta 用。
+ public long Size { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/InitMultipartEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/InitMultipartEndpoint.cs
new file mode 100644
index 0000000..0173a9c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/InitMultipartEndpoint.cs
@@ -0,0 +1,25 @@
+using File.Application.Features.Multipart.Commands;
+using File.Application.Features.Multipart.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+
+/// 初始化分片上传端点。POST /api/files/multipart/init。
+[Authorize]
+///
+/// undefined
+///
+public class InitMultipartEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/files/multipart/init");
+ }
+
+ public override async Task HandleAsync(InitMultipartRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new InitMultipartCommand(req.ObjectKey, req.ContentHash, req.TotalParts, req.Size), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/InitMultipartRequest.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/InitMultipartRequest.cs
new file mode 100644
index 0000000..e4a792a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/InitMultipartRequest.cs
@@ -0,0 +1,14 @@
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+/// 初始化分片上传请求。ContentHash/TotalParts 可选,断点续传时由前端附带。
+public class InitMultipartRequest
+{
+ /// S3 对象 key(断点续传建议用内容哈希派生稳定值)。
+ public string ObjectKey { get; init; } = default!;
+ /// 文件内容哈希(可选),仅落库记录用。
+ public string? ContentHash { get; init; }
+ /// 预计分片总数(可选),进度参考。
+ public int? TotalParts { get; init; }
+ /// 文件大小(可选),秒传判定用(防哈希碰撞)。
+ public long? Size { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/ListPartsEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/ListPartsEndpoint.cs
new file mode 100644
index 0000000..ad33099
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/ListPartsEndpoint.cs
@@ -0,0 +1,27 @@
+using File.Application.Features.Multipart.DTOs;
+using File.Application.Features.Multipart.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+///
+/// 查询分片上传已传分片清单端点。GET /api/files/multipart/parts?upload_id=xxx。
+/// 断点续传恢复点:前端续传前调用,据此跳过已传 partNumber。
+///
+[Authorize]
+///
+/// undefined
+///
+public class ListPartsEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/files/multipart/parts");
+ }
+
+ public override async Task HandleAsync(ListPartsRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new ListPartsQuery(req.UploadId), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/ListPartsRequest.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/ListPartsRequest.cs
new file mode 100644
index 0000000..e7f0eba
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/ListPartsRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+/// 查询已传分片清单请求。upload_id 经 query string 传入。
+public class ListPartsRequest
+{
+ /// 分片上传会话的 S3 UploadId。
+ [QueryParam] public string UploadId { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/PartRequest.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/PartRequest.cs
new file mode 100644
index 0000000..eff74e1
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/PartRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+/// 分片上传的单片信息(CompleteMultipart 时用)。
+public class PartRequest
+{
+ public int PartNumber { get; init; }
+ public string ETag { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/UploadPartEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/UploadPartEndpoint.cs
new file mode 100644
index 0000000..b68ddd8
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/UploadPartEndpoint.cs
@@ -0,0 +1,28 @@
+using File.Application.Features.Multipart.Commands;
+using File.Application.Features.Multipart.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+
+/// 上传单个分片端点。PUT /api/files/multipart/part。
+[Authorize]
+///
+/// undefined
+///
+public class UploadPartEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/files/multipart/part");
+ AllowFileUploads();
+ }
+
+ public override async Task HandleAsync(UploadPartRequest req, CancellationToken ct)
+ {
+ await using var stream = req.File.OpenReadStream();
+ var result = await mediator.Send(new UploadPartCommand(req.ObjectKey,
+ req.UploadId, req.PartNumber, stream), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Multipart/UploadPartRequest.cs b/src/BuildFlow.Host/Endpoints/File/Multipart/UploadPartRequest.cs
new file mode 100644
index 0000000..a485a12
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Multipart/UploadPartRequest.cs
@@ -0,0 +1,10 @@
+namespace BuildFlow.Host.Endpoints.File.Multipart;
+
+/// 上传单个分片请求(含 multipart 文件流)。
+public class UploadPartRequest
+{
+ public string ObjectKey { get; init; } = default!;
+ public string UploadId { get; init; } = default!;
+ public int PartNumber { get; init; }
+ public IFormFile File { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Settings/GetFileSettingEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Settings/GetFileSettingEndpoint.cs
new file mode 100644
index 0000000..0047109
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Settings/GetFileSettingEndpoint.cs
@@ -0,0 +1,18 @@
+using File.Application.Features.Settings.DTOs;
+using File.Application.Features.Settings.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Settings;
+
+/// 查询文件系统设置端点。GET /api/file-settings,返回当前默认存储桶。
+[Authorize]
+public class GetFileSettingEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure() => Get("/file-settings");
+
+ public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetFileSettingQuery(), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Settings/UpdateDefaultBucketEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Settings/UpdateDefaultBucketEndpoint.cs
new file mode 100644
index 0000000..494541a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Settings/UpdateDefaultBucketEndpoint.cs
@@ -0,0 +1,18 @@
+using File.Application.Features.Settings.Commands;
+using File.Application.Features.Settings.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Settings;
+
+/// 更新默认存储桶端点。PUT /api/file-settings/default-bucket。
+[Authorize]
+public class UpdateDefaultBucketEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure() => Put("/file-settings/default-bucket");
+
+ public override async Task HandleAsync(UpdateDefaultBucketRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new UpdateDefaultBucketCommand(req.DefaultBucket), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Settings/UpdateDefaultBucketRequest.cs b/src/BuildFlow.Host/Endpoints/File/Settings/UpdateDefaultBucketRequest.cs
new file mode 100644
index 0000000..5159726
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Settings/UpdateDefaultBucketRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Settings;
+
+/// 更新默认存储桶请求。
+public class UpdateDefaultBucketRequest
+{
+ public string DefaultBucket { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Shares/CreateShareEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Shares/CreateShareEndpoint.cs
new file mode 100644
index 0000000..7eee996
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Shares/CreateShareEndpoint.cs
@@ -0,0 +1,29 @@
+using File.Application.Features.Shares.Commands;
+using File.Application.Features.Shares.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Shares;
+
+
+/// 创建分享链接端点。POST /api/share。
+[Authorize]
+///
+/// undefined
+///
+public class CreateShareEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/share");
+ }
+
+ public override async Task HandleAsync(CreateShareRequest req, CancellationToken ct)
+ {
+ var expiresAt = string.IsNullOrEmpty(req.ExpiresAt)
+ ? (DateTime?)null
+ : DateTime.Parse(req.ExpiresAt, null, System.Globalization.DateTimeStyles.RoundtripKind);
+ var result = await mediator.Send(new CreateShareCommand(req.ResourceType,
+ Guid.Parse(req.ResourceId), req.Password, expiresAt, req.MaxDownloads), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Shares/CreateShareRequest.cs b/src/BuildFlow.Host/Endpoints/File/Shares/CreateShareRequest.cs
new file mode 100644
index 0000000..11a221b
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Shares/CreateShareRequest.cs
@@ -0,0 +1,11 @@
+namespace BuildFlow.Host.Endpoints.File.Shares;
+
+/// 创建分享请求(资源、密码、过期、下载上限)。
+public class CreateShareRequest
+{
+ public string ResourceType { get; init; } = default!;
+ public string ResourceId { get; init; } = default!;
+ public string? Password { get; init; }
+ public string? ExpiresAt { get; init; } // ISO 8601 字符串
+ public int? MaxDownloads { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Shares/DeleteShareEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Shares/DeleteShareEndpoint.cs
new file mode 100644
index 0000000..f07deec
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Shares/DeleteShareEndpoint.cs
@@ -0,0 +1,24 @@
+using File.Application.Features.Shares.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Shares;
+
+
+/// 删除分享端点。DELETE /api/share/{Id}。
+[Authorize]
+///
+/// undefined
+///
+public class DeleteShareEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/share/{Id}");
+ }
+
+ public override async Task HandleAsync(DeleteShareRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new DeleteShareCommand(Guid.Parse(req.Id)), ct);
+ await Send.OkAsync(new { message = "deleted" }, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Shares/DeleteShareRequest.cs b/src/BuildFlow.Host/Endpoints/File/Shares/DeleteShareRequest.cs
new file mode 100644
index 0000000..88df3d3
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Shares/DeleteShareRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Shares;
+
+/// 删除分享请求(路由取 Id)。
+public class DeleteShareRequest
+{
+ [RouteParam] public string Id { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Shares/DownloadShareEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Shares/DownloadShareEndpoint.cs
new file mode 100644
index 0000000..3582eda
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Shares/DownloadShareEndpoint.cs
@@ -0,0 +1,22 @@
+using File.Application.Features.Shares.DTOs;
+using File.Application.Features.Shares.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Shares;
+
+
+/// 下载分享端点(匿名访问)。POST /api/share/{Token}/download。
+public class DownloadShareEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/share/{Token}/download");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(DownloadShareRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new DownloadShareQuery(req.Token, req.Password), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Shares/DownloadShareRequest.cs b/src/BuildFlow.Host/Endpoints/File/Shares/DownloadShareRequest.cs
new file mode 100644
index 0000000..a3d7baf
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Shares/DownloadShareRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.File.Shares;
+
+/// 下载分享请求(Token 与可选密码)。
+public class DownloadShareRequest
+{
+ [RouteParam] public string Token { get; init; } = default!;
+ public string? Password { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Shares/GetShareInfoEndpoint.cs b/src/BuildFlow.Host/Endpoints/File/Shares/GetShareInfoEndpoint.cs
new file mode 100644
index 0000000..9a89c48
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Shares/GetShareInfoEndpoint.cs
@@ -0,0 +1,21 @@
+using File.Application.Features.Shares.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.File.Shares;
+
+
+/// 查询分享信息端点(匿名访问)。GET /api/share/{Token}。
+public class GetShareInfoEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/share/{Token}");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(GetShareInfoRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetShareInfoQuery(req.Token), ct);
+ await Send.OkAsync(result!, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/File/Shares/GetShareInfoRequest.cs b/src/BuildFlow.Host/Endpoints/File/Shares/GetShareInfoRequest.cs
new file mode 100644
index 0000000..d0f9f08
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/File/Shares/GetShareInfoRequest.cs
@@ -0,0 +1,7 @@
+namespace BuildFlow.Host.Endpoints.File.Shares;
+
+/// 查询分享信息请求(路由取 Token)。
+public class GetShareInfoRequest
+{
+ [RouteParam] public string Token { get; init; } = default!;
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/AddGroupMemberEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/AddGroupMemberEndpoint.cs
new file mode 100644
index 0000000..82a43f3
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/AddGroupMemberEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+
+///
+/// 批量添加群成员端点。POST /groups/{GroupId}/members。
+///
+public class AddGroupMemberEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/groups/{GroupId}/members");
+ Permissions("im:group:manage");
+ }
+
+ public override async Task HandleAsync(AddGroupMemberEndpointRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ await mediator.Send(new AddGroupMemberCommand(req.GroupId, userId, req.UserIds), ct);
+ HttpContext.Response.StatusCode = 204;
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/AddGroupMemberEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/AddGroupMemberEndpointRequest.cs
new file mode 100644
index 0000000..6c7ead5
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/AddGroupMemberEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 批量添加群成员请求体。
+///
+public record AddGroupMemberEndpointRequest(Guid GroupId, List UserIds);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/CreateGroupEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/CreateGroupEndpoint.cs
new file mode 100644
index 0000000..f3b8c7b
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/CreateGroupEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using IM.Application.Chat.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 创建群组端点。POST /groups。
+///
+public class CreateGroupEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/groups");
+ Permissions("im:group:create");
+ }
+
+ public override async Task HandleAsync(CreateGroupRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var result = await mediator.Send(new CreateGroupCommand(req.Name, req.Avatar, req.Description, userId, req.MemberUserIds), ct);
+ await Send.ResponseAsync(result, 201, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/CreatePrivateConversationEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/CreatePrivateConversationEndpoint.cs
new file mode 100644
index 0000000..8377fd9
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/CreatePrivateConversationEndpoint.cs
@@ -0,0 +1,28 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using IM.Application.Chat.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 创建或复用私聊会话。若当前用户与目标用户间已存在私聊会话则直接返回,保证幂等。
+///
+public class CreatePrivateConversationEndpoint(
+ IMediator mediator,
+ ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/conversations/private");
+ Permissions("im:chat");
+ }
+
+ public override async Task HandleAsync(CreatePrivateConversationRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var result = await mediator.Send(
+ new CreatePrivateConversationCommand(userId, req.TargetUserId), ct);
+ await Send.ResponseAsync(result, 200, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/DeleteGroupEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/DeleteGroupEndpoint.cs
new file mode 100644
index 0000000..e1d8e43
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/DeleteGroupEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+
+///
+/// 解散群组端点。DELETE /groups/{GroupId}。
+///
+public class DeleteGroupEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/groups/{GroupId}");
+ Permissions("im:group:manage");
+ }
+
+ public override async Task HandleAsync(DeleteGroupEndpointRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ await mediator.Send(new DeleteGroupCommand(req.GroupId, userId), ct);
+ HttpContext.Response.StatusCode = 204;
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/DeleteGroupEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/DeleteGroupEndpointRequest.cs
new file mode 100644
index 0000000..26fba49
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/DeleteGroupEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 解散群组请求体(路由参数绑定)。
+///
+public record DeleteGroupEndpointRequest(Guid GroupId);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/GetConversationsEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/GetConversationsEndpoint.cs
new file mode 100644
index 0000000..a914eb6
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/GetConversationsEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.DTOs;
+using IM.Application.Chat.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 查询用户会话列表端点。GET /conversations。
+///
+public class GetConversationsEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/conversations");
+ Permissions("im:read");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var result = await mediator.Send(new GetConversationsQuery(userId), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupEndpoint.cs
new file mode 100644
index 0000000..e464c56
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.DTOs;
+using IM.Application.Chat.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 查询单个群详情(含公告/成员数)。
+///
+public class GetGroupEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/groups/{GroupId}");
+ Permissions("im:read");
+ }
+
+ public override async Task HandleAsync(GetGroupEndpointRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var result = await mediator.Send(new GetGroupQuery(req.GroupId, userId), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupEndpointRequest.cs
new file mode 100644
index 0000000..94501a9
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupEndpointRequest.cs
@@ -0,0 +1,4 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+/// 查询群详情请求。GroupId 由路由参数绑定。
+public record GetGroupEndpointRequest(Guid GroupId);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupMembersEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupMembersEndpoint.cs
new file mode 100644
index 0000000..d8fcc6c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupMembersEndpoint.cs
@@ -0,0 +1,26 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.DTOs;
+using IM.Application.Chat.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+
+///
+/// 查询群成员列表端点。GET /groups/{GroupId}/members。
+///
+public class GetGroupMembersEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/groups/{GroupId}/members");
+ Permissions("im:read");
+ }
+
+ public override async Task HandleAsync(GetGroupMembersEndpointRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var result = await mediator.Send(new GetGroupMembersQuery(userId, req.GroupId), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupMembersEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupMembersEndpointRequest.cs
new file mode 100644
index 0000000..92556b3
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/GetGroupMembersEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 群成员列表查询请求体(路由参数绑定)。
+///
+public record GetGroupMembersEndpointRequest(Guid GroupId);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/GetMessageHistoryEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/GetMessageHistoryEndpoint.cs
new file mode 100644
index 0000000..47c07e5
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/GetMessageHistoryEndpoint.cs
@@ -0,0 +1,27 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.DTOs;
+using IM.Application.Chat.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+
+///
+/// 查询历史消息端点。GET /conversations/{ConversationId}/messages。
+///
+public class GetMessageHistoryEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/conversations/{ConversationId}/messages");
+ Permissions("im:read");
+ }
+
+ public override async Task HandleAsync(GetMessageHistoryRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var result = await mediator.Send(
+ new GetMessageHistoryQuery(userId, req.ConversationId, req.BeforeId, req.Limit), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/GetMessageHistoryRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/GetMessageHistoryRequest.cs
new file mode 100644
index 0000000..a4fad1d
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/GetMessageHistoryRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 历史消息查询请求体。含游标与分页大小。
+///
+public record GetMessageHistoryRequest(Guid ConversationId, string? BeforeId, int Limit = 50);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/GetUnreadCountEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/GetUnreadCountEndpoint.cs
new file mode 100644
index 0000000..231954e
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/GetUnreadCountEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.DTOs;
+using IM.Application.Chat.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 查询未读消息数端点。GET /conversations/unread。
+///
+public class GetUnreadCountEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Get("/unread");
+ Permissions("im:read");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var result = await mediator.Send(new GetUnreadCountQuery(userId), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/MarkConversationUnreadEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/MarkConversationUnreadEndpoint.cs
new file mode 100644
index 0000000..07a630a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/MarkConversationUnreadEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 标记会话未读端点。POST /conversations/{ConversationId}/unread。
+///
+public class MarkConversationUnreadEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Post("/conversations/{ConversationId}/unread");
+ Permissions("im:conversation:manage");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var conversationId = Route("ConversationId");
+ await mediator.Send(new MarkConversationUnreadCommand(conversationId, userId), ct);
+ await Send.OkAsync(ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/RecallMessageEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/RecallMessageEndpoint.cs
new file mode 100644
index 0000000..3d9b69b
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/RecallMessageEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 撤回消息 REST 端点(SignalR 的非实时兜底通道)。
+/// 权限与时间窗校验在 RecallMessageCommandHandler 内统一执行。
+///
+public class RecallMessageEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/conversations/{ConversationId}/messages/{MessageId}/recall");
+ Permissions("im:chat");
+ }
+
+ public override async Task HandleAsync(RecallMessageEndpointRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ await mediator.Send(new RecallMessageCommand(req.ConversationId, req.MessageId, userId), ct);
+ await Send.OkAsync(ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/RecallMessageEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/RecallMessageEndpointRequest.cs
new file mode 100644
index 0000000..b3b0731
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/RecallMessageEndpointRequest.cs
@@ -0,0 +1,4 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+/// 撤回消息请求。ConversationId 与 MessageId 由路由参数绑定。
+public record RecallMessageEndpointRequest(Guid ConversationId, string MessageId);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/RemoveGroupMemberEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/RemoveGroupMemberEndpoint.cs
new file mode 100644
index 0000000..55be69f
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/RemoveGroupMemberEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+
+///
+/// 移除群成员端点。DELETE /groups/{GroupId}/members/{UserId}。
+///
+public class RemoveGroupMemberEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/groups/{GroupId}/members/{TargetUserId}");
+ Permissions("im:group:manage");
+ }
+
+ public override async Task HandleAsync(RemoveGroupMemberEndpointRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ await mediator.Send(new RemoveGroupMemberCommand(req.GroupId, userId, req.TargetUserId), ct);
+ HttpContext.Response.StatusCode = 204;
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/RemoveGroupMemberEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/RemoveGroupMemberEndpointRequest.cs
new file mode 100644
index 0000000..925e5c2
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/RemoveGroupMemberEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 移除群成员请求体(路由参数绑定)。
+///
+public record RemoveGroupMemberEndpointRequest(Guid GroupId, Guid TargetUserId);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationMutedEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationMutedEndpoint.cs
new file mode 100644
index 0000000..a6471de
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationMutedEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 设置会话免打扰端点。PUT /conversations/{ConversationId}/muted。
+///
+public class SetConversationMutedEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/conversations/{ConversationId}/muted");
+ Permissions("im:conversation:manage");
+ }
+
+ public override async Task HandleAsync(SetConversationStateRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var conversationId = Route("ConversationId");
+ await mediator.Send(new SetConversationMutedCommand(conversationId, userId, req.Value), ct);
+ await Send.OkAsync(ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationPinnedEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationPinnedEndpoint.cs
new file mode 100644
index 0000000..b402831
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationPinnedEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 设置会话置顶端点。PUT /conversations/{ConversationId}/pinned。
+///
+public class SetConversationPinnedEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/conversations/{ConversationId}/pinned");
+ Permissions("im:conversation:manage");
+ }
+
+ public override async Task HandleAsync(SetConversationStateRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var conversationId = Route("ConversationId");
+ await mediator.Send(new SetConversationPinnedCommand(conversationId, userId, req.Value), ct);
+ await Send.OkAsync(ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationRemarkEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationRemarkEndpoint.cs
new file mode 100644
index 0000000..6c50672
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationRemarkEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 设置会话备注名端点。PUT /conversations/{ConversationId}/remark。
+///
+public class SetConversationRemarkEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/conversations/{ConversationId}/remark");
+ Permissions("im:conversation:manage");
+ }
+
+ public override async Task HandleAsync(SetConversationRemarkEndpointRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var conversationId = Route("ConversationId");
+ await mediator.Send(new SetConversationRemarkCommand(conversationId, userId, req.Remark), ct);
+ await Send.OkAsync(ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationRemarkEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationRemarkEndpointRequest.cs
new file mode 100644
index 0000000..3cfaa61
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationRemarkEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 设置会话备注名请求体。Remark 为空或 null 表示清除备注。
+///
+public record SetConversationRemarkEndpointRequest(string? Remark);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationStateRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationStateRequest.cs
new file mode 100644
index 0000000..6a5ea5e
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/SetConversationStateRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 设置会话置顶/免打扰状态的请求体。isPinned 与 isMuted 不同时使用,视端点语义而定。
+///
+public record SetConversationStateRequest(bool Value);
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/UpdateGroupEndpoint.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/UpdateGroupEndpoint.cs
new file mode 100644
index 0000000..c3512f3
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/UpdateGroupEndpoint.cs
@@ -0,0 +1,26 @@
+using BuildFlow.Shared.Domain.Common;
+using IM.Application.Chat.Commands;
+using IM.Application.Chat.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+
+///
+/// 更新群信息端点。PUT /groups/{GroupId}。
+///
+public class UpdateGroupEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/groups/{GroupId}");
+ Permissions("im:group:manage");
+ }
+
+ public override async Task HandleAsync(UpdateGroupEndpointRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserGuid();
+ var result = await mediator.Send(new UpdateGroupCommand(req.GroupId, userId, req.Name, req.Avatar, req.Description, req.Announcement), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/IM/Chat/UpdateGroupEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/IM/Chat/UpdateGroupEndpointRequest.cs
new file mode 100644
index 0000000..79e6d65
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/IM/Chat/UpdateGroupEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.IM.Chat;
+
+///
+/// 更新群信息请求体。
+///
+public record UpdateGroupEndpointRequest(Guid GroupId, string? Name, string? Avatar, string? Description, string? Announcement = null);
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/ChangePasswordEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/ChangePasswordEndpoint.cs
new file mode 100644
index 0000000..19b45b8
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/ChangePasswordEndpoint.cs
@@ -0,0 +1,44 @@
+using BuildFlow.Platform.Infrastructure.Persistence;
+using BuildFlow.Shared.Domain.Common;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 修改当前用户密码。验证旧密码后更新为新密码。
+/// POST /api/user/change-password
+///
+public class ChangePasswordEndpoint(PlatformDbContext db, ICurrentUserContext userContext)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/user/change-password");
+ }
+
+ public override async Task HandleAsync(ChangePasswordRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserId();
+
+ var user = await db.Users
+ .FirstOrDefaultAsync(u => u.Id.ToString() == userId, ct)
+ ?? throw new UnauthorizedException("用户不存在");
+
+ // 验证旧密码
+ if (!BCrypt.Net.BCrypt.Verify(req.OldPassword, user.PasswordHash))
+ {
+ throw new BusinessException("旧密码不正确");
+ }
+
+ // 新密码不能与旧密码相同
+ if (req.OldPassword == req.NewPassword)
+ {
+ throw new BusinessException("新密码不能与旧密码相同");
+ }
+
+ // 更新密码
+ user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.NewPassword);
+ await db.SaveChangesAsync(ct);
+
+ await Send.OkAsync(ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/ChangePasswordRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/ChangePasswordRequest.cs
new file mode 100644
index 0000000..d8fa9e0
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/ChangePasswordRequest.cs
@@ -0,0 +1,11 @@
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+/// 修改密码请求。包含旧密码(用于二次校验)与新密码。
+public class ChangePasswordRequest
+{
+ /// 旧密码(登录态下二次校验,防止会话被劫持后改密)。
+ public string OldPassword { get; set; } = string.Empty;
+
+ /// 新密码(明文传入,服务端 BCrypt 哈希后存储)。
+ public string NewPassword { get; set; } = string.Empty;
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/GetAccessCodesEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/GetAccessCodesEndpoint.cs
new file mode 100644
index 0000000..41a0b20
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/GetAccessCodesEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Auth.Queries;
+using BuildFlow.Shared.Domain.Common;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 获取当前用户的权限码列表。前端据此渲染按钮/菜单的可见性(细粒度前端权限控制)。
+/// 鉴权通过 GetRequiredUserId 强制——未登录直接抛 401。
+///
+public class GetAccessCodesEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/auth/codes");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserId();
+ var codes = await mediator.Send(new GetAccessCodesQuery(userId), ct);
+ await Send.OkAsync(codes, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/GetCurrentUserEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/GetCurrentUserEndpoint.cs
new file mode 100644
index 0000000..3f6828b
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/GetCurrentUserEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Auth.Queries;
+using BuildFlow.Shared.Domain.Common;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 获取当前登录用户信息端点。前端登录后调用以填充用户菜单、权限、头像。
+/// GetRequiredUserId 在未登录时抛 UnauthorizedException,等同强制鉴权(无需显式 Permissions)。
+///
+public class GetCurrentUserEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Get("/user/info");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserId();
+ var result = await mediator.Send(new GetCurrentUserQuery(userId), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/LoginEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/LoginEndpoint.cs
new file mode 100644
index 0000000..96dc332
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/LoginEndpoint.cs
@@ -0,0 +1,37 @@
+using BuildFlow.Platform.Application.Auth.Commands;
+using BuildFlow.Platform.Application.Auth.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+
+///
+/// 登录端点。
+///
+public class LoginEndpoint(IMediator mediator, IConfiguration config) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/auth/login");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(LoginRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new LoginCommand(req.Username, req.Password), ct);
+
+ // refresh_token 写入 HttpOnly Cookie:浏览器自动随请求带上,前端 JS 不可读,防 XSS
+ HttpContext.Response.Cookies.Append("jwt", result.RefreshToken, new CookieOptions
+ {
+ HttpOnly = true,
+ Secure = config.GetValue("Cookie:Secure"),
+ SameSite = SameSiteMode.Lax,
+ Expires = DateTimeOffset.UtcNow.AddDays(7),
+ Path = "/"
+ });
+
+ await Send.OkAsync(result, ct);
+ }
+}
+
+/// 登录请求校验。规则与 LoginCommandValidator 一致,HTTP 入口层提前拦截非法输入。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/LoginRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/LoginRequestValidator.cs
new file mode 100644
index 0000000..45d0c22
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/LoginRequestValidator.cs
@@ -0,0 +1,21 @@
+using BuildFlow.Platform.Application.Auth.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 登录Request的请求校验器(FluentValidation)。
+///
+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/BuildFlow.Host/Endpoints/Platform/Auth/LogoutEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/LogoutEndpoint.cs
new file mode 100644
index 0000000..6a91578
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/LogoutEndpoint.cs
@@ -0,0 +1,33 @@
+using BuildFlow.Platform.Application.Auth.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 登出接口:撤销 refresh token 并清除 jwt Cookie。
+/// 前端通过 baseRequestClient 调用(withCredentials: true 发送 Cookie)。
+///
+public class LogoutEndpoint(IMediator mediator) : EndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Post("/auth/logout");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ // 从 Cookie 中读取 refresh token
+ var refreshToken = HttpContext.Request.Cookies["jwt"];
+
+ if (!string.IsNullOrEmpty(refreshToken))
+ {
+ await mediator.Send(new LogoutCommand(refreshToken), ct);
+ }
+
+ // 清除 Cookie
+ HttpContext.Response.Cookies.Delete("jwt");
+
+ await Send.OkAsync(string.Empty, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/RefreshTokenEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/RefreshTokenEndpoint.cs
new file mode 100644
index 0000000..f420b50
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/RefreshTokenEndpoint.cs
@@ -0,0 +1,41 @@
+using BuildFlow.Platform.Application.Auth.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 刷新令牌端点。从 HttpOnly Cookie 读 refresh_token(前端无法读取,只能由浏览器自动随请求带上),
+/// 换取新 access_token 以纯文本响应(被 ApiResponseMiddleware 列入 ExcludedPaths 不包裹)。
+/// ExcludeFromDescription 避免 Swagger 暴露此内部接口。
+///
+public class RefreshTokenEndpoint(IMediator mediator, IConfiguration config) : EndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Post("/auth/refresh");
+ AllowAnonymous();
+ Options(b => b.ExcludeFromDescription());
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var refreshToken = HttpContext.Request.Cookies["jwt"]
+ ?? throw new UnauthorizedException("缺少刷新令牌");
+
+ var result = await mediator.Send(new RefreshTokenCommand(refreshToken), ct);
+
+ // 新 refresh_token 写回 Cookie(Rotation:旧 token 已在 Handler 中作废)
+ HttpContext.Response.Cookies.Append("jwt", result.RefreshToken, new CookieOptions
+ {
+ HttpOnly = true,
+ Secure = config.GetValue("Cookie:Secure"),
+ SameSite = SameSiteMode.Lax,
+ Expires = DateTimeOffset.UtcNow.AddDays(7),
+ Path = "/"
+ });
+
+ // 用 FastEndpoints 的 Send.StringAsync 返回纯文本 access token。
+ // 之前用 HttpContext.Response.WriteAsync 被 FastEndpoints 管线覆盖成 204 空响应。
+ await Send.StringAsync(result.AccessToken, 200, "text/plain; charset=utf-8", ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/RegisterEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/RegisterEndpoint.cs
new file mode 100644
index 0000000..c19e6bc
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/RegisterEndpoint.cs
@@ -0,0 +1,26 @@
+using BuildFlow.Platform.Application.Auth.Commands;
+using BuildFlow.Platform.Application.Auth.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+
+///
+/// 注册端点。
+///
+public class RegisterEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/auth/register");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(RegisterRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new RegisterCommand(req.Username, req.Email, req.Password), ct);
+ await Send.OkAsync(true, ct);
+ }
+}
+
+/// 自助注册请求校验。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/RegisterRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/RegisterRequestValidator.cs
new file mode 100644
index 0000000..124cfd2
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/RegisterRequestValidator.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Auth.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 注册Request的请求校验器(FluentValidation)。
+///
+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/BuildFlow.Host/Endpoints/Platform/Auth/RevokeTokenEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/RevokeTokenEndpoint.cs
new file mode 100644
index 0000000..b3970cd
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/RevokeTokenEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Platform.Application.Auth.Commands;
+using BuildFlow.Platform.Application.Auth.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+
+///
+/// 吊销令牌端点。
+///
+public class RevokeTokenEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/auth/revoke");
+ }
+
+ public override async Task HandleAsync(RevokeTokenRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new RevokeTokenCommand(req.RefreshToken), ct);
+ await Send.OkAsync(true, ct);
+ }
+}
+
+/// 吊销刷新令牌请求校验。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/RevokeTokenRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/RevokeTokenRequestValidator.cs
new file mode 100644
index 0000000..5af69a8
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/RevokeTokenRequestValidator.cs
@@ -0,0 +1,15 @@
+using BuildFlow.Platform.Application.Auth.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 吊销令牌Request的请求校验器(FluentValidation)。
+///
+public class RevokeTokenRequestValidator : Validator
+{
+ public RevokeTokenRequestValidator()
+ {
+ RuleFor(x => x.RefreshToken)
+ .NotEmpty().WithMessage("RefreshToken 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/UpdateProfileEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/UpdateProfileEndpoint.cs
new file mode 100644
index 0000000..d9b50f4
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/UpdateProfileEndpoint.cs
@@ -0,0 +1,31 @@
+using BuildFlow.Platform.Application.Auth.Commands;
+using BuildFlow.Platform.Application.Auth.Queries;
+using BuildFlow.Shared.Domain.Common;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+///
+/// 更新当前用户个人资料(姓名、头像、简介)。
+/// PUT /api/user/profile
+///
+public class UpdateProfileEndpoint(IMediator mediator, ICurrentUserContext userContext)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/user/profile");
+ }
+
+ public override async Task HandleAsync(UpdateProfileRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserId();
+ var result = await mediator.Send(new UpdateProfileCommand(
+ UserId: userId,
+ RealName: req.RealName,
+ Avatar: req.Avatar,
+ Description: req.Description), ct);
+
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Auth/UpdateProfileRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Auth/UpdateProfileRequest.cs
new file mode 100644
index 0000000..0e3526d
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Auth/UpdateProfileRequest.cs
@@ -0,0 +1,14 @@
+namespace BuildFlow.Host.Endpoints.Platform.Auth;
+
+/// 更新个人资料请求。三个字段均可选,仅更新非 null 的字段。
+public class UpdateProfileRequest
+{
+ /// 真实姓名(可选)。
+ public string? RealName { get; set; }
+
+ /// 头像 URL(可选)。
+ public string? Avatar { get; set; }
+
+ /// 个人简介(可选)。
+ public string? Description { get; set; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/CreateMenuEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/CreateMenuEndpoint.cs
new file mode 100644
index 0000000..45500d4
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/CreateMenuEndpoint.cs
@@ -0,0 +1,42 @@
+using BuildFlow.Platform.Application.Menus.Commands;
+using BuildFlow.Platform.Application.Menus.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 创建菜单端点。
+///
+public class CreateMenuEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/menus");
+ Permissions("menu:create");
+ }
+
+ public override async Task HandleAsync(CreateMenuRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new CreateMenuCommand
+ {
+ ParentId = req.ParentId,
+ Name = req.Name,
+ Path = req.Path,
+ Component = req.Component,
+ Redirect = req.Redirect,
+ Title = req.Title,
+ Code = req.Code,
+ Icon = req.Icon,
+ ActiveIcon = req.ActiveIcon,
+ Order = req.Order,
+ HideInMenu = req.HideInMenu,
+ KeepAlive = req.KeepAlive,
+ AffixTab = req.AffixTab,
+ Link = req.Link,
+ NoBasicLayout = req.NoBasicLayout,
+ MenuVisibleWithForbidden = req.MenuVisibleWithForbidden,
+ Authority = req.Authority,
+ }, ct);
+ await Send.ResponseAsync(result, 201, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/CreateMenuRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/CreateMenuRequestValidator.cs
new file mode 100644
index 0000000..c50a0e5
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/CreateMenuRequestValidator.cs
@@ -0,0 +1,27 @@
+using BuildFlow.Platform.Application.Menus.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 创建菜单请求校验。
+///
+public class CreateMenuRequestValidator : Validator
+{
+ public CreateMenuRequestValidator()
+ {
+ RuleFor(x => x.Name).NotEmpty().WithMessage("菜单路由名称不能为空")
+ .MaximumLength(100).WithMessage("菜单路由名称长度不能超过 100 个字符");
+ RuleFor(x => x.Path).NotEmpty().WithMessage("路由路径不能为空")
+ .MaximumLength(200).WithMessage("路由路径长度不能超过 200 个字符");
+ RuleFor(x => x.Title).NotEmpty().WithMessage("菜单标题不能为空")
+ .MaximumLength(100).WithMessage("菜单标题长度不能超过 100 个字符");
+ When(x => x.Component is not null, () =>
+ {
+ RuleFor(x => x.Component!).MaximumLength(200).WithMessage("组件路径长度不能超过 200 个字符");
+ });
+ When(x => x.Authority is not null, () =>
+ {
+ RuleFor(x => x.Authority!).MaximumLength(200).WithMessage("Authority 长度不能超过 200 个字符");
+ });
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpoint.cs
new file mode 100644
index 0000000..3c9dc56
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpoint.cs
@@ -0,0 +1,22 @@
+using BuildFlow.Platform.Application.Menus.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 删除菜单端点。
+///
+public class DeleteMenuEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/menus/{Id}");
+ Permissions("menu:delete");
+ }
+
+ public override async Task HandleAsync(DeleteMenuEndpointRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new DeleteMenuCommand(req.Id), ct);
+ await Send.OkAsync(true, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpointRequest.cs
new file mode 100644
index 0000000..758c710
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 删除菜单请求体。携带路由 Id。
+///
+public record DeleteMenuEndpointRequest(Guid Id);
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpointRequestValidator.cs
new file mode 100644
index 0000000..296a62d
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/DeleteMenuEndpointRequestValidator.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 删除菜单请求校验。
+///
+public class DeleteMenuEndpointRequestValidator : Validator
+{
+ public DeleteMenuEndpointRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("菜单 ID 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/GetAllMenusEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetAllMenusEndpoint.cs
new file mode 100644
index 0000000..7cba30b
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetAllMenusEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Menus.DTOs;
+using BuildFlow.Platform.Application.Menus.Queries;
+using BuildFlow.Shared.Domain.Common;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// GetAll菜单s端点。
+///
+public class GetAllMenusEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/menu/all");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var userId = userContext.GetRequiredUserId();
+ var menus = await mediator.Send(new GetAllMenusQuery(userId), ct);
+ await Send.OkAsync(menus, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdEndpoint.cs
new file mode 100644
index 0000000..90a03de
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdEndpoint.cs
@@ -0,0 +1,23 @@
+using BuildFlow.Platform.Application.Menus.DTOs;
+using BuildFlow.Platform.Application.Menus.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 按主键查询单个菜单端点。
+///
+public class GetMenuByIdEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/menus/{Id}");
+ Permissions("menu:read");
+ }
+
+ public override async Task HandleAsync(GetMenuByIdRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetMenuByIdQuery(req.Id), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdRequest.cs
new file mode 100644
index 0000000..0bb4039
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 按主键查询菜单请求体。携带路由 Id。
+///
+public record GetMenuByIdRequest(Guid Id);
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdRequestValidator.cs
new file mode 100644
index 0000000..dc8dfaf
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuByIdRequestValidator.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 按主键查询菜单请求校验。
+///
+public class GetMenuByIdRequestValidator : Validator
+{
+ public GetMenuByIdRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("菜单 ID 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuTreeEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuTreeEndpoint.cs
new file mode 100644
index 0000000..b708816
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/GetMenuTreeEndpoint.cs
@@ -0,0 +1,23 @@
+using BuildFlow.Platform.Application.Menus.DTOs;
+using BuildFlow.Platform.Application.Menus.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 菜单管理树端点。返回全部菜单,不做角色过滤,供管理端展示。
+///
+public class GetMenuTreeEndpoint(IMediator mediator) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/menu/tree");
+ Permissions("menu:read");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var tree = await mediator.Send(new GetMenuTreeQuery(), ct);
+ await Send.OkAsync(tree, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpoint.cs
new file mode 100644
index 0000000..ffcb3b0
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpoint.cs
@@ -0,0 +1,27 @@
+using BuildFlow.Platform.Application.Menus.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 批量排序菜单端点。供菜单管理页拖拽排序(同级调整 + 跨层级移动)落库使用。
+/// 路由 PUT /menus/sort,需 menu:update 权限,事务内原子更新多个菜单的 ParentId 与 Order。
+///
+public class SortMenusEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/menus/sort");
+ Permissions("menu:update");
+ }
+
+ public override async Task HandleAsync(SortMenusEndpointRequest req, CancellationToken ct)
+ {
+ var items = req.Items
+ .Select(i => new MenuSortItem(i.Id, i.ParentId, i.Order))
+ .ToList();
+
+ var affected = await mediator.Send(new SortMenusCommand(items), ct);
+ await Send.OkAsync(affected, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpointRequest.cs
new file mode 100644
index 0000000..0fe475d
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpointRequest.cs
@@ -0,0 +1,14 @@
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 批量排序菜单请求体。
+///
+public record SortMenusEndpointRequest(List Items);
+
+///
+/// 排序单项 DTO。与后端 MenuSortItem 对应。
+///
+/// 被排序菜单主键。
+/// 目标父菜单 Id,null 表示顶级菜单。
+/// 同级内序号(从 0 开始)。
+public record MenuSortItemDto(Guid Id, Guid? ParentId, int Order);
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpointRequestValidator.cs
new file mode 100644
index 0000000..0a9ad96
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/SortMenusEndpointRequestValidator.cs
@@ -0,0 +1,16 @@
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 批量排序菜单请求校验。
+///
+public class SortMenusEndpointRequestValidator : Validator
+{
+ public SortMenusEndpointRequestValidator()
+ {
+ RuleFor(x => x.Items).NotEmpty().WithMessage("排序项不能为空");
+ RuleForEach(x => x.Items).ChildRules(item =>
+ {
+ item.RuleFor(i => i.Id).NotEmpty().WithMessage("菜单 ID 不能为空");
+ });
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpoint.cs
new file mode 100644
index 0000000..c6702da
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpoint.cs
@@ -0,0 +1,43 @@
+using BuildFlow.Platform.Application.Menus.Commands;
+using BuildFlow.Platform.Application.Menus.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 更新菜单端点。
+///
+public class UpdateMenuEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/menus/{Id}");
+ Permissions("menu:update");
+ }
+
+ public override async Task HandleAsync(UpdateMenuEndpointRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new UpdateMenuCommand
+ {
+ Id = req.Id,
+ ParentId = req.ParentId,
+ Name = req.Name,
+ Path = req.Path,
+ Component = req.Component,
+ Redirect = req.Redirect,
+ Title = req.Title,
+ Code = req.Code,
+ Icon = req.Icon,
+ ActiveIcon = req.ActiveIcon,
+ Order = req.Order,
+ HideInMenu = req.HideInMenu,
+ KeepAlive = req.KeepAlive,
+ AffixTab = req.AffixTab,
+ Link = req.Link,
+ NoBasicLayout = req.NoBasicLayout,
+ MenuVisibleWithForbidden = req.MenuVisibleWithForbidden,
+ Authority = req.Authority,
+ }, ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpointRequest.cs
new file mode 100644
index 0000000..d53bbf4
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpointRequest.cs
@@ -0,0 +1,24 @@
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 更新菜单请求体。携带路由 Id。
+///
+public record UpdateMenuEndpointRequest(
+ Guid Id,
+ Guid? ParentId,
+ string? Name,
+ string? Path,
+ string? Component,
+ string? Redirect,
+ string? Title,
+ string? Code,
+ string? Icon,
+ string? ActiveIcon,
+ int? Order,
+ bool? HideInMenu,
+ bool? KeepAlive,
+ bool? AffixTab,
+ string? Link,
+ bool? NoBasicLayout,
+ bool? MenuVisibleWithForbidden,
+ string? Authority);
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpointRequestValidator.cs
new file mode 100644
index 0000000..ed6a750
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Menus/UpdateMenuEndpointRequestValidator.cs
@@ -0,0 +1,20 @@
+namespace BuildFlow.Host.Endpoints.Platform.Menus;
+
+///
+/// 更新菜单请求校验。
+///
+public class UpdateMenuEndpointRequestValidator : Validator
+{
+ public UpdateMenuEndpointRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("菜单 ID 不能为空");
+ When(x => x.Name is not null, () =>
+ {
+ RuleFor(x => x.Name!).MaximumLength(100).WithMessage("菜单路由名称长度不能超过 100 个字符");
+ });
+ When(x => x.Path is not null, () =>
+ {
+ RuleFor(x => x.Path!).MaximumLength(200).WithMessage("路由路径长度不能超过 200 个字符");
+ });
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsEndpoint.cs
new file mode 100644
index 0000000..3e94b49
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Notifications.DTOs;
+using BuildFlow.Platform.Application.Notifications.Queries;
+using BuildFlow.Shared.Domain.Common;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+/// 获取当前用户的通知列表(分页,可只看未读)。
+public class GetNotificationsEndpoint(IMediator mediator, ICurrentUserContext userContext)
+ : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/notifications");
+ }
+
+ public override async Task HandleAsync(GetNotificationsRequest req, CancellationToken ct)
+ {
+ var userId = userContext.GetRecipientId();
+ var result = await mediator.Send(
+ new GetNotificationsQuery(userId, req.UnreadOnly, req.PageIndex, req.PageSize), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsRequest.cs
new file mode 100644
index 0000000..cf5bf58
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsRequest.cs
@@ -0,0 +1,13 @@
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+///
+/// Get通知s请求体。
+///
+public record GetNotificationsRequest
+{
+ public bool UnreadOnly { get; init; }
+ public int PageIndex { get; init; } = 1;
+ public int PageSize { get; init; } = 20;
+}
+
+/// 查询通知列表请求校验(分页参数取值范围)。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsRequestValidator.cs
new file mode 100644
index 0000000..f5c590a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetNotificationsRequestValidator.cs
@@ -0,0 +1,16 @@
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+///
+/// Get通知sRequest的请求校验器(FluentValidation)。
+///
+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");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetUnreadNotificationCountEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetUnreadNotificationCountEndpoint.cs
new file mode 100644
index 0000000..c212c91
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/GetUnreadNotificationCountEndpoint.cs
@@ -0,0 +1,22 @@
+using BuildFlow.Platform.Application.Notifications.Queries;
+using BuildFlow.Shared.Domain.Common;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+/// 获取当前用户未读通知数(前端铃铛角标)。
+public class GetUnreadNotificationCountEndpoint(IMediator mediator, ICurrentUserContext userContext)
+ : EndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Get("/notifications/unread-count");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var userId = userContext.GetRecipientId();
+ var count = await mediator.Send(new GetUnreadNotificationCountQuery(userId), ct);
+ await Send.OkAsync(count, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/MarkAllNotificationsReadEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/MarkAllNotificationsReadEndpoint.cs
new file mode 100644
index 0000000..2f028e6
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/MarkAllNotificationsReadEndpoint.cs
@@ -0,0 +1,22 @@
+using BuildFlow.Platform.Application.Notifications.Commands;
+using BuildFlow.Shared.Domain.Common;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+/// 标记当前用户所有通知为已读。
+public class MarkAllNotificationsReadEndpoint(IMediator mediator, ICurrentUserContext userContext)
+ : EndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Post("/notifications/read-all");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var userId = userContext.GetRecipientId();
+ var count = await mediator.Send(new MarkAllNotificationsReadCommand(userId), ct);
+ await Send.OkAsync(count, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/MarkNotificationReadEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/MarkNotificationReadEndpoint.cs
new file mode 100644
index 0000000..197bc9e
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/MarkNotificationReadEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Platform.Application.Notifications.Commands;
+using BuildFlow.Shared.Domain.Common;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+/// 标记单条通知为已读。
+public class MarkNotificationReadEndpoint(IMediator mediator, ICurrentUserContext userContext)
+ : EndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Post("/notifications/{Id}/read");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var id = Route("Id");
+ var userId = userContext.GetRecipientId();
+ var ok = await mediator.Send(new MarkNotificationReadCommand(id, userId), ct);
+ if (!ok)
+ throw new NotFoundException("通知不存在");
+ await Send.OkAsync(ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/NotificationEndpointsExtensions.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/NotificationEndpointsExtensions.cs
new file mode 100644
index 0000000..68e13e7
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/NotificationEndpointsExtensions.cs
@@ -0,0 +1,15 @@
+using BuildFlow.Shared.Domain.Common;
+
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+///
+/// 通知EndpointsExtensions扩展方法。
+///
+internal static class NotificationEndpointsExtensions
+{
+ public static Guid GetRecipientId(this ICurrentUserContext ctx)
+ {
+ var id = ctx.GetRequiredUserId();
+ return Guid.Parse(id);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationEndpoint.cs
new file mode 100644
index 0000000..108d65f
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationEndpoint.cs
@@ -0,0 +1,23 @@
+using BuildFlow.Platform.Application.Notifications.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+/// 发布通知(Fanout 扇出,业务系统只需推一条消息)。
+public class PublishNotificationEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/notifications/publish");
+ Permissions("notification:publish");
+ }
+
+ public override async Task HandleAsync(PublishNotificationRequest req, CancellationToken ct)
+ {
+ var count = await mediator.Send(new PublishNotificationCommand(
+ req.Type, req.Title, req.Content, req.Source,
+ req.RelatedId, req.RelatedType,
+ req.RecipientUserIds, req.RecipientRoles), ct);
+ await Send.OkAsync(count, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationRequest.cs
new file mode 100644
index 0000000..c822687
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationRequest.cs
@@ -0,0 +1,24 @@
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+///
+/// Publish通知请求体。
+///
+public record PublishNotificationRequest
+{
+ public string Type { get; init; } = "system";
+ public string Title { get; init; } = default!;
+ public string Content { get; init; } = default!;
+ public string Source { get; init; } = "system";
+ public string? RelatedId { get; init; }
+ public string? RelatedType { get; init; }
+ /// 精确接收人用户 ID 列表(与 RecipientRoles 二选一或并存)。
+ public List? RecipientUserIds { get; init; }
+ /// 接收角色名列表(中心按角色展开为用户)。
+ public List? RecipientRoles { get; init; }
+}
+
+///
+/// 发布通知请求校验。
+/// 接收人范围(RecipientUserIds / RecipientRoles)可同时为空——表示全量广播,
+/// 具体扇出语义由 PublishNotificationCommand 决定,此处仅校验必填字段与长度。
+///
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationRequestValidator.cs
new file mode 100644
index 0000000..35a4cba
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Notifications/PublishNotificationRequestValidator.cs
@@ -0,0 +1,24 @@
+namespace BuildFlow.Host.Endpoints.Platform.Notifications;
+
+///
+/// Publish通知Request的请求校验器(FluentValidation)。
+///
+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 个字符");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/CreatePermissionEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/CreatePermissionEndpoint.cs
new file mode 100644
index 0000000..7c97835
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/CreatePermissionEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Permissions.Commands;
+using BuildFlow.Platform.Application.Permissions.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 创建权限端点。
+///
+public class CreatePermissionEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/permissions");
+ Permissions("permission:create");
+ }
+
+ public override async Task HandleAsync(CreatePermissionRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new CreatePermissionCommand(req.Name, req.Code, req.Description, req.Group), ct);
+ await Send.ResponseAsync(result, 201, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/CreatePermissionRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/CreatePermissionRequestValidator.cs
new file mode 100644
index 0000000..8375cfe
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/CreatePermissionRequestValidator.cs
@@ -0,0 +1,28 @@
+using BuildFlow.Platform.Application.Permissions.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 创建权限请求校验。
+///
+public class CreatePermissionRequestValidator : Validator
+{
+ public CreatePermissionRequestValidator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("权限名称不能为空")
+ .Length(2, 50).WithMessage("权限名称长度 2-50 个字符");
+ RuleFor(x => x.Code)
+ .NotEmpty().WithMessage("权限编码不能为空")
+ .Matches(@"^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)+$")
+ .WithMessage("权限编码格式应为 resource:action,且仅小写字母、数字、连字符");
+ When(x => x.Description is not null, () =>
+ {
+ RuleFor(x => x.Description!).MaximumLength(500).WithMessage("权限描述长度不能超过 500 个字符");
+ });
+ When(x => x.Group is not null, () =>
+ {
+ RuleFor(x => x.Group!).MaximumLength(50).WithMessage("权限分组长度不能超过 50 个字符");
+ });
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpoint.cs
new file mode 100644
index 0000000..86344e5
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpoint.cs
@@ -0,0 +1,22 @@
+using BuildFlow.Platform.Application.Permissions.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 删除权限端点。
+///
+public class DeletePermissionEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/permissions/{Id}");
+ Permissions("permission:delete");
+ }
+
+ public override async Task HandleAsync(DeletePermissionEndpointRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new DeletePermissionCommand(req.Id), ct);
+ await Send.OkAsync(true, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpointRequest.cs
new file mode 100644
index 0000000..241131e
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 删除权限请求体。携带路由 Id。
+///
+public record DeletePermissionEndpointRequest(Guid Id);
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpointRequestValidator.cs
new file mode 100644
index 0000000..98b00c7
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/DeletePermissionEndpointRequestValidator.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 删除权限请求校验。
+///
+public class DeletePermissionEndpointRequestValidator : Validator
+{
+ public DeletePermissionEndpointRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("权限 ID 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdEndpoint.cs
new file mode 100644
index 0000000..3c37bf7
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdEndpoint.cs
@@ -0,0 +1,23 @@
+using BuildFlow.Platform.Application.Permissions.DTOs;
+using BuildFlow.Platform.Application.Permissions.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 按主键查询单个权限端点。
+///
+public class GetPermissionByIdEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/permissions/{Id}");
+ Permissions("permission:read");
+ }
+
+ public override async Task HandleAsync(GetPermissionByIdRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetPermissionByIdQuery(req.Id), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdRequest.cs
new file mode 100644
index 0000000..ca645e7
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 按主键查询权限请求体。携带路由 Id。
+///
+public record GetPermissionByIdRequest(Guid Id);
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdRequestValidator.cs
new file mode 100644
index 0000000..35f8ac2
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionByIdRequestValidator.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 按主键查询权限请求校验。
+///
+public class GetPermissionByIdRequestValidator : Validator
+{
+ public GetPermissionByIdRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("权限 ID 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionListEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionListEndpoint.cs
new file mode 100644
index 0000000..559198c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionListEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Permissions.DTOs;
+using BuildFlow.Platform.Application.Permissions.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 获取全部权限列表(供角色分配权限页选择)。
+/// 需 permission:read 权限——权限码本身是敏感信息,仅授权用户可见。
+///
+public class GetPermissionListEndpoint(IMediator mediator) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/permissions");
+ Permissions("permission:read");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetPermissionListQuery(), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionTreeEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionTreeEndpoint.cs
new file mode 100644
index 0000000..26aab35
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/GetPermissionTreeEndpoint.cs
@@ -0,0 +1,25 @@
+using BuildFlow.Platform.Application.Permissions.DTOs;
+using BuildFlow.Platform.Application.Permissions.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 获取权限分组树。按 Group 聚合,供管理端树形表格展示。
+/// 需 permission:read 权限——权限码本身是敏感信息,仅授权用户可见。
+///
+public class GetPermissionTreeEndpoint(IMediator mediator)
+ : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/permission/tree");
+ Permissions("permission:read");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var tree = await mediator.Send(new GetPermissionTreeQuery(), ct);
+ await Send.OkAsync(tree, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpoint.cs
new file mode 100644
index 0000000..fd209e6
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Permissions.Commands;
+using BuildFlow.Platform.Application.Permissions.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 更新权限端点。编码不可改。
+///
+public class UpdatePermissionEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/permissions/{Id}");
+ Permissions("permission:update");
+ }
+
+ public override async Task HandleAsync(UpdatePermissionEndpointRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new UpdatePermissionCommand(req.Id, req.Name, req.Description, req.Group), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpointRequest.cs
new file mode 100644
index 0000000..df8d0b2
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpointRequest.cs
@@ -0,0 +1,6 @@
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 更新权限请求体。携带路由 Id。
+///
+public record UpdatePermissionEndpointRequest(Guid Id, string? Name, string? Description, string? Group);
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpointRequestValidator.cs
new file mode 100644
index 0000000..9c64255
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Permissions/UpdatePermissionEndpointRequestValidator.cs
@@ -0,0 +1,24 @@
+namespace BuildFlow.Host.Endpoints.Platform.Permissions;
+
+///
+/// 更新权限请求校验。
+///
+public class UpdatePermissionEndpointRequestValidator : Validator
+{
+ public UpdatePermissionEndpointRequestValidator()
+ {
+ 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 个字符");
+ });
+ When(x => x.Group is not null, () =>
+ {
+ RuleFor(x => x.Group!).MaximumLength(50).WithMessage("权限分组长度不能超过 50 个字符");
+ });
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpoint.cs
new file mode 100644
index 0000000..71437b8
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Roles.Commands;
+using BuildFlow.Platform.Application.Roles.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+
+///
+/// Assign权限s端点。
+///
+public class AssignPermissionsEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/roles/{RoleId}/permissions");
+ Permissions("role:assign-permission");
+ }
+
+ public override async Task HandleAsync(AssignPermissionsEndpointRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new AssignPermissionsCommand(req.RoleId, req.PermissionIds), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpointRequest.cs
new file mode 100644
index 0000000..8d6fb10
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpointRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Assign权限sEndpoint请求体。
+///
+public record AssignPermissionsEndpointRequest(Guid RoleId, List PermissionIds);
+
+/// 给角色分配权限请求校验。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpointRequestValidator.cs
new file mode 100644
index 0000000..d0364e4
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/AssignPermissionsEndpointRequestValidator.cs
@@ -0,0 +1,14 @@
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Assign权限sEndpointRequest的请求校验器(FluentValidation)。
+///
+public class AssignPermissionsEndpointRequestValidator : Validator
+{
+ public AssignPermissionsEndpointRequestValidator()
+ {
+ RuleFor(x => x.RoleId).NotEmpty().WithMessage("角色 ID 不能为空");
+ RuleFor(x => x.PermissionIds)
+ .NotEmpty().WithMessage("权限列表不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/CreateRoleEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/CreateRoleEndpoint.cs
new file mode 100644
index 0000000..0ded8a9
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/CreateRoleEndpoint.cs
@@ -0,0 +1,26 @@
+using BuildFlow.Platform.Application.Roles.Commands;
+using BuildFlow.Platform.Application.Roles.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+
+///
+/// Create角色端点。
+///
+public class CreateRoleEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/roles");
+ Permissions("role:create");
+ }
+
+ public override async Task HandleAsync(CreateRoleRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new CreateRoleCommand(req.Name, req.Description), ct);
+ await Send.ResponseAsync(result, 201, ct);
+ }
+}
+
+/// 创建角色请求校验。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/CreateRoleRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/CreateRoleRequestValidator.cs
new file mode 100644
index 0000000..0b266a0
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/CreateRoleRequestValidator.cs
@@ -0,0 +1,22 @@
+using BuildFlow.Platform.Application.Roles.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Create角色Request的请求校验器(FluentValidation)。
+///
+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/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpoint.cs
new file mode 100644
index 0000000..b2f4fb9
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpoint.cs
@@ -0,0 +1,23 @@
+using BuildFlow.Platform.Application.Roles.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+
+///
+/// Delete角色端点。
+///
+public class DeleteRoleEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/roles/{Id}");
+ Permissions("role:delete");
+ }
+
+ public override async Task HandleAsync(DeleteRoleEndpointRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new DeleteRoleCommand(req.Id), ct);
+ await Send.OkAsync(true, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpointRequest.cs
new file mode 100644
index 0000000..446f8ec
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpointRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Delete角色Endpoint请求体。
+///
+public record DeleteRoleEndpointRequest(Guid Id);
+
+/// 删除角色请求校验(Id 由路由提供)。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpointRequestValidator.cs
new file mode 100644
index 0000000..81a30d2
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/DeleteRoleEndpointRequestValidator.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Delete角色EndpointRequest的请求校验器(FluentValidation)。
+///
+public class DeleteRoleEndpointRequestValidator : Validator
+{
+ public DeleteRoleEndpointRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("角色 ID 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdEndpoint.cs
new file mode 100644
index 0000000..a310074
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Roles.DTOs;
+using BuildFlow.Platform.Application.Roles.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+
+///
+/// Get角色ById端点。
+///
+public class GetRoleByIdEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/roles/{Id}");
+ Permissions("role:read");
+ }
+
+ public override async Task HandleAsync(GetRoleByIdRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetRoleByIdQuery(req.Id), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdRequest.cs
new file mode 100644
index 0000000..8bdbd17
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Get角色ById请求体。
+///
+public record GetRoleByIdRequest(Guid Id);
+
+/// 根据角色 ID 查询请求校验(Id 由路由提供)。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdRequestValidator.cs
new file mode 100644
index 0000000..86ecace
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleByIdRequestValidator.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Get角色ByIdRequest的请求校验器(FluentValidation)。
+///
+public class GetRoleByIdRequestValidator : Validator
+{
+ public GetRoleByIdRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("角色 ID 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleListEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleListEndpoint.cs
new file mode 100644
index 0000000..a495061
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/GetRoleListEndpoint.cs
@@ -0,0 +1,23 @@
+using BuildFlow.Platform.Application.Roles.DTOs;
+using BuildFlow.Platform.Application.Roles.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Get角色List端点。
+///
+public class GetRoleListEndpoint(IMediator mediator) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/roles");
+ Permissions("role:read");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetRoleListQuery(), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpoint.cs
new file mode 100644
index 0000000..05dfac7
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Roles.Commands;
+using BuildFlow.Platform.Application.Roles.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+
+///
+/// Update角色端点。
+///
+public class UpdateRoleEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/roles/{Id}");
+ Permissions("role:update");
+ }
+
+ public override async Task HandleAsync(UpdateRoleEndpointRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new UpdateRoleCommand(req.Id, req.Name, req.Description), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpointRequest.cs
new file mode 100644
index 0000000..947a922
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpointRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Update角色Endpoint请求体。
+///
+public record UpdateRoleEndpointRequest(Guid Id, string? Name, string? Description);
+
+/// 更新角色请求校验。Name 为可选更新字段,仅当提供时校验长度。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpointRequestValidator.cs
new file mode 100644
index 0000000..a413276
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Roles/UpdateRoleEndpointRequestValidator.cs
@@ -0,0 +1,24 @@
+namespace BuildFlow.Host.Endpoints.Platform.Roles;
+
+///
+/// Update角色EndpointRequest的请求校验器(FluentValidation)。
+///
+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/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpoint.cs
new file mode 100644
index 0000000..0f7c407
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Users.Commands;
+using BuildFlow.Platform.Application.Users.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+
+///
+/// Assign角色s端点。
+///
+public class AssignRolesEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/users/{UserId}/roles");
+ Permissions("user:assign-role");
+ }
+
+ public override async Task HandleAsync(AssignRolesEndpointRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new AssignRolesCommand(req.UserId, req.RoleIds), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpointRequest.cs
new file mode 100644
index 0000000..decb6b1
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpointRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Assign角色sEndpoint请求体。
+///
+public record AssignRolesEndpointRequest(Guid UserId, List RoleIds);
+
+/// 给用户分配角色请求校验。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpointRequestValidator.cs
new file mode 100644
index 0000000..bf1f367
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/AssignRolesEndpointRequestValidator.cs
@@ -0,0 +1,14 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Assign角色sEndpointRequest的请求校验器(FluentValidation)。
+///
+public class AssignRolesEndpointRequestValidator : Validator
+{
+ public AssignRolesEndpointRequestValidator()
+ {
+ RuleFor(x => x.UserId).NotEmpty().WithMessage("用户 ID 不能为空");
+ RuleFor(x => x.RoleIds)
+ .NotEmpty().WithMessage("角色列表不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/CreateUserEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/CreateUserEndpoint.cs
new file mode 100644
index 0000000..02ea7cd
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/CreateUserEndpoint.cs
@@ -0,0 +1,26 @@
+using BuildFlow.Platform.Application.Users.Commands;
+using BuildFlow.Platform.Application.Users.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+
+///
+/// Create用户端点。
+///
+public class CreateUserEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/users");
+ Permissions("user:create");
+ }
+
+ public override async Task HandleAsync(CreateUserRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new CreateUserCommand(req.Username, req.Email, req.Password), ct);
+ await Send.ResponseAsync(result, 201, ct);
+ }
+}
+
+/// 管理员创建用户请求校验。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/CreateUserRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/CreateUserRequestValidator.cs
new file mode 100644
index 0000000..826b1b0
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/CreateUserRequestValidator.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Users.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Create用户Request的请求校验器(FluentValidation)。
+///
+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/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpoint.cs
new file mode 100644
index 0000000..83daebd
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpoint.cs
@@ -0,0 +1,23 @@
+using BuildFlow.Platform.Application.Users.Commands;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+
+///
+/// Delete用户端点。
+///
+public class DeleteUserEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/users/{Id}");
+ Permissions("user:delete");
+ }
+
+ public override async Task HandleAsync(DeleteUserEndpointRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new DeleteUserCommand(req.Id), ct);
+ await Send.OkAsync(true, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpointRequest.cs
new file mode 100644
index 0000000..85c8c4e
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpointRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Delete用户Endpoint请求体。
+///
+public record DeleteUserEndpointRequest(Guid Id);
+
+/// 删除用户请求校验(Id 由路由提供)。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpointRequestValidator.cs
new file mode 100644
index 0000000..76c53fb
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/DeleteUserEndpointRequestValidator.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Delete用户EndpointRequest的请求校验器(FluentValidation)。
+///
+public class DeleteUserEndpointRequestValidator : Validator
+{
+ public DeleteUserEndpointRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("用户 ID 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdEndpoint.cs
new file mode 100644
index 0000000..25c2192
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Users.DTOs;
+using BuildFlow.Platform.Application.Users.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+
+///
+/// Get用户ById端点。
+///
+public class GetUserByIdEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/users/{Id}");
+ Permissions("user:read");
+ }
+
+ public override async Task HandleAsync(GetUserByIdRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetUserByIdQuery(req.Id), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdRequest.cs
new file mode 100644
index 0000000..82c8ec6
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Get用户ById请求体。
+///
+public record GetUserByIdRequest(Guid Id);
+
+/// 根据用户 ID 查询请求校验(Id 由路由提供)。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdRequestValidator.cs
new file mode 100644
index 0000000..63fa4d1
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserByIdRequestValidator.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Get用户ByIdRequest的请求校验器(FluentValidation)。
+///
+public class GetUserByIdRequestValidator : Validator
+{
+ public GetUserByIdRequestValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("用户 ID 不能为空");
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserListEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserListEndpoint.cs
new file mode 100644
index 0000000..709db40
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserListEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Users.DTOs;
+using BuildFlow.Platform.Application.Users.Queries;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+
+///
+/// Get用户List端点。
+///
+public class GetUserListEndpoint(IMediator mediator) : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/users");
+ Permissions("user:read");
+ }
+
+ public override async Task HandleAsync(GetUserListRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetUserListQuery(req.Search), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserListRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserListRequest.cs
new file mode 100644
index 0000000..c79fdf9
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/GetUserListRequest.cs
@@ -0,0 +1,9 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Get用户List请求体。
+///
+public record GetUserListRequest
+{
+ public string? Search { get; init; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpoint.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpoint.cs
new file mode 100644
index 0000000..019b205
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpoint.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Platform.Application.Users.Commands;
+using BuildFlow.Platform.Application.Users.DTOs;
+using MediatR;
+
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+
+///
+/// Update用户端点。
+///
+public class UpdateUserEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/users/{Id}");
+ Permissions("user:update");
+ }
+
+ public override async Task HandleAsync(UpdateUserEndpointRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new UpdateUserCommand(req.Id, req.Email, req.Password, req.IsActive), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpointRequest.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpointRequest.cs
new file mode 100644
index 0000000..203ec94
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpointRequest.cs
@@ -0,0 +1,8 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Update用户Endpoint请求体。
+///
+public record UpdateUserEndpointRequest(Guid Id, string? Email, string? Password, bool? IsActive);
+
+/// 更新用户请求校验。Id 由路由提供;Email/Password 为可选更新字段,仅当提供时校验格式。
diff --git a/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpointRequestValidator.cs b/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpointRequestValidator.cs
new file mode 100644
index 0000000..56d0be1
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Platform/Users/UpdateUserEndpointRequestValidator.cs
@@ -0,0 +1,24 @@
+namespace BuildFlow.Host.Endpoints.Platform.Users;
+
+///
+/// Update用户EndpointRequest的请求校验器(FluentValidation)。
+///
+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/BuildFlow.Host/Endpoints/Rag/AiModel/AiModelEndpoints.cs b/src/BuildFlow.Host/Endpoints/Rag/AiModel/AiModelEndpoints.cs
new file mode 100644
index 0000000..8a06845
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Rag/AiModel/AiModelEndpoints.cs
@@ -0,0 +1,96 @@
+using FastEndpoints;
+using MediatR;
+using BuildFlow.Application.AiModel.Commands;
+using BuildFlow.Application.AiModel.DTOs;
+using BuildFlow.Application.AiModel.Queries;
+
+namespace BuildFlow.Host.Endpoints.Rag.AiModel;
+
+/// 查询模型列表。可选 modelType 筛选(0=Chat, 1=Embedding)。
+public class GetAiModelsEndpoint(IMediator mediator) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/ai/models");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ // Query 第三参数 isRequired=false,让 modelType 成为可选参数
+ var modelType = Query("modelType", false);
+ var result = await mediator.Send(new GetAiModelsQuery(modelType), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
+
+/// 新建模型。
+public class CreateAiModelEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/ai/models");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CreateAiModelRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new CreateAiModelCommand(
+ req.Name, req.Provider, req.ModelId, req.BaseUrl, req.ApiKey,
+ req.ModelType, req.IsDefault, req.Temperature, req.MaxTokens, req.DefaultInstructions), ct);
+ await Send.ResponseAsync(result, 201, ct);
+ }
+}
+
+/// 更新模型。
+public class UpdateAiModelEndpoint(IMediator mediator) : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/ai/models/{Id}");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateAiModelRequest req, CancellationToken ct)
+ {
+ var id = Route("Id");
+ var result = await mediator.Send(new UpdateAiModelCommand(
+ id, req.Name, req.Provider, req.ModelId, req.BaseUrl, req.ApiKey,
+ req.ModelType, req.IsActive, req.Temperature, req.MaxTokens, req.DefaultInstructions), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
+
+/// 删除模型。
+public class DeleteAiModelEndpoint(IMediator mediator) : EndpointWithoutRequest