From 4ba4769cc17469f5d54ab9015e830ce4f79322a9 Mon Sep 17 00:00:00 2001 From: xiangning Date: Fri, 24 Jul 2026 03:03:57 +0800 Subject: [PATCH] feat: migrate workflow job reliability state --- ...0151_AddWorkflowJobReliability.Designer.cs | 2190 +++++++++++++++++ ...0260723110151_AddWorkflowJobReliability.cs | 153 ++ .../WorkflowDbContextModelSnapshot.cs | 153 +- .../Reliability/WorkflowJobMigrationTests.cs | 223 ++ 4 files changed, 2717 insertions(+), 2 deletions(-) create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260723110151_AddWorkflowJobReliability.Designer.cs create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260723110151_AddWorkflowJobReliability.cs create mode 100644 tests/BuildFlow.Workflow.Tests/Reliability/WorkflowJobMigrationTests.cs diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/20260723110151_AddWorkflowJobReliability.Designer.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260723110151_AddWorkflowJobReliability.Designer.cs new file mode 100644 index 0000000..7342f88 --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260723110151_AddWorkflowJobReliability.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("20260723110151_AddWorkflowJobReliability")] + partial class AddWorkflowJobReliability + { + /// + 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("FormDefinitionId") + .HasDatabaseName("ix_b_form_data_form_definition_id"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_form_data_instance_id"); + + b.ToTable("b_form_data", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("code") + .HasComment("表单编码,唯一标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description") + .HasComment("表单描述"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("表单名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SchemaJson") + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("表单Schema JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("表单状态(Draft=草稿, Published=已发布, Disabled=已禁用)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_form_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_form_definitions_code"); + + b.ToTable("b_form_definitions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormDefinitionVersion", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ChangeSummary") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("change_summary") + .HasComment("变更摘要"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("所属表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SchemaJson") + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("该版本的Schema JSON快照"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source") + .HasComment("触发来源 Update/Publish"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_form_definition_versions"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_definition_versions_form_definition_id"); + + b.HasIndex("FormDefinitionId", "Version") + .HasDatabaseName("ix_b_form_definition_versions_form_definition_id_version"); + + b.ToTable("b_form_definition_versions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("通知分类"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("content") + .HasComment("通知正文"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_read") + .HasComment("是否已读"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("read_at") + .HasComment("阅读时间"); + + b.Property("RecipientRole") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("recipient_role") + .HasComment("收件角色名(role:规则)"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("recipient_user_id") + .HasComment("收件人用户ID(user:规则)"); + + b.Property("RelatedInstanceId") + .HasColumnType("uuid") + .HasColumnName("related_instance_id") + .HasComment("关联流程实例ID"); + + b.Property("RelatedTaskId") + .HasColumnType("uuid") + .HasColumnName("related_task_id") + .HasComment("关联任务ID"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("通知标题"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_notifications"); + + b.HasIndex("RelatedInstanceId") + .HasDatabaseName("ix_wf_notifications_related_instance_id"); + + b.HasIndex("RecipientUserId", "IsRead") + .HasDatabaseName("ix_wf_notifications_recipient_user_id_is_read"); + + b.ToTable("b_notifications", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WebhookDelivery", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts") + .HasComment("已尝试次数"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("通知分类"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("http_method") + .HasComment("HTTP方法"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error") + .HasComment("最近一次错误信息"); + + b.Property("MaxAttempts") + .HasColumnType("integer") + .HasColumnName("max_attempts") + .HasComment("最大尝试次数"); + + b.Property("NextRetryAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("next_retry_at") + .HasComment("下次重试时间"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasComment("投递JSON body"); + + b.Property("RelatedInstanceId") + .HasColumnType("uuid") + .HasColumnName("related_instance_id") + .HasComment("关联流程实例ID"); + + b.Property("RelatedTaskId") + .HasColumnType("uuid") + .HasColumnName("related_task_id") + .HasComment("关联任务ID"); + + b.Property("ResponseBody") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("response_body") + .HasComment("响应体"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status") + .HasComment("投递状态:pending/delivered/failed"); + + b.Property("StatusCode") + .HasColumnType("integer") + .HasColumnName("status_code") + .HasComment("HTTP响应码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("url") + .HasComment("目标URL"); + + b.HasKey("Id") + .HasName("pk_b_webhook_deliveries"); + + b.HasIndex("NextRetryAt") + .HasDatabaseName("ix_wf_webhook_deliveries_next_retry_at"); + + b.HasIndex("Status") + .HasDatabaseName("ix_wf_webhook_deliveries_status"); + + b.ToTable("b_webhook_deliveries", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("code") + .HasComment("工作流编码,唯一标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionJson") + .HasColumnType("jsonb") + .HasColumnName("definition_json") + .HasComment("工作流定义JSON(含节点/边的完整结构)"); + + b.Property("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/20260723110151_AddWorkflowJobReliability.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260723110151_AddWorkflowJobReliability.cs new file mode 100644 index 0000000..cd71ea2 --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260723110151_AddWorkflowJobReliability.cs @@ -0,0 +1,153 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Workflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddWorkflowJobReliability : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_b_import_jobs_status", + table: "b_import_jobs"); + + migrationBuilder.AddColumn( + name: "attempt_count", + table: "b_import_jobs", + type: "integer", + nullable: false, + defaultValue: 0, + comment: "累计领取次数"); + + migrationBuilder.AddColumn( + name: "last_heartbeat_at", + table: "b_import_jobs", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次处理心跳时间(UTC)"); + + migrationBuilder.AddColumn( + name: "lease_expires_at", + table: "b_import_jobs", + type: "timestamp with time zone", + nullable: true, + comment: "租约到期时间(UTC)"); + + migrationBuilder.AddColumn( + name: "lease_owner", + table: "b_import_jobs", + type: "character varying(200)", + maxLength: 200, + nullable: true, + comment: "当前处理租约的Host实例标识"); + + migrationBuilder.CreateTable( + name: "b_workflow_timer_jobs", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, comment: "主键ID"), + instance_id = table.Column(type: "uuid", nullable: false, comment: "所属流程实例ID"), + token_id = table.Column(type: "uuid", nullable: false, comment: "等待到期的源令牌ID"), + node_id = table.Column(type: "uuid", nullable: false, comment: "Timer节点ID"), + due_at = table.Column(type: "timestamp with time zone", nullable: false, comment: "计划触发时间(UTC)"), + status = table.Column(type: "integer", nullable: false, comment: "任务状态(Pending/Processing/Completed/Failed/Cancelled)"), + lease_owner = table.Column(type: "character varying(200)", maxLength: 200, nullable: true, comment: "当前租约持有者"), + lease_expires_at = table.Column(type: "timestamp with time zone", nullable: true, comment: "租约到期时间(UTC)"), + attempt_count = table.Column(type: "integer", nullable: false, defaultValue: 0, comment: "累计领取次数"), + last_error = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true, comment: "最近一次失败原因"), + completed_at = table.Column(type: "timestamp with time zone", nullable: true, comment: "进入终态的时间(UTC)"), + created_by = table.Column(type: "character varying(100)", maxLength: 100, nullable: false, comment: "创建人ID"), + created_at = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间"), + updated_by = table.Column(type: "character varying(100)", maxLength: 100, nullable: false, comment: "更新人ID"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk_b_workflow_timer_jobs", x => x.id); + table.ForeignKey( + name: "fk_b_workflow_timer_jobs_b_workflow_instances_instance_id", + column: x => x.instance_id, + principalTable: "b_workflow_instances", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_b_workflow_timer_jobs_workflow_tokens_token_id", + column: x => x.token_id, + principalTable: "b_workflow_tokens", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_b_import_jobs_status_created_at", + table: "b_import_jobs", + columns: new[] { "status", "created_at" }); + + migrationBuilder.CreateIndex( + name: "ix_b_import_jobs_status_lease_expires_at", + table: "b_import_jobs", + columns: new[] { "status", "lease_expires_at" }); + + migrationBuilder.CreateIndex( + name: "ix_b_workflow_timer_jobs_instance_id", + table: "b_workflow_timer_jobs", + column: "instance_id"); + + migrationBuilder.CreateIndex( + name: "ix_b_workflow_timer_jobs_status_due_at", + table: "b_workflow_timer_jobs", + columns: new[] { "status", "due_at" }); + + migrationBuilder.CreateIndex( + name: "ix_b_workflow_timer_jobs_status_lease_expires_at", + table: "b_workflow_timer_jobs", + columns: new[] { "status", "lease_expires_at" }); + + migrationBuilder.CreateIndex( + name: "ix_b_workflow_timer_jobs_token_id", + table: "b_workflow_timer_jobs", + column: "token_id", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "b_workflow_timer_jobs"); + + migrationBuilder.DropIndex( + name: "ix_b_import_jobs_status_created_at", + table: "b_import_jobs"); + + migrationBuilder.DropIndex( + name: "ix_b_import_jobs_status_lease_expires_at", + table: "b_import_jobs"); + + migrationBuilder.DropColumn( + name: "attempt_count", + table: "b_import_jobs"); + + migrationBuilder.DropColumn( + name: "last_heartbeat_at", + table: "b_import_jobs"); + + migrationBuilder.DropColumn( + name: "lease_expires_at", + table: "b_import_jobs"); + + migrationBuilder.DropColumn( + name: "lease_owner", + table: "b_import_jobs"); + + migrationBuilder.CreateIndex( + name: "ix_b_import_jobs_status", + table: "b_import_jobs", + column: "status"); + } + } +} diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs index e14745e..2aa6495 100644 --- a/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/WorkflowDbContextModelSnapshot.cs @@ -455,6 +455,13 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations .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") @@ -507,6 +514,23 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations .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) @@ -564,8 +588,11 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations b.HasIndex("FormDefinitionId") .HasDatabaseName("ix_b_import_jobs_form_definition_id"); - b.HasIndex("Status") - .HasDatabaseName("ix_b_import_jobs_status"); + 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); }); @@ -1900,6 +1927,111 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations 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") @@ -2009,6 +2141,23 @@ namespace BuildFlow.Workflow.Infrastructure.Persistence.Migrations .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) diff --git a/tests/BuildFlow.Workflow.Tests/Reliability/WorkflowJobMigrationTests.cs b/tests/BuildFlow.Workflow.Tests/Reliability/WorkflowJobMigrationTests.cs new file mode 100644 index 0000000..88bcd81 --- /dev/null +++ b/tests/BuildFlow.Workflow.Tests/Reliability/WorkflowJobMigrationTests.cs @@ -0,0 +1,223 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql; +using Workflow.Domain.Enums; +using Workflow.Infrastructure.Persistence; +using Xunit; + +namespace BuildFlow.Workflow.Tests.Reliability; + +/// +/// 工作流可靠性迁移的空库安装与旧结构升级测试。 +/// +public class WorkflowJobMigrationTests +{ + private const string PreviousMigration = + "20260719041812_FixWorkflowTaskAttachmentsColumn"; + + private const string DefaultConnection = + "Host=127.0.0.1;Port=55432;Username=buildflow;Password=buildflow;Database=postgres"; + + [Fact] + public async Task EmptyDatabaseMigratesToWorkflowJobReliabilitySchema() + { + var databaseName = $"workflow_migration_empty_{Guid.NewGuid():N}"; + var connectionString = await CreateDatabaseAsync(databaseName); + try + { + await using var db = CreateDbContext(connectionString); + await db.Database.MigrateAsync(); + + await using var connection = new NpgsqlConnection(connectionString); + await connection.OpenAsync(); + (await TableExistsAsync(connection, "b_workflow_timer_jobs")).Should().BeTrue(); + var columns = await ReadColumnNamesAsync(connection, "b_import_jobs"); + columns.Should().Contain( + "lease_owner", + "lease_expires_at", + "attempt_count", + "last_heartbeat_at"); + } + finally + { + await DropDatabaseAsync(databaseName); + } + } + + [Fact] + public async Task ExistingImportJobSurvivesReliabilityMigration() + { + var databaseName = $"workflow_migration_upgrade_{Guid.NewGuid():N}"; + var connectionString = await CreateDatabaseAsync(databaseName); + var jobId = Guid.NewGuid(); + var formDefinitionId = Guid.NewGuid(); + const string filePath = "/tmp/existing-import.xlsx"; + try + { + await using var db = CreateDbContext(connectionString); + var migrator = db.GetService(); + await migrator.MigrateAsync(PreviousMigration); + + var now = DateTime.UtcNow; + await db.Database.ExecuteSqlInterpolatedAsync($""" + INSERT INTO b_import_jobs ( + id, + form_definition_id, + status, + file_url, + total_rows, + success_rows, + failed_rows, + created_by, + created_at, + updated_by, + updated_at, + is_deleted, + operator_ip) + VALUES ( + {jobId}, + {formDefinitionId}, + {(int)BuildFlowImportJobStatus.Pending}, + {filePath}, + {0}, + {0}, + {0}, + {"migration-test"}, + {now}, + {"migration-test"}, + {now}, + {false}, + {"127.0.0.1"}) + """); + + await db.Database.MigrateAsync(); + + await using var connection = new NpgsqlConnection(connectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT id, + status, + file_url, + attempt_count, + lease_owner, + lease_expires_at, + last_heartbeat_at + FROM b_import_jobs + WHERE id = @job_id + """; + command.Parameters.AddWithValue("job_id", jobId); + await using var reader = await command.ExecuteReaderAsync(); + (await reader.ReadAsync()).Should().BeTrue(); + reader.GetGuid(0).Should().Be(jobId); + reader.GetInt32(1).Should().Be((int)BuildFlowImportJobStatus.Pending); + reader.GetString(2).Should().Be(filePath); + reader.GetInt32(3).Should().Be(0); + reader.IsDBNull(4).Should().BeTrue(); + reader.IsDBNull(5).Should().BeTrue(); + reader.IsDBNull(6).Should().BeTrue(); + } + finally + { + await DropDatabaseAsync(databaseName); + } + } + + private static WorkflowDbContext CreateDbContext(string connectionString) + { + var options = new DbContextOptionsBuilder() + .UseNpgsql(connectionString) + .UseSnakeCaseNamingConvention() + .Options; + return new WorkflowDbContext(options); + } + + private static async Task CreateDatabaseAsync(string databaseName) + { + var configured = Environment.GetEnvironmentVariable("BUILDFLOW_WORKFLOW_TEST_CONNECTION") + ?? DefaultConnection; + var adminBuilder = new NpgsqlConnectionStringBuilder(configured) + { + Database = "postgres", + Pooling = false, + }; + await using (var connection = new NpgsqlConnection(adminBuilder.ConnectionString)) + { + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = $"CREATE DATABASE \"{databaseName}\""; + await command.ExecuteNonQueryAsync(); + } + + return new NpgsqlConnectionStringBuilder(configured) + { + Database = databaseName, + Pooling = true, + }.ConnectionString; + } + + private static async Task DropDatabaseAsync(string databaseName) + { + NpgsqlConnection.ClearAllPools(); + var configured = Environment.GetEnvironmentVariable("BUILDFLOW_WORKFLOW_TEST_CONNECTION") + ?? DefaultConnection; + var adminBuilder = new NpgsqlConnectionStringBuilder(configured) + { + Database = "postgres", + Pooling = false, + }; + await using var connection = new NpgsqlConnection(adminBuilder.ConnectionString); + await connection.OpenAsync(); + await using (var terminate = connection.CreateCommand()) + { + terminate.CommandText = + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = @database AND pid <> pg_backend_pid()"; + terminate.Parameters.AddWithValue("database", databaseName); + await terminate.ExecuteNonQueryAsync(); + } + + await using var drop = connection.CreateCommand(); + drop.CommandText = $"DROP DATABASE IF EXISTS \"{databaseName}\""; + await drop.ExecuteNonQueryAsync(); + } + + private static async Task TableExistsAsync( + NpgsqlConnection connection, + string tableName) + { + await using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = @table_name) + """; + command.Parameters.AddWithValue("table_name", tableName); + return (bool)(await command.ExecuteScalarAsync())!; + } + + private static async Task> ReadColumnNamesAsync( + NpgsqlConnection connection, + string tableName) + { + await using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT column_name + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = @table_name + """; + command.Parameters.AddWithValue("table_name", tableName); + var columns = new List(); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + columns.Add(reader.GetString(0)); + } + + return columns; + } +}