From baaff8db6336e96c0582460bec50d102277bf4cb Mon Sep 17 00:00:00 2001 From: xiangning Date: Sun, 5 Jul 2026 09:30:45 +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=E8=A1=A8=E5=8D=95=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E5=8F=8A=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增自定义函数与表单定义绑定关系字段 FormDefinitionId - 实现按钮调用函数时的表单作用域权限校验机制 - 实现按钮提交表单时的作用域权限检查功能 - 添加自定义函数列表按表单定义筛选查询能力 - 新增 RelationPicker 关联选择表单组件支持 - 完善菜单权限体系支持受限但可见菜单配置 - 添加文件消息大小限制验证(最大200MB) - 优化表单聚合变量同步异常处理机制 - 增加BuildFlow管理后台入口菜单种子数据 - 补充相关单元测试和架构约束测试用例 --- .../CreateCustomFunctionEndpoint.cs | 3 +- .../CreateCustomFunctionRequest.cs | 1 + .../GetCustomFunctionListEndpoint.cs | 7 +- .../GetCustomFunctionListRequest.cs | 1 + .../Validators/SendMessageCommandValidator.cs | 6 + .../Menus/Handlers/GetAllMenusQueryHandler.cs | 26 +- .../Validators/CreateMenuCommandValidator.cs | 8 + .../Validators/UpdateMenuCommandValidator.cs | 14 + .../Infrastructure/Persistence/SeedData.cs | 6 + .../Services/BuildFlowButtonActionExecutor.cs | 51 +- .../Commands/CreateCustomFunctionCommand.cs | 3 +- .../Commands/CustomFunctionMapper.cs | 1 + .../CustomFunctions/DTOs/CustomFunctionDto.cs | 1 + .../CreateCustomFunctionCommandHandler.cs | 1 + .../GetCustomFunctionListQueryHandler.cs | 6 + .../Queries/GetCustomFunctionListQuery.cs | 3 +- .../Aggregates/FormAggregateRefreshService.cs | 23 +- .../Form/Schema/SchemaValidator.cs | 1 + .../Domain/Entities/CustomFunction.cs | 20 + .../CustomFunctionConfiguration.cs | 2 + ...7_AddCustomFunctionFormBinding.Designer.cs | 1977 +++++++++++++++++ ...0704152427_AddCustomFunctionFormBinding.cs | 39 + .../WorkflowDbContextModelSnapshot.cs | 8 + .../Infrastructure/Persistence/SeedData.cs | 18 +- .../BuildFlowEntrySeedTests.cs | 54 + .../BuildFlowFormComponentSeedTests.cs | 49 + .../Chat/SendMessageCommandValidatorTests.cs | 17 + .../MenuRuntimeViewModeValidatorTests.cs | 54 + .../Menus/MenuSortTests.cs | 133 +- .../BuildFlow/BuildFlowApplicationTests.cs | 42 + .../BuildFlow/BuildFlowButtonActionTests.cs | 68 +- .../Form/SchemaValidatorTests.cs | 27 + 32 files changed, 2648 insertions(+), 22 deletions(-) create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260704152427_AddCustomFunctionFormBinding.Designer.cs create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260704152427_AddCustomFunctionFormBinding.cs create mode 100644 tests/BuildFlow.Architecture.Tests/BuildFlowEntrySeedTests.cs create mode 100644 tests/BuildFlow.Architecture.Tests/BuildFlowFormComponentSeedTests.cs create mode 100644 tests/BuildFlow.Platform.Tests/Menus/MenuRuntimeViewModeValidatorTests.cs diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionEndpoint.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionEndpoint.cs index 8396162..65eb114 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionEndpoint.cs @@ -24,7 +24,8 @@ public class CreateCustomFunctionEndpoint(IMediator mediator) req.Code, req.InputSchema, req.OutputSchema, - req.TimeoutMs), + req.TimeoutMs, + req.FormDefinitionId), ct); await Send.ResponseAsync(result, 201, ct); } diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionRequest.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionRequest.cs index 78bfa05..cb19326 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionRequest.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/CreateCustomFunctionRequest.cs @@ -2,6 +2,7 @@ namespace BuildFlow.Host.Endpoints.Workflow.BuildFlow.CustomFunctions; /// 创建自定义函数请求。 public sealed record CreateCustomFunctionRequest( + Guid? FormDefinitionId, string Name, string DisplayName, string? Description, diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/GetCustomFunctionListEndpoint.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/GetCustomFunctionListEndpoint.cs index 79d330b..8cc7060 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/GetCustomFunctionListEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/GetCustomFunctionListEndpoint.cs @@ -18,7 +18,12 @@ public class GetCustomFunctionListEndpoint(IMediator mediator) public override async Task HandleAsync(GetCustomFunctionListRequest req, CancellationToken ct) { var result = await mediator.Send( - new GetCustomFunctionListQuery(req.PageIndex, req.PageSize, req.Status, req.Keyword), + new GetCustomFunctionListQuery( + req.PageIndex, + req.PageSize, + req.Status, + req.Keyword, + req.FormDefinitionId), ct); await Send.OkAsync(result, ct); } diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/GetCustomFunctionListRequest.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/GetCustomFunctionListRequest.cs index 2c61275..1b503ef 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/GetCustomFunctionListRequest.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/CustomFunctions/GetCustomFunctionListRequest.cs @@ -9,4 +9,5 @@ public class GetCustomFunctionListRequest public int PageSize { get; set; } = 20; public BuildFlowMetadataStatus? Status { get; set; } public string? Keyword { get; set; } + public Guid? FormDefinitionId { get; set; } } diff --git a/src/modules/im/Application/Chat/Validators/SendMessageCommandValidator.cs b/src/modules/im/Application/Chat/Validators/SendMessageCommandValidator.cs index 4f9cebb..728b7a0 100644 --- a/src/modules/im/Application/Chat/Validators/SendMessageCommandValidator.cs +++ b/src/modules/im/Application/Chat/Validators/SendMessageCommandValidator.cs @@ -10,6 +10,8 @@ namespace IM.Application.Chat.Validators; /// public class SendMessageCommandValidator : AbstractValidator { + private const long MaxFileMessageSize = 200L * 1024 * 1024; + public SendMessageCommandValidator() { RuleFor(x => x.ConversationId).NotEmpty().WithMessage("会话 ID 不能为空"); @@ -33,6 +35,10 @@ public class SendMessageCommandValidator : AbstractValidator .NotEmpty() .When(x => x.Metadata is not null) .WithMessage("文件消息必须包含文件名"); + RuleFor(x => x.Metadata!.FileSize) + .LessThanOrEqualTo(MaxFileMessageSize) + .When(x => x.Metadata?.FileSize is not null) + .WithMessage("文件不能超过 200MB"); }); // 图片消息同样要求至少有一种引用方式(直链 URL 或 bucket+objectKey) diff --git a/src/modules/platform/Application/Menus/Handlers/GetAllMenusQueryHandler.cs b/src/modules/platform/Application/Menus/Handlers/GetAllMenusQueryHandler.cs index 4b73f6a..d638757 100644 --- a/src/modules/platform/Application/Menus/Handlers/GetAllMenusQueryHandler.cs +++ b/src/modules/platform/Application/Menus/Handlers/GetAllMenusQueryHandler.cs @@ -23,25 +23,29 @@ public class GetAllMenusQueryHandler(PlatformDbContext db) : IRequestHandler(userRoles); - var visibleMenus = allMenus.Where(m => IsMenuVisible(m, userRoleSet)).ToList(); + var visibleMenus = allMenus.Where(m => ShouldReturnMenu(m, userRoleSet)).ToList(); - return BuildTree(visibleMenus, null); + return BuildTree(visibleMenus, null, userRoleSet); } /// - /// 判断菜单对当前用户是否可见。 - /// 规则:Authority 为空/nu​​ll → 所有已认证用户可见; - /// 否则 Authority(逗号分隔)中任一角色与用户角色交集即为可见。 - /// 父菜单如果不可见,子菜单即使可见也不应出现(通过递归构建树时自然处理)。 + /// 判断用户是否具备菜单访问角色。 + /// 规则:Authority 为空/null 表示所有已认证用户可访问; + /// 否则 Authority(逗号分隔)中任一角色与用户角色交集即为有权限。 /// - private static bool IsMenuVisible(Domain.Entities.Menu menu, HashSet userRoles) + private static bool HasMenuAuthority(Domain.Entities.Menu menu, HashSet userRoles) { if (string.IsNullOrWhiteSpace(menu.Authority)) return true; var allowedRoles = menu.Authority.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); return allowedRoles.Any(userRoles.Contains); } - private static List BuildTree(List menus, long? parentId) + private static bool ShouldReturnMenu(Domain.Entities.Menu menu, HashSet userRoles) + { + return HasMenuAuthority(menu, userRoles) || menu.MenuVisibleWithForbidden; + } + + private static List BuildTree(List menus, long? parentId, HashSet userRoles) { return menus .Where(m => m.ParentId == parentId) @@ -64,7 +68,9 @@ public class GetAllMenusQueryHandler(PlatformDbContext db) : IRequestHandler { RuleFor(x => x.FormDefinitionId).NotEmpty().WithMessage("自定义表单菜单必须绑定表单定义"); }); + RuleFor(x => x.RuntimeViewMode) + .Must(IsSupportedRuntimeViewMode) + .WithMessage("自定义表单菜单运行形态只支持列表和表单"); + } + + private static bool IsSupportedRuntimeViewMode(MenuRuntimeViewMode runtimeViewMode) + { + return runtimeViewMode is MenuRuntimeViewMode.List or MenuRuntimeViewMode.Form; } } diff --git a/src/modules/platform/Application/Menus/Validators/UpdateMenuCommandValidator.cs b/src/modules/platform/Application/Menus/Validators/UpdateMenuCommandValidator.cs index d871c26..8d6a661 100644 --- a/src/modules/platform/Application/Menus/Validators/UpdateMenuCommandValidator.cs +++ b/src/modules/platform/Application/Menus/Validators/UpdateMenuCommandValidator.cs @@ -28,5 +28,19 @@ public class UpdateMenuCommandValidator : AbstractValidator { RuleFor(x => x.FormDefinitionId).NotEmpty().WithMessage("自定义表单菜单必须绑定表单定义"); }); + RuleFor(x => x).Custom((command, context) => + { + if (!IsSupportedRuntimeViewMode(command.RuntimeViewMode)) + { + context.AddFailure( + nameof(UpdateMenuCommand.RuntimeViewMode), + "自定义表单菜单运行形态只支持列表和表单"); + } + }); + } + + private static bool IsSupportedRuntimeViewMode(MenuRuntimeViewMode? runtimeViewMode) + { + return runtimeViewMode is null or MenuRuntimeViewMode.List or MenuRuntimeViewMode.Form; } } diff --git a/src/modules/platform/Infrastructure/Persistence/SeedData.cs b/src/modules/platform/Infrastructure/Persistence/SeedData.cs index 43b0610..a1d1f8e 100644 --- a/src/modules/platform/Infrastructure/Persistence/SeedData.cs +++ b/src/modules/platform/Infrastructure/Persistence/SeedData.cs @@ -234,6 +234,12 @@ public static class SeedData new SeedMenu(41, "workflow.definition", "workflow", "WorkflowDefinitionList", "list", "/workflow/list/index", null, "流程定义", "lucide:list", 1, false, null, null, false, false, "Admin"), // 流程设计器:hideInMenu,仅作为可路由页面存在,不进侧边栏。 new SeedMenu(42, "workflow.designer", "workflow", "WorkflowDesigner", "designer/:id", "/workflow/designer/index", null, "流程设计器", null, 2, true, null, null, false, false, "Admin"), + + // ===== BuildFlow 低代码元数据 buildflow ===== + new SeedMenu(60, "buildflow", "", "BuildFlow", "/buildflow", null, "/buildflow/forms", "BuildFlow", "lucide:blocks", 7, false, null, null, false, false, "Admin"), + new SeedMenu(61, "buildflow.forms", "buildflow", "BuildFlowForms", "forms", "/buildflow/forms/index", null, "表单定义", "lucide:file-pen-line", 1, false, null, null, false, false, "Admin"), + new SeedMenu(62, "buildflow.buttons", "buildflow", "BuildFlowButtons", "buttons", "/buildflow/buttons/index", null, "自定义按钮", "lucide:mouse-pointer-click", 2, false, null, null, false, false, "Admin"), + new SeedMenu(63, "buildflow.functions", "buildflow", "BuildFlowCustomFunctions", "custom-functions", "/buildflow/custom-functions/index", null, "自定义函数", "lucide:braces", 3, false, null, null, false, false, "Admin"), }; // 按 Code 去重插入缺失菜单;已存在的系统种子菜单只同步路由契约字段, diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs b/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs index d8a9ea9..aced06f 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs @@ -59,7 +59,7 @@ public class BuildFlowButtonActionExecutor( var type = action["type"]?.GetValue(); if (type == "submitForm") { - await SubmitFormAsync(action, input, ct); + await SubmitFormAsync(button, action, input, ct); continue; } @@ -74,6 +74,7 @@ public class BuildFlowButtonActionExecutor( 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); @@ -95,10 +96,39 @@ public class BuildFlowButtonActionExecutor( return new BuildFlowButtonActionResult(true, results); } - private async Task SubmitFormAsync(JsonObject action, JsonObject input, CancellationToken ct) + private async Task EnsureFunctionScopeAsync( + BuildFlowButtonDefinition button, + Guid functionId, + CancellationToken ct) + { + var function = await db.CustomFunctions + .Where(x => x.Id == functionId) + .Select(x => new { x.FormDefinitionId }) + .FirstOrDefaultAsync(ct) + ?? throw new BusinessException($"自定义函数 {functionId} 不存在"); + + if (function.FormDefinitionId is null) + { + return; + } + + if (button.FormDefinitionId == function.FormDefinitionId) + { + return; + } + + throw new BusinessException("按钮不能调用其他表单的自定义函数"); + } + + private async Task SubmitFormAsync( + BuildFlowButtonDefinition button, + JsonObject action, + JsonObject input, + CancellationToken ct) { var formDefinitionId = ReadGuid(action, "formDefinitionId") ?? ReadGuid(input, "formDefinitionId") ?? throw new BusinessException("submitForm 动作缺少有效表单定义ID"); + EnsureSubmitFormScope(button, formDefinitionId); var instanceId = ReadGuid(input, "instanceId") ?? ReadGuid(action, "instanceId") ?? Guid.NewGuid(); if (input["values"] is not JsonObject values) { @@ -121,6 +151,23 @@ public class BuildFlowButtonActionExecutor( input["values"] = JsonNode.Parse(saved.DataJson)?.DeepClone(); } + private static void EnsureSubmitFormScope( + BuildFlowButtonDefinition button, + Guid formDefinitionId) + { + if (button.FormDefinitionId is null) + { + return; + } + + if (button.FormDefinitionId == formDefinitionId) + { + return; + } + + throw new BusinessException("按钮不能提交其他表单的数据"); + } + private async Task EnsurePermissionAsync(BuildFlowButtonDefinition button, CancellationToken ct) { if (string.IsNullOrWhiteSpace(button.PermissionCode)) diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CreateCustomFunctionCommand.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CreateCustomFunctionCommand.cs index 9157034..5df042b 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CreateCustomFunctionCommand.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CreateCustomFunctionCommand.cs @@ -11,4 +11,5 @@ public sealed record CreateCustomFunctionCommand( string Code, string? InputSchema, string? OutputSchema, - int TimeoutMs) : IRequest; + int TimeoutMs, + Guid? FormDefinitionId = null) : IRequest; diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CustomFunctionMapper.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CustomFunctionMapper.cs index 41e0485..19a043e 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CustomFunctionMapper.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Commands/CustomFunctionMapper.cs @@ -9,6 +9,7 @@ internal static class CustomFunctionMapper public static CustomFunctionDto ToDto(CustomFunction entity) => new( entity.Id, + entity.FormDefinitionId, entity.Name, entity.DisplayName, entity.Description, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/DTOs/CustomFunctionDto.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/DTOs/CustomFunctionDto.cs index b42bd7e..959fdd5 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/DTOs/CustomFunctionDto.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/DTOs/CustomFunctionDto.cs @@ -5,6 +5,7 @@ namespace Workflow.Application.BuildFlow.CustomFunctions.DTOs; /// 自定义函数 DTO,用于函数管理页面和按钮动作选择器。 public sealed record CustomFunctionDto( Guid Id, + Guid? FormDefinitionId, string Name, string DisplayName, string? Description, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/CreateCustomFunctionCommandHandler.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/CreateCustomFunctionCommandHandler.cs index 03e7ac5..2b8dfdd 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/CreateCustomFunctionCommandHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/CreateCustomFunctionCommandHandler.cs @@ -20,6 +20,7 @@ public class CreateCustomFunctionCommandHandler(WorkflowDbContext db) var entity = new CustomFunction { Id = Guid.NewGuid(), + FormDefinitionId = request.FormDefinitionId, Name = request.Name, DisplayName = request.DisplayName, Description = request.Description, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/GetCustomFunctionListQueryHandler.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/GetCustomFunctionListQueryHandler.cs index 3a517ab..125ab4f 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/GetCustomFunctionListQueryHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Handlers/GetCustomFunctionListQueryHandler.cs @@ -21,6 +21,11 @@ public sealed class GetCustomFunctionListQueryHandler(WorkflowDbContext db) query = query.Where(x => x.Status == request.Status.Value); } + if (request.FormDefinitionId.HasValue) + { + query = query.Where(x => x.FormDefinitionId == request.FormDefinitionId.Value); + } + if (!string.IsNullOrWhiteSpace(request.Keyword)) { query = query.Where(x => @@ -35,6 +40,7 @@ public sealed class GetCustomFunctionListQueryHandler(WorkflowDbContext db) .Take(request.PageSize) .Select(x => new CustomFunctionDto( x.Id, + x.FormDefinitionId, x.Name, x.DisplayName, x.Description, diff --git a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Queries/GetCustomFunctionListQuery.cs b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Queries/GetCustomFunctionListQuery.cs index 1aecc94..f037f3e 100644 --- a/src/modules/workflow/Application/BuildFlow/CustomFunctions/Queries/GetCustomFunctionListQuery.cs +++ b/src/modules/workflow/Application/BuildFlow/CustomFunctions/Queries/GetCustomFunctionListQuery.cs @@ -10,4 +10,5 @@ public sealed record GetCustomFunctionListQuery( int PageIndex, int PageSize, BuildFlowMetadataStatus? Status, - string? Keyword) : IRequest>; + string? Keyword, + Guid? FormDefinitionId = null) : IRequest>; diff --git a/src/modules/workflow/Application/Form/Aggregates/FormAggregateRefreshService.cs b/src/modules/workflow/Application/Form/Aggregates/FormAggregateRefreshService.cs index 996043c..6c09d78 100644 --- a/src/modules/workflow/Application/Form/Aggregates/FormAggregateRefreshService.cs +++ b/src/modules/workflow/Application/Form/Aggregates/FormAggregateRefreshService.cs @@ -1,4 +1,5 @@ using System.Text.Encodings.Web; +using System.Data.Common; using System.Text.Json; using System.Text.Json.Nodes; using Workflow.Application.Form.Derivation; @@ -54,12 +55,32 @@ public sealed class FormAggregateRefreshService(WorkflowDbContext db) : IFormAgg } targetRow.DataJson = targetData.ToJsonString(JsonOptions); - await MergeAggregateVariablesAsync(instanceId, variableUpdates, ct); + await TryMergeAggregateVariablesAsync(instanceId, variableUpdates, ct); } await db.SaveChangesAsync(ct); } + /// + /// 尝试把统计字段同步到流程实例变量。动态表单可脱离流程实例独立使用, + /// 且运行库可能存在历史 workflow schema 漂移;变量同步失败不能阻断表单数据自身落库。 + /// + private async Task TryMergeAggregateVariablesAsync( + Guid instanceId, + IReadOnlyCollection<(string Path, JsonNode? Value)> variableUpdates, + CancellationToken ct) + { + try + { + await MergeAggregateVariablesAsync(instanceId, variableUpdates, ct); + } + catch (DbException) + { + // WorkflowInstance.Variables 只是流程运行时的派生镜像; + // 自定义表单提交的权威结果已经写回 targetRow.DataJson,不能因镜像同步失败回滚。 + } + } + private async Task MergeAggregateVariablesAsync( Guid instanceId, IReadOnlyCollection<(string Path, JsonNode? Value)> variableUpdates, diff --git a/src/modules/workflow/Application/Form/Schema/SchemaValidator.cs b/src/modules/workflow/Application/Form/Schema/SchemaValidator.cs index 715395e..eb5b589 100644 --- a/src/modules/workflow/Application/Form/Schema/SchemaValidator.cs +++ b/src/modules/workflow/Application/Form/Schema/SchemaValidator.cs @@ -19,6 +19,7 @@ public static class SchemaValidator [ "Input", "Input.TextArea", "Input.Password", "Input.Number", "InputNumber", "Select", "DatePicker", "TimePicker", "Switch", + "RelationPicker", "Radio.Group", "Checkbox.Group", "Upload", "FormGrid", "FormLayout", "FormItem", "ArrayItems", "ArrayItems.Item", "ArrayItems.Addition", diff --git a/src/modules/workflow/Domain/Entities/CustomFunction.cs b/src/modules/workflow/Domain/Entities/CustomFunction.cs index 3167b62..4904696 100644 --- a/src/modules/workflow/Domain/Entities/CustomFunction.cs +++ b/src/modules/workflow/Domain/Entities/CustomFunction.cs @@ -6,14 +6,34 @@ namespace Workflow.Domain.Entities; /// BuildFlow 自定义函数定义。Name 是英文标识,Code 保存受控脚本内容。 public class CustomFunction : BaseEntity, IFullAudit { + /// 绑定的自定义表单定义 ID。为空表示全局可复用函数。 + public Guid? FormDefinitionId { get; set; } + + /// 函数英文标识,供按钮动作链稳定引用。 public string Name { get; set; } = string.Empty; + + /// 函数显示名称。 public string DisplayName { get; set; } = string.Empty; + + /// 函数说明。 public string? Description { get; set; } + + /// 受控脚本代码。 public string Code { get; set; } = string.Empty; + + /// 函数输入 Schema JSON。 public string? InputSchema { get; set; } + + /// 函数输出 Schema JSON。 public string? OutputSchema { get; set; } + + /// 函数发布状态。 public BuildFlowMetadataStatus Status { get; set; } = BuildFlowMetadataStatus.Draft; + + /// 函数版本号。 public int Version { get; set; } = 1; + + /// 执行超时时间,单位毫秒。 public int TimeoutMs { get; set; } public string CreatedBy { get; set; } = default!; diff --git a/src/modules/workflow/Infrastructure/Persistence/Configurations/CustomFunctionConfiguration.cs b/src/modules/workflow/Infrastructure/Persistence/Configurations/CustomFunctionConfiguration.cs index 35f0d3e..f85dc94 100644 --- a/src/modules/workflow/Infrastructure/Persistence/Configurations/CustomFunctionConfiguration.cs +++ b/src/modules/workflow/Infrastructure/Persistence/Configurations/CustomFunctionConfiguration.cs @@ -12,6 +12,7 @@ public class CustomFunctionConfiguration : IEntityTypeConfiguration e.Id); builder.Property(e => e.Id).ValueGeneratedNever().HasComment("主键ID"); + builder.Property(e => e.FormDefinitionId).HasComment("绑定的自定义表单定义ID,为空表示全局可复用函数"); builder.Property(e => e.Name).HasMaxLength(200).IsRequired().HasComment("函数英文标识"); builder.Property(e => e.DisplayName).HasMaxLength(200).IsRequired().HasComment("函数显示名称"); builder.Property(e => e.Description).HasMaxLength(1000).HasComment("函数说明"); @@ -27,6 +28,7 @@ public class CustomFunctionConfiguration : IEntityTypeConfiguration e.UpdatedAt).HasComment("更新时间"); builder.Property(e => e.IsDeleted).HasDefaultValue(false).HasComment("是否软删除"); builder.Property(e => e.OperatorIP).HasMaxLength(500).HasComment("操作人IP地址"); + builder.HasIndex(e => e.FormDefinitionId); builder.HasIndex(e => e.Name).IsUnique(); } } diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/20260704152427_AddCustomFunctionFormBinding.Designer.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260704152427_AddCustomFunctionFormBinding.Designer.cs new file mode 100644 index 0000000..cb1126b --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260704152427_AddCustomFunctionFormBinding.Designer.cs @@ -0,0 +1,1977 @@ +// +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("20260704152427_AddCustomFunctionFormBinding")] + partial class AddCustomFunctionFormBinding + { + /// + 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_wf_buildflow_action_execution_logs"); + + b.HasIndex("ButtonDefinitionId") + .HasDatabaseName("ix_wf_buildflow_action_execution_logs_button_definition_id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_wf_buildflow_action_execution_logs_created_at"); + + b.ToTable("wf_buildflow_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_wf_buildflow_applications"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_wf_buildflow_applications_code"); + + b.ToTable("wf_buildflow_applications", (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("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id") + .HasComment("所属 BuildFlow 应用ID"); + + 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_wf_buildflow_button_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_wf_buildflow_button_definitions_code"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_wf_buildflow_button_definitions_form_definition_id"); + + b.HasIndex("PageDefinitionId") + .HasDatabaseName("ix_wf_buildflow_button_definitions_page_definition_id"); + + b.ToTable("wf_buildflow_button_definitions", (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_wf_buildflow_page_definitions"); + + b.HasIndex("PageType") + .HasDatabaseName("ix_wf_buildflow_page_definitions_page_type"); + + b.HasIndex("ApplicationId", "Code") + .IsUnique() + .HasDatabaseName("ix_wf_buildflow_page_definitions_application_id_code"); + + b.ToTable("wf_buildflow_page_definitions", (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# 脚本代码"); + + 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("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("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_wf_custom_functions"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_wf_custom_functions_form_definition_id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_wf_custom_functions_name"); + + b.ToTable("wf_custom_functions", (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_wf_custom_function_execution_logs"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_wf_custom_function_execution_logs_created_at"); + + b.HasIndex("FunctionId") + .HasDatabaseName("ix_wf_custom_function_execution_logs_function_id"); + + b.ToTable("wf_custom_function_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_wf_form_component_registry"); + + b.HasIndex("Category") + .HasDatabaseName("ix_wf_form_component_registry_category"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_wf_form_component_registry_name"); + + b.ToTable("wf_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_wf_form_data"); + + b.HasIndex("DataJson") + .HasDatabaseName("ix_wf_form_data_data_json"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("DataJson"), "GIN"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("DataJson"), new[] { "jsonb_path_ops" }); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_wf_form_data_form_definition_id"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_wf_form_data_instance_id"); + + b.ToTable("wf_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_wf_form_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_wf_form_definitions_code"); + + b.ToTable("wf_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_wf_form_definition_versions"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_wf_form_definition_versions_form_definition_id"); + + b.HasIndex("FormDefinitionId", "Version") + .HasDatabaseName("ix_wf_form_definition_versions_form_definition_id_version"); + + b.ToTable("wf_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_wf_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("wf_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_wf_webhook_deliveries"); + + b.HasIndex("NextRetryAt") + .HasDatabaseName("ix_wf_webhook_deliveries_next_retry_at"); + + b.HasIndex("Status") + .HasDatabaseName("ix_wf_webhook_deliveries_status"); + + b.ToTable("wf_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_wf_workflow_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_wf_workflow_definitions_code"); + + b.ToTable("wf_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_wf_workflow_definition_snapshots"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_wf_workflow_definition_snapshots_definition_id"); + + b.HasIndex("DefinitionId", "Version") + .IsUnique() + .HasDatabaseName("ix_wf_workflow_definition_snapshots_definition_id_version"); + + b.ToTable("wf_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_wf_workflow_edges"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_wf_workflow_edges_definition_id"); + + b.ToTable("wf_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_wf_workflow_instances"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_wf_workflow_instances_definition_id"); + + b.ToTable("wf_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_wf_workflow_nodes"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_wf_workflow_nodes_definition_id"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_wf_workflow_nodes_form_definition_id"); + + b.ToTable("wf_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_wf_workflow_tasks"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_wf_workflow_tasks_instance_id"); + + b.ToTable("wf_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_wf_workflow_tokens"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_wf_workflow_tokens_instance_id"); + + b.ToTable("wf_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_wf_workflow_edges_wf_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_wf_workflow_nodes_wf_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_wf_workflow_tasks_wf_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_wf_workflow_tokens_wf_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/20260704152427_AddCustomFunctionFormBinding.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260704152427_AddCustomFunctionFormBinding.cs new file mode 100644 index 0000000..854ba31 --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260704152427_AddCustomFunctionFormBinding.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Workflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCustomFunctionFormBinding : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "form_definition_id", + table: "wf_custom_functions", + type: "uuid", + nullable: true, + comment: "绑定的自定义表单定义ID,为空表示全局可复用函数"); + + migrationBuilder.CreateIndex( + name: "ix_wf_custom_functions_form_definition_id", + table: "wf_custom_functions", + column: "form_definition_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_wf_custom_functions_form_definition_id", + table: "wf_custom_functions"); + + migrationBuilder.DropColumn( + name: "form_definition_id", + table: "wf_custom_functions"); + } + } +} diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs index 19edf92..5b6bc84 100644 --- a/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs @@ -467,6 +467,11 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations .HasColumnName("display_name") .HasComment("函数显示名称"); + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("绑定的自定义表单定义ID,为空表示全局可复用函数"); + b.Property("InputSchema") .HasColumnType("jsonb") .HasColumnName("input_schema") @@ -528,6 +533,9 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations b.HasKey("Id") .HasName("pk_wf_custom_functions"); + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_wf_custom_functions_form_definition_id"); + b.HasIndex("Name") .IsUnique() .HasDatabaseName("ix_wf_custom_functions_name"); diff --git a/src/modules/workflow/Infrastructure/Persistence/SeedData.cs b/src/modules/workflow/Infrastructure/Persistence/SeedData.cs index 23238c1..27d664a 100644 --- a/src/modules/workflow/Infrastructure/Persistence/SeedData.cs +++ b/src/modules/workflow/Infrastructure/Persistence/SeedData.cs @@ -554,6 +554,20 @@ public static class SeedData CreatedBy = systemUserId, UpdatedBy = systemUserId, }, + new() + { + // 关联选择:从另一个已发布自定义表单读取候选项,提交时由后端写入稳定快照。 + Id = Guid.Parse("D0000030-0000-0000-0000-000000000001"), + Name = "RelationPicker", + DisplayName = "关联选择", + Category = "选择控件", + Icon = "lucide:link-2", + SortOrder = 14, + DefaultSchema = """{"type":"object","title":"未命名","x-decorator":"FormItem","x-component":"RelationPicker","x-field-kind":"relation","x-relation":{"sourceCode":"supplier","idField":"id","labelField":"name","snapshotFields":["code","name"]}}""", + SupportedProps = """{"relation":[{"key":"sourceCode","label":"关联源编码","type":"string"},{"key":"idField","label":"ID 字段","type":"string"},{"key":"labelField","label":"显示字段","type":"string"},{"key":"snapshotFields","label":"快照字段","type":"string"}]}""", + CreatedBy = systemUserId, + UpdatedBy = systemUserId, + }, // ── 选择控件·新增(需渲染器分支):级联选择、滑块 ── new() { @@ -563,7 +577,7 @@ public static class SeedData DisplayName = "级联选择", Category = "选择控件", Icon = "lucide:list-tree", - SortOrder = 14, + SortOrder = 15, DefaultSchema = """{"type":"array","title":"未命名","x-decorator":"FormItem","x-component":"Cascader","x-component-props":{"placeholder":"请选择"},"x-data-source":{"type":"static","options":[]}}""", SupportedProps = """{"basic":[{"key":"placeholder","label":"提示文字","type":"string"},{"key":"allowClear","label":"允许清除","type":"boolean"},{"key":"multiple","label":"多选","type":"boolean"}],"dataSource":{"supported":true,"types":["static","remote"]},"validation":[{"key":"customMessage","label":"错误提示","type":"string"}]}""", CreatedBy = systemUserId, @@ -577,7 +591,7 @@ public static class SeedData DisplayName = "滑块", Category = "选择控件", Icon = "lucide:sliders-horizontal", - SortOrder = 15, + SortOrder = 16, DefaultSchema = """{"type":"number","title":"未命名","x-decorator":"FormItem","x-component":"Slider","x-component-props":{"min":0,"max":100}}""", SupportedProps = """{"basic":[{"key":"min","label":"最小值","type":"number"},{"key":"max","label":"最大值","type":"number"},{"key":"step","label":"步长","type":"number"},{"key":"range","label":"双滑块","type":"boolean"}],"validation":[{"key":"customMessage","label":"错误提示","type":"string"}]}""", CreatedBy = systemUserId, diff --git a/tests/BuildFlow.Architecture.Tests/BuildFlowEntrySeedTests.cs b/tests/BuildFlow.Architecture.Tests/BuildFlowEntrySeedTests.cs new file mode 100644 index 0000000..24e2252 --- /dev/null +++ b/tests/BuildFlow.Architecture.Tests/BuildFlowEntrySeedTests.cs @@ -0,0 +1,54 @@ +namespace BuildFlow.Architecture.Tests; + +using Xunit; + +/// +/// BuildFlow 低代码后台入口种子约束。保证干净库自动具备表单、按钮、函数管理菜单。 +/// +public class BuildFlowEntrySeedTests +{ + [Fact] + public void PlatformSeedDataShouldContainBuildFlowMetadataMenus() + { + var seedData = ReadPlatformSeedData(); + + Assert.Contains("\"buildflow\"", seedData, StringComparison.Ordinal); + Assert.Contains("\"buildflow.forms\"", seedData, StringComparison.Ordinal); + Assert.Contains("\"/buildflow/forms/index\"", seedData, StringComparison.Ordinal); + Assert.Contains("\"buildflow.buttons\"", seedData, StringComparison.Ordinal); + Assert.Contains("\"/buildflow/buttons/index\"", seedData, StringComparison.Ordinal); + Assert.Contains("\"buildflow.functions\"", seedData, StringComparison.Ordinal); + Assert.Contains("\"/buildflow/custom-functions/index\"", seedData, StringComparison.Ordinal); + } + + private static string ReadPlatformSeedData() + { + var repoRoot = FindRepoRoot(); + var seedPath = Path.Combine( + repoRoot.FullName, + "src", + "modules", + "platform", + "Infrastructure", + "Persistence", + "SeedData.cs"); + + return File.ReadAllText(seedPath); + } + + private static DirectoryInfo FindRepoRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current is not null) + { + if (File.Exists(Path.Combine(current.FullName, "BuildFlow.slnx"))) + { + return current; + } + + current = current.Parent; + } + + throw new InvalidOperationException("无法定位 BuildFlow.slnx,架构测试无法确定仓库边界。"); + } +} diff --git a/tests/BuildFlow.Architecture.Tests/BuildFlowFormComponentSeedTests.cs b/tests/BuildFlow.Architecture.Tests/BuildFlowFormComponentSeedTests.cs new file mode 100644 index 0000000..6fb2809 --- /dev/null +++ b/tests/BuildFlow.Architecture.Tests/BuildFlowFormComponentSeedTests.cs @@ -0,0 +1,49 @@ +namespace BuildFlow.Architecture.Tests; + +using Xunit; + +/// +/// BuildFlow 表单组件种子约束。保证干净库可创建前端设计器内置的运行时字段。 +/// +public class BuildFlowFormComponentSeedTests +{ + [Fact] + public void WorkflowSeedDataShouldContainRelationPickerComponent() + { + var seedData = ReadWorkflowSeedData(); + + Assert.Contains("RelationPicker", seedData, StringComparison.Ordinal); + Assert.Contains("\"x-field-kind\":\"relation\"", seedData, StringComparison.Ordinal); + } + + private static string ReadWorkflowSeedData() + { + var repoRoot = FindRepoRoot(); + var seedPath = Path.Combine( + repoRoot.FullName, + "src", + "modules", + "workflow", + "Infrastructure", + "Persistence", + "SeedData.cs"); + + return File.ReadAllText(seedPath); + } + + private static DirectoryInfo FindRepoRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current is not null) + { + if (File.Exists(Path.Combine(current.FullName, "BuildFlow.slnx"))) + { + return current; + } + + current = current.Parent; + } + + throw new InvalidOperationException("无法定位 BuildFlow.slnx,架构测试无法确定仓库边界。"); + } +} diff --git a/tests/BuildFlow.IM.Tests/Chat/SendMessageCommandValidatorTests.cs b/tests/BuildFlow.IM.Tests/Chat/SendMessageCommandValidatorTests.cs index 8166221..6d3f3fe 100644 --- a/tests/BuildFlow.IM.Tests/Chat/SendMessageCommandValidatorTests.cs +++ b/tests/BuildFlow.IM.Tests/Chat/SendMessageCommandValidatorTests.cs @@ -37,6 +37,23 @@ public class SendMessageCommandValidatorTests Assert.True(result.IsValid); } + [Fact] + public void Rejects_file_message_larger_than_200mb() + { + var metadata = new MessageMetadataDto("report.zip", 200L * 1024 * 1024 + 1, null, null, null, null, null) + { + BucketName = "im-files", + ObjectKey = "im/20260705/report.zip", + ContentType = "application/zip" + }; + var command = CreateCommand(MessageContentType.File, "report.zip", metadata); + + var result = _validator.Validate(command); + + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.ErrorMessage == "文件不能超过 200MB"); + } + [Fact] public void Rejects_image_message_without_preview_or_storage_reference() { diff --git a/tests/BuildFlow.Platform.Tests/Menus/MenuRuntimeViewModeValidatorTests.cs b/tests/BuildFlow.Platform.Tests/Menus/MenuRuntimeViewModeValidatorTests.cs new file mode 100644 index 0000000..c80c58f --- /dev/null +++ b/tests/BuildFlow.Platform.Tests/Menus/MenuRuntimeViewModeValidatorTests.cs @@ -0,0 +1,54 @@ +using BuildFlow.Platform.Application.Menus.Commands; +using BuildFlow.Platform.Application.Menus.Validators; +using BuildFlow.Platform.Domain.Enums; +using FluentAssertions; +using Xunit; + +namespace BuildFlow.Platform.Tests.Menus; + +/// +/// 菜单运行时形态校验测试。BuildFlow 自定义菜单当前只支持列表和表单两种形态。 +/// +public class MenuRuntimeViewModeValidatorTests +{ + [Fact] + public void CreateMenuCommand_ShouldRejectDetailRuntimeViewMode() + { + var validator = new CreateMenuCommandValidator(); + + var result = validator.Validate(new CreateMenuCommand + { + Code = "buildflow.form.expense.detail", + FormDefinitionId = Guid.NewGuid(), + Name = "BuildFlowFormExpenseDetail", + Path = "/buildflow/runtime/forms/expense/detail", + RenderType = MenuRenderType.BuildFlowForm, + RuntimeViewMode = MenuRuntimeViewMode.Detail, + Title = "费用申请详情", + }); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(CreateMenuCommand.RuntimeViewMode) && + x.ErrorMessage == "自定义表单菜单运行形态只支持列表和表单"); + } + + [Fact] + public void UpdateMenuCommand_ShouldRejectDetailRuntimeViewMode() + { + var validator = new UpdateMenuCommandValidator(); + + var result = validator.Validate(new UpdateMenuCommand + { + Id = 100, + FormDefinitionId = Guid.NewGuid(), + RenderType = MenuRenderType.BuildFlowForm, + RuntimeViewMode = MenuRuntimeViewMode.Detail, + }); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(UpdateMenuCommand.RuntimeViewMode) && + x.ErrorMessage == "自定义表单菜单运行形态只支持列表和表单"); + } +} diff --git a/tests/BuildFlow.Platform.Tests/Menus/MenuSortTests.cs b/tests/BuildFlow.Platform.Tests/Menus/MenuSortTests.cs index caea8e9..074883b 100644 --- a/tests/BuildFlow.Platform.Tests/Menus/MenuSortTests.cs +++ b/tests/BuildFlow.Platform.Tests/Menus/MenuSortTests.cs @@ -127,6 +127,133 @@ public class MenuSortTests route.Meta.RuntimeViewMode.Should().Be("List"); } + [Fact] + public async Task GetAllMenusQuery_ShouldHideRestrictedMenuWithoutVisibleForbidden() + { + await using var db = BuildInMemoryDb(); + var userId = Guid.NewGuid(); + var roleId = Guid.NewGuid(); + + db.Users.Add(new User + { + Id = userId, + Username = "viewer", + Email = "viewer@buildflow.local", + PasswordHash = "hash", + CreatedBy = "test", + UpdatedBy = "test", + OperatorIP = "127.0.0.1", + }); + db.Roles.Add(new Role + { + Id = roleId, + Name = "Viewer", + CreatedBy = "test", + UpdatedBy = "test", + OperatorIP = "127.0.0.1", + }); + db.UserRoles.Add(new UserRole + { + UserId = userId, + RoleId = roleId, + CreatedBy = "test", + UpdatedBy = "test", + }); + db.Menus.Add(BuildMenu(200, "finance.expense", null, 0, authority: "Admin")); + await db.SaveChangesAsync(); + + var handler = new GetAllMenusQueryHandler(db); + + var routes = await handler.Handle(new GetAllMenusQuery(userId.ToString()), CancellationToken.None); + + routes.Should().BeEmpty(); + } + + [Fact] + public async Task GetAllMenusQuery_ShouldReturnForbiddenVisibleMenuOnlyForUnauthorizedUser() + { + await using var db = BuildInMemoryDb(); + var viewerId = Guid.NewGuid(); + var adminId = Guid.NewGuid(); + var viewerRoleId = Guid.NewGuid(); + var adminRoleId = Guid.NewGuid(); + + db.Users.AddRange( + new User + { + Id = viewerId, + Username = "viewer", + Email = "viewer@buildflow.local", + PasswordHash = "hash", + CreatedBy = "test", + UpdatedBy = "test", + OperatorIP = "127.0.0.1", + }, + new User + { + Id = adminId, + Username = "admin", + Email = "admin@buildflow.local", + PasswordHash = "hash", + CreatedBy = "test", + UpdatedBy = "test", + OperatorIP = "127.0.0.1", + }); + db.Roles.AddRange( + new Role + { + Id = viewerRoleId, + Name = "Viewer", + CreatedBy = "test", + UpdatedBy = "test", + OperatorIP = "127.0.0.1", + }, + new Role + { + Id = adminRoleId, + Name = "Admin", + CreatedBy = "test", + UpdatedBy = "test", + OperatorIP = "127.0.0.1", + }); + db.UserRoles.AddRange( + new UserRole + { + UserId = viewerId, + RoleId = viewerRoleId, + CreatedBy = "test", + UpdatedBy = "test", + }, + new UserRole + { + UserId = adminId, + RoleId = adminRoleId, + CreatedBy = "test", + UpdatedBy = "test", + }); + db.Menus.Add(BuildMenu( + 200, + "finance.expense", + null, + 0, + authority: "Admin", + menuVisibleWithForbidden: true)); + await db.SaveChangesAsync(); + + var handler = new GetAllMenusQueryHandler(db); + + var viewerRoutes = await handler.Handle(new GetAllMenusQuery(viewerId.ToString()), CancellationToken.None); + var adminRoutes = await handler.Handle(new GetAllMenusQuery(adminId.ToString()), CancellationToken.None); + + var viewerRoute = viewerRoutes.Should().ContainSingle().Subject; + viewerRoute.Meta.Authority.Should().Equal("Admin"); + viewerRoute.Meta.MenuVisibleWithForbidden.Should().BeTrue(); + + var adminRoute = adminRoutes.Should().ContainSingle().Subject; + adminRoute.Meta.Authority.Should().Equal("Admin"); + adminRoute.Meta.MenuVisibleWithForbidden.Should().BeNull(); + } + private static PlatformDbContext BuildInMemoryDb() { var services = new ServiceCollection(); @@ -141,7 +268,9 @@ public class MenuSortTests int order, MenuRenderType renderType = MenuRenderType.Route, Guid? formDefinitionId = null, - MenuRuntimeViewMode runtimeViewMode = MenuRuntimeViewMode.List) + MenuRuntimeViewMode runtimeViewMode = MenuRuntimeViewMode.List, + string? authority = null, + bool menuVisibleWithForbidden = false) => new() { Id = id, @@ -154,6 +283,8 @@ public class MenuSortTests RenderType = renderType, FormDefinitionId = formDefinitionId, RuntimeViewMode = runtimeViewMode, + Authority = authority, + MenuVisibleWithForbidden = menuVisibleWithForbidden, CreatedBy = "test", CreatedAt = DateTime.UtcNow, UpdatedBy = "test", diff --git a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowApplicationTests.cs b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowApplicationTests.cs index f4c627d..7bed353 100644 --- a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowApplicationTests.cs +++ b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowApplicationTests.cs @@ -176,4 +176,46 @@ public class BuildFlowApplicationTests result.Items.Should().ContainSingle(x => x.Id == published.Id && x.Name == "publishedRule"); result.Items.Should().NotContain(x => x.Id == draft.Id); } + + [Fact] + public async Task GetCustomFunctionList_FiltersByFormDefinition() + { + await using var db = TestDbFactory.Create(); + var expenseFormId = Guid.NewGuid(); + var purchaseFormId = Guid.NewGuid(); + var createHandler = new CreateCustomFunctionCommandHandler(db); + var expenseFunction = await createHandler.Handle( + new CreateCustomFunctionCommand( + "expenseAmountCalc", + "费用金额计算", + null, + "return input.amount;", + null, + null, + 1000, + expenseFormId), + CancellationToken.None); + await createHandler.Handle( + new CreateCustomFunctionCommand( + "purchaseAmountCalc", + "采购金额计算", + null, + "return input.amount;", + null, + null, + 1000, + purchaseFormId), + CancellationToken.None); + + var handler = new GetCustomFunctionListQueryHandler(db); + var result = await handler.Handle( + new GetCustomFunctionListQuery(1, 20, null, null, expenseFormId), + CancellationToken.None); + + result.Total.Should().Be(1); + result.Items.Should().ContainSingle(x => + x.Id == expenseFunction.Id && + x.FormDefinitionId == expenseFormId && + x.Name == "expenseAmountCalc"); + } } diff --git a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs index d3a7f9e..4d3523e 100644 --- a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs +++ b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs @@ -215,7 +215,70 @@ public class BuildFlowButtonActionTests (await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0); } - private static async Task CreatePublishedFunctionAsync(WorkflowDbContext db) + [Fact] + public async Task ExecuteAsync_ShouldRejectFunctionBoundToAnotherForm() + { + await using var db = TestDbFactory.Create(); + var buttonFormId = Guid.NewGuid(); + var functionFormId = Guid.NewGuid(); + var function = await CreatePublishedFunctionAsync(db, functionFormId); + var button = CreateButton(function.Id); + button.FormDefinitionId = buttonFormId; + db.BuildFlowButtons.Add(button); + await db.SaveChangesAsync(); + + var executor = new BuildFlowButtonActionExecutor( + db, + new CustomFunctionExecutionService(db)); + + var act = () => executor.ExecuteAsync( + button.Id, + """{"amount":1200}""", + CancellationToken.None); + + await act.Should().ThrowAsync() + .WithMessage("按钮不能调用其他表单的自定义函数"); + (await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0); + (await db.CustomFunctionExecutionLogs.CountAsync()).Should().Be(0); + } + + [Fact] + public async Task ExecuteAsync_ShouldRejectSubmitFormForAnotherForm() + { + await using var db = TestDbFactory.Create(); + var buttonFormId = Guid.NewGuid(); + var submittedFormId = Guid.NewGuid(); + db.FormDefinitions.Add(CreatePublishedFormDefinition(submittedFormId)); + var function = await CreatePublishedFunctionAsync(db); + var button = CreateSubmitAndRunButton(function.Id); + button.FormDefinitionId = buttonFormId; + db.BuildFlowButtons.Add(button); + await db.SaveChangesAsync(); + + var executor = new BuildFlowButtonActionExecutor( + db, + new CustomFunctionExecutionService(db)); + + var act = () => executor.ExecuteAsync( + button.Id, + $$""" + { + "formDefinitionId": "{{submittedFormId}}", + "values": { "name": "张三", "amount": 1200 } + } + """, + CancellationToken.None); + + await act.Should().ThrowAsync() + .WithMessage("按钮不能提交其他表单的数据"); + (await db.FormData.CountAsync()).Should().Be(0); + (await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0); + (await db.CustomFunctionExecutionLogs.CountAsync()).Should().Be(0); + } + + private static async Task CreatePublishedFunctionAsync( + WorkflowDbContext db, + Guid? formDefinitionId = null) { var createHandler = new CreateCustomFunctionCommandHandler(db); var publishHandler = new PublishCustomFunctionCommandHandler(db); @@ -227,7 +290,8 @@ public class BuildFlowButtonActionTests "((System.Text.Json.JsonElement)Variables[\"amount\"]).GetDecimal() > 1000 ? \"high\" : \"normal\"", null, null, - 5000), + 5000, + formDefinitionId), CancellationToken.None); await publishHandler.Handle(new PublishCustomFunctionCommand(function.Id), CancellationToken.None); return await db.CustomFunctions.FindAsync(function.Id) ?? throw new InvalidOperationException(); diff --git a/tests/BuildFlow.Workflow.Tests/Form/SchemaValidatorTests.cs b/tests/BuildFlow.Workflow.Tests/Form/SchemaValidatorTests.cs index 2eb20e9..7242c31 100644 --- a/tests/BuildFlow.Workflow.Tests/Form/SchemaValidatorTests.cs +++ b/tests/BuildFlow.Workflow.Tests/Form/SchemaValidatorTests.cs @@ -477,4 +477,31 @@ public class SchemaValidatorTests result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("sourceCode")); } + + [Fact] + public void Validate_DefaultComponentsAcceptRelationPicker() + { + var result = SchemaValidator.Validate(""" + { + "type": "object", + "properties": { + "supplier": { + "type": "object", + "title": "供应商", + "x-component": "RelationPicker", + "x-field-kind": "relation", + "x-relation": { + "sourceCode": "supplier", + "idField": "id", + "labelField": "name", + "snapshotFields": ["code", "name"] + } + } + } + } + """); + + result.IsValid.Should().BeTrue(string.Join("; ", result.Errors)); + result.Fields.Should().ContainSingle(f => f.Path == "supplier"); + } }