feat(scripting): 完整 CQRS 层 + 10 个管理端点
Commands: Create/Update/Delete/Publish/Disable/Test/Invoke
Queries: List/Detail/ExecutionLogs
DTOs: ScriptDto/ScriptExecutionLogDto + PagedResult
Endpoints (GET/POST/PUT/DELETE /scripts/*):
- GET /scripts scripts.read
- GET /scripts/{id} scripts.read
- POST /scripts scripts.create
- PUT /scripts/{id} scripts.update
- DELETE /scripts/{id} scripts.delete
- POST /scripts/{id}/publish scripts.publish
- POST /scripts/{id}/disable scripts.disable
- POST /scripts/{id}/test scripts.test
- GET /scripts/{id}/logs scripts.read
- POST /scripts/{name}/invoke scripts.run
Task T5 of scripting module refactor
This commit is contained in:
parent
bfb83957b6
commit
3d5604d80c
@ -0,0 +1,32 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>创建脚本端点。POST /scripts。需 scripts.create 权限。</summary>
|
||||
public class CreateScriptEndpoint(IMediator mediator)
|
||||
: Endpoint<CreateScriptRequest, ScriptDto>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>创建脚本请求。仅承载 C# 脚本字段。</summary>
|
||||
public sealed record CreateScriptRequest(
|
||||
string Name,
|
||||
string DisplayName,
|
||||
string? Description,
|
||||
string Code,
|
||||
string? InputSchema,
|
||||
string? OutputSchema,
|
||||
int TimeoutMs,
|
||||
Guid? FormDefinitionId = null);
|
||||
@ -0,0 +1,21 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>删除脚本端点(软删除)。DELETE /scripts/{id}。需 scripts.delete 权限。</summary>
|
||||
public class DeleteScriptEndpoint(IMediator mediator)
|
||||
: Endpoint<DeleteScriptRequest>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>删除脚本请求。Id 由路由 {id} 绑定。</summary>
|
||||
public sealed record DeleteScriptRequest(Guid Id);
|
||||
@ -0,0 +1,22 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>禁用脚本端点。POST /scripts/{id}/disable。需 scripts.disable 权限。</summary>
|
||||
public class DisableScriptEndpoint(IMediator mediator)
|
||||
: Endpoint<DisableScriptRequest, ScriptDto>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>禁用脚本请求。Id 由路由 {id} 绑定。</summary>
|
||||
public sealed record DisableScriptRequest(Guid Id);
|
||||
@ -0,0 +1,22 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.DTOs;
|
||||
using Scripting.Application.Queries;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>脚本详情端点。GET /scripts/{id}。需 scripts.read 权限。Id 由路由 {id} 按名绑定到请求。</summary>
|
||||
public class GetScriptDetailEndpoint(IMediator mediator)
|
||||
: Endpoint<GetScriptDetailRequest, ScriptDto>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>脚本详情查询请求。Id 由路由 {id} 绑定。</summary>
|
||||
public sealed record GetScriptDetailRequest(Guid Id);
|
||||
@ -0,0 +1,29 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Common;
|
||||
using Scripting.Application.DTOs;
|
||||
using Scripting.Application.Queries;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>脚本执行日志查询端点。GET /scripts/{id}/logs。需 scripts.read 权限。</summary>
|
||||
public class GetScriptExecutionLogsEndpoint(IMediator mediator)
|
||||
: Endpoint<GetScriptExecutionLogsRequest, PagedResult<ScriptExecutionLogDto>>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using Scripting.Domain.Enums;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>脚本执行日志查询请求。Id(脚本 Id)由路由 {id} 绑定。</summary>
|
||||
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; }
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Common;
|
||||
using Scripting.Application.DTOs;
|
||||
using Scripting.Application.Queries;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>脚本分页查询端点。GET /scripts。需 scripts.read 权限。</summary>
|
||||
public class GetScriptListEndpoint(IMediator mediator)
|
||||
: Endpoint<GetScriptListRequest, PagedResult<ScriptDto>>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
using Scripting.Domain.Enums;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>脚本分页查询请求。支持按状态、表单作用域、关键字筛选。</summary>
|
||||
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; }
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>
|
||||
/// 调用(按名执行)脚本端点。POST /scripts/{name}/invoke。需 scripts.run 权限。
|
||||
/// 请求体 { "input": { ... } };input 可为任意 JSON 值,端点序列化为字符串后委托执行服务。
|
||||
/// </summary>
|
||||
public class InvokeScriptEndpoint(IMediator mediator)
|
||||
: Endpoint<InvokeScriptRequest, ScriptExecutionResult>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>
|
||||
/// 调用脚本请求。Name 由路由 {name} 绑定;Input 为任意 JSON 对象,端点将其序列化为字符串传给执行服务。
|
||||
/// </summary>
|
||||
public sealed class InvokeScriptRequest
|
||||
{
|
||||
/// <summary>由路由 {name} 绑定的脚本英文名。</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>调用输入,可为任意 JSON 值(对象/数组/标量)。</summary>
|
||||
public JsonElement? Input { get; set; }
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>发布脚本端点。POST /scripts/{id}/publish。需 scripts.publish 权限。</summary>
|
||||
public class PublishScriptEndpoint(IMediator mediator)
|
||||
: Endpoint<PublishScriptRequest, ScriptDto>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>发布脚本请求。Id 由路由 {id} 绑定。</summary>
|
||||
public sealed record PublishScriptRequest(Guid Id);
|
||||
22
src/BuildFlow.Host/Endpoints/Scripting/TestScriptEndpoint.cs
Normal file
22
src/BuildFlow.Host/Endpoints/Scripting/TestScriptEndpoint.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>测试执行脚本端点。POST /scripts/{id}/test。需 scripts.test 权限。</summary>
|
||||
public class TestScriptEndpoint(IMediator mediator)
|
||||
: Endpoint<TestScriptRequest, ScriptExecutionResult>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>测试执行脚本请求。Id 由路由 {id} 绑定,InputJson 为请求体。</summary>
|
||||
public sealed record TestScriptRequest(Guid Id, string InputJson);
|
||||
@ -0,0 +1,31 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>更新脚本端点。PUT /scripts/{id}。需 scripts.update 权限。Id 由路由 {id} 绑定。</summary>
|
||||
public class UpdateScriptEndpoint(IMediator mediator)
|
||||
: Endpoint<UpdateScriptRequest, ScriptDto>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
namespace BuildFlow.Host.Endpoints.Scripting;
|
||||
|
||||
/// <summary>更新脚本请求。Id 由路由 {id} 绑定;Name/FormDefinitionId 不可改。</summary>
|
||||
public sealed record UpdateScriptRequest(
|
||||
Guid Id,
|
||||
string DisplayName,
|
||||
string? Description,
|
||||
string Code,
|
||||
string? InputSchema,
|
||||
string? OutputSchema,
|
||||
int TimeoutMs);
|
||||
@ -0,0 +1,18 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace Scripting.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建脚本函数命令。新建脚本默认是草稿(Draft),必须发布后才能被动作链/HTTP invoke 执行。
|
||||
/// Name 为稳定英文标识(创建后不可改),供按钮引用与按名调用。
|
||||
/// </summary>
|
||||
public sealed record CreateScriptCommand(
|
||||
string Name,
|
||||
string DisplayName,
|
||||
string? Description,
|
||||
string Code,
|
||||
string? InputSchema,
|
||||
string? OutputSchema,
|
||||
int TimeoutMs,
|
||||
Guid? FormDefinitionId = null) : IRequest<ScriptDto>;
|
||||
@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Scripting.Application.Commands;
|
||||
|
||||
/// <summary>删除脚本函数命令(软删除)。Published 状态必须先 Disable 才能删除。</summary>
|
||||
public sealed record DeleteScriptCommand(Guid Id) : IRequest;
|
||||
@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace Scripting.Application.Commands;
|
||||
|
||||
/// <summary>禁用脚本函数命令。禁用后动作链将拒绝执行该脚本,HTTP invoke 返回 400。</summary>
|
||||
public sealed record DisableScriptCommand(Guid Id) : IRequest<ScriptDto>;
|
||||
@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace Scripting.Application.Commands;
|
||||
|
||||
/// <summary>按 Name 调用已发布脚本命令(HTTP /scripts/{name}/invoke)。</summary>
|
||||
public sealed record InvokeScriptCommand(string Name, string InputJson) : IRequest<ScriptExecutionResult>;
|
||||
@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace Scripting.Application.Commands;
|
||||
|
||||
/// <summary>发布脚本函数命令。发布将状态置为 Published 并递增 Version,便于后续审计变更。</summary>
|
||||
public sealed record PublishScriptCommand(Guid Id) : IRequest<ScriptDto>;
|
||||
33
src/modules/scripting/Application/Commands/ScriptMapper.cs
Normal file
33
src/modules/scripting/Application/Commands/ScriptMapper.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Scripting.Application.DTOs;
|
||||
using Scripting.Domain.Entities;
|
||||
|
||||
namespace Scripting.Application.Commands;
|
||||
|
||||
/// <summary>脚本 DTO 映射器,集中保持命令返回结构一致。注意执行日志的字段重命名(FunctionId→ScriptId)。</summary>
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace Scripting.Application.Commands;
|
||||
|
||||
/// <summary>测试执行脚本命令。不校验 Published 状态,允许草稿直接测,仍写日志(FunctionName 标 [TEST])。</summary>
|
||||
public sealed record TestScriptCommand(Guid Id, string InputJson) : IRequest<ScriptExecutionResult>;
|
||||
@ -0,0 +1,17 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace Scripting.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新脚本函数命令。仅允许编辑 DisplayName/Description/Code/InputSchema/OutputSchema/TimeoutMs。
|
||||
/// Name 与 FormDefinitionId 创建后即固化,不可改。Disabled 状态禁止编辑;Published 编辑保持已发布但 Version++。
|
||||
/// </summary>
|
||||
public sealed record UpdateScriptCommand(
|
||||
Guid Id,
|
||||
string DisplayName,
|
||||
string? Description,
|
||||
string Code,
|
||||
string? InputSchema,
|
||||
string? OutputSchema,
|
||||
int TimeoutMs) : IRequest<ScriptDto>;
|
||||
13
src/modules/scripting/Application/Common/PagedResult.cs
Normal file
13
src/modules/scripting/Application/Common/PagedResult.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Scripting.Application.Common;
|
||||
|
||||
/// <summary>
|
||||
/// 通用分页结果包装。脚本模块本地副本,避免对 Workflow 模块的跨模块依赖。
|
||||
/// 字段形状与 Workflow.Application.Common.PagedResult 完全一致。
|
||||
/// </summary>
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = [];
|
||||
public int Total { get; set; }
|
||||
public int PageIndex { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
20
src/modules/scripting/Application/DTOs/ScriptDto.cs
Normal file
20
src/modules/scripting/Application/DTOs/ScriptDto.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Scripting.Domain.Enums;
|
||||
|
||||
namespace Scripting.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// 脚本函数 DTO,用于脚本管理页面和按钮动作选择器。
|
||||
/// 仅承载 C# 脚本字段(HTTP 类型函数已按 §11 YAGNI 移除)。
|
||||
/// </summary>
|
||||
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);
|
||||
@ -0,0 +1,19 @@
|
||||
using Scripting.Domain.Enums;
|
||||
|
||||
namespace Scripting.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// 脚本执行日志 DTO。注意:底层实体字段名仍是 FunctionId/FunctionName(T2 迁移保留),
|
||||
/// 此处 DTO 使用 ScriptId/ScriptName 以提升对外清晰度,由映射器做字段重命名。
|
||||
/// </summary>
|
||||
public sealed record ScriptExecutionLogDto(
|
||||
Guid Id,
|
||||
Guid ScriptId,
|
||||
string ScriptName,
|
||||
ScriptExecutionStatus Status,
|
||||
string InputJson,
|
||||
string? ResultJson,
|
||||
string? ErrorMessage,
|
||||
long DurationMs,
|
||||
DateTime CreatedAt,
|
||||
string? LogLines);
|
||||
@ -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;
|
||||
|
||||
/// <summary>创建脚本函数处理器。校验 Name 唯一,保证按钮动作引用稳定;TimeoutMs 默认 5000ms。</summary>
|
||||
public class CreateScriptCommandHandler(ScriptingDbContext db)
|
||||
: IRequestHandler<CreateScriptCommand, ScriptDto>
|
||||
{
|
||||
public async Task<ScriptDto> 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>删除脚本函数处理器(软删除)。Published 状态必须先 Disable 才能删除。</summary>
|
||||
public class DeleteScriptCommandHandler(ScriptingDbContext db)
|
||||
: IRequestHandler<DeleteScriptCommand>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>禁用脚本函数处理器。置 Disabled 后动作链与 HTTP invoke 都将拒绝执行。</summary>
|
||||
public class DisableScriptCommandHandler(ScriptingDbContext db)
|
||||
: IRequestHandler<DisableScriptCommand, ScriptDto>
|
||||
{
|
||||
public async Task<ScriptDto> 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>脚本详情查询处理器:FindAsync 取实体,缺失抛 BusinessException。</summary>
|
||||
public sealed class GetScriptDetailQueryHandler(ScriptingDbContext db)
|
||||
: IRequestHandler<GetScriptDetailQuery, ScriptDto>
|
||||
{
|
||||
public async Task<ScriptDto> Handle(GetScriptDetailQuery request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Scripts.FindAsync([request.Id], ct)
|
||||
?? throw new BusinessException($"脚本 {request.Id} 不存在");
|
||||
return ScriptMapper.ToDto(entity);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>脚本执行日志查询处理器:按 FunctionId 过滤,可选状态过滤,按 CreatedAt 倒序分页。</summary>
|
||||
public sealed class GetScriptExecutionLogsQueryHandler(ScriptingDbContext db)
|
||||
: IRequestHandler<GetScriptExecutionLogsQuery, PagedResult<ScriptExecutionLogDto>>
|
||||
{
|
||||
public async Task<PagedResult<ScriptExecutionLogDto>> 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<ScriptExecutionLogDto>
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
PageIndex = request.PageIndex,
|
||||
PageSize = request.PageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>脚本列表查询处理器:过滤 + 按 UpdatedAt 倒序分页,投影到 ScriptDto。</summary>
|
||||
public sealed class GetScriptListQueryHandler(ScriptingDbContext db)
|
||||
: IRequestHandler<GetScriptListQuery, PagedResult<ScriptDto>>
|
||||
{
|
||||
public async Task<PagedResult<ScriptDto>> 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<ScriptDto>
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
PageIndex = request.PageIndex,
|
||||
PageSize = request.PageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
using Scripting.Application.DTOs;
|
||||
using Scripting.Application.Services;
|
||||
|
||||
namespace Scripting.Application.Handlers;
|
||||
|
||||
/// <summary>按 Name 调用脚本处理器,委托执行服务(仅 Published 脚本可调用)。</summary>
|
||||
public class InvokeScriptCommandHandler(IScriptExecutionService executionService)
|
||||
: IRequestHandler<InvokeScriptCommand, ScriptExecutionResult>
|
||||
{
|
||||
public Task<ScriptExecutionResult> Handle(InvokeScriptCommand request, CancellationToken ct)
|
||||
=> executionService.ExecuteByNameAsync(request.Name, request.InputJson, ct);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>发布脚本函数处理器。置 Published 并 Version++,便于后续审计变更。</summary>
|
||||
public class PublishScriptCommandHandler(ScriptingDbContext db)
|
||||
: IRequestHandler<PublishScriptCommand, ScriptDto>
|
||||
{
|
||||
public async Task<ScriptDto> 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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Commands;
|
||||
using Scripting.Application.DTOs;
|
||||
using Scripting.Application.Services;
|
||||
|
||||
namespace Scripting.Application.Handlers;
|
||||
|
||||
/// <summary>测试执行脚本处理器,委托执行服务以保证测试与运行时一致。</summary>
|
||||
public class TestScriptCommandHandler(IScriptExecutionService executionService)
|
||||
: IRequestHandler<TestScriptCommand, ScriptExecutionResult>
|
||||
{
|
||||
public Task<ScriptExecutionResult> Handle(TestScriptCommand request, CancellationToken ct)
|
||||
=> executionService.ExecuteTestAsync(request.Id, request.InputJson, ct);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 更新脚本函数处理器。Disabled 禁止编辑;Published 编辑保持已发布但 Version++(调用方可据此感知变更)。
|
||||
/// </summary>
|
||||
public class UpdateScriptCommandHandler(ScriptingDbContext db)
|
||||
: IRequestHandler<UpdateScriptCommand, ScriptDto>
|
||||
{
|
||||
public async Task<ScriptDto> 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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.DTOs;
|
||||
|
||||
namespace Scripting.Application.Queries;
|
||||
|
||||
/// <summary>按 Id 查询脚本函数详情。</summary>
|
||||
public sealed record GetScriptDetailQuery(Guid Id) : IRequest<ScriptDto>;
|
||||
@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Common;
|
||||
using Scripting.Application.DTOs;
|
||||
using Scripting.Domain.Enums;
|
||||
|
||||
namespace Scripting.Application.Queries;
|
||||
|
||||
/// <summary>分页查询某脚本的执行日志。注意底层实体字段是 FunctionId(映射自 ScriptId)。</summary>
|
||||
public sealed record GetScriptExecutionLogsQuery(
|
||||
Guid ScriptId,
|
||||
int PageIndex,
|
||||
int PageSize,
|
||||
ScriptExecutionStatus? Status = null) : IRequest<PagedResult<ScriptExecutionLogDto>>;
|
||||
@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
using Scripting.Application.Common;
|
||||
using Scripting.Application.DTOs;
|
||||
using Scripting.Domain.Enums;
|
||||
|
||||
namespace Scripting.Application.Queries;
|
||||
|
||||
/// <summary>分页查询脚本函数。支持按状态、表单作用域、关键字(Name/DisplayName)筛选。</summary>
|
||||
public sealed record GetScriptListQuery(
|
||||
int PageIndex,
|
||||
int PageSize,
|
||||
ScriptStatus? Status,
|
||||
string? Keyword,
|
||||
Guid? FormDefinitionId = null) : IRequest<PagedResult<ScriptDto>>;
|
||||
Loading…
x
Reference in New Issue
Block a user