diff --git a/src/Domain/TakeoutSaaS.Domain/SystemParameters/Entities/SystemParameter.cs b/src/Domain/TakeoutSaaS.Domain/SystemParameters/Entities/SystemParameter.cs new file mode 100644 index 0000000..a6c7826 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/SystemParameters/Entities/SystemParameter.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.SystemParameters.Entities; + +/// +/// 系统参数实体:支持按租户维护的键值型配置。 +/// +public sealed class SystemParameter : MultiTenantEntityBase +{ + /// + /// 参数键,租户内唯一。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 参数值,支持文本或 JSON。 + /// + public string Value { get; set; } = string.Empty; + + /// + /// 描述信息。 + /// + public string? Description { get; set; } + + /// + /// 排序值,越小越靠前。 + /// + public int SortOrder { get; set; } = 100; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs index c6949ce..d2926e1 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Domain.Dictionary.Entities; using TakeoutSaaS.Domain.Dictionary.Enums; +using TakeoutSaaS.Domain.SystemParameters.Entities; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Infrastructure.App.Options; @@ -219,8 +220,9 @@ public sealed class AppDataSeeder( await dbContext.SaveChangesAsync(cancellationToken); } + /// - /// 确保系统参数以字典形式幂等种子。 + /// 确保系统参数以独立表形式可重复种子。 /// private async Task EnsureSystemParametersAsync(DictionaryDbContext dbContext, long? defaultTenantId, CancellationToken cancellationToken) { @@ -245,37 +247,64 @@ public sealed class AppDataSeeder( foreach (var group in grouped) { var tenantId = group.Key; - var dictionaryGroup = await dbContext.DictionaryGroups + var existingParameters = await dbContext.SystemParameters .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Code == "system_parameters", cancellationToken); + .Where(x => x.TenantId == tenantId) + .ToListAsync(cancellationToken); - if (dictionaryGroup == null) + foreach (var seed in group) { - dictionaryGroup = new DictionaryGroup + var key = seed.Key.Trim(); + var existing = existingParameters.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + + if (existing == null) { - Id = 0, - TenantId = tenantId, - Code = "system_parameters", - Name = "系统参数", - Scope = tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business, - Description = "系统参数配置", - IsEnabled = true - }; + var parameter = new SystemParameter + { + Id = 0, + TenantId = tenantId, + Key = key, + Value = seed.Value.Trim(), + Description = seed.Description?.Trim(), + SortOrder = seed.SortOrder, + IsEnabled = seed.IsEnabled + }; - await dbContext.DictionaryGroups.AddAsync(dictionaryGroup, cancellationToken); - logger.LogInformation("AppSeed 创建系统参数分组 (Tenant: {TenantId})", tenantId); + await dbContext.SystemParameters.AddAsync(parameter, cancellationToken); + continue; + } + + var updated = false; + + if (!string.Equals(existing.Value, seed.Value, StringComparison.Ordinal)) + { + existing.Value = seed.Value.Trim(); + updated = true; + } + + if (!string.Equals(existing.Description, seed.Description, StringComparison.Ordinal)) + { + existing.Description = seed.Description?.Trim(); + updated = true; + } + + if (existing.SortOrder != seed.SortOrder) + { + existing.SortOrder = seed.SortOrder; + updated = true; + } + + if (existing.IsEnabled != seed.IsEnabled) + { + existing.IsEnabled = seed.IsEnabled; + updated = true; + } + + if (updated) + { + dbContext.SystemParameters.Update(existing); + } } - - var seedItems = group.Select(x => new DictionarySeedItemOptions - { - Key = x.Key.Trim(), - Value = x.Value.Trim(), - Description = x.Description?.Trim(), - SortOrder = x.SortOrder, - IsEnabled = x.IsEnabled - }); - - await UpsertDictionaryItemsAsync(dbContext, dictionaryGroup, seedItems, tenantId, cancellationToken); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs index 6025e35..53e7e24 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TakeoutSaaS.Domain.Dictionary.Entities; +using TakeoutSaaS.Domain.SystemParameters.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; @@ -9,7 +10,7 @@ using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; /// -/// 参数字典 DbContext。 +/// 参数字典 DbContext:承载字典与系统参数。 /// public sealed class DictionaryDbContext( DbContextOptions options, @@ -19,15 +20,20 @@ public sealed class DictionaryDbContext( : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// - /// 字典分组集。 + /// 字典分组集合。 /// public DbSet DictionaryGroups => Set(); /// - /// 字典项集。 + /// 字典项集合。 /// public DbSet DictionaryItems => Set(); + /// + /// 系统参数集合。 + /// + public DbSet SystemParameters => Set(); + /// /// 配置实体模型。 /// @@ -37,6 +43,7 @@ public sealed class DictionaryDbContext( base.OnModelCreating(modelBuilder); ConfigureGroup(modelBuilder.Entity()); ConfigureItem(modelBuilder.Entity()); + ConfigureSystemParameter(modelBuilder.Entity()); ApplyTenantQueryFilters(modelBuilder); } @@ -87,4 +94,25 @@ public sealed class DictionaryDbContext( builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.GroupId, x.Key }).IsUnique(); } + + /// + /// 配置系统参数。 + /// + /// 实体构建器。 + private static void ConfigureSystemParameter(EntityTypeBuilder builder) + { + builder.ToTable("system_parameters"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Key).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Value).HasColumnType("text").IsRequired(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.SortOrder).HasDefaultValue(100); + builder.Property(x => x.IsEnabled).HasDefaultValue(true); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.Key }).IsUnique(); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.Designer.cs new file mode 100644 index 0000000..8d3c8c2 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.Designer.cs @@ -0,0 +1,288 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb +{ + [DbContext(typeof(DictionaryDbContext))] + [Migration("20251202043204_AddSystemParametersTable")] + partial class AddSystemParametersTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组编码(唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("分组名称。"); + + b.Property("Scope") + .HasColumnType("integer") + .HasComment("分组作用域:系统/业务。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("dictionary_groups", null, t => + { + t.HasComment("参数字典分组(系统参数、业务参数)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("GroupId") + .HasColumnType("bigint") + .HasComment("关联分组 ID。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认项。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("字典项键。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值,越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("字典项值。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("GroupId", "Key") + .IsUnique(); + + b.ToTable("dictionary_items", null, t => + { + t.HasComment("参数字典项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.SystemParameters.Entities.SystemParameter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("参数键,租户内唯一。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值,越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasComment("参数值,支持文本或 JSON。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Key") + .IsUnique(); + + b.ToTable("system_parameters", null, t => + { + t.HasComment("系统参数实体:支持按租户维护的键值型配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group") + .WithMany("Items") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.cs new file mode 100644 index 0000000..816fe03 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb +{ + /// + public partial class AddSystemParametersTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "system_parameters", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Key = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "参数键,租户内唯一。"), + Value = table.Column(type: "text", nullable: false, comment: "参数值,支持文本或 JSON。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "描述信息。"), + SortOrder = table.Column(type: "integer", nullable: false, defaultValue: 100, comment: "排序值,越小越靠前。"), + IsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true, comment: "是否启用。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_system_parameters", x => x.Id); + }, + comment: "系统参数实体:支持按租户维护的键值型配置。"); + + migrationBuilder.CreateIndex( + name: "IX_system_parameters_TenantId", + table: "system_parameters", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_system_parameters_TenantId_Key", + table: "system_parameters", + columns: new[] { "TenantId", "Key" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "system_parameters"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs index 1df55bf..35585f3 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs @@ -186,6 +186,84 @@ namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.SystemParameters.Entities.SystemParameter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("参数键,租户内唯一。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值,越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasComment("参数值,支持文本或 JSON。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Key") + .IsUnique(); + + b.ToTable("system_parameters", null, t => + { + t.HasComment("系统参数实体:支持按租户维护的键值型配置。"); + }); + }); + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => { b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group")