From a978e7f8683ceb0c401fc56a4473ee3b06e06568 Mon Sep 17 00:00:00 2001 From: xiangning Date: Mon, 6 Jul 2026 18:03:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(workflow):=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=87=BD=E6=95=B0=E7=B1=BB=E5=9E=8B=E5=92=8C?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E4=B8=89=E9=98=B6=E6=AE=B5=E5=8A=A8=E4=BD=9C?= =?UTF-8?q?=E9=93=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 CustomFunctionType 枚举,支持 Script 和 Http 两种函数类型 - 在自定义函数中添加 Http 相关字段(HttpMethod、HttpUrl、HttpHeaders、HttpBodyTemplate) - 实现 HTTP 函数执行器,支持变量替换和 SSRF 防护 - 将按钮动作链拆分为 before、main、after 三个阶段 - 新增前置和后置动作 JSON 字段存储 - 实现批量执行多个函数的 executeFunctions 动作类型 - 更新数据库迁移以支持新的字段结构 - 调整相关 DTO、命令和处理器以兼容新功能 --- ...CreateBuildFlowButtonDefinitionEndpoint.cs | 2 + .../CreateBuildFlowButtonDefinitionRequest.cs | 4 +- .../CreateCustomFunctionEndpoint.cs | 5 + .../CreateCustomFunctionRequest.cs | 9 +- .../Menus/CreateMenuRequestValidator.cs | 7 +- .../Integration/HttpFunctionExecutor.cs | 189 ++ .../CreateBuildFlowButtonDefinitionCommand.cs | 4 +- .../DTOs/BuildFlowButtonDefinitionDto.cs | 4 +- ...BuildFlowButtonDefinitionCommandHandler.cs | 4 + ...BuildFlowButtonDefinitionCommandHandler.cs | 2 + ...ildFlowButtonDefinitionListQueryHandler.cs | 2 + ...BuildFlowButtonDefinitionCommandHandler.cs | 2 + .../Services/BuildFlowButtonActionExecutor.cs | 117 +- .../Commands/CreateCustomFunctionCommand.cs | 11 +- .../Commands/CustomFunctionMapper.cs | 5 + .../CustomFunctions/DTOs/CustomFunctionDto.cs | 7 +- .../CreateCustomFunctionCommandHandler.cs | 5 + .../GetCustomFunctionListQueryHandler.cs | 5 + .../CustomFunctionExecutionService.cs | 32 +- .../Services/IHttpFunctionExecutor.cs | 16 + ...tomFunctionTypeAndButtonPhases.Designer.cs | 2243 +++++++++++++++++ ...33_AddCustomFunctionTypeAndButtonPhases.cs | 138 + .../WorkflowDbContextModelSnapshot.cs | 41 +- .../BuildFlow/BuildFlowApplicationTests.cs | 18 +- .../BuildFlow/BuildFlowButtonActionTests.cs | 7 + .../BuildFlow/CustomFunctionTests.cs | 10 + 26 files changed, 2848 insertions(+), 41 deletions(-) create mode 100644 src/BuildFlow.Host/Services/Integration/HttpFunctionExecutor.cs create mode 100644 src/modules/workflow/Application/BuildFlow/CustomFunctions/Services/IHttpFunctionExecutor.cs create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260706092733_AddCustomFunctionTypeAndButtonPhases.Designer.cs create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260706092733_AddCustomFunctionTypeAndButtonPhases.cs diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Buttons/CreateBuildFlowButtonDefinitionEndpoint.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Buttons/CreateBuildFlowButtonDefinitionEndpoint.cs index 57ed8f4..5a94862 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Buttons/CreateBuildFlowButtonDefinitionEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Buttons/CreateBuildFlowButtonDefinitionEndpoint.cs @@ -30,7 +30,9 @@ public class CreateBuildFlowButtonDefinitionEndpoint(IMediator mediator) req.VisibleWhenJson, req.DisabledWhenJson, req.ConfirmJson, + req.BeforeActionsJson, req.ActionsJson, + req.AfterActionsJson, req.SortOrder), ct); await Send.ResponseAsync(result, 201, ct); diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Buttons/CreateBuildFlowButtonDefinitionRequest.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Buttons/CreateBuildFlowButtonDefinitionRequest.cs index c101674..f2e32a6 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Buttons/CreateBuildFlowButtonDefinitionRequest.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Buttons/CreateBuildFlowButtonDefinitionRequest.cs @@ -2,7 +2,7 @@ using Workflow.Domain.Enums; namespace BuildFlow.Host.Endpoints.Workflow.BuildFlow.Buttons; -/// 创建 BuildFlow 按钮定义请求。 +/// 创建 BuildFlow 按钮定义请求。含三阶段动作链(before/main/after)。 public sealed record CreateBuildFlowButtonDefinitionRequest( Guid? ApplicationId, Guid? PageDefinitionId, @@ -16,5 +16,7 @@ public sealed record CreateBuildFlowButtonDefinitionRequest( string? VisibleWhenJson, string? DisabledWhenJson, string? ConfirmJson, + string? BeforeActionsJson, string ActionsJson, + string? AfterActionsJson, int SortOrder); diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionEndpoint.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionEndpoint.cs index 65eb114..614141a 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionEndpoint.cs @@ -21,7 +21,12 @@ public class CreateCustomFunctionEndpoint(IMediator mediator) req.Name, req.DisplayName, req.Description, + req.Type, req.Code, + req.HttpMethod, + req.HttpUrl, + req.HttpHeaders, + req.HttpBodyTemplate, req.InputSchema, req.OutputSchema, req.TimeoutMs, diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionRequest.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionRequest.cs index cb19326..15f2caf 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionRequest.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionRequest.cs @@ -1,12 +1,19 @@ +using Workflow.Domain.Enums; + namespace BuildFlow.Host.Endpoints.Workflow.BuildFlow.CustomFunctions; -/// 创建自定义函数请求。 +/// 创建自定义函数请求。Type=Script 使用 Code;Type=Http 使用 HttpMethod/Url/Headers/BodyTemplate。 public sealed record CreateCustomFunctionRequest( Guid? FormDefinitionId, string Name, string DisplayName, string? Description, + CustomFunctionType Type, string Code, + string? HttpMethod, + string? HttpUrl, + string? HttpHeaders, + string? HttpBodyTemplate, string? InputSchema, string? OutputSchema, int TimeoutMs); diff --git a/src/BuildFlow.Host/Endpoints/platform/Menus/CreateMenuRequestValidator.cs b/src/BuildFlow.Host/Endpoints/platform/Menus/CreateMenuRequestValidator.cs index c50a0e5..7960fea 100644 --- a/src/BuildFlow.Host/Endpoints/platform/Menus/CreateMenuRequestValidator.cs +++ b/src/BuildFlow.Host/Endpoints/platform/Menus/CreateMenuRequestValidator.cs @@ -9,8 +9,11 @@ public class CreateMenuRequestValidator : Validator { public CreateMenuRequestValidator() { - RuleFor(x => x.Name).NotEmpty().WithMessage("菜单路由名称不能为空") - .MaximumLength(100).WithMessage("菜单路由名称长度不能超过 100 个字符"); + // Name 可选:留空时由后端根据 Code 派生,无需用户填写。 + When(x => x.Name is not null, () => + { + RuleFor(x => x.Name!).MaximumLength(100).WithMessage("菜单路由名称长度不能超过 100 个字符"); + }); RuleFor(x => x.Path).NotEmpty().WithMessage("路由路径不能为空") .MaximumLength(200).WithMessage("路由路径长度不能超过 200 个字符"); RuleFor(x => x.Title).NotEmpty().WithMessage("菜单标题不能为空") diff --git a/src/BuildFlow.Host/Services/Integration/HttpFunctionExecutor.cs b/src/BuildFlow.Host/Services/Integration/HttpFunctionExecutor.cs new file mode 100644 index 0000000..7c32763 --- /dev/null +++ b/src/BuildFlow.Host/Services/Integration/HttpFunctionExecutor.cs @@ -0,0 +1,189 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Sockets; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using BuildFlow.Shared.Domain.Common; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Workflow.Application.BuildFlow.CustomFunctions.Services; +using Workflow.Application.Notifications; +using Workflow.Domain.Entities; + +namespace BuildFlow.Host.Services.Integration; + +/// +/// HTTP 自定义函数执行器实现。 +/// 复用 Webhook 的 IHttpClientFactory("Webhook" 命名客户端)和 SSRF 白名单防护。 +/// 对 URL/Header/Body 中的 $input.xxx 占位符做变量替换后发起请求, +/// 非 2xx 响应、SSRF 拦截、超时均抛 BusinessException。 +/// +public class HttpFunctionExecutor( + IHttpClientFactory httpClientFactory, + IOptions options, + ILogger logger) : IHttpFunctionExecutor +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + private readonly NotificationOptions _options = options.Value; + + public async Task ExecuteAsync(CustomFunction function, string inputJson, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(function.HttpUrl)) + { + throw new BusinessException($"HTTP 函数 {function.DisplayName} 未配置目标 URL"); + } + + var url = ReplaceVariables(function.HttpUrl, inputJson); + if (!IsHostAllowed(url)) + { + logger.LogWarning("HTTP 函数 {FunctionId} URL {Url} 被拒绝(SSRF 白名单拦截)", function.Id, url); + throw new BusinessException($"HTTP 函数目标地址不在允许的白名单内:{new Uri(url).Host}"); + } + + var client = httpClientFactory.CreateClient("Webhook"); + using var request = new HttpRequestMessage( + new HttpMethod(string.IsNullOrWhiteSpace(function.HttpMethod) ? "POST" : function.HttpMethod.ToUpperInvariant()), + url); + + // 请求头 + if (!string.IsNullOrWhiteSpace(function.HttpHeaders)) + { + var headersJson = ReplaceVariables(function.HttpHeaders, inputJson); + var headers = JsonSerializer.Deserialize>(headersJson) + ?? new Dictionary(); + foreach (var (key, value) in headers) + { + request.Headers.TryAddWithoutValidation(key, value); + } + } + + // 请求体 + if (!string.IsNullOrWhiteSpace(function.HttpBodyTemplate)) + { + var body = ReplaceVariables(function.HttpBodyTemplate, inputJson); + request.Content = new StringContent(body, Encoding.UTF8, "application/json"); + } + + // 执行请求 + var response = await client.SendAsync(request, ct); + var responseBody = await response.Content.ReadAsStringAsync(ct); + + if (!response.IsSuccessStatusCode) + { + logger.LogWarning("HTTP 函数 {FunctionId} 调用 {Url} 返回非成功状态 {Status}", function.Id, url, (int)response.StatusCode); + throw new BusinessException( + $"HTTP 函数调用失败:{(int)response.StatusCode} {response.StatusCode}" + + (string.IsNullOrEmpty(responseBody) ? "" : $"\n{Truncate(responseBody, 500)}")); + } + + // 尝试将响应体解析为 JSON;若不是合法 JSON 则包装为 { result: "" } + try + { + using var doc = JsonDocument.Parse(responseBody); + return JsonSerializer.Serialize(doc.RootElement, JsonOptions); + } + catch (JsonException) + { + return JsonSerializer.Serialize(new { result = responseBody }, JsonOptions); + } + } + + /// + /// 替换模板中的 $input.xxx 占位符为 inputJson 中对应路径的值。 + /// 占位符出现在字符串中会被原地替换为 JSON 字面量(去掉外层引号的 JSON token)。 + /// + private static string ReplaceVariables(string template, string inputJson) + { + if (string.IsNullOrEmpty(template)) return template; + + using var input = string.IsNullOrWhiteSpace(inputJson) + ? JsonDocument.Parse("{}") + : JsonDocument.Parse(inputJson); + + var result = new StringBuilder(template); + var i = 0; + while ((i = result.ToString().IndexOf("$input.", i, StringComparison.Ordinal)) >= 0) + { + // 提取 $input. 后的路径(支持字母数字下划线和点) + var end = i + "$input.".Length; + while (end < result.Length && (char.IsLetterOrDigit(result[end]) || result[end] == '_' || result[end] == '.')) + { + end++; + } + var path = result.ToString(i + "$input.".Length, end - i - "$input.".Length); + var value = ReadPath(input.RootElement, path); + var literal = value switch + { + { } v when v.ValueKind == JsonValueKind.String => v.GetString() ?? "", + { } v when v.ValueKind == JsonValueKind.Number => v.GetRawText(), + { } v when v.ValueKind == JsonValueKind.True => "true", + { } v when v.ValueKind == JsonValueKind.False => "false", + { } v when v.ValueKind == JsonValueKind.Null => "null", + { } v when v.ValueKind == JsonValueKind.Object => v.GetRawText(), + { } v when v.ValueKind == JsonValueKind.Array => v.GetRawText(), + _ => "", + }; + result.Remove(i, end - i); + result.Insert(i, literal); + i += literal.Length; + } + return result.ToString(); + } + + private static JsonElement? ReadPath(JsonElement root, string path) + { + var current = root; + foreach (var segment in path.Split('.', StringSplitOptions.RemoveEmptyEntries)) + { + if (current.ValueKind != JsonValueKind.Object || + !current.TryGetProperty(segment, out current)) + { + return null; + } + } + return current; + } + + /// SSRF 防护:校验 URL 主机在白名单内,拒绝 localhost 与私有/内网 IP。 + internal bool IsHostAllowed(string url) + { + if (_options.Webhook.AllowedHosts is null || _options.Webhook.AllowedHosts.Length == 0) + { + return false; + } + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + return false; + } + + var host = uri.Host; + if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (IPAddress.TryParse(host, out var ip)) + { + if (IPAddress.IsLoopback(ip)) return false; + if (ip.AddressFamily == AddressFamily.InterNetwork) + { + var bytes = ip.GetAddressBytes(); + if (bytes[0] == 10) return false; + if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return false; + if (bytes[0] == 192 && bytes[1] == 168) return false; + } + } + + return _options.Webhook.AllowedHosts + .Any(allowed => string.Equals(allowed, host, StringComparison.OrdinalIgnoreCase)); + } + + private static string Truncate(string text, int maxLength) => + text.Length <= maxLength ? text : $"{text[..maxLength]}..."; +} diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/Commands/CreateBuildFlowButtonDefinitionCommand.cs b/src/modules/workflow/Application/BuildFlow/Buttons/Commands/CreateBuildFlowButtonDefinitionCommand.cs index 6e09314..ac37b09 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/Commands/CreateBuildFlowButtonDefinitionCommand.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/Commands/CreateBuildFlowButtonDefinitionCommand.cs @@ -4,7 +4,7 @@ using Workflow.Domain.Enums; namespace Workflow.Application.BuildFlow.Buttons.Commands; -/// 创建 BuildFlow 按钮定义命令。按钮动作链以后端元数据形式保存。 +/// 创建 BuildFlow 按钮定义命令。三阶段动作链(before/main/after)以后端元数据形式保存。 public sealed record CreateBuildFlowButtonDefinitionCommand( Guid? ApplicationId, Guid? PageDefinitionId, @@ -18,5 +18,7 @@ public sealed record CreateBuildFlowButtonDefinitionCommand( string? VisibleWhenJson, string? DisabledWhenJson, string? ConfirmJson, + string? BeforeActionsJson, string ActionsJson, + string? AfterActionsJson, int SortOrder) : IRequest; diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/DTOs/BuildFlowButtonDefinitionDto.cs b/src/modules/workflow/Application/BuildFlow/Buttons/DTOs/BuildFlowButtonDefinitionDto.cs index 1f5b51b..076a30c 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/DTOs/BuildFlowButtonDefinitionDto.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/DTOs/BuildFlowButtonDefinitionDto.cs @@ -2,7 +2,7 @@ using Workflow.Domain.Enums; namespace Workflow.Application.BuildFlow.Buttons.DTOs; -/// BuildFlow 按钮定义 DTO,用于运行时渲染按钮和动作配置。 +/// BuildFlow 按钮定义 DTO,用于运行时渲染按钮和动作配置。含三阶段动作链(before/main/after)。 public sealed record BuildFlowButtonDefinitionDto( Guid Id, Guid? ApplicationId, @@ -17,6 +17,8 @@ public sealed record BuildFlowButtonDefinitionDto( string? VisibleWhenJson, string? DisabledWhenJson, string? ConfirmJson, + string? BeforeActionsJson, string ActionsJson, + string? AfterActionsJson, BuildFlowMetadataStatus Status, int SortOrder); diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/CreateBuildFlowButtonDefinitionCommandHandler.cs b/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/CreateBuildFlowButtonDefinitionCommandHandler.cs index 5f66c91..51aedcf 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/CreateBuildFlowButtonDefinitionCommandHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/CreateBuildFlowButtonDefinitionCommandHandler.cs @@ -29,7 +29,9 @@ public class CreateBuildFlowButtonDefinitionCommandHandler(WorkflowDbContext db) VisibleWhenJson = request.VisibleWhenJson, DisabledWhenJson = request.DisabledWhenJson, ConfirmJson = request.ConfirmJson, + BeforeActionsJson = request.BeforeActionsJson, ActionsJson = string.IsNullOrWhiteSpace(request.ActionsJson) ? "[]" : request.ActionsJson, + AfterActionsJson = request.AfterActionsJson, SortOrder = request.SortOrder, }; db.BuildFlowButtons.Add(entity); @@ -52,7 +54,9 @@ public class CreateBuildFlowButtonDefinitionCommandHandler(WorkflowDbContext db) entity.VisibleWhenJson, entity.DisabledWhenJson, entity.ConfirmJson, + entity.BeforeActionsJson, entity.ActionsJson, + entity.AfterActionsJson, entity.Status, entity.SortOrder); } diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/DisableBuildFlowButtonDefinitionCommandHandler.cs b/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/DisableBuildFlowButtonDefinitionCommandHandler.cs index efefaa8..a9532c7 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/DisableBuildFlowButtonDefinitionCommandHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/DisableBuildFlowButtonDefinitionCommandHandler.cs @@ -38,7 +38,9 @@ public class DisableBuildFlowButtonDefinitionCommandHandler(WorkflowDbContext db entity.VisibleWhenJson, entity.DisabledWhenJson, entity.ConfirmJson, + entity.BeforeActionsJson, entity.ActionsJson, + entity.AfterActionsJson, entity.Status, entity.SortOrder); } diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/GetBuildFlowButtonDefinitionListQueryHandler.cs b/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/GetBuildFlowButtonDefinitionListQueryHandler.cs index de4d14b..6fbaaae 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/GetBuildFlowButtonDefinitionListQueryHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/GetBuildFlowButtonDefinitionListQueryHandler.cs @@ -66,7 +66,9 @@ public sealed class GetBuildFlowButtonDefinitionListQueryHandler(WorkflowDbConte x.VisibleWhenJson, x.DisabledWhenJson, x.ConfirmJson, + x.BeforeActionsJson, x.ActionsJson, + x.AfterActionsJson, x.Status, x.SortOrder)) .ToListAsync(ct); diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/PublishBuildFlowButtonDefinitionCommandHandler.cs b/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/PublishBuildFlowButtonDefinitionCommandHandler.cs index 988c6b6..8cf6a43 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/PublishBuildFlowButtonDefinitionCommandHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/Handlers/PublishBuildFlowButtonDefinitionCommandHandler.cs @@ -38,7 +38,9 @@ public class PublishBuildFlowButtonDefinitionCommandHandler(WorkflowDbContext db entity.VisibleWhenJson, entity.DisabledWhenJson, entity.ConfirmJson, + entity.BeforeActionsJson, entity.ActionsJson, + entity.AfterActionsJson, entity.Status, entity.SortOrder); } diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs b/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs index aced06f..6505c6f 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs @@ -51,34 +51,13 @@ public class BuildFlowButtonActionExecutor( var stopwatch = Stopwatch.StartNew(); var results = new List(); - var actions = ParseActions(button.ActionsJson); var input = ParseInput(inputJson); - foreach (var action in actions) - { - var type = action["type"]?.GetValue(); - if (type == "submitForm") - { - await SubmitFormAsync(button, action, input, ct); - continue; - } - - if (type != "runFunction") - { - throw new BusinessException($"按钮动作类型 {type} 暂不支持"); - } - - var functionIdText = action["target"]?.GetValue(); - if (!Guid.TryParse(functionIdText, out var functionId)) - { - throw new BusinessException("runFunction 动作缺少有效函数ID"); - } - - await EnsureFunctionScopeAsync(button, functionId, ct); - var functionInput = BuildFunctionInputJson(action["params"] as JsonObject, input); - var result = await functionExecutionService.ExecuteAsync(functionId, functionInput, ct); - results.Add(result); - } + // 三阶段执行:before(执行期/前置钩子)→ main(执行中/主链)→ after(执行后/后置钩子) + // 任一阶段失败即中断,input 对象在阶段间和动作间流转 + await ExecuteActionsList(button, button.BeforeActionsJson, input, results, "前置动作", ct); + await ExecuteActionsList(button, button.ActionsJson, input, results, "主动作", ct); + await ExecuteActionsList(button, button.AfterActionsJson, input, results, "后置动作", ct); stopwatch.Stop(); var resultJson = JsonSerializer.Serialize(results, JsonOptions); @@ -96,6 +75,92 @@ public class BuildFlowButtonActionExecutor( return new BuildFlowButtonActionResult(true, results); } + /// + /// 执行一条动作链(before/main/after 共用)。 + /// actionsJson 为空或 "[]" 时直接返回。任一动作失败即中断。 + /// + private async Task ExecuteActionsList( + BuildFlowButtonDefinition button, + string? actionsJson, + JsonObject input, + List results, + string phaseLabel, + CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(actionsJson)) + { + return; + } + + var actions = ParseActions(actionsJson); + foreach (var action in actions) + { + await ExecuteSingleAction(button, action, input, results, phaseLabel, ct); + } + } + + private async Task ExecuteSingleAction( + BuildFlowButtonDefinition button, + JsonObject action, + JsonObject input, + List results, + string phaseLabel, + CancellationToken ct) + { + var type = action["type"]?.GetValue(); + + if (type == "submitForm") + { + await SubmitFormAsync(button, action, input, ct); + return; + } + + if (type == "runFunction") + { + var functionId = ResolveFunctionId(action, "runFunction"); + await EnsureFunctionScopeAsync(button, functionId, ct); + var functionInput = BuildFunctionInputJson(action["params"] as JsonObject, input); + var result = await functionExecutionService.ExecuteAsync(functionId, functionInput, ct); + results.Add(result); + return; + } + + if (type == "executeFunctions") + { + // 批量执行多个函数,targets 是函数 ID 数组 + var targets = action["targets"]; + if (targets is not JsonArray targetArray || targetArray.Count == 0) + { + throw new BusinessException("executeFunctions 动作缺少 targets 函数ID数组"); + } + foreach (var targetNode in targetArray) + { + var functionIdText = targetNode?.GetValue(); + if (!Guid.TryParse(functionIdText, out var functionId)) + { + throw new BusinessException("executeFunctions 动作 targets 包含无效函数ID"); + } + await EnsureFunctionScopeAsync(button, functionId, ct); + var functionInput = BuildFunctionInputJson(action["params"] as JsonObject, input); + var result = await functionExecutionService.ExecuteAsync(functionId, functionInput, ct); + results.Add(result); + } + return; + } + + throw new BusinessException($"按钮{phaseLabel}类型 {type} 暂不支持"); + } + + private static Guid ResolveFunctionId(JsonObject action, string actionType) + { + var functionIdText = action["target"]?.GetValue(); + if (!Guid.TryParse(functionIdText, out var functionId)) + { + throw new BusinessException($"{actionType} 动作缺少有效函数ID"); + } + return functionId; + } + private async Task EnsureFunctionScopeAsync( BuildFlowButtonDefinition button, Guid functionId, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CreateCustomFunctionCommand.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CreateCustomFunctionCommand.cs index 5df042b..ed873e1 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CreateCustomFunctionCommand.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CreateCustomFunctionCommand.cs @@ -1,14 +1,23 @@ using MediatR; using Workflow.Application.BuildFlow.CustomFunctions.DTOs; +using Workflow.Domain.Enums; namespace Workflow.Application.BuildFlow.CustomFunctions.Commands; -/// 创建自定义函数命令。新函数默认是草稿,必须发布后才能被动作链执行。 +/// +/// 创建自定义函数命令。新函数默认是草稿,必须发布后才能被动作链执行。 +/// Type=Script 时使用 Code 字段;Type=Http 时使用 HttpMethod/HttpUrl/HttpHeaders/HttpBodyTemplate。 +/// public sealed record CreateCustomFunctionCommand( string Name, string DisplayName, string? Description, + CustomFunctionType Type, string Code, + string? HttpMethod, + string? HttpUrl, + string? HttpHeaders, + string? HttpBodyTemplate, string? InputSchema, string? OutputSchema, int TimeoutMs, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CustomFunctionMapper.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CustomFunctionMapper.cs index 19a043e..c10cbf2 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CustomFunctionMapper.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CustomFunctionMapper.cs @@ -13,7 +13,12 @@ internal static class CustomFunctionMapper entity.Name, entity.DisplayName, entity.Description, + entity.Type, entity.Code, + entity.HttpMethod, + entity.HttpUrl, + entity.HttpHeaders, + entity.HttpBodyTemplate, entity.InputSchema, entity.OutputSchema, entity.Status, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/DTOs/CustomFunctionDto.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/DTOs/CustomFunctionDto.cs index 959fdd5..bb72cfc 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/DTOs/CustomFunctionDto.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/DTOs/CustomFunctionDto.cs @@ -2,14 +2,19 @@ using Workflow.Domain.Enums; namespace Workflow.Application.BuildFlow.CustomFunctions.DTOs; -/// 自定义函数 DTO,用于函数管理页面和按钮动作选择器。 +/// 自定义函数 DTO,用于函数管理页面和按钮动作选择器。含执行类型和 HTTP 配置。 public sealed record CustomFunctionDto( Guid Id, Guid? FormDefinitionId, string Name, string DisplayName, string? Description, + CustomFunctionType Type, string Code, + string? HttpMethod, + string? HttpUrl, + string? HttpHeaders, + string? HttpBodyTemplate, string? InputSchema, string? OutputSchema, BuildFlowMetadataStatus Status, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/CreateCustomFunctionCommandHandler.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/CreateCustomFunctionCommandHandler.cs index 2b8dfdd..becf8b0 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/CreateCustomFunctionCommandHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/CreateCustomFunctionCommandHandler.cs @@ -24,7 +24,12 @@ public class CreateCustomFunctionCommandHandler(WorkflowDbContext db) Name = request.Name, DisplayName = request.DisplayName, Description = request.Description, + Type = request.Type, Code = request.Code, + HttpMethod = request.HttpMethod, + HttpUrl = request.HttpUrl, + HttpHeaders = request.HttpHeaders, + HttpBodyTemplate = request.HttpBodyTemplate, InputSchema = request.InputSchema, OutputSchema = request.OutputSchema, TimeoutMs = request.TimeoutMs <= 0 ? 5000 : request.TimeoutMs, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/GetCustomFunctionListQueryHandler.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/GetCustomFunctionListQueryHandler.cs index 125ab4f..67160f9 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/GetCustomFunctionListQueryHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/GetCustomFunctionListQueryHandler.cs @@ -44,7 +44,12 @@ public sealed class GetCustomFunctionListQueryHandler(WorkflowDbContext db) x.Name, x.DisplayName, x.Description, + x.Type, x.Code, + x.HttpMethod, + x.HttpUrl, + x.HttpHeaders, + x.HttpBodyTemplate, x.InputSchema, x.OutputSchema, x.Status, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Services/CustomFunctionExecutionService.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Services/CustomFunctionExecutionService.cs index 59aa1a3..c77d37a 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Services/CustomFunctionExecutionService.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Services/CustomFunctionExecutionService.cs @@ -7,13 +7,17 @@ using Workflow.Application.BuildFlow.CustomFunctions.DTOs; using Workflow.Domain.Entities; using Workflow.Domain.Enums; using Workflow.Infrastructure.Persistence; +using BuildFlow.Shared.Domain.Common; namespace Workflow.Application.BuildFlow.CustomFunctions.Services; /// /// 自定义函数执行服务。仅运行已发布函数,使用超时令牌限制脚本执行,并写入函数执行日志。 +/// 按 function.Type 分派到脚本执行器(CSharpScript)或 HTTP 执行器(IHttpFunctionExecutor)。 /// -public class CustomFunctionExecutionService(WorkflowDbContext db) : ICustomFunctionExecutionService +public class CustomFunctionExecutionService( + WorkflowDbContext db, + IHttpFunctionExecutor? httpExecutor = null) : ICustomFunctionExecutionService { private static readonly JsonSerializerOptions JsonOptions = new() { @@ -43,9 +47,24 @@ public class CustomFunctionExecutionService(WorkflowDbContext db) : ICustomFunct { using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(function.TimeoutMs <= 0 ? 5000 : function.TimeoutMs)); - var result = await EvaluateAsync(function.Code, inputJson, timeoutCts.Token); + + string resultJson; + if (function.Type == CustomFunctionType.Http) + { + if (httpExecutor is null) + { + throw new BusinessException("HTTP 函数执行器未注册,无法执行 HTTP 类型函数"); + } + var httpResult = await httpExecutor.ExecuteAsync(function, inputJson, timeoutCts.Token); + resultJson = httpResult; + } + else + { + var result = await EvaluateAsync(function.Code, inputJson, timeoutCts.Token); + resultJson = JsonSerializer.Serialize(result, JsonOptions); + } + stopwatch.Stop(); - var resultJson = JsonSerializer.Serialize(result, JsonOptions); await AddLogAsync(function, CustomFunctionExecutionStatus.Success, inputJson, resultJson, null, stopwatch.ElapsedMilliseconds, ct); return new CustomFunctionExecutionResult( CustomFunctionExecutionStatus.Success, @@ -60,6 +79,13 @@ public class CustomFunctionExecutionService(WorkflowDbContext db) : ICustomFunct await AddLogAsync(function, CustomFunctionExecutionStatus.Timeout, inputJson, null, message, stopwatch.ElapsedMilliseconds, CancellationToken.None); throw new BusinessException(message); } + catch (BusinessException) + { + // HTTP 执行器抛出的 BusinessException(如 SSRF 拦截、非 2xx)直接冒泡,不包裹 + stopwatch.Stop(); + await AddLogAsync(function, CustomFunctionExecutionStatus.Failed, inputJson, null, "HTTP 函数执行失败", stopwatch.ElapsedMilliseconds, ct); + throw; + } catch (Exception ex) when (ex is CompilationErrorException or InvalidOperationException or JsonException) { stopwatch.Stop(); diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Services/IHttpFunctionExecutor.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Services/IHttpFunctionExecutor.cs new file mode 100644 index 0000000..ba6cd04 --- /dev/null +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Services/IHttpFunctionExecutor.cs @@ -0,0 +1,16 @@ +using Workflow.Domain.Entities; + +namespace Workflow.Application.BuildFlow.CustomFunctions.Services; + +/// +/// HTTP 函数执行器抽象。实现位于 Host 层(可访问 IHttpClientFactory 和 SSRF 白名单配置)。 +/// 由 CustomFunctionExecutionService 按 function.Type=Http 分派调用。 +/// +public interface IHttpFunctionExecutor +{ + /// + /// 执行 HTTP 类型函数。返回响应体 JSON 字符串。 + /// 超时、SSRF 校验、状态码非 2xx 均抛 BusinessException。 + /// + Task ExecuteAsync(CustomFunction function, string inputJson, CancellationToken ct); +} diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/20260706092733_AddCustomFunctionTypeAndButtonPhases.Designer.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260706092733_AddCustomFunctionTypeAndButtonPhases.Designer.cs new file mode 100644 index 0000000..e47aff9 --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260706092733_AddCustomFunctionTypeAndButtonPhases.Designer.cs @@ -0,0 +1,2243 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Workflow.Infrastructure.Persistence; + +#nullable disable + +namespace Workflow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(WorkflowDbContext))] + [Migration("20260706092733_AddCustomFunctionTypeAndButtonPhases")] + partial class AddCustomFunctionTypeAndButtonPhases + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowActionExecutionLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ButtonCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("button_code") + .HasComment("按钮编码"); + + b.Property("ButtonDefinitionId") + .HasColumnType("uuid") + .HasColumnName("button_definition_id") + .HasComment("按钮定义ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms") + .HasComment("执行耗时(毫秒)"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error_message") + .HasComment("错误信息"); + + b.Property("InputJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("input_json") + .HasComment("按钮输入JSON"); + + b.Property("ResultJson") + .HasColumnType("jsonb") + .HasColumnName("result_json") + .HasComment("动作链结果JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("执行状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_action_execution_logs"); + + b.HasIndex("ButtonDefinitionId") + .HasDatabaseName("ix_b_action_execution_logs_button_definition_id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_b_action_execution_logs_created_at"); + + b.ToTable("b_action_execution_logs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowApplication", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("应用编码,平台内唯一"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description") + .HasComment("应用描述"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("应用名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("应用状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("应用元数据版本"); + + b.HasKey("Id") + .HasName("pk_b_apps"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_b_apps_code"); + + b.ToTable("b_apps", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowButtonDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ActionsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("actions_json") + .HasComment("执行中(主链)动作链JSON"); + + b.Property("AfterActionsJson") + .HasColumnType("jsonb") + .HasColumnName("after_actions_json") + .HasComment("执行后(后置)动作链JSON"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id") + .HasComment("所属 BuildFlow 应用ID"); + + b.Property("BeforeActionsJson") + .HasColumnType("jsonb") + .HasColumnName("before_actions_json") + .HasComment("执行期(前置)动作链JSON"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("按钮编码,作用域内唯一"); + + b.Property("ConfirmJson") + .HasColumnType("jsonb") + .HasColumnName("confirm_json") + .HasComment("按钮确认弹窗JSON"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DisabledWhenJson") + .HasColumnType("jsonb") + .HasColumnName("disabled_when_json") + .HasComment("按钮禁用条件JSON"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联表单定义ID"); + + b.Property("Icon") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon") + .HasComment("按钮图标"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("label") + .HasComment("按钮显示文本"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PageDefinitionId") + .HasColumnType("uuid") + .HasColumnName("page_definition_id") + .HasComment("所属页面定义ID"); + + b.Property("PermissionCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("permission_code") + .HasComment("按钮权限码"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement") + .HasComment("按钮展示位置"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order") + .HasComment("排序号"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("按钮状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("VisibleWhenJson") + .HasColumnType("jsonb") + .HasColumnName("visible_when_json") + .HasComment("按钮可见条件JSON"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uuid") + .HasColumnName("workflow_definition_id") + .HasComment("关联流程定义ID"); + + b.HasKey("Id") + .HasName("pk_b_buttons"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_buttons_code"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_buttons_form_definition_id"); + + b.HasIndex("PageDefinitionId") + .HasDatabaseName("ix_b_buttons_page_definition_id"); + + b.ToTable("b_buttons", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowFormShare", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("AllowAnonymous") + .HasColumnType("boolean") + .HasColumnName("allow_anonymous") + .HasComment("是否允许匿名访问"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at") + .HasComment("过期时间(UTC),null 表示永不过期"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("被分享的表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("MaxViews") + .HasColumnType("integer") + .HasColumnName("max_views") + .HasComment("最大查看次数,null 表示不限"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password") + .HasComment("bcrypt 密码哈希,null 表示无需密码"); + + b.Property("Permission") + .HasColumnType("integer") + .HasColumnName("permission") + .HasComment("分享权限级别(View/Edit/Admin)"); + + b.Property("Remark") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("remark") + .HasComment("分享备注"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token") + .HasComment("匿名访问 token(32位hex)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("ViewCount") + .HasColumnType("integer") + .HasColumnName("view_count") + .HasComment("已被查看次数"); + + b.HasKey("Id") + .HasName("pk_b_form_shares"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_shares_form_definition_id"); + + b.HasIndex("Token") + .IsUnique() + .HasDatabaseName("ix_b_form_shares_token"); + + b.ToTable("b_form_shares", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowImportJob", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("ErrorJson") + .HasColumnType("jsonb") + .HasColumnName("error_json") + .HasComment("错误明细JSON数组"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("error_message") + .HasComment("任务级错误消息"); + + b.Property("FailedRows") + .HasColumnType("integer") + .HasColumnName("failed_rows") + .HasComment("失败行数"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("file_url") + .HasComment("上传文件存储路径"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at") + .HasComment("处理完成时间(UTC)"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("导入目标表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("OriginalFileName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("original_file_name") + .HasComment("原始上传文件名"); + + b.Property("ResultUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("result_url") + .HasComment("结果报告下载URL"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasComment("处理开始时间(UTC)"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("任务状态(Pending/Running/Success/Failed/PartialSuccess)"); + + b.Property("SuccessRows") + .HasColumnType("integer") + .HasColumnName("success_rows") + .HasComment("成功行数"); + + b.Property("TotalRows") + .HasColumnType("integer") + .HasColumnName("total_rows") + .HasComment("总行数(不含表头)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_import_jobs"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_import_jobs_form_definition_id"); + + b.HasIndex("Status") + .HasDatabaseName("ix_b_import_jobs_status"); + + b.ToTable("b_import_jobs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowPageDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id") + .HasComment("所属 BuildFlow 应用ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("页面编码,应用内唯一"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("页面名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PageType") + .HasColumnType("integer") + .HasColumnName("page_type") + .HasComment("页面类型"); + + b.Property("SchemaJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("页面定义JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("页面状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("页面元数据版本"); + + b.HasKey("Id") + .HasName("pk_b_pages"); + + b.HasIndex("PageType") + .HasDatabaseName("ix_b_pages_page_type"); + + b.HasIndex("ApplicationId", "Code") + .IsUnique() + .HasDatabaseName("ix_b_pages_application_id_code"); + + b.ToTable("b_pages", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.CustomFunction", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text") + .HasColumnName("code") + .HasComment("C# 脚本代码(Script类型使用)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description") + .HasComment("函数说明"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name") + .HasComment("函数显示名称"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("绑定的自定义表单定义ID,为空表示全局可复用函数"); + + b.Property("HttpBodyTemplate") + .HasColumnType("text") + .HasColumnName("http_body_template") + .HasComment("HTTP请求体模板(Http类型使用)"); + + b.Property("HttpHeaders") + .HasColumnType("jsonb") + .HasColumnName("http_headers") + .HasComment("HTTP请求头JSON(Http类型使用)"); + + b.Property("HttpMethod") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("http_method") + .HasComment("HTTP方法(Http类型使用)"); + + b.Property("HttpUrl") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("http_url") + .HasComment("HTTP目标URL(Http类型使用)"); + + b.Property("InputSchema") + .HasColumnType("jsonb") + .HasColumnName("input_schema") + .HasComment("函数输入 Schema JSON"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("函数英文标识"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("OutputSchema") + .HasColumnType("jsonb") + .HasColumnName("output_schema") + .HasComment("函数输出 Schema JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("函数状态"); + + b.Property("TimeoutMs") + .HasColumnType("integer") + .HasColumnName("timeout_ms") + .HasComment("执行超时时间(毫秒)"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type") + .HasComment("函数执行类型:Script=受控C#脚本;Http=HTTP请求"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("函数版本"); + + b.HasKey("Id") + .HasName("pk_b_funcs"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_funcs_form_definition_id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_b_funcs_name"); + + b.ToTable("b_funcs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.CustomFunctionExecutionLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms") + .HasComment("执行耗时(毫秒)"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error_message") + .HasComment("错误信息"); + + b.Property("FunctionId") + .HasColumnType("uuid") + .HasColumnName("function_id") + .HasComment("函数ID"); + + b.Property("FunctionName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("function_name") + .HasComment("函数英文标识"); + + b.Property("InputJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("input_json") + .HasComment("执行输入JSON"); + + b.Property("ResultJson") + .HasColumnType("jsonb") + .HasColumnName("result_json") + .HasComment("执行结果JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("执行状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_func_execution_logs"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_b_func_execution_logs_created_at"); + + b.HasIndex("FunctionId") + .HasDatabaseName("ix_b_func_execution_logs_function_id"); + + b.ToTable("b_func_execution_logs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormComponentRegistry", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("组件分类"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefaultSchema") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("default_schema") + .HasComment("组件默认Schema JSON"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("display_name") + .HasComment("组件显示名称"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon") + .HasComment("组件图标"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active") + .HasComment("是否启用"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("组件名称(英文标识)"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order") + .HasComment("排序号"); + + b.Property("SupportedProps") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("supported_props") + .HasComment("组件支持的属性配置JSON"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_form_component_registry"); + + b.HasIndex("Category") + .HasDatabaseName("ix_b_form_component_registry_category"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_b_form_component_registry_name"); + + b.ToTable("b_form_component_registry", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormData", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("data_json") + .HasComment("表单数据JSON"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("关联的流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_form_data"); + + b.HasIndex("DataJson") + .HasDatabaseName("ix_b_form_data_data_json"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("DataJson"), "GIN"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("DataJson"), new[] { "jsonb_path_ops" }); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_data_form_definition_id"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_form_data_instance_id"); + + b.ToTable("b_form_data", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("code") + .HasComment("表单编码,唯一标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description") + .HasComment("表单描述"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("表单名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SchemaJson") + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("表单Schema JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("表单状态(Draft=草稿, Published=已发布, Disabled=已禁用)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_form_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_form_definitions_code"); + + b.ToTable("b_form_definitions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormDefinitionVersion", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ChangeSummary") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("change_summary") + .HasComment("变更摘要"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("所属表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SchemaJson") + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("该版本的Schema JSON快照"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source") + .HasComment("触发来源 Update/Publish"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_form_definition_versions"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_definition_versions_form_definition_id"); + + b.HasIndex("FormDefinitionId", "Version") + .HasDatabaseName("ix_b_form_definition_versions_form_definition_id_version"); + + b.ToTable("b_form_definition_versions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("通知分类"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("content") + .HasComment("通知正文"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_read") + .HasComment("是否已读"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("read_at") + .HasComment("阅读时间"); + + b.Property("RecipientRole") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("recipient_role") + .HasComment("收件角色名(role:规则)"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("recipient_user_id") + .HasComment("收件人用户ID(user:规则)"); + + b.Property("RelatedInstanceId") + .HasColumnType("uuid") + .HasColumnName("related_instance_id") + .HasComment("关联流程实例ID"); + + b.Property("RelatedTaskId") + .HasColumnType("uuid") + .HasColumnName("related_task_id") + .HasComment("关联任务ID"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("通知标题"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_notifications"); + + b.HasIndex("RelatedInstanceId") + .HasDatabaseName("ix_wf_notifications_related_instance_id"); + + b.HasIndex("RecipientUserId", "IsRead") + .HasDatabaseName("ix_wf_notifications_recipient_user_id_is_read"); + + b.ToTable("b_notifications", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WebhookDelivery", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts") + .HasComment("已尝试次数"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("通知分类"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("http_method") + .HasComment("HTTP方法"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error") + .HasComment("最近一次错误信息"); + + b.Property("MaxAttempts") + .HasColumnType("integer") + .HasColumnName("max_attempts") + .HasComment("最大尝试次数"); + + b.Property("NextRetryAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("next_retry_at") + .HasComment("下次重试时间"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasComment("投递JSON body"); + + b.Property("RelatedInstanceId") + .HasColumnType("uuid") + .HasColumnName("related_instance_id") + .HasComment("关联流程实例ID"); + + b.Property("RelatedTaskId") + .HasColumnType("uuid") + .HasColumnName("related_task_id") + .HasComment("关联任务ID"); + + b.Property("ResponseBody") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("response_body") + .HasComment("响应体"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status") + .HasComment("投递状态:pending/delivered/failed"); + + b.Property("StatusCode") + .HasColumnType("integer") + .HasColumnName("status_code") + .HasComment("HTTP响应码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("url") + .HasComment("目标URL"); + + b.HasKey("Id") + .HasName("pk_b_webhook_deliveries"); + + b.HasIndex("NextRetryAt") + .HasDatabaseName("ix_wf_webhook_deliveries_next_retry_at"); + + b.HasIndex("Status") + .HasDatabaseName("ix_wf_webhook_deliveries_status"); + + b.ToTable("b_webhook_deliveries", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("code") + .HasComment("工作流编码,唯一标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionJson") + .HasColumnType("jsonb") + .HasColumnName("definition_json") + .HasComment("工作流定义JSON(含节点/边的完整结构)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description") + .HasComment("工作流描述"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled") + .HasComment("是否启用"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("工作流名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("定义状态(Draft=草稿, Published=已发布)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_workflow_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_workflow_definitions_code"); + + b.ToTable("b_workflow_definitions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinitionSnapshot", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ChangeSummary") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("change_summary") + .HasComment("变更摘要"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("DefinitionJson") + .HasColumnType("jsonb") + .HasColumnName("definition_json") + .HasComment("该版本的完整X6 graph JSON快照(不可变)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source") + .HasComment("触发来源 Publish"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号(发布时的 WorkflowDefinition.Version)"); + + b.HasKey("Id") + .HasName("pk_b_workflow_definition_snapshots"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_definition_snapshots_definition_id"); + + b.HasIndex("DefinitionId", "Version") + .IsUnique() + .HasDatabaseName("ix_b_workflow_definition_snapshots_definition_id_version"); + + b.ToTable("b_workflow_definition_snapshots", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Condition") + .HasColumnType("text") + .HasColumnName("condition") + .HasComment("条件表达式"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("EdgeType") + .HasColumnType("integer") + .HasColumnName("edge_type") + .HasComment("边类型(Normal=普通, Condition=条件)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Label") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("label") + .HasComment("边标签"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Order") + .HasColumnType("integer") + .HasColumnName("order") + .HasComment("排序号"); + + b.Property("SourceNodeId") + .HasColumnType("uuid") + .HasColumnName("source_node_id") + .HasComment("起始节点ID"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id") + .HasComment("目标节点ID"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_edges"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_edges_definition_id"); + + b.ToTable("b_workflow_edges", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("完成时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("definition_code") + .HasComment("启动时快照的流程定义Code(审计追溯)"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("关联的工作流定义ID"); + + b.Property("DefinitionVersion") + .HasColumnType("integer") + .HasColumnName("definition_version") + .HasComment("启动时绑定的流程定义版本号(版本隔离)"); + + b.Property("InitiatorId") + .HasColumnType("uuid") + .HasColumnName("initiator_id") + .HasComment("发起人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("ParentInstanceId") + .HasColumnType("uuid") + .HasColumnName("parent_instance_id") + .HasComment("父流程实例ID(子流程场景)"); + + b.Property("ParentTokenId") + .HasColumnType("uuid") + .HasColumnName("parent_token_id") + .HasComment("父流程中等待的令牌ID"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasComment("启动时间"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("实例状态(Running/Suspended/Completed/Terminated)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("流程实例标题"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Variables") + .HasColumnType("jsonb") + .HasColumnName("variables") + .HasComment("流程变量JSON"); + + b.HasKey("Id") + .HasName("pk_b_workflow_instances"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_instances_definition_id"); + + b.ToTable("b_workflow_instances", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Config") + .HasColumnType("jsonb") + .HasColumnName("config") + .HasComment("节点配置JSON(审批人、表单绑定等)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID,用于审批/抄送节点"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("节点名称"); + + b.Property("NodeType") + .HasColumnType("integer") + .HasColumnName("node_type") + .HasComment("节点类型(Start/End/Approval/Cc/Condition/Parallel/SubProcess)"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PositionX") + .HasColumnType("integer") + .HasColumnName("position_x") + .HasComment("画布X坐标"); + + b.Property("PositionY") + .HasColumnType("integer") + .HasColumnName("position_y") + .HasComment("画布Y坐标"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_nodes"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_nodes_definition_id"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_workflow_nodes_form_definition_id"); + + b.ToTable("b_workflow_nodes", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("AssigneeId") + .HasColumnType("uuid") + .HasColumnName("assignee_id") + .HasComment("当前处理人ID"); + + b.Property("AssigneeRole") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("assignee_role") + .HasComment("当前处理人角色"); + + b.Property("Attachments") + .HasColumnType("text") + .HasColumnName("attachments"); + + b.Property("CandidateRoles") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("candidate_roles") + .HasComment("候选角色列表,逗号分隔"); + + b.Property("CandidateUsers") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("candidate_users") + .HasComment("候选用户ID列表,逗号分隔"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("comment") + .HasComment("审批意见"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("完成时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DelegatedFromId") + .HasColumnType("uuid") + .HasColumnName("delegated_from_id") + .HasComment("原处理人ID(委托场景)"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("due_at") + .HasComment("截止时间"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("所属流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id") + .HasComment("关联的节点定义ID"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Result") + .HasColumnType("jsonb") + .HasColumnName("result") + .HasComment("处理结果JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("任务状态(Pending/Approved/Rejected/Transferred/Delegated)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("任务标题"); + + b.Property("TokenId") + .HasColumnType("uuid") + .HasColumnName("token_id") + .HasComment("关联的令牌ID"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type") + .HasComment("任务类型(Approval=审批, Cc=抄送)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_tasks"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_workflow_tasks_instance_id"); + + b.ToTable("b_workflow_tasks", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ArrivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("arrived_at") + .HasComment("到达当前节点的时间"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("令牌完成/消费时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("所属流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id") + .HasComment("当前所在节点ID"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("令牌状态(Active/Consumed/Terminated)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_tokens"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_workflow_tokens_instance_id"); + + b.ToTable("b_workflow_tokens", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null) + .WithMany("Edges") + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_edges_b_workflow_definitions_definition_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null) + .WithMany("Nodes") + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_nodes_b_workflow_definitions_definition_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null) + .WithMany("Tasks") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_tasks_b_workflow_instances_instance_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null) + .WithMany("Tokens") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_tokens_b_workflow_instances_instance_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b => + { + b.Navigation("Edges"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b => + { + b.Navigation("Tasks"); + + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/20260706092733_AddCustomFunctionTypeAndButtonPhases.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260706092733_AddCustomFunctionTypeAndButtonPhases.cs new file mode 100644 index 0000000..e3c86e9 --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260706092733_AddCustomFunctionTypeAndButtonPhases.cs @@ -0,0 +1,138 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Workflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCustomFunctionTypeAndButtonPhases : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "code", + table: "b_funcs", + type: "text", + nullable: false, + comment: "C# 脚本代码(Script类型使用)", + oldClrType: typeof(string), + oldType: "text", + oldComment: "C# 脚本代码"); + + migrationBuilder.AddColumn( + name: "http_body_template", + table: "b_funcs", + type: "text", + nullable: true, + comment: "HTTP请求体模板(Http类型使用)"); + + migrationBuilder.AddColumn( + name: "http_headers", + table: "b_funcs", + type: "jsonb", + nullable: true, + comment: "HTTP请求头JSON(Http类型使用)"); + + migrationBuilder.AddColumn( + name: "http_method", + table: "b_funcs", + type: "character varying(20)", + maxLength: 20, + nullable: true, + comment: "HTTP方法(Http类型使用)"); + + migrationBuilder.AddColumn( + name: "http_url", + table: "b_funcs", + type: "character varying(2000)", + maxLength: 2000, + nullable: true, + comment: "HTTP目标URL(Http类型使用)"); + + migrationBuilder.AddColumn( + name: "type", + table: "b_funcs", + type: "integer", + nullable: false, + defaultValue: 0, + comment: "函数执行类型:Script=受控C#脚本;Http=HTTP请求"); + + migrationBuilder.AlterColumn( + name: "actions_json", + table: "b_buttons", + type: "jsonb", + nullable: false, + comment: "执行中(主链)动作链JSON", + oldClrType: typeof(string), + oldType: "jsonb", + oldComment: "按钮动作链JSON"); + + migrationBuilder.AddColumn( + name: "after_actions_json", + table: "b_buttons", + type: "jsonb", + nullable: true, + comment: "执行后(后置)动作链JSON"); + + migrationBuilder.AddColumn( + name: "before_actions_json", + table: "b_buttons", + type: "jsonb", + nullable: true, + comment: "执行期(前置)动作链JSON"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "http_body_template", + table: "b_funcs"); + + migrationBuilder.DropColumn( + name: "http_headers", + table: "b_funcs"); + + migrationBuilder.DropColumn( + name: "http_method", + table: "b_funcs"); + + migrationBuilder.DropColumn( + name: "http_url", + table: "b_funcs"); + + migrationBuilder.DropColumn( + name: "type", + table: "b_funcs"); + + migrationBuilder.DropColumn( + name: "after_actions_json", + table: "b_buttons"); + + migrationBuilder.DropColumn( + name: "before_actions_json", + table: "b_buttons"); + + migrationBuilder.AlterColumn( + name: "code", + table: "b_funcs", + type: "text", + nullable: false, + comment: "C# 脚本代码", + oldClrType: typeof(string), + oldType: "text", + oldComment: "C# 脚本代码(Script类型使用)"); + + migrationBuilder.AlterColumn( + name: "actions_json", + table: "b_buttons", + type: "jsonb", + nullable: false, + comment: "按钮动作链JSON", + oldClrType: typeof(string), + oldType: "jsonb", + oldComment: "执行中(主链)动作链JSON"); + } + } +} diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs index ba8445e..759ec2e 100644 --- a/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs @@ -200,13 +200,23 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations .IsRequired() .HasColumnType("jsonb") .HasColumnName("actions_json") - .HasComment("按钮动作链JSON"); + .HasComment("执行中(主链)动作链JSON"); + + b.Property("AfterActionsJson") + .HasColumnType("jsonb") + .HasColumnName("after_actions_json") + .HasComment("执行后(后置)动作链JSON"); b.Property("ApplicationId") .HasColumnType("uuid") .HasColumnName("application_id") .HasComment("所属 BuildFlow 应用ID"); + b.Property("BeforeActionsJson") + .HasColumnType("jsonb") + .HasColumnName("before_actions_json") + .HasComment("执行期(前置)动作链JSON"); + b.Property("Code") .IsRequired() .HasMaxLength(100) @@ -669,7 +679,7 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations .IsRequired() .HasColumnType("text") .HasColumnName("code") - .HasComment("C# 脚本代码"); + .HasComment("C# 脚本代码(Script类型使用)"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") @@ -701,6 +711,28 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations .HasColumnName("form_definition_id") .HasComment("绑定的自定义表单定义ID,为空表示全局可复用函数"); + b.Property("HttpBodyTemplate") + .HasColumnType("text") + .HasColumnName("http_body_template") + .HasComment("HTTP请求体模板(Http类型使用)"); + + b.Property("HttpHeaders") + .HasColumnType("jsonb") + .HasColumnName("http_headers") + .HasComment("HTTP请求头JSON(Http类型使用)"); + + b.Property("HttpMethod") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("http_method") + .HasComment("HTTP方法(Http类型使用)"); + + b.Property("HttpUrl") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("http_url") + .HasComment("HTTP目标URL(Http类型使用)"); + b.Property("InputSchema") .HasColumnType("jsonb") .HasColumnName("input_schema") @@ -742,6 +774,11 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations .HasColumnName("timeout_ms") .HasComment("执行超时时间(毫秒)"); + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type") + .HasComment("函数执行类型:Script=受控C#脚本;Http=HTTP请求"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasColumnName("updated_at") diff --git a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowApplicationTests.cs b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowApplicationTests.cs index 7bed353..289b134 100644 --- a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowApplicationTests.cs +++ b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowApplicationTests.cs @@ -115,7 +115,9 @@ public class BuildFlowApplicationTests null, null, null, + null, "[]", + null, 10), CancellationToken.None); await buttonHandler.Handle( @@ -132,7 +134,9 @@ public class BuildFlowApplicationTests null, null, null, + null, "[]", + null, 20), CancellationToken.None); @@ -160,10 +164,10 @@ public class BuildFlowApplicationTests var createHandler = new CreateCustomFunctionCommandHandler(db); var publishHandler = new PublishCustomFunctionCommandHandler(db); var draft = await createHandler.Handle( - new CreateCustomFunctionCommand("draftRule", "草稿规则", null, "1", null, null, 1000), + new CreateCustomFunctionCommand("draftRule", "草稿规则", null, CustomFunctionType.Script, "1", null, null, null, null, null, null, 1000), CancellationToken.None); var published = await createHandler.Handle( - new CreateCustomFunctionCommand("publishedRule", "已发布规则", null, "2", null, null, 1000), + new CreateCustomFunctionCommand("publishedRule", "已发布规则", null, CustomFunctionType.Script, "2", null, null, null, null, null, null, 1000), CancellationToken.None); await publishHandler.Handle(new PublishCustomFunctionCommand(published.Id), CancellationToken.None); @@ -189,9 +193,14 @@ public class BuildFlowApplicationTests "expenseAmountCalc", "费用金额计算", null, + CustomFunctionType.Script, "return input.amount;", null, null, + null, + null, + null, + null, 1000, expenseFormId), CancellationToken.None); @@ -200,9 +209,14 @@ public class BuildFlowApplicationTests "purchaseAmountCalc", "采购金额计算", null, + CustomFunctionType.Script, "return input.amount;", null, null, + null, + null, + null, + null, 1000, purchaseFormId), CancellationToken.None); diff --git a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs index 4d3523e..0655535 100644 --- a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs +++ b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs @@ -40,7 +40,9 @@ public class BuildFlowButtonActionTests null, null, """{"title":"确认提交?"}""", + null, """[]""", + null, 10), CancellationToken.None); @@ -287,9 +289,14 @@ public class BuildFlowButtonActionTests "button-amount-level", "按钮金额分级", null, + CustomFunctionType.Script, "((System.Text.Json.JsonElement)Variables[\"amount\"]).GetDecimal() > 1000 ? \"high\" : \"normal\"", null, null, + null, + null, + null, + null, 5000, formDefinitionId), CancellationToken.None); diff --git a/tests/BuildFlow.Workflow.Tests/BuildFlow/CustomFunctionTests.cs b/tests/BuildFlow.Workflow.Tests/BuildFlow/CustomFunctionTests.cs index ddcd77a..b6bc1cb 100644 --- a/tests/BuildFlow.Workflow.Tests/BuildFlow/CustomFunctionTests.cs +++ b/tests/BuildFlow.Workflow.Tests/BuildFlow/CustomFunctionTests.cs @@ -25,7 +25,12 @@ public class CustomFunctionTests "amount-level", "金额分级", "按金额输出审批等级", + CustomFunctionType.Script, "((System.Text.Json.JsonElement)Variables[\"amount\"]).GetDecimal() > 1000 ? \"high\" : \"normal\"", + null, + null, + null, + null, """{"type":"object"}""", """{"type":"string"}""", 5000), @@ -56,9 +61,14 @@ public class CustomFunctionTests "disabled-func", "禁用函数", null, + CustomFunctionType.Script, """"ok"""", null, null, + null, + null, + null, + null, 5000), CancellationToken.None);