From d9730db6a9a5521a486f16aa93ef4c3a05ff43e2 Mon Sep 17 00:00:00 2001 From: xiangning Date: Fri, 24 Jul 2026 11:13:41 +0800 Subject: [PATCH] perf: index form data exports --- .../Configurations/FormDataConfiguration.cs | 2 +- ...3_AddFormDataExportKeysetIndex.Designer.cs | 2190 +++++++++++++++++ ...0724030743_AddFormDataExportKeysetIndex.cs | 36 + .../WorkflowDbContextModelSnapshot.cs | 6 +- .../Performance/FormDataExportIndexTests.cs | 30 + 5 files changed, 2260 insertions(+), 4 deletions(-) create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260724030743_AddFormDataExportKeysetIndex.Designer.cs create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260724030743_AddFormDataExportKeysetIndex.cs create mode 100644 tests/BuildFlow.Workflow.Tests/Performance/FormDataExportIndexTests.cs diff --git a/src/modules/workflow/Infrastructure/Persistence/Configurations/FormDataConfiguration.cs b/src/modules/workflow/Infrastructure/Persistence/Configurations/FormDataConfiguration.cs index d842dcb..a77294b 100644 --- a/src/modules/workflow/Infrastructure/Persistence/Configurations/FormDataConfiguration.cs +++ b/src/modules/workflow/Infrastructure/Persistence/Configurations/FormDataConfiguration.cs @@ -26,7 +26,7 @@ public class FormDataConfiguration : IEntityTypeConfiguration builder.Property(e => e.OperatorIP).HasMaxLength(500).HasComment("操作人IP地址"); builder.HasIndex(e => e.InstanceId); - builder.HasIndex(e => e.FormDefinitionId); + builder.HasIndex(e => new { e.FormDefinitionId, e.CreatedAt, e.Id }); // GIN 索引支持对 DataJson 内容的 @> 包含查询 builder.HasIndex(e => e.DataJson) diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/20260724030743_AddFormDataExportKeysetIndex.Designer.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260724030743_AddFormDataExportKeysetIndex.Designer.cs new file mode 100644 index 0000000..7974627 --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260724030743_AddFormDataExportKeysetIndex.Designer.cs @@ -0,0 +1,2190 @@ +// +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("20260724030743_AddFormDataExportKeysetIndex")] + partial class AddFormDataExportKeysetIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowActionExecutionLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ButtonCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("button_code") + .HasComment("按钮编码"); + + b.Property("ButtonDefinitionId") + .HasColumnType("uuid") + .HasColumnName("button_definition_id") + .HasComment("按钮定义ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms") + .HasComment("执行耗时(毫秒)"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error_message") + .HasComment("错误信息"); + + b.Property("InputJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("input_json") + .HasComment("按钮输入JSON"); + + b.Property("ResultJson") + .HasColumnType("jsonb") + .HasColumnName("result_json") + .HasComment("动作链结果JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("执行状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_action_execution_logs"); + + b.HasIndex("ButtonDefinitionId") + .HasDatabaseName("ix_b_action_execution_logs_button_definition_id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_b_action_execution_logs_created_at"); + + b.ToTable("b_action_execution_logs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowApplication", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("应用编码,平台内唯一"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description") + .HasComment("应用描述"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("应用名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("应用状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("应用元数据版本"); + + b.HasKey("Id") + .HasName("pk_b_apps"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_b_apps_code"); + + b.ToTable("b_apps", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowButtonDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ActionsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("actions_json") + .HasComment("执行中(主链)动作链JSON"); + + b.Property("AfterActionsJson") + .HasColumnType("jsonb") + .HasColumnName("after_actions_json") + .HasComment("执行后(后置)动作链JSON"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id") + .HasComment("所属 BuildFlow 应用ID"); + + b.Property("BeforeActionsJson") + .HasColumnType("jsonb") + .HasColumnName("before_actions_json") + .HasComment("执行期(前置)动作链JSON"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("按钮编码,作用域内唯一"); + + b.Property("ConfirmJson") + .HasColumnType("jsonb") + .HasColumnName("confirm_json") + .HasComment("按钮确认弹窗JSON"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DisabledWhenJson") + .HasColumnType("jsonb") + .HasColumnName("disabled_when_json") + .HasComment("按钮禁用条件JSON"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联表单定义ID"); + + b.Property("Icon") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon") + .HasComment("按钮图标"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("label") + .HasComment("按钮显示文本"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PageDefinitionId") + .HasColumnType("uuid") + .HasColumnName("page_definition_id") + .HasComment("所属页面定义ID"); + + b.Property("PermissionCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("permission_code") + .HasComment("按钮权限码"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement") + .HasComment("按钮展示位置"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order") + .HasComment("排序号"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("按钮状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("VisibleWhenJson") + .HasColumnType("jsonb") + .HasColumnName("visible_when_json") + .HasComment("按钮可见条件JSON"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uuid") + .HasColumnName("workflow_definition_id") + .HasComment("关联流程定义ID"); + + b.HasKey("Id") + .HasName("pk_b_buttons"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_buttons_code"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_buttons_form_definition_id"); + + b.HasIndex("PageDefinitionId") + .HasDatabaseName("ix_b_buttons_page_definition_id"); + + b.ToTable("b_buttons", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowFormShare", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("AllowAnonymous") + .HasColumnType("boolean") + .HasColumnName("allow_anonymous") + .HasComment("是否允许匿名访问"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at") + .HasComment("过期时间(UTC),null 表示永不过期"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("被分享的表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("MaxViews") + .HasColumnType("integer") + .HasColumnName("max_views") + .HasComment("最大查看次数,null 表示不限"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password") + .HasComment("bcrypt 密码哈希,null 表示无需密码"); + + b.Property("Permission") + .HasColumnType("integer") + .HasColumnName("permission") + .HasComment("分享权限级别(View/Edit/Admin)"); + + b.Property("Remark") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("remark") + .HasComment("分享备注"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token") + .HasComment("匿名访问 token(32位hex)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("ViewCount") + .HasColumnType("integer") + .HasColumnName("view_count") + .HasComment("已被查看次数"); + + b.HasKey("Id") + .HasName("pk_b_form_shares"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_shares_form_definition_id"); + + b.HasIndex("Token") + .IsUnique() + .HasDatabaseName("ix_b_form_shares_token"); + + b.ToTable("b_form_shares", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowImportJob", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("attempt_count") + .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("ErrorJson") + .HasColumnType("jsonb") + .HasColumnName("error_json") + .HasComment("错误明细JSON数组"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("error_message") + .HasComment("任务级错误消息"); + + b.Property("FailedRows") + .HasColumnType("integer") + .HasColumnName("failed_rows") + .HasComment("失败行数"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("file_url") + .HasComment("上传文件存储路径"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at") + .HasComment("处理完成时间(UTC)"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("导入目标表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("LastHeartbeatAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_at") + .HasComment("最近一次处理心跳时间(UTC)"); + + b.Property("LeaseExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lease_expires_at") + .HasComment("租约到期时间(UTC)"); + + b.Property("LeaseOwner") + .IsConcurrencyToken() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("lease_owner") + .HasComment("当前处理租约的Host实例标识"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("OriginalFileName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("original_file_name") + .HasComment("原始上传文件名"); + + b.Property("ResultUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("result_url") + .HasComment("结果报告下载URL"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasComment("处理开始时间(UTC)"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("任务状态(Pending/Running/Success/Failed/PartialSuccess)"); + + b.Property("SuccessRows") + .HasColumnType("integer") + .HasColumnName("success_rows") + .HasComment("成功行数"); + + b.Property("TotalRows") + .HasColumnType("integer") + .HasColumnName("total_rows") + .HasComment("总行数(不含表头)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_import_jobs"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_import_jobs_form_definition_id"); + + b.HasIndex("Status", "CreatedAt") + .HasDatabaseName("ix_b_import_jobs_status_created_at"); + + b.HasIndex("Status", "LeaseExpiresAt") + .HasDatabaseName("ix_b_import_jobs_status_lease_expires_at"); + + b.ToTable("b_import_jobs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowPageDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id") + .HasComment("所属 BuildFlow 应用ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("页面编码,应用内唯一"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("页面名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PageType") + .HasColumnType("integer") + .HasColumnName("page_type") + .HasComment("页面类型"); + + b.Property("SchemaJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("页面定义JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("页面状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("页面元数据版本"); + + b.HasKey("Id") + .HasName("pk_b_pages"); + + b.HasIndex("PageType") + .HasDatabaseName("ix_b_pages_page_type"); + + b.HasIndex("ApplicationId", "Code") + .IsUnique() + .HasDatabaseName("ix_b_pages_application_id_code"); + + b.ToTable("b_pages", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormComponentRegistry", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("组件分类"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefaultSchema") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("default_schema") + .HasComment("组件默认Schema JSON"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("display_name") + .HasComment("组件显示名称"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon") + .HasComment("组件图标"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active") + .HasComment("是否启用"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("组件名称(英文标识)"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order") + .HasComment("排序号"); + + b.Property("SupportedProps") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("supported_props") + .HasComment("组件支持的属性配置JSON"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_form_component_registry"); + + b.HasIndex("Category") + .HasDatabaseName("ix_b_form_component_registry_category"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_b_form_component_registry_name"); + + b.ToTable("b_form_component_registry", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormData", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("data_json") + .HasComment("表单数据JSON"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("关联的流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_form_data"); + + b.HasIndex("DataJson") + .HasDatabaseName("ix_b_form_data_data_json"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("DataJson"), "GIN"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("DataJson"), new[] { "jsonb_path_ops" }); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_form_data_instance_id"); + + b.HasIndex("FormDefinitionId", "CreatedAt", "Id") + .HasDatabaseName("ix_b_form_data_form_definition_id_created_at_id"); + + b.ToTable("b_form_data", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("code") + .HasComment("表单编码,唯一标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description") + .HasComment("表单描述"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("表单名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SchemaJson") + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("表单Schema JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("表单状态(Draft=草稿, Published=已发布, Disabled=已禁用)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_form_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_form_definitions_code"); + + b.ToTable("b_form_definitions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormDefinitionVersion", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ChangeSummary") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("change_summary") + .HasComment("变更摘要"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("所属表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SchemaJson") + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("该版本的Schema JSON快照"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source") + .HasComment("触发来源 Update/Publish"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_form_definition_versions"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_definition_versions_form_definition_id"); + + b.HasIndex("FormDefinitionId", "Version") + .HasDatabaseName("ix_b_form_definition_versions_form_definition_id_version"); + + b.ToTable("b_form_definition_versions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("通知分类"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("content") + .HasComment("通知正文"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_read") + .HasComment("是否已读"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("read_at") + .HasComment("阅读时间"); + + b.Property("RecipientRole") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("recipient_role") + .HasComment("收件角色名(role:规则)"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("recipient_user_id") + .HasComment("收件人用户ID(user:规则)"); + + b.Property("RelatedInstanceId") + .HasColumnType("uuid") + .HasColumnName("related_instance_id") + .HasComment("关联流程实例ID"); + + b.Property("RelatedTaskId") + .HasColumnType("uuid") + .HasColumnName("related_task_id") + .HasComment("关联任务ID"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("通知标题"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_notifications"); + + b.HasIndex("RelatedInstanceId") + .HasDatabaseName("ix_wf_notifications_related_instance_id"); + + b.HasIndex("RecipientUserId", "IsRead") + .HasDatabaseName("ix_wf_notifications_recipient_user_id_is_read"); + + b.ToTable("b_notifications", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WebhookDelivery", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts") + .HasComment("已尝试次数"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("通知分类"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("http_method") + .HasComment("HTTP方法"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error") + .HasComment("最近一次错误信息"); + + b.Property("MaxAttempts") + .HasColumnType("integer") + .HasColumnName("max_attempts") + .HasComment("最大尝试次数"); + + b.Property("NextRetryAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("next_retry_at") + .HasComment("下次重试时间"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasComment("投递JSON body"); + + b.Property("RelatedInstanceId") + .HasColumnType("uuid") + .HasColumnName("related_instance_id") + .HasComment("关联流程实例ID"); + + b.Property("RelatedTaskId") + .HasColumnType("uuid") + .HasColumnName("related_task_id") + .HasComment("关联任务ID"); + + b.Property("ResponseBody") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("response_body") + .HasComment("响应体"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status") + .HasComment("投递状态:pending/delivered/failed"); + + b.Property("StatusCode") + .HasColumnType("integer") + .HasColumnName("status_code") + .HasComment("HTTP响应码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("url") + .HasComment("目标URL"); + + b.HasKey("Id") + .HasName("pk_b_webhook_deliveries"); + + b.HasIndex("NextRetryAt") + .HasDatabaseName("ix_wf_webhook_deliveries_next_retry_at"); + + b.HasIndex("Status") + .HasDatabaseName("ix_wf_webhook_deliveries_status"); + + b.ToTable("b_webhook_deliveries", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("code") + .HasComment("工作流编码,唯一标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionJson") + .HasColumnType("jsonb") + .HasColumnName("definition_json") + .HasComment("工作流定义JSON(含节点/边的完整结构)"); + + b.Property("DefinitionTree") + .HasColumnType("jsonb") + .HasColumnName("definition_tree") + .HasComment("钉钉式审批流树JSON(编辑态单一事实源,发布时编译为DefinitionJson)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description") + .HasComment("工作流描述"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled") + .HasComment("是否启用"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("工作流名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("定义状态(Draft=草稿, Published=已发布)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_workflow_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_workflow_definitions_code"); + + b.ToTable("b_workflow_definitions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinitionSnapshot", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ChangeSummary") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("change_summary") + .HasComment("变更摘要"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("DefinitionJson") + .HasColumnType("jsonb") + .HasColumnName("definition_json") + .HasComment("该版本的完整X6 graph JSON快照(不可变)"); + + b.Property("DefinitionTree") + .HasColumnType("jsonb") + .HasColumnName("definition_tree") + .HasComment("该版本的钉钉式树JSON快照(审计回显用,引擎不读)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source") + .HasComment("触发来源 Publish"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号(发布时的 WorkflowDefinition.Version)"); + + b.HasKey("Id") + .HasName("pk_b_workflow_definition_snapshots"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_definition_snapshots_definition_id"); + + b.HasIndex("DefinitionId", "Version") + .IsUnique() + .HasDatabaseName("ix_b_workflow_definition_snapshots_definition_id_version"); + + b.ToTable("b_workflow_definition_snapshots", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Condition") + .HasColumnType("text") + .HasColumnName("condition") + .HasComment("条件表达式"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("EdgeType") + .HasColumnType("integer") + .HasColumnName("edge_type") + .HasComment("边类型(Normal=普通, Condition=条件)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Label") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("label") + .HasComment("边标签"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Order") + .HasColumnType("integer") + .HasColumnName("order") + .HasComment("排序号"); + + b.Property("SourceNodeId") + .HasColumnType("uuid") + .HasColumnName("source_node_id") + .HasComment("起始节点ID"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id") + .HasComment("目标节点ID"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_edges"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_edges_definition_id"); + + b.ToTable("b_workflow_edges", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("完成时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("definition_code") + .HasComment("启动时快照的流程定义Code(审计追溯)"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("关联的工作流定义ID"); + + b.Property("DefinitionVersion") + .HasColumnType("integer") + .HasColumnName("definition_version") + .HasComment("启动时绑定的流程定义版本号(版本隔离)"); + + b.Property("InitiatorId") + .HasColumnType("uuid") + .HasColumnName("initiator_id") + .HasComment("发起人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("ParentInstanceId") + .HasColumnType("uuid") + .HasColumnName("parent_instance_id") + .HasComment("父流程实例ID(子流程场景)"); + + b.Property("ParentTokenId") + .HasColumnType("uuid") + .HasColumnName("parent_token_id") + .HasComment("父流程中等待的令牌ID"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasComment("启动时间"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("实例状态(Running/Suspended/Completed/Terminated)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("流程实例标题"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Variables") + .HasColumnType("jsonb") + .HasColumnName("variables") + .HasComment("流程变量JSON"); + + b.HasKey("Id") + .HasName("pk_b_workflow_instances"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_instances_definition_id"); + + b.ToTable("b_workflow_instances", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Config") + .HasColumnType("jsonb") + .HasColumnName("config") + .HasComment("节点配置JSON(审批人、表单绑定等)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID,用于审批/抄送节点"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("节点名称"); + + b.Property("NodeType") + .HasColumnType("integer") + .HasColumnName("node_type") + .HasComment("节点类型(Start/End/Approval/Cc/Condition/Parallel/SubProcess)"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PositionX") + .HasColumnType("integer") + .HasColumnName("position_x") + .HasComment("画布X坐标"); + + b.Property("PositionY") + .HasColumnType("integer") + .HasColumnName("position_y") + .HasComment("画布Y坐标"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_nodes"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_nodes_definition_id"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_workflow_nodes_form_definition_id"); + + b.ToTable("b_workflow_nodes", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("AssigneeId") + .HasColumnType("uuid") + .HasColumnName("assignee_id") + .HasComment("当前处理人ID"); + + b.Property("AssigneeRole") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("assignee_role") + .HasComment("当前处理人角色"); + + b.Property("Attachments") + .HasColumnType("text") + .HasColumnName("attachments"); + + b.Property("CandidateRoles") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("candidate_roles") + .HasComment("候选角色列表,逗号分隔"); + + b.Property("CandidateUsers") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("candidate_users") + .HasComment("候选用户ID列表,逗号分隔"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("comment") + .HasComment("审批意见"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("完成时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DelegatedFromId") + .HasColumnType("uuid") + .HasColumnName("delegated_from_id") + .HasComment("原处理人ID(委托场景)"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("due_at") + .HasComment("截止时间"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("所属流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id") + .HasComment("关联的节点定义ID"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Result") + .HasColumnType("jsonb") + .HasColumnName("result") + .HasComment("处理结果JSON"); + + b.Property("SequentialAssignees") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("sequential_assignees") + .HasComment("依次审批:完整 assigneeRules JSON 数组,仅 sequential 模式"); + + b.Property("SequentialOrder") + .HasColumnType("integer") + .HasColumnName("sequential_order") + .HasComment("依次审批:当前审批人序号(0起),仅 sequential 模式"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("任务状态(Pending/Approved/Rejected/Transferred/Delegated)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("任务标题"); + + b.Property("TokenId") + .HasColumnType("uuid") + .HasColumnName("token_id") + .HasComment("关联的令牌ID"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type") + .HasComment("任务类型(Approval=审批, Cc=抄送)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_tasks"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_workflow_tasks_instance_id"); + + b.ToTable("b_workflow_tasks", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTimerJob", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("attempt_count") + .HasComment("累计领取次数"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("进入终态的时间(UTC)"); + + 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("DueAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("due_at") + .HasComment("计划触发时间(UTC)"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("所属流程实例ID"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error") + .HasComment("最近一次失败原因"); + + b.Property("LeaseExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lease_expires_at") + .HasComment("租约到期时间(UTC)"); + + b.Property("LeaseOwner") + .IsConcurrencyToken() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("lease_owner") + .HasComment("当前租约持有者"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id") + .HasComment("Timer节点ID"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("任务状态(Pending/Processing/Completed/Failed/Cancelled)"); + + b.Property("TokenId") + .HasColumnType("uuid") + .HasColumnName("token_id") + .HasComment("等待到期的源令牌ID"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_timer_jobs"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_workflow_timer_jobs_instance_id"); + + b.HasIndex("TokenId") + .IsUnique() + .HasDatabaseName("ix_b_workflow_timer_jobs_token_id"); + + b.HasIndex("Status", "DueAt") + .HasDatabaseName("ix_b_workflow_timer_jobs_status_due_at"); + + b.HasIndex("Status", "LeaseExpiresAt") + .HasDatabaseName("ix_b_workflow_timer_jobs_status_lease_expires_at"); + + b.ToTable("b_workflow_timer_jobs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ArrivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("arrived_at") + .HasComment("到达当前节点的时间"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("令牌完成/消费时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("所属流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id") + .HasComment("当前所在节点ID"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("令牌状态(Active/Consumed/Terminated)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_tokens"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_workflow_tokens_instance_id"); + + b.ToTable("b_workflow_tokens", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null) + .WithMany("Edges") + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_edges_b_workflow_definitions_definition_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null) + .WithMany("Nodes") + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_nodes_b_workflow_definitions_definition_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null) + .WithMany("Tasks") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_tasks_b_workflow_instances_instance_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTimerJob", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null) + .WithMany() + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_timer_jobs_b_workflow_instances_instance_id"); + + b.HasOne("Workflow.Domain.Entities.WorkflowToken", null) + .WithOne() + .HasForeignKey("Workflow.Domain.Entities.WorkflowTimerJob", "TokenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_timer_jobs_workflow_tokens_token_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null) + .WithMany("Tokens") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_tokens_b_workflow_instances_instance_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b => + { + b.Navigation("Edges"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b => + { + b.Navigation("Tasks"); + + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/20260724030743_AddFormDataExportKeysetIndex.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260724030743_AddFormDataExportKeysetIndex.cs new file mode 100644 index 0000000..7ec78cd --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260724030743_AddFormDataExportKeysetIndex.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Workflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddFormDataExportKeysetIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_b_form_data_form_definition_id", + table: "b_form_data"); + + migrationBuilder.CreateIndex( + name: "ix_b_form_data_form_definition_id_created_at_id", + table: "b_form_data", + columns: new[] { "form_definition_id", "created_at", "id" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_b_form_data_form_definition_id_created_at_id", + table: "b_form_data"); + + migrationBuilder.CreateIndex( + name: "ix_b_form_data_form_definition_id", + table: "b_form_data", + column: "form_definition_id"); + } + } +} diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs index 2aa6495..e2a0054 100644 --- a/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs @@ -873,12 +873,12 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("DataJson"), "GIN"); NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("DataJson"), new[] { "jsonb_path_ops" }); - b.HasIndex("FormDefinitionId") - .HasDatabaseName("ix_b_form_data_form_definition_id"); - b.HasIndex("InstanceId") .HasDatabaseName("ix_b_form_data_instance_id"); + b.HasIndex("FormDefinitionId", "CreatedAt", "Id") + .HasDatabaseName("ix_b_form_data_form_definition_id_created_at_id"); + b.ToTable("b_form_data", (string)null); }); diff --git a/tests/BuildFlow.Workflow.Tests/Performance/FormDataExportIndexTests.cs b/tests/BuildFlow.Workflow.Tests/Performance/FormDataExportIndexTests.cs new file mode 100644 index 0000000..9e4f57d --- /dev/null +++ b/tests/BuildFlow.Workflow.Tests/Performance/FormDataExportIndexTests.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Workflow.Domain.Entities; +using Xunit; + +namespace BuildFlow.Workflow.Tests.Performance; + +/// 表单数据导出 keyset 查询的 EF 索引模型测试。 +public class FormDataExportIndexTests +{ + [Fact] + public void FormDataShouldHaveExportKeysetCompositeIndex() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"form-data-export-index-{Guid.NewGuid():N}") + .Options; + using var db = new WorkflowDbContext(options); + var entity = db.Model.FindEntityType(typeof(FormData)); + + entity.Should().NotBeNull(); + entity!.GetIndexes().Should().Contain(index => index.Properties + .Select(property => property.Name) + .SequenceEqual(new[] + { + nameof(FormData.FormDefinitionId), + nameof(FormData.CreatedAt), + nameof(FormData.Id), + })); + } +}