From d1c8988054c93e7bdfd8e7aa223eed59a1cdf0c5 Mon Sep 17 00:00:00 2001 From: xiangning Date: Mon, 6 Jul 2026 17:56:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(scripting):=20=E8=BF=81=E7=A7=BB=20Domain?= =?UTF-8?q?=20=E5=AE=9E=E4=BD=93+EF=20=E9=85=8D=E7=BD=AE+=E8=A1=A8?= =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScriptFunction/ScriptExecutionLog 实体(从 CustomFunction 迁移) - ScriptStatus/ScriptExecutionStatus 枚举 - ScriptingDbContext + 两个 Configuration(表名 funcs/func_execution_logs) - EF Core 迁移:RenameTablesForScriptingModule(b_funcs→funcs 保数据) - 新增 LogLines 字段(Logger 输出收集) Task T2 of scripting module refactor --- .../Domain/Entities/ScriptExecutionLog.cs | 24 ++ .../Domain/Entities/ScriptFunction.cs | 51 ++++ .../Domain/Enums/ScriptExecutionStatus.cs | 9 + .../scripting/Domain/Enums/ScriptStatus.cs | 9 + ...RenameTablesForScriptingModule.Designer.cs | 236 ++++++++++++++++++ ...06095113_RenameTablesForScriptingModule.cs | 146 +++++++++++ .../ScriptingDbContextModelSnapshot.cs | 233 +++++++++++++++++ .../ScriptExecutionLogConfiguration.cs | 30 +++ .../ScriptFunctionConfiguration.cs | 35 +++ .../Persistence/ScriptingDbContext.cs | 63 +++++ .../ScriptingDbContextDesignTimeFactory.cs | 17 ++ .../ScriptingInfrastructureModule.cs | 32 ++- 12 files changed, 882 insertions(+), 3 deletions(-) create mode 100644 src/modules/scripting/Domain/Entities/ScriptExecutionLog.cs create mode 100644 src/modules/scripting/Domain/Entities/ScriptFunction.cs create mode 100644 src/modules/scripting/Domain/Enums/ScriptExecutionStatus.cs create mode 100644 src/modules/scripting/Domain/Enums/ScriptStatus.cs create mode 100644 src/modules/scripting/Infrastructure/Migrations/20260706095113_RenameTablesForScriptingModule.Designer.cs create mode 100644 src/modules/scripting/Infrastructure/Migrations/20260706095113_RenameTablesForScriptingModule.cs create mode 100644 src/modules/scripting/Infrastructure/Migrations/ScriptingDbContextModelSnapshot.cs create mode 100644 src/modules/scripting/Infrastructure/Persistence/Configurations/ScriptExecutionLogConfiguration.cs create mode 100644 src/modules/scripting/Infrastructure/Persistence/Configurations/ScriptFunctionConfiguration.cs create mode 100644 src/modules/scripting/Infrastructure/Persistence/ScriptingDbContext.cs create mode 100644 src/modules/scripting/Infrastructure/Persistence/ScriptingDbContextDesignTimeFactory.cs diff --git a/src/modules/scripting/Domain/Entities/ScriptExecutionLog.cs b/src/modules/scripting/Domain/Entities/ScriptExecutionLog.cs new file mode 100644 index 0000000..2774839 --- /dev/null +++ b/src/modules/scripting/Domain/Entities/ScriptExecutionLog.cs @@ -0,0 +1,24 @@ +using BuildFlow.Shared.Domain.Common; +using Scripting.Domain.Enums; + +namespace Scripting.Domain.Entities; + +/// 脚本执行日志,记录输入、输出、耗时和异常,便于审计脚本运行结果。 +public class ScriptExecutionLog : BaseEntity, IAuditable +{ + public Guid FunctionId { get; set; } + public string FunctionName { get; set; } = string.Empty; + public ScriptExecutionStatus Status { get; set; } + public string InputJson { get; set; } = "{}"; + public string? ResultJson { get; set; } + public string? ErrorMessage { get; set; } + public long DurationMs { get; set; } + + /// 执行期间由 Logger 收集的输出日志(多行文本)。 + public string? LogLines { get; set; } + + public string CreatedBy { get; set; } = default!; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public string UpdatedBy { get; set; } = default!; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/modules/scripting/Domain/Entities/ScriptFunction.cs b/src/modules/scripting/Domain/Entities/ScriptFunction.cs new file mode 100644 index 0000000..d11615d --- /dev/null +++ b/src/modules/scripting/Domain/Entities/ScriptFunction.cs @@ -0,0 +1,51 @@ +using BuildFlow.Shared.Domain.Common; +using Scripting.Domain.Enums; + +namespace Scripting.Domain.Entities; + +/// +/// 脚本函数定义。Name 为英文标识,Code 保存受控 C# 脚本内容,由 Roslyn 编译执行。 +/// 按钮通过 runFunction/executeFunctions 引用。本模块仅承载 C# 脚本类型(不含 HTTP 请求函数)。 +/// +public class ScriptFunction : BaseEntity, IFullAudit +{ + /// 绑定的自定义表单定义 ID。为空表示全局可复用函数。 + public Guid? FormDefinitionId { get; set; } + + /// 函数英文标识,供按钮动作链稳定引用。 + public string Name { get; set; } = string.Empty; + + /// 函数显示名称。 + public string DisplayName { get; set; } = string.Empty; + + /// 函数说明。 + public string? Description { get; set; } + + /// 受控 C# 脚本代码。 + public string Code { get; set; } = string.Empty; + + /// 函数输入 Schema JSON。 + public string? InputSchema { get; set; } + + /// 函数输出 Schema JSON。 + public string? OutputSchema { get; set; } + + /// 函数发布状态。 + public ScriptStatus Status { get; set; } = ScriptStatus.Draft; + + /// 函数版本号。 + public int Version { get; set; } = 1; + + /// 执行超时时间,单位毫秒。 + public int TimeoutMs { get; set; } + + /// 测试运行期间由 Logger 收集的输出日志(多行文本)。 + public string? LogLines { get; set; } + + public string CreatedBy { get; set; } = default!; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public string UpdatedBy { get; set; } = default!; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public bool IsDeleted { get; set; } + public string OperatorIP { get; set; } = "unknown"; +} diff --git a/src/modules/scripting/Domain/Enums/ScriptExecutionStatus.cs b/src/modules/scripting/Domain/Enums/ScriptExecutionStatus.cs new file mode 100644 index 0000000..0421941 --- /dev/null +++ b/src/modules/scripting/Domain/Enums/ScriptExecutionStatus.cs @@ -0,0 +1,9 @@ +namespace Scripting.Domain.Enums; + +/// 脚本执行结果状态。 +public enum ScriptExecutionStatus +{ + Success = 0, + Failed = 1, + Timeout = 2, +} diff --git a/src/modules/scripting/Domain/Enums/ScriptStatus.cs b/src/modules/scripting/Domain/Enums/ScriptStatus.cs new file mode 100644 index 0000000..4b73197 --- /dev/null +++ b/src/modules/scripting/Domain/Enums/ScriptStatus.cs @@ -0,0 +1,9 @@ +namespace Scripting.Domain.Enums; + +/// 脚本函数生命周期:Draft 草稿、Published 可运行、Disabled 停用阻断。 +public enum ScriptStatus +{ + Draft = 0, + Published = 1, + Disabled = 2, +} diff --git a/src/modules/scripting/Infrastructure/Migrations/20260706095113_RenameTablesForScriptingModule.Designer.cs b/src/modules/scripting/Infrastructure/Migrations/20260706095113_RenameTablesForScriptingModule.Designer.cs new file mode 100644 index 0000000..eda7e6f --- /dev/null +++ b/src/modules/scripting/Infrastructure/Migrations/20260706095113_RenameTablesForScriptingModule.Designer.cs @@ -0,0 +1,236 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Scripting.Infrastructure.Persistence; + +#nullable disable + +namespace Scripting.Infrastructure.Migrations +{ + [DbContext(typeof(ScriptingDbContext))] + [Migration("20260706095113_RenameTablesForScriptingModule")] + partial class RenameTablesForScriptingModule + { + /// + 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("Scripting.Domain.Entities.ScriptExecutionLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms") + .HasComment("执行耗时(毫秒)"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error_message") + .HasComment("错误信息"); + + b.Property("FunctionId") + .HasColumnType("uuid") + .HasColumnName("function_id") + .HasComment("函数ID"); + + b.Property("FunctionName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("function_name") + .HasComment("函数英文标识"); + + b.Property("InputJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("input_json") + .HasComment("执行输入JSON"); + + b.Property("LogLines") + .HasColumnType("text") + .HasColumnName("log_lines") + .HasComment("执行期间 Logger 收集的输出日志"); + + 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_func_execution_logs"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_func_execution_logs_created_at"); + + b.HasIndex("FunctionId") + .HasDatabaseName("ix_func_execution_logs_function_id"); + + b.ToTable("func_execution_logs", (string)null); + }); + + modelBuilder.Entity("Scripting.Domain.Entities.ScriptFunction", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text") + .HasColumnName("code") + .HasComment("C# 脚本代码"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description") + .HasComment("函数说明"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name") + .HasComment("函数显示名称"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("绑定的自定义表单定义ID,为空表示全局可复用函数"); + + b.Property("InputSchema") + .HasColumnType("jsonb") + .HasColumnName("input_schema") + .HasComment("函数输入 Schema JSON"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("LogLines") + .HasColumnType("text") + .HasColumnName("log_lines") + .HasComment("测试运行期间 Logger 收集的输出日志"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("函数英文标识"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("OutputSchema") + .HasColumnType("jsonb") + .HasColumnName("output_schema") + .HasComment("函数输出 Schema JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("函数状态"); + + b.Property("TimeoutMs") + .HasColumnType("integer") + .HasColumnName("timeout_ms") + .HasComment("执行超时时间(毫秒)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("函数版本"); + + b.HasKey("Id") + .HasName("pk_funcs"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_funcs_form_definition_id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_funcs_name"); + + b.ToTable("funcs", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/scripting/Infrastructure/Migrations/20260706095113_RenameTablesForScriptingModule.cs b/src/modules/scripting/Infrastructure/Migrations/20260706095113_RenameTablesForScriptingModule.cs new file mode 100644 index 0000000..cc24988 --- /dev/null +++ b/src/modules/scripting/Infrastructure/Migrations/20260706095113_RenameTablesForScriptingModule.cs @@ -0,0 +1,146 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Scripting.Infrastructure.Migrations +{ + /// + /// + /// 数据保留式迁移:将 workflow 模块下的 b_funcs / b_func_execution_logs 表连同主键、索引 + /// 重命名为 funcs / func_execution_logs,并新增 log_lines 列(可空,已有行落 NULL)。 + /// 不进行 Drop/Create,避免数据丢失。Scripting 模块从 CustomFunction 拆分而来(见 spec §11 YAGNI)。 + /// 主键/索引沿用 pk_<table> / ix_<table>_<col> 命名约定,操作顺序对齐 + /// 既有的 RenameTablesToBPrefix 迁移。 + /// + public partial class RenameTablesForScriptingModule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // 1) 先卸下旧主键约束(RenameTable 之前) + migrationBuilder.DropPrimaryKey( + name: "pk_b_funcs", + table: "b_funcs"); + + migrationBuilder.DropPrimaryKey( + name: "pk_b_func_execution_logs", + table: "b_func_execution_logs"); + + // 2) 重命名表(保留数据) + migrationBuilder.RenameTable( + name: "b_funcs", + newName: "funcs"); + + migrationBuilder.RenameTable( + name: "b_func_execution_logs", + newName: "func_execution_logs"); + + // 3) 重建主键约束(新表名) + migrationBuilder.AddPrimaryKey( + name: "pk_funcs", + table: "funcs", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_func_execution_logs", + table: "func_execution_logs", + column: "id"); + + // 4) 重命名索引(沿用 ix__命名约定) + migrationBuilder.RenameIndex( + name: "ix_b_funcs_name", + table: "funcs", + newName: "ix_funcs_name"); + + migrationBuilder.RenameIndex( + name: "ix_b_funcs_form_definition_id", + table: "funcs", + newName: "ix_funcs_form_definition_id"); + + migrationBuilder.RenameIndex( + name: "ix_b_func_execution_logs_function_id", + table: "func_execution_logs", + newName: "ix_func_execution_logs_function_id"); + + migrationBuilder.RenameIndex( + name: "ix_b_func_execution_logs_created_at", + table: "func_execution_logs", + newName: "ix_func_execution_logs_created_at"); + + // 5) 新增 LogLines 列(可空 text,已有行落 NULL,无数据损失) + migrationBuilder.AddColumn( + name: "log_lines", + table: "funcs", + type: "text", + nullable: true, + comment: "测试运行期间 Logger 收集的输出日志"); + + migrationBuilder.AddColumn( + name: "log_lines", + table: "func_execution_logs", + type: "text", + nullable: true, + comment: "执行期间 Logger 收集的输出日志"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // 回滚:移除 log_lines 列并还原表名/主键/索引 + migrationBuilder.DropColumn( + name: "log_lines", + table: "func_execution_logs"); + + migrationBuilder.DropColumn( + name: "log_lines", + table: "funcs"); + + migrationBuilder.RenameIndex( + name: "ix_func_execution_logs_created_at", + table: "func_execution_logs", + newName: "ix_b_func_execution_logs_created_at"); + + migrationBuilder.RenameIndex( + name: "ix_func_execution_logs_function_id", + table: "func_execution_logs", + newName: "ix_b_func_execution_logs_function_id"); + + migrationBuilder.RenameIndex( + name: "ix_funcs_form_definition_id", + table: "funcs", + newName: "ix_b_funcs_form_definition_id"); + + migrationBuilder.RenameIndex( + name: "ix_funcs_name", + table: "funcs", + newName: "ix_b_funcs_name"); + + migrationBuilder.DropPrimaryKey( + name: "pk_funcs", + table: "funcs"); + + migrationBuilder.DropPrimaryKey( + name: "pk_func_execution_logs", + table: "func_execution_logs"); + + migrationBuilder.RenameTable( + name: "func_execution_logs", + newName: "b_func_execution_logs"); + + migrationBuilder.RenameTable( + name: "funcs", + newName: "b_funcs"); + + migrationBuilder.AddPrimaryKey( + name: "pk_b_funcs", + table: "b_funcs", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_b_func_execution_logs", + table: "b_func_execution_logs", + column: "id"); + } + } +} diff --git a/src/modules/scripting/Infrastructure/Migrations/ScriptingDbContextModelSnapshot.cs b/src/modules/scripting/Infrastructure/Migrations/ScriptingDbContextModelSnapshot.cs new file mode 100644 index 0000000..7b6a38c --- /dev/null +++ b/src/modules/scripting/Infrastructure/Migrations/ScriptingDbContextModelSnapshot.cs @@ -0,0 +1,233 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Scripting.Infrastructure.Persistence; + +#nullable disable + +namespace Scripting.Infrastructure.Migrations +{ + [DbContext(typeof(ScriptingDbContext))] + partial class ScriptingDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Scripting.Domain.Entities.ScriptExecutionLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms") + .HasComment("执行耗时(毫秒)"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error_message") + .HasComment("错误信息"); + + b.Property("FunctionId") + .HasColumnType("uuid") + .HasColumnName("function_id") + .HasComment("函数ID"); + + b.Property("FunctionName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("function_name") + .HasComment("函数英文标识"); + + b.Property("InputJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("input_json") + .HasComment("执行输入JSON"); + + b.Property("LogLines") + .HasColumnType("text") + .HasColumnName("log_lines") + .HasComment("执行期间 Logger 收集的输出日志"); + + 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_func_execution_logs"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_func_execution_logs_created_at"); + + b.HasIndex("FunctionId") + .HasDatabaseName("ix_func_execution_logs_function_id"); + + b.ToTable("func_execution_logs", (string)null); + }); + + modelBuilder.Entity("Scripting.Domain.Entities.ScriptFunction", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text") + .HasColumnName("code") + .HasComment("C# 脚本代码"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description") + .HasComment("函数说明"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name") + .HasComment("函数显示名称"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("绑定的自定义表单定义ID,为空表示全局可复用函数"); + + b.Property("InputSchema") + .HasColumnType("jsonb") + .HasColumnName("input_schema") + .HasComment("函数输入 Schema JSON"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("LogLines") + .HasColumnType("text") + .HasColumnName("log_lines") + .HasComment("测试运行期间 Logger 收集的输出日志"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("函数英文标识"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("OutputSchema") + .HasColumnType("jsonb") + .HasColumnName("output_schema") + .HasComment("函数输出 Schema JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("函数状态"); + + b.Property("TimeoutMs") + .HasColumnType("integer") + .HasColumnName("timeout_ms") + .HasComment("执行超时时间(毫秒)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("函数版本"); + + b.HasKey("Id") + .HasName("pk_funcs"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_funcs_form_definition_id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_funcs_name"); + + b.ToTable("funcs", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/scripting/Infrastructure/Persistence/Configurations/ScriptExecutionLogConfiguration.cs b/src/modules/scripting/Infrastructure/Persistence/Configurations/ScriptExecutionLogConfiguration.cs new file mode 100644 index 0000000..2ef1375 --- /dev/null +++ b/src/modules/scripting/Infrastructure/Persistence/Configurations/ScriptExecutionLogConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Scripting.Domain.Entities; + +namespace Scripting.Infrastructure.Persistence.Configurations; + +/// 脚本执行日志 EF 配置,用于审计脚本运行结果。 +public class ScriptExecutionLogConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("func_execution_logs"); + builder.HasKey(e => e.Id); + builder.Property(e => e.Id).ValueGeneratedNever().HasComment("主键ID"); + builder.Property(e => e.FunctionId).HasComment("函数ID"); + builder.Property(e => e.FunctionName).HasMaxLength(200).IsRequired().HasComment("函数英文标识"); + builder.Property(e => e.Status).HasComment("执行状态"); + builder.Property(e => e.InputJson).HasColumnType("jsonb").IsRequired().HasComment("执行输入JSON"); + builder.Property(e => e.ResultJson).HasColumnType("jsonb").HasComment("执行结果JSON"); + builder.Property(e => e.ErrorMessage).HasMaxLength(4000).HasComment("错误信息"); + builder.Property(e => e.DurationMs).HasComment("执行耗时(毫秒)"); + builder.Property(e => e.LogLines).HasColumnType("text").HasComment("执行期间 Logger 收集的输出日志"); + builder.Property(e => e.CreatedBy).HasMaxLength(100).HasComment("创建人ID"); + builder.Property(e => e.CreatedAt).HasComment("创建时间"); + builder.Property(e => e.UpdatedBy).HasMaxLength(100).HasComment("更新人ID"); + builder.Property(e => e.UpdatedAt).HasComment("更新时间"); + builder.HasIndex(e => e.FunctionId); + builder.HasIndex(e => e.CreatedAt); + } +} diff --git a/src/modules/scripting/Infrastructure/Persistence/Configurations/ScriptFunctionConfiguration.cs b/src/modules/scripting/Infrastructure/Persistence/Configurations/ScriptFunctionConfiguration.cs new file mode 100644 index 0000000..9276c00 --- /dev/null +++ b/src/modules/scripting/Infrastructure/Persistence/Configurations/ScriptFunctionConfiguration.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Scripting.Domain.Entities; + +namespace Scripting.Infrastructure.Persistence.Configurations; + +/// 脚本函数 EF 配置,函数代码和输入输出 Schema 均由后端受控执行。 +public class ScriptFunctionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("funcs"); + builder.HasKey(e => e.Id); + builder.Property(e => e.Id).ValueGeneratedNever().HasComment("主键ID"); + builder.Property(e => e.FormDefinitionId).HasComment("绑定的自定义表单定义ID,为空表示全局可复用函数"); + builder.Property(e => e.Name).HasMaxLength(200).IsRequired().HasComment("函数英文标识"); + builder.Property(e => e.DisplayName).HasMaxLength(200).IsRequired().HasComment("函数显示名称"); + builder.Property(e => e.Description).HasMaxLength(1000).HasComment("函数说明"); + builder.Property(e => e.Code).HasColumnType("text").HasComment("C# 脚本代码"); + builder.Property(e => e.InputSchema).HasColumnType("jsonb").HasComment("函数输入 Schema JSON"); + builder.Property(e => e.OutputSchema).HasColumnType("jsonb").HasComment("函数输出 Schema JSON"); + builder.Property(e => e.Status).HasComment("函数状态"); + builder.Property(e => e.Version).HasComment("函数版本"); + builder.Property(e => e.TimeoutMs).HasComment("执行超时时间(毫秒)"); + builder.Property(e => e.LogLines).HasColumnType("text").HasComment("测试运行期间 Logger 收集的输出日志"); + builder.Property(e => e.CreatedBy).HasMaxLength(100).HasComment("创建人ID"); + builder.Property(e => e.CreatedAt).HasComment("创建时间"); + builder.Property(e => e.UpdatedBy).HasMaxLength(100).HasComment("更新人ID"); + builder.Property(e => e.UpdatedAt).HasComment("更新时间"); + builder.Property(e => e.IsDeleted).HasDefaultValue(false).HasComment("是否软删除"); + builder.Property(e => e.OperatorIP).HasMaxLength(500).HasComment("操作人IP地址"); + builder.HasIndex(e => e.FormDefinitionId); + builder.HasIndex(e => e.Name).IsUnique(); + } +} diff --git a/src/modules/scripting/Infrastructure/Persistence/ScriptingDbContext.cs b/src/modules/scripting/Infrastructure/Persistence/ScriptingDbContext.cs new file mode 100644 index 0000000..f444af5 --- /dev/null +++ b/src/modules/scripting/Infrastructure/Persistence/ScriptingDbContext.cs @@ -0,0 +1,63 @@ +using System.Linq.Expressions; +using BuildFlow.Shared.Domain.Common; +using BuildFlow.Shared.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Scripting.Domain.Entities; + +namespace Scripting.Infrastructure.Persistence; + +/// +/// Scripting 持久化上下文。承载脚本函数定义与执行日志。 +/// 注册 AuditInterceptor 自动维护审计字段;对实现 ISoftDelete 的实体统一生成全局查询过滤器, +/// 自动排除已软删除记录。约定与 WorkflowDbContext 保持一致。 +/// +public class ScriptingDbContext : DbContext +{ + private readonly ICurrentUserContext? _currentUserContext; + + public DbSet Scripts => Set(); + public DbSet ScriptExecutionLogs => Set(); + + public ScriptingDbContext(DbContextOptions options) : base(options) { } + + public ScriptingDbContext(DbContextOptions options, ICurrentUserContext currentUserContext) + : base(options) + { + _currentUserContext = currentUserContext; + } + + // 注意:此处通过 OnConfiguring 注册拦截器(而非 DI AddInterceptors),与 rag-backend 约定一致, + // 保证无参构造(如设计时工厂、测试夹具)也能正常工作。 + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (_currentUserContext is not null) + { + optionsBuilder.AddInterceptors(new AuditInterceptor(_currentUserContext)); + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // 自动发现本程序集内所有 IEntityTypeConfiguration 实现 + modelBuilder.ApplyConfigurationsFromAssembly(typeof(ScriptingDbContext).Assembly); + + // 为所有实现 ISoftDelete 的实体动态生成 HasQueryFilter(e => !e.IsDeleted) + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + if (typeof(ISoftDelete).IsAssignableFrom(entityType.ClrType)) + { + modelBuilder.Entity(entityType.ClrType).HasQueryFilter( + CreateSoftDeleteFilter(entityType.ClrType)); + } + } + } + + /// 用表达式树动态构造 e => !e.IsDeleted,避免对每个实体类型手写泛型过滤。 + private static LambdaExpression CreateSoftDeleteFilter(Type entityType) + { + var param = Expression.Parameter(entityType, "e"); + var isDeletedProp = Expression.Property(param, nameof(ISoftDelete.IsDeleted)); + var filter = Expression.Lambda(Expression.Not(isDeletedProp), param); + return filter; + } +} diff --git a/src/modules/scripting/Infrastructure/Persistence/ScriptingDbContextDesignTimeFactory.cs b/src/modules/scripting/Infrastructure/Persistence/ScriptingDbContextDesignTimeFactory.cs new file mode 100644 index 0000000..cd6255d --- /dev/null +++ b/src/modules/scripting/Infrastructure/Persistence/ScriptingDbContextDesignTimeFactory.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Scripting.Infrastructure.Persistence; + +/// EF Core 设计时工厂。硬编码连接串,不依赖 appsettings 路径。 +public class ScriptingDbContextDesignTimeFactory : IDesignTimeDbContextFactory +{ + public ScriptingDbContext CreateDbContext(string[] args) + { + var options = new DbContextOptionsBuilder() + .UseNpgsql("Host=localhost;Port=15432;Database=workflow;Username=rag;Password=dev") + .UseSnakeCaseNamingConvention() + .Options; + return new ScriptingDbContext(options); + } +} diff --git a/src/modules/scripting/Infrastructure/ScriptingInfrastructureModule.cs b/src/modules/scripting/Infrastructure/ScriptingInfrastructureModule.cs index 4ac3242..9310af0 100644 --- a/src/modules/scripting/Infrastructure/ScriptingInfrastructureModule.cs +++ b/src/modules/scripting/Infrastructure/ScriptingInfrastructureModule.cs @@ -1,11 +1,37 @@ +using BuildFlow.Shared.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Scripting.Domain; +using Scripting.Infrastructure.Persistence; using Volo.Abp.Modularity; namespace Scripting.Infrastructure; /// -/// Scripting Infrastructure 层模块。T1 骨架:仅声明对 Domain 的依赖, -/// DbContext 等基础设施注册留待后续任务。 +/// Scripting Infrastructure 层模块。注册 ScriptingDbContext(挂载内核审计拦截器)。 +/// 过渡期 ScriptingDbContext 与 WorkflowDbContext 共用 workflow 数据库, +/// 本任务的迁移会把 b_funcs / b_func_execution_logs 重命名为 funcs / func_execution_logs(保数据)。 /// [DependsOn(typeof(ScriptingDomainModule))] -public class ScriptingInfrastructureModule : AbpModule; +public class ScriptingInfrastructureModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var services = context.Services; + var config = services.GetConfiguration(); + + // 过渡期连接串与 WorkflowDbContext 一致(同名 workflow 库),迁移完成后由后续任务切换独立库。 + var conn = $"{config.GetConnectionString("Default")!};Database={config["DatabaseNames:Workflow"]}"; + services.AddDbContext((sp, options) => + { + options.UseNpgsql(conn, npgsql => + { + npgsql.MigrationsAssembly(typeof(ScriptingDbContext).Assembly.FullName); + }); + options.UseSnakeCaseNamingConvention(); + }); + + services.AddScoped(); + } +}