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