376 lines
19 KiB
C#
376 lines
19 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
using Tiku.Domain.Catalog;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Tenancy;
|
|
|
|
namespace Tiku.Infrastructure.Persistence.Configurations;
|
|
|
|
internal sealed class TenantExternalProviderConfiguration : IEntityTypeConfiguration<TenantExternalProvider>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TenantExternalProvider> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("tenant_external_providers");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.Capability).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Provider).HasMaxLength(50);
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.DisplayName).HasMaxLength(200);
|
|
builder.Property(entity => entity.SecretRef).HasMaxLength(300);
|
|
builder.Property(entity => entity.ConfigPublic).IsJson("{}");
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Capability, entity.Provider }).IsUnique();
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Capability, entity.Status, entity.Priority });
|
|
}
|
|
}
|
|
|
|
internal sealed class TenantSecretConfiguration : IEntityTypeConfiguration<TenantSecret>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TenantSecret> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("tenant_secrets");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.Purpose).HasMaxLength(80);
|
|
builder.Property(entity => entity.Provider).HasMaxLength(50);
|
|
builder.Property(entity => entity.SecretKey).HasMaxLength(120);
|
|
builder.Property(entity => entity.SecretRef).HasMaxLength(300);
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.EncryptionKeyId).HasMaxLength(100);
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.SecretRef }).IsUnique();
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Purpose, entity.Provider, entity.SecretKey })
|
|
.IsUnique();
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Purpose, entity.Provider, entity.Status });
|
|
builder.ToTable(table => table.HasCheckConstraint(
|
|
"ck_tenant_secrets_encryption_envelope",
|
|
"octet_length(encrypted_payload) > 0 and octet_length(encryption_nonce) = 12 and " +
|
|
"octet_length(encryption_tag) = 16 and encryption_key_id <> ''"));
|
|
}
|
|
}
|
|
|
|
internal sealed class SmsVerificationCodeConfiguration : IEntityTypeConfiguration<SmsVerificationCode>
|
|
{
|
|
public void Configure(EntityTypeBuilder<SmsVerificationCode> builder)
|
|
{
|
|
builder.ConfigureEntity("sms_verification_codes");
|
|
builder.Property(entity => entity.Phone).HasMaxLength(32);
|
|
builder.Property(entity => entity.Purpose).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.CodeHash).HasMaxLength(256);
|
|
builder.Property(entity => entity.Provider).HasMaxLength(50);
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.IpAddress).HasMaxLength(64);
|
|
builder.Property(entity => entity.UserAgent).HasMaxLength(1000);
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Phone, entity.Purpose, entity.ExpiresAt });
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Phone, entity.Purpose, entity.CreatedAt })
|
|
.HasFilter("consumed_at is null and status in ('pending', 'sent')");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Phone, entity.Purpose })
|
|
.IsUnique()
|
|
.HasFilter("consumed_at is null and status in ('pending', 'sent')");
|
|
builder.ToTable(table => { table.HasCheckConstraint("ck_sms_verification_codes_attempts", "attempts >= 0"); });
|
|
|
|
builder.HasOne<Tenant>().WithMany()
|
|
.HasForeignKey(entity => entity.TenantId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class AuthLoginEventConfiguration : IEntityTypeConfiguration<AuthLoginEvent>
|
|
{
|
|
public void Configure(EntityTypeBuilder<AuthLoginEvent> builder)
|
|
{
|
|
builder.ConfigureEntity("auth_login_events");
|
|
builder.Property(entity => entity.Provider).HasMaxLength(50);
|
|
builder.Property(entity => entity.Identifier).HasMaxLength(320);
|
|
builder.Property(entity => entity.Result).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.FailureCode).HasMaxLength(100);
|
|
builder.Property(entity => entity.IpAddress).HasMaxLength(64);
|
|
builder.Property(entity => entity.UserAgent).HasMaxLength(1000);
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.CreatedAt });
|
|
|
|
builder.HasOne<Tenant>().WithMany()
|
|
.HasForeignKey(entity => entity.TenantId)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
}
|
|
}
|
|
|
|
internal sealed class AuthSessionConfiguration : IEntityTypeConfiguration<AuthSession>
|
|
{
|
|
public void Configure(EntityTypeBuilder<AuthSession> builder)
|
|
{
|
|
builder.ConfigureEntity("auth_sessions");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.Realm).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.TokenHash).HasMaxLength(256);
|
|
builder.Property(entity => entity.SecurityStamp).HasMaxLength(128);
|
|
builder.Property(entity => entity.Provider).HasMaxLength(50);
|
|
builder.Property(entity => entity.RevokedReason).HasMaxLength(100);
|
|
builder.Property(entity => entity.IpAddress).HasMaxLength(64);
|
|
builder.Property(entity => entity.UserAgent).HasMaxLength(1000);
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => entity.TokenHash)
|
|
.IsUnique()
|
|
.HasAnnotation("Tiku:GlobalUnique", true);
|
|
builder.HasIndex(entity => new { entity.Realm, entity.TenantId, entity.UserId, entity.ExpiresAt })
|
|
.HasFilter("revoked_at is null");
|
|
builder.HasIndex(entity => new { entity.TokenFamilyId, entity.RevokedAt });
|
|
|
|
builder.ToTable(table => table.HasCheckConstraint(
|
|
"ck_auth_sessions_realm_tenant",
|
|
"(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)"));
|
|
|
|
builder.HasOne<Tenant>().WithMany()
|
|
.HasForeignKey(entity => entity.TenantId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class AuthChallengeConfiguration : IEntityTypeConfiguration<AuthChallenge>
|
|
{
|
|
public void Configure(EntityTypeBuilder<AuthChallenge> builder)
|
|
{
|
|
builder.ConfigureEntity("auth_challenges");
|
|
builder.Property(entity => entity.Realm).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Purpose).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.TokenHash).HasMaxLength(64);
|
|
builder.Property(entity => entity.SecurityStamp).HasMaxLength(128);
|
|
builder.Property(entity => entity.Provider).HasMaxLength(50);
|
|
builder.Property(entity => entity.IpAddress).HasMaxLength(100);
|
|
builder.Property(entity => entity.UserAgent).HasMaxLength(1024);
|
|
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => entity.TokenHash).IsUnique();
|
|
builder.HasIndex(entity => new { entity.UserId, entity.Purpose, entity.ExpiresAt });
|
|
builder.ToTable(table => table.HasCheckConstraint(
|
|
"ck_auth_challenges_realm_tenant",
|
|
"(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)"));
|
|
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class SmsSendRateLimitConfiguration : IEntityTypeConfiguration<SmsSendRateLimit>
|
|
{
|
|
public void Configure(EntityTypeBuilder<SmsSendRateLimit> builder)
|
|
{
|
|
builder.ToTable("sms_send_rate_limits");
|
|
builder.HasKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.Dimension,
|
|
entity.ScopeHash,
|
|
entity.BucketStart
|
|
});
|
|
builder.Property(entity => entity.Dimension).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.ScopeHash).HasMaxLength(128);
|
|
builder.Property(entity => entity.RequestCount).HasDefaultValue(0);
|
|
builder.Property(entity => entity.UpdatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => entity.UpdatedAt);
|
|
builder.ToTable(table =>
|
|
{
|
|
table.HasCheckConstraint("ck_sms_send_rate_limits_request_count", "request_count >= 0");
|
|
});
|
|
|
|
builder.HasOne<Tenant>().WithMany()
|
|
.HasForeignKey(entity => entity.TenantId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class SmsChannelConfiguration : IEntityTypeConfiguration<SmsChannel>
|
|
{
|
|
public void Configure(EntityTypeBuilder<SmsChannel> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("sms_channels");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.Provider).HasMaxLength(80);
|
|
builder.Property(entity => entity.Name).HasMaxLength(200);
|
|
builder.Property(entity => entity.Signature).HasMaxLength(100);
|
|
builder.Property(entity => entity.Scene).HasMaxLength(100);
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.SecretRef).HasMaxLength(300);
|
|
builder.Property(entity => entity.ConfigPublic).IsJson("{}");
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Provider, entity.Scene }).IsUnique();
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.Priority });
|
|
builder.ToTable(table => table.HasCheckConstraint("ck_sms_channels_quota", "monthly_quota >= 0"));
|
|
}
|
|
}
|
|
|
|
internal sealed class SmsTemplateConfiguration : IEntityTypeConfiguration<SmsTemplate>
|
|
{
|
|
public void Configure(EntityTypeBuilder<SmsTemplate> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("sms_templates");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.Code).HasMaxLength(120);
|
|
builder.Property(entity => entity.Name).HasMaxLength(200);
|
|
builder.Property(entity => entity.Type).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.AuditStatus).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.ProviderTemplateCode).HasMaxLength(120);
|
|
builder.Property(entity => entity.Content).HasMaxLength(1000);
|
|
builder.Property(entity => entity.Remark).HasMaxLength(500);
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique();
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.ChannelId, entity.Status });
|
|
builder.HasOne<SmsChannel>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, entity.ChannelId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class SmsSendLogConfiguration : IEntityTypeConfiguration<SmsSendLog>
|
|
{
|
|
public void Configure(EntityTypeBuilder<SmsSendLog> builder)
|
|
{
|
|
builder.ToTable("sms_send_logs");
|
|
builder.HasKey(entity => entity.Id);
|
|
builder.Property(entity => entity.Id).HasDefaultValueSql("gen_random_uuid()");
|
|
builder.Property(entity => entity.Provider).HasMaxLength(80);
|
|
builder.Property(entity => entity.Scene).HasMaxLength(100);
|
|
builder.Property(entity => entity.PhoneMasked).HasMaxLength(40);
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.ProviderMessageId).HasMaxLength(200);
|
|
builder.Property(entity => entity.ErrorCode).HasMaxLength(120);
|
|
builder.Property(entity => entity.ErrorMessage).HasMaxLength(1000);
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.CreatedAt });
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.ChannelId, entity.CreatedAt });
|
|
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<SmsChannel>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, entity.ChannelId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
builder.HasOne<SmsTemplate>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, entity.TemplateId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
}
|
|
}
|
|
|
|
internal sealed class TenantClassConfiguration : IEntityTypeConfiguration<TenantClass>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TenantClass> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("tenant_classes");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.LegacyId).HasMaxLength(64);
|
|
builder.Property(entity => entity.Code).HasMaxLength(100);
|
|
builder.Property(entity => entity.Name).HasMaxLength(200);
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Code })
|
|
.IsUnique()
|
|
.HasFilter("code is not null and code <> ''");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.SortOrder, entity.CreatedAt });
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.RegionId })
|
|
.HasFilter("region_id is not null");
|
|
|
|
builder.HasOne<Region>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, entity.RegionId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.CreatedBy)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UpdatedBy)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
}
|
|
}
|
|
|
|
internal sealed class TenantClassMemberConfiguration : IEntityTypeConfiguration<TenantClassMember>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TenantClassMember> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("tenant_class_members");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.MemberType).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.JoinedAt).HasDefaultValueSql("now()");
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.UserId, entity.MemberType })
|
|
.IsUnique();
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.Status, entity.MemberType });
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.Status, entity.MemberType });
|
|
|
|
builder.HasOne<TenantClass>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, entity.ClassId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class TenantStudentNoteConfiguration : IEntityTypeConfiguration<TenantStudentNote>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TenantStudentNote> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("tenant_student_notes");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.NoteType).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Visibility).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.StudentUserId, entity.IsPinned, entity.CreatedAt });
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.CreatedBy, entity.CreatedAt })
|
|
.HasFilter("created_by is not null");
|
|
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.StudentUserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.CreatedBy)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UpdatedBy)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
}
|
|
}
|
|
|
|
internal sealed class TenantStudentFollowupConfiguration : IEntityTypeConfiguration<TenantStudentFollowup>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TenantStudentFollowup> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("tenant_student_followups");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.Title).HasMaxLength(300);
|
|
builder.Property(entity => entity.FollowupType).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Priority).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new
|
|
{ entity.TenantId, entity.StudentUserId, entity.Status, entity.DueAt, entity.CreatedAt });
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.AssignedToUserId, entity.Status, entity.DueAt })
|
|
.HasFilter("assigned_to_user_id is not null");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.Status, entity.DueAt })
|
|
.HasFilter("class_id is not null");
|
|
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.StudentUserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.AssignedToUserId)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
builder.HasOne<TenantClass>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, entity.ClassId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.CompletedBy)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
}
|
|
} |