diff --git a/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptEndpoint.cs new file mode 100644 index 0000000..ff83987 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptEndpoint.cs @@ -0,0 +1,32 @@ +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 创建脚本端点。POST /scripts。需 scripts.create 权限。 +public class CreateScriptEndpoint(IMediator mediator) + : Endpoint +{ + public override void Configure() + { + Post("/scripts"); + Permissions("scripts.create"); + } + + public override async Task HandleAsync(CreateScriptRequest req, CancellationToken ct) + { + var result = await mediator.Send( + new CreateScriptCommand( + req.Name, + req.DisplayName, + req.Description, + req.Code, + req.InputSchema, + req.OutputSchema, + req.TimeoutMs, + req.FormDefinitionId), + ct); + await Send.ResponseAsync(result, 201, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptRequest.cs new file mode 100644 index 0000000..f24d881 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptRequest.cs @@ -0,0 +1,12 @@ +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 创建脚本请求。仅承载 C# 脚本字段。 +public sealed record CreateScriptRequest( + string Name, + string DisplayName, + string? Description, + string Code, + string? InputSchema, + string? OutputSchema, + int TimeoutMs, + Guid? FormDefinitionId = null); diff --git a/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptEndpoint.cs new file mode 100644 index 0000000..7eb024c --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptEndpoint.cs @@ -0,0 +1,21 @@ +using MediatR; +using Scripting.Application.Commands; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 删除脚本端点(软删除)。DELETE /scripts/{id}。需 scripts.delete 权限。 +public class DeleteScriptEndpoint(IMediator mediator) + : Endpoint +{ + public override void Configure() + { + Delete("/scripts/{id}"); + Permissions("scripts.delete"); + } + + public override async Task HandleAsync(DeleteScriptRequest req, CancellationToken ct) + { + await mediator.Send(new DeleteScriptCommand(req.Id), ct); + await Send.OkAsync(true, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptRequest.cs new file mode 100644 index 0000000..73cbde0 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptRequest.cs @@ -0,0 +1,4 @@ +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 删除脚本请求。Id 由路由 {id} 绑定。 +public sealed record DeleteScriptRequest(Guid Id); diff --git a/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptEndpoint.cs new file mode 100644 index 0000000..7685954 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptEndpoint.cs @@ -0,0 +1,22 @@ +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 禁用脚本端点。POST /scripts/{id}/disable。需 scripts.disable 权限。 +public class DisableScriptEndpoint(IMediator mediator) + : Endpoint +{ + public override void Configure() + { + Post("/scripts/{id}/disable"); + Permissions("scripts.disable"); + } + + public override async Task HandleAsync(DisableScriptRequest req, CancellationToken ct) + { + var result = await mediator.Send(new DisableScriptCommand(req.Id), ct); + await Send.OkAsync(result, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptRequest.cs new file mode 100644 index 0000000..6dc302a --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptRequest.cs @@ -0,0 +1,4 @@ +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 禁用脚本请求。Id 由路由 {id} 绑定。 +public sealed record DisableScriptRequest(Guid Id); diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailEndpoint.cs new file mode 100644 index 0000000..b3cc9e9 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailEndpoint.cs @@ -0,0 +1,22 @@ +using MediatR; +using Scripting.Application.DTOs; +using Scripting.Application.Queries; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 脚本详情端点。GET /scripts/{id}。需 scripts.read 权限。Id 由路由 {id} 按名绑定到请求。 +public class GetScriptDetailEndpoint(IMediator mediator) + : Endpoint +{ + public override void Configure() + { + Get("/scripts/{id}"); + Permissions("scripts.read"); + } + + public override async Task HandleAsync(GetScriptDetailRequest req, CancellationToken ct) + { + var result = await mediator.Send(new GetScriptDetailQuery(req.Id), ct); + await Send.OkAsync(result, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailRequest.cs new file mode 100644 index 0000000..ed596ab --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailRequest.cs @@ -0,0 +1,4 @@ +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 脚本详情查询请求。Id 由路由 {id} 绑定。 +public sealed record GetScriptDetailRequest(Guid Id); diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsEndpoint.cs new file mode 100644 index 0000000..ad12ffd --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsEndpoint.cs @@ -0,0 +1,29 @@ +using MediatR; +using Scripting.Application.Common; +using Scripting.Application.DTOs; +using Scripting.Application.Queries; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 脚本执行日志查询端点。GET /scripts/{id}/logs。需 scripts.read 权限。 +public class GetScriptExecutionLogsEndpoint(IMediator mediator) + : Endpoint> +{ + public override void Configure() + { + Get("/scripts/{id}/logs"); + Permissions("scripts.read"); + } + + public override async Task HandleAsync(GetScriptExecutionLogsRequest req, CancellationToken ct) + { + var result = await mediator.Send( + new GetScriptExecutionLogsQuery( + req.Id, + req.PageIndex, + req.PageSize, + req.Status), + ct); + await Send.OkAsync(result, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsRequest.cs new file mode 100644 index 0000000..8f9cc3a --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsRequest.cs @@ -0,0 +1,12 @@ +using Scripting.Domain.Enums; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 脚本执行日志查询请求。Id(脚本 Id)由路由 {id} 绑定。 +public sealed class GetScriptExecutionLogsRequest +{ + public Guid Id { get; set; } + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; + public ScriptExecutionStatus? Status { get; set; } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListEndpoint.cs new file mode 100644 index 0000000..c426a4c --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListEndpoint.cs @@ -0,0 +1,30 @@ +using MediatR; +using Scripting.Application.Common; +using Scripting.Application.DTOs; +using Scripting.Application.Queries; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 脚本分页查询端点。GET /scripts。需 scripts.read 权限。 +public class GetScriptListEndpoint(IMediator mediator) + : Endpoint> +{ + public override void Configure() + { + Get("/scripts"); + Permissions("scripts.read"); + } + + public override async Task HandleAsync(GetScriptListRequest req, CancellationToken ct) + { + var result = await mediator.Send( + new GetScriptListQuery( + req.PageIndex, + req.PageSize, + req.Status, + req.Keyword, + req.FormDefinitionId), + ct); + await Send.OkAsync(result, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListRequest.cs new file mode 100644 index 0000000..e561b25 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListRequest.cs @@ -0,0 +1,13 @@ +using Scripting.Domain.Enums; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 脚本分页查询请求。支持按状态、表单作用域、关键字筛选。 +public sealed class GetScriptListRequest +{ + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; + public ScriptStatus? Status { get; set; } + public string? Keyword { get; set; } + public Guid? FormDefinitionId { get; set; } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptEndpoint.cs new file mode 100644 index 0000000..616e337 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptEndpoint.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// +/// 调用(按名执行)脚本端点。POST /scripts/{name}/invoke。需 scripts.run 权限。 +/// 请求体 { "input": { ... } };input 可为任意 JSON 值,端点序列化为字符串后委托执行服务。 +/// +public class InvokeScriptEndpoint(IMediator mediator) + : Endpoint +{ + public override void Configure() + { + Post("/scripts/{name}/invoke"); + Permissions("scripts.run"); + } + + public override async Task HandleAsync(InvokeScriptRequest req, CancellationToken ct) + { + // Input 可省略(默认 {}),序列化为紧凑 JSON 字符串交给执行服务。 + var inputJson = req.Input is null + ? "{}" + : JsonSerializer.Serialize(req.Input.Value); + + var result = await mediator.Send( + new InvokeScriptCommand(req.Name, inputJson), + ct); + await Send.OkAsync(result, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptRequest.cs new file mode 100644 index 0000000..4276ace --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptRequest.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// +/// 调用脚本请求。Name 由路由 {name} 绑定;Input 为任意 JSON 对象,端点将其序列化为字符串传给执行服务。 +/// +public sealed class InvokeScriptRequest +{ + /// 由路由 {name} 绑定的脚本英文名。 + public string Name { get; set; } = string.Empty; + + /// 调用输入,可为任意 JSON 值(对象/数组/标量)。 + public JsonElement? Input { get; set; } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptEndpoint.cs new file mode 100644 index 0000000..0b1ee6a --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptEndpoint.cs @@ -0,0 +1,22 @@ +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 发布脚本端点。POST /scripts/{id}/publish。需 scripts.publish 权限。 +public class PublishScriptEndpoint(IMediator mediator) + : Endpoint +{ + public override void Configure() + { + Post("/scripts/{id}/publish"); + Permissions("scripts.publish"); + } + + public override async Task HandleAsync(PublishScriptRequest req, CancellationToken ct) + { + var result = await mediator.Send(new PublishScriptCommand(req.Id), ct); + await Send.OkAsync(result, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptRequest.cs new file mode 100644 index 0000000..76a7b5a --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptRequest.cs @@ -0,0 +1,4 @@ +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 发布脚本请求。Id 由路由 {id} 绑定。 +public sealed record PublishScriptRequest(Guid Id); diff --git a/src/BuildFlow.Host/Endpoints/Scripting/TestScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/TestScriptEndpoint.cs new file mode 100644 index 0000000..7783899 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/TestScriptEndpoint.cs @@ -0,0 +1,22 @@ +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 测试执行脚本端点。POST /scripts/{id}/test。需 scripts.test 权限。 +public class TestScriptEndpoint(IMediator mediator) + : Endpoint +{ + public override void Configure() + { + Post("/scripts/{id}/test"); + Permissions("scripts.test"); + } + + public override async Task HandleAsync(TestScriptRequest req, CancellationToken ct) + { + var result = await mediator.Send(new TestScriptCommand(req.Id, req.InputJson), ct); + await Send.OkAsync(result, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/TestScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/TestScriptRequest.cs new file mode 100644 index 0000000..1ff03d6 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/TestScriptRequest.cs @@ -0,0 +1,4 @@ +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 测试执行脚本请求。Id 由路由 {id} 绑定,InputJson 为请求体。 +public sealed record TestScriptRequest(Guid Id, string InputJson); diff --git a/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptEndpoint.cs new file mode 100644 index 0000000..293512d --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptEndpoint.cs @@ -0,0 +1,31 @@ +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; + +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 更新脚本端点。PUT /scripts/{id}。需 scripts.update 权限。Id 由路由 {id} 绑定。 +public class UpdateScriptEndpoint(IMediator mediator) + : Endpoint +{ + public override void Configure() + { + Put("/scripts/{id}"); + Permissions("scripts.update"); + } + + public override async Task HandleAsync(UpdateScriptRequest req, CancellationToken ct) + { + var result = await mediator.Send( + new UpdateScriptCommand( + req.Id, + req.DisplayName, + req.Description, + req.Code, + req.InputSchema, + req.OutputSchema, + req.TimeoutMs), + ct); + await Send.OkAsync(result, ct); + } +} diff --git a/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptRequest.cs new file mode 100644 index 0000000..407ab1c --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptRequest.cs @@ -0,0 +1,11 @@ +namespace BuildFlow.Host.Endpoints.Scripting; + +/// 更新脚本请求。Id 由路由 {id} 绑定;Name/FormDefinitionId 不可改。 +public sealed record UpdateScriptRequest( + Guid Id, + string DisplayName, + string? Description, + string Code, + string? InputSchema, + string? OutputSchema, + int TimeoutMs); diff --git a/src/modules/scripting/Application/Commands/CreateScriptCommand.cs b/src/modules/scripting/Application/Commands/CreateScriptCommand.cs new file mode 100644 index 0000000..b9f9574 --- /dev/null +++ b/src/modules/scripting/Application/Commands/CreateScriptCommand.cs @@ -0,0 +1,18 @@ +using MediatR; +using Scripting.Application.DTOs; + +namespace Scripting.Application.Commands; + +/// +/// 创建脚本函数命令。新建脚本默认是草稿(Draft),必须发布后才能被动作链/HTTP invoke 执行。 +/// Name 为稳定英文标识(创建后不可改),供按钮引用与按名调用。 +/// +public sealed record CreateScriptCommand( + string Name, + string DisplayName, + string? Description, + string Code, + string? InputSchema, + string? OutputSchema, + int TimeoutMs, + Guid? FormDefinitionId = null) : IRequest; diff --git a/src/modules/scripting/Application/Commands/DeleteScriptCommand.cs b/src/modules/scripting/Application/Commands/DeleteScriptCommand.cs new file mode 100644 index 0000000..222f72d --- /dev/null +++ b/src/modules/scripting/Application/Commands/DeleteScriptCommand.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace Scripting.Application.Commands; + +/// 删除脚本函数命令(软删除)。Published 状态必须先 Disable 才能删除。 +public sealed record DeleteScriptCommand(Guid Id) : IRequest; diff --git a/src/modules/scripting/Application/Commands/DisableScriptCommand.cs b/src/modules/scripting/Application/Commands/DisableScriptCommand.cs new file mode 100644 index 0000000..96d8ad5 --- /dev/null +++ b/src/modules/scripting/Application/Commands/DisableScriptCommand.cs @@ -0,0 +1,7 @@ +using MediatR; +using Scripting.Application.DTOs; + +namespace Scripting.Application.Commands; + +/// 禁用脚本函数命令。禁用后动作链将拒绝执行该脚本,HTTP invoke 返回 400。 +public sealed record DisableScriptCommand(Guid Id) : IRequest; diff --git a/src/modules/scripting/Application/Commands/InvokeScriptCommand.cs b/src/modules/scripting/Application/Commands/InvokeScriptCommand.cs new file mode 100644 index 0000000..aa6aab6 --- /dev/null +++ b/src/modules/scripting/Application/Commands/InvokeScriptCommand.cs @@ -0,0 +1,7 @@ +using MediatR; +using Scripting.Application.DTOs; + +namespace Scripting.Application.Commands; + +/// 按 Name 调用已发布脚本命令(HTTP /scripts/{name}/invoke)。 +public sealed record InvokeScriptCommand(string Name, string InputJson) : IRequest; diff --git a/src/modules/scripting/Application/Commands/PublishScriptCommand.cs b/src/modules/scripting/Application/Commands/PublishScriptCommand.cs new file mode 100644 index 0000000..570a7fa --- /dev/null +++ b/src/modules/scripting/Application/Commands/PublishScriptCommand.cs @@ -0,0 +1,7 @@ +using MediatR; +using Scripting.Application.DTOs; + +namespace Scripting.Application.Commands; + +/// 发布脚本函数命令。发布将状态置为 Published 并递增 Version,便于后续审计变更。 +public sealed record PublishScriptCommand(Guid Id) : IRequest; diff --git a/src/modules/scripting/Application/Commands/ScriptMapper.cs b/src/modules/scripting/Application/Commands/ScriptMapper.cs new file mode 100644 index 0000000..51557f6 --- /dev/null +++ b/src/modules/scripting/Application/Commands/ScriptMapper.cs @@ -0,0 +1,33 @@ +using Scripting.Application.DTOs; +using Scripting.Domain.Entities; + +namespace Scripting.Application.Commands; + +/// 脚本 DTO 映射器,集中保持命令返回结构一致。注意执行日志的字段重命名(FunctionId→ScriptId)。 +internal static class ScriptMapper +{ + public static ScriptDto ToDto(ScriptFunction e) => new( + e.Id, + e.FormDefinitionId, + e.Name, + e.DisplayName, + e.Description, + e.Code, + e.InputSchema, + e.OutputSchema, + e.Status, + e.Version, + e.TimeoutMs); + + public static ScriptExecutionLogDto ToLogDto(ScriptExecutionLog e) => new( + e.Id, + e.FunctionId, + e.FunctionName, + e.Status, + e.InputJson, + e.ResultJson, + e.ErrorMessage, + e.DurationMs, + e.CreatedAt, + e.LogLines); +} diff --git a/src/modules/scripting/Application/Commands/TestScriptCommand.cs b/src/modules/scripting/Application/Commands/TestScriptCommand.cs new file mode 100644 index 0000000..2634a78 --- /dev/null +++ b/src/modules/scripting/Application/Commands/TestScriptCommand.cs @@ -0,0 +1,7 @@ +using MediatR; +using Scripting.Application.DTOs; + +namespace Scripting.Application.Commands; + +/// 测试执行脚本命令。不校验 Published 状态,允许草稿直接测,仍写日志(FunctionName 标 [TEST])。 +public sealed record TestScriptCommand(Guid Id, string InputJson) : IRequest; diff --git a/src/modules/scripting/Application/Commands/UpdateScriptCommand.cs b/src/modules/scripting/Application/Commands/UpdateScriptCommand.cs new file mode 100644 index 0000000..a666944 --- /dev/null +++ b/src/modules/scripting/Application/Commands/UpdateScriptCommand.cs @@ -0,0 +1,17 @@ +using MediatR; +using Scripting.Application.DTOs; + +namespace Scripting.Application.Commands; + +/// +/// 更新脚本函数命令。仅允许编辑 DisplayName/Description/Code/InputSchema/OutputSchema/TimeoutMs。 +/// Name 与 FormDefinitionId 创建后即固化,不可改。Disabled 状态禁止编辑;Published 编辑保持已发布但 Version++。 +/// +public sealed record UpdateScriptCommand( + Guid Id, + string DisplayName, + string? Description, + string Code, + string? InputSchema, + string? OutputSchema, + int TimeoutMs) : IRequest; diff --git a/src/modules/scripting/Application/Common/PagedResult.cs b/src/modules/scripting/Application/Common/PagedResult.cs new file mode 100644 index 0000000..c37803e --- /dev/null +++ b/src/modules/scripting/Application/Common/PagedResult.cs @@ -0,0 +1,13 @@ +namespace Scripting.Application.Common; + +/// +/// 通用分页结果包装。脚本模块本地副本,避免对 Workflow 模块的跨模块依赖。 +/// 字段形状与 Workflow.Application.Common.PagedResult 完全一致。 +/// +public class PagedResult +{ + public List Items { get; set; } = []; + public int Total { get; set; } + public int PageIndex { get; set; } + public int PageSize { get; set; } +} diff --git a/src/modules/scripting/Application/DTOs/ScriptDto.cs b/src/modules/scripting/Application/DTOs/ScriptDto.cs new file mode 100644 index 0000000..310b234 --- /dev/null +++ b/src/modules/scripting/Application/DTOs/ScriptDto.cs @@ -0,0 +1,20 @@ +using Scripting.Domain.Enums; + +namespace Scripting.Application.DTOs; + +/// +/// 脚本函数 DTO,用于脚本管理页面和按钮动作选择器。 +/// 仅承载 C# 脚本字段(HTTP 类型函数已按 §11 YAGNI 移除)。 +/// +public sealed record ScriptDto( + Guid Id, + Guid? FormDefinitionId, + string Name, + string DisplayName, + string? Description, + string Code, + string? InputSchema, + string? OutputSchema, + ScriptStatus Status, + int Version, + int TimeoutMs); diff --git a/src/modules/scripting/Application/DTOs/ScriptExecutionLogDto.cs b/src/modules/scripting/Application/DTOs/ScriptExecutionLogDto.cs new file mode 100644 index 0000000..ee5f702 --- /dev/null +++ b/src/modules/scripting/Application/DTOs/ScriptExecutionLogDto.cs @@ -0,0 +1,19 @@ +using Scripting.Domain.Enums; + +namespace Scripting.Application.DTOs; + +/// +/// 脚本执行日志 DTO。注意:底层实体字段名仍是 FunctionId/FunctionName(T2 迁移保留), +/// 此处 DTO 使用 ScriptId/ScriptName 以提升对外清晰度,由映射器做字段重命名。 +/// +public sealed record ScriptExecutionLogDto( + Guid Id, + Guid ScriptId, + string ScriptName, + ScriptExecutionStatus Status, + string InputJson, + string? ResultJson, + string? ErrorMessage, + long DurationMs, + DateTime CreatedAt, + string? LogLines); diff --git a/src/modules/scripting/Application/Handlers/CreateScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/CreateScriptCommandHandler.cs new file mode 100644 index 0000000..a1c97fe --- /dev/null +++ b/src/modules/scripting/Application/Handlers/CreateScriptCommandHandler.cs @@ -0,0 +1,41 @@ +using BuildFlow.Shared.Domain.Exceptions; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; +using Scripting.Domain.Entities; +using Scripting.Domain.Enums; +using Scripting.Infrastructure.Persistence; + +namespace Scripting.Application.Handlers; + +/// 创建脚本函数处理器。校验 Name 唯一,保证按钮动作引用稳定;TimeoutMs 默认 5000ms。 +public class CreateScriptCommandHandler(ScriptingDbContext db) + : IRequestHandler +{ + public async Task Handle(CreateScriptCommand request, CancellationToken ct) + { + if (await db.Scripts.AnyAsync(x => x.Name == request.Name, ct)) + { + throw new BusinessException($"脚本 {request.Name} 已存在"); + } + + var entity = new ScriptFunction + { + Id = Guid.NewGuid(), + FormDefinitionId = request.FormDefinitionId, + Name = request.Name, + DisplayName = request.DisplayName, + Description = request.Description, + Code = request.Code, + InputSchema = request.InputSchema, + OutputSchema = request.OutputSchema, + Status = ScriptStatus.Draft, + Version = 1, + TimeoutMs = request.TimeoutMs <= 0 ? 5000 : request.TimeoutMs, + }; + db.Scripts.Add(entity); + await db.SaveChangesAsync(ct); + return ScriptMapper.ToDto(entity); + } +} diff --git a/src/modules/scripting/Application/Handlers/DeleteScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/DeleteScriptCommandHandler.cs new file mode 100644 index 0000000..6a8f4c3 --- /dev/null +++ b/src/modules/scripting/Application/Handlers/DeleteScriptCommandHandler.cs @@ -0,0 +1,27 @@ +using BuildFlow.Shared.Domain.Exceptions; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Scripting.Application.Commands; +using Scripting.Domain.Enums; +using Scripting.Infrastructure.Persistence; + +namespace Scripting.Application.Handlers; + +/// 删除脚本函数处理器(软删除)。Published 状态必须先 Disable 才能删除。 +public class DeleteScriptCommandHandler(ScriptingDbContext db) + : IRequestHandler +{ + public async Task Handle(DeleteScriptCommand request, CancellationToken ct) + { + var entity = await db.Scripts.FindAsync([request.Id], ct) + ?? throw new BusinessException($"脚本 {request.Id} 不存在"); + + if (entity.Status == ScriptStatus.Published) + { + throw new BusinessException("已发布的脚本不能删除,请先禁用"); + } + + entity.IsDeleted = true; + await db.SaveChangesAsync(ct); + } +} diff --git a/src/modules/scripting/Application/Handlers/DisableScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/DisableScriptCommandHandler.cs new file mode 100644 index 0000000..d624f06 --- /dev/null +++ b/src/modules/scripting/Application/Handlers/DisableScriptCommandHandler.cs @@ -0,0 +1,23 @@ +using BuildFlow.Shared.Domain.Exceptions; +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; +using Scripting.Domain.Enums; +using Scripting.Infrastructure.Persistence; + +namespace Scripting.Application.Handlers; + +/// 禁用脚本函数处理器。置 Disabled 后动作链与 HTTP invoke 都将拒绝执行。 +public class DisableScriptCommandHandler(ScriptingDbContext db) + : IRequestHandler +{ + public async Task Handle(DisableScriptCommand request, CancellationToken ct) + { + var entity = await db.Scripts.FindAsync([request.Id], ct) + ?? throw new BusinessException($"脚本 {request.Id} 不存在"); + + entity.Status = ScriptStatus.Disabled; + await db.SaveChangesAsync(ct); + return ScriptMapper.ToDto(entity); + } +} diff --git a/src/modules/scripting/Application/Handlers/GetScriptDetailQueryHandler.cs b/src/modules/scripting/Application/Handlers/GetScriptDetailQueryHandler.cs new file mode 100644 index 0000000..b966e2c --- /dev/null +++ b/src/modules/scripting/Application/Handlers/GetScriptDetailQueryHandler.cs @@ -0,0 +1,20 @@ +using BuildFlow.Shared.Domain.Exceptions; +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; +using Scripting.Application.Queries; +using Scripting.Infrastructure.Persistence; + +namespace Scripting.Application.Handlers; + +/// 脚本详情查询处理器:FindAsync 取实体,缺失抛 BusinessException。 +public sealed class GetScriptDetailQueryHandler(ScriptingDbContext db) + : IRequestHandler +{ + public async Task Handle(GetScriptDetailQuery request, CancellationToken ct) + { + var entity = await db.Scripts.FindAsync([request.Id], ct) + ?? throw new BusinessException($"脚本 {request.Id} 不存在"); + return ScriptMapper.ToDto(entity); + } +} diff --git a/src/modules/scripting/Application/Handlers/GetScriptExecutionLogsQueryHandler.cs b/src/modules/scripting/Application/Handlers/GetScriptExecutionLogsQueryHandler.cs new file mode 100644 index 0000000..02fdca7 --- /dev/null +++ b/src/modules/scripting/Application/Handlers/GetScriptExecutionLogsQueryHandler.cs @@ -0,0 +1,52 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Scripting.Application.Common; +using Scripting.Application.DTOs; +using Scripting.Application.Queries; +using Scripting.Infrastructure.Persistence; + +namespace Scripting.Application.Handlers; + +/// 脚本执行日志查询处理器:按 FunctionId 过滤,可选状态过滤,按 CreatedAt 倒序分页。 +public sealed class GetScriptExecutionLogsQueryHandler(ScriptingDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetScriptExecutionLogsQuery request, + CancellationToken ct) + { + var query = db.ScriptExecutionLogs + .Where(x => x.FunctionId == request.ScriptId); + + if (request.Status.HasValue) + { + query = query.Where(x => x.Status == request.Status.Value); + } + + var total = await query.CountAsync(ct); + var items = await query + .OrderByDescending(x => x.CreatedAt) + .Skip((request.PageIndex - 1) * request.PageSize) + .Take(request.PageSize) + .Select(x => new ScriptExecutionLogDto( + x.Id, + x.FunctionId, + x.FunctionName, + x.Status, + x.InputJson, + x.ResultJson, + x.ErrorMessage, + x.DurationMs, + x.CreatedAt, + x.LogLines)) + .ToListAsync(ct); + + return new PagedResult + { + Items = items, + Total = total, + PageIndex = request.PageIndex, + PageSize = request.PageSize, + }; + } +} diff --git a/src/modules/scripting/Application/Handlers/GetScriptListQueryHandler.cs b/src/modules/scripting/Application/Handlers/GetScriptListQueryHandler.cs new file mode 100644 index 0000000..7cab3e4 --- /dev/null +++ b/src/modules/scripting/Application/Handlers/GetScriptListQueryHandler.cs @@ -0,0 +1,64 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Scripting.Application.Common; +using Scripting.Application.DTOs; +using Scripting.Application.Queries; +using Scripting.Infrastructure.Persistence; + +namespace Scripting.Application.Handlers; + +/// 脚本列表查询处理器:过滤 + 按 UpdatedAt 倒序分页,投影到 ScriptDto。 +public sealed class GetScriptListQueryHandler(ScriptingDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetScriptListQuery request, + CancellationToken ct) + { + var query = db.Scripts.AsQueryable(); + + if (request.Status.HasValue) + { + query = query.Where(x => x.Status == request.Status.Value); + } + + if (request.FormDefinitionId.HasValue) + { + query = query.Where(x => x.FormDefinitionId == request.FormDefinitionId.Value); + } + + if (!string.IsNullOrWhiteSpace(request.Keyword)) + { + query = query.Where(x => + x.Name.Contains(request.Keyword) || + x.DisplayName.Contains(request.Keyword)); + } + + var total = await query.CountAsync(ct); + var items = await query + .OrderByDescending(x => x.UpdatedAt) + .Skip((request.PageIndex - 1) * request.PageSize) + .Take(request.PageSize) + .Select(x => new ScriptDto( + x.Id, + x.FormDefinitionId, + x.Name, + x.DisplayName, + x.Description, + x.Code, + x.InputSchema, + x.OutputSchema, + x.Status, + x.Version, + x.TimeoutMs)) + .ToListAsync(ct); + + return new PagedResult + { + Items = items, + Total = total, + PageIndex = request.PageIndex, + PageSize = request.PageSize, + }; + } +} diff --git a/src/modules/scripting/Application/Handlers/InvokeScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/InvokeScriptCommandHandler.cs new file mode 100644 index 0000000..3fc5237 --- /dev/null +++ b/src/modules/scripting/Application/Handlers/InvokeScriptCommandHandler.cs @@ -0,0 +1,14 @@ +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; +using Scripting.Application.Services; + +namespace Scripting.Application.Handlers; + +/// 按 Name 调用脚本处理器,委托执行服务(仅 Published 脚本可调用)。 +public class InvokeScriptCommandHandler(IScriptExecutionService executionService) + : IRequestHandler +{ + public Task Handle(InvokeScriptCommand request, CancellationToken ct) + => executionService.ExecuteByNameAsync(request.Name, request.InputJson, ct); +} diff --git a/src/modules/scripting/Application/Handlers/PublishScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/PublishScriptCommandHandler.cs new file mode 100644 index 0000000..4e3ac0f --- /dev/null +++ b/src/modules/scripting/Application/Handlers/PublishScriptCommandHandler.cs @@ -0,0 +1,24 @@ +using BuildFlow.Shared.Domain.Exceptions; +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; +using Scripting.Domain.Enums; +using Scripting.Infrastructure.Persistence; + +namespace Scripting.Application.Handlers; + +/// 发布脚本函数处理器。置 Published 并 Version++,便于后续审计变更。 +public class PublishScriptCommandHandler(ScriptingDbContext db) + : IRequestHandler +{ + public async Task Handle(PublishScriptCommand request, CancellationToken ct) + { + var entity = await db.Scripts.FindAsync([request.Id], ct) + ?? throw new BusinessException($"脚本 {request.Id} 不存在"); + + entity.Status = ScriptStatus.Published; + entity.Version++; + await db.SaveChangesAsync(ct); + return ScriptMapper.ToDto(entity); + } +} diff --git a/src/modules/scripting/Application/Handlers/TestScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/TestScriptCommandHandler.cs new file mode 100644 index 0000000..d66fce9 --- /dev/null +++ b/src/modules/scripting/Application/Handlers/TestScriptCommandHandler.cs @@ -0,0 +1,14 @@ +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; +using Scripting.Application.Services; + +namespace Scripting.Application.Handlers; + +/// 测试执行脚本处理器,委托执行服务以保证测试与运行时一致。 +public class TestScriptCommandHandler(IScriptExecutionService executionService) + : IRequestHandler +{ + public Task Handle(TestScriptCommand request, CancellationToken ct) + => executionService.ExecuteTestAsync(request.Id, request.InputJson, ct); +} diff --git a/src/modules/scripting/Application/Handlers/UpdateScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/UpdateScriptCommandHandler.cs new file mode 100644 index 0000000..4b333c4 --- /dev/null +++ b/src/modules/scripting/Application/Handlers/UpdateScriptCommandHandler.cs @@ -0,0 +1,42 @@ +using BuildFlow.Shared.Domain.Exceptions; +using MediatR; +using Scripting.Application.Commands; +using Scripting.Application.DTOs; +using Scripting.Domain.Enums; +using Scripting.Infrastructure.Persistence; + +namespace Scripting.Application.Handlers; + +/// +/// 更新脚本函数处理器。Disabled 禁止编辑;Published 编辑保持已发布但 Version++(调用方可据此感知变更)。 +/// +public class UpdateScriptCommandHandler(ScriptingDbContext db) + : IRequestHandler +{ + public async Task Handle(UpdateScriptCommand request, CancellationToken ct) + { + var entity = await db.Scripts.FindAsync([request.Id], ct) + ?? throw new BusinessException($"脚本 {request.Id} 不存在"); + + if (entity.Status == ScriptStatus.Disabled) + { + throw new BusinessException("已禁用的脚本不能编辑"); + } + + entity.DisplayName = request.DisplayName; + entity.Description = request.Description; + entity.Code = request.Code; + entity.InputSchema = request.InputSchema; + entity.OutputSchema = request.OutputSchema; + entity.TimeoutMs = request.TimeoutMs <= 0 ? 5000 : request.TimeoutMs; + + // 已发布脚本被编辑后保持已发布,但递增版本号,便于调用方/审计感知变更。 + if (entity.Status == ScriptStatus.Published) + { + entity.Version++; + } + + await db.SaveChangesAsync(ct); + return ScriptMapper.ToDto(entity); + } +} diff --git a/src/modules/scripting/Application/Queries/GetScriptDetailQuery.cs b/src/modules/scripting/Application/Queries/GetScriptDetailQuery.cs new file mode 100644 index 0000000..638caa8 --- /dev/null +++ b/src/modules/scripting/Application/Queries/GetScriptDetailQuery.cs @@ -0,0 +1,7 @@ +using MediatR; +using Scripting.Application.DTOs; + +namespace Scripting.Application.Queries; + +/// 按 Id 查询脚本函数详情。 +public sealed record GetScriptDetailQuery(Guid Id) : IRequest; diff --git a/src/modules/scripting/Application/Queries/GetScriptExecutionLogsQuery.cs b/src/modules/scripting/Application/Queries/GetScriptExecutionLogsQuery.cs new file mode 100644 index 0000000..8f126b0 --- /dev/null +++ b/src/modules/scripting/Application/Queries/GetScriptExecutionLogsQuery.cs @@ -0,0 +1,13 @@ +using MediatR; +using Scripting.Application.Common; +using Scripting.Application.DTOs; +using Scripting.Domain.Enums; + +namespace Scripting.Application.Queries; + +/// 分页查询某脚本的执行日志。注意底层实体字段是 FunctionId(映射自 ScriptId)。 +public sealed record GetScriptExecutionLogsQuery( + Guid ScriptId, + int PageIndex, + int PageSize, + ScriptExecutionStatus? Status = null) : IRequest>; diff --git a/src/modules/scripting/Application/Queries/GetScriptListQuery.cs b/src/modules/scripting/Application/Queries/GetScriptListQuery.cs new file mode 100644 index 0000000..de45305 --- /dev/null +++ b/src/modules/scripting/Application/Queries/GetScriptListQuery.cs @@ -0,0 +1,14 @@ +using MediatR; +using Scripting.Application.Common; +using Scripting.Application.DTOs; +using Scripting.Domain.Enums; + +namespace Scripting.Application.Queries; + +/// 分页查询脚本函数。支持按状态、表单作用域、关键字(Name/DisplayName)筛选。 +public sealed record GetScriptListQuery( + int PageIndex, + int PageSize, + ScriptStatus? Status, + string? Keyword, + Guid? FormDefinitionId = null) : IRequest>;