Files
tiku-backend.net/Tiku.Infrastructure/Persistence/Configurations/LearningAccessV2Configurations.cs
xiong 3f5957d744
Some checks failed
ci / release-gate (push) Has been cancelled
feat(content): establish v2 learning delivery foundation
2026-08-06 09:52:18 +08:00

304 lines
15 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Tiku.Domain.Content;
using Tiku.Domain.Identity;
using Tiku.Domain.Learning;
using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.Persistence.Configurations;
internal sealed class TenantBusinessLicenseConfiguration : IEntityTypeConfiguration<TenantBusinessLicense>
{
public void Configure(EntityTypeBuilder<TenantBusinessLicense> builder)
{
builder.ConfigureTenantEntity("tenant_business_licenses");
builder.ConfigureTimestamps();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.Version).IsConcurrencyToken().HasDefaultValue(1L);
builder.HasIndex(entity => new { entity.TenantId, entity.BusinessLineId }).IsUnique();
builder.HasOne<BusinessLine>().WithMany().HasForeignKey(entity => entity.BusinessLineId)
.OnDelete(DeleteBehavior.Restrict);
builder.ToTable(table => table.HasCheckConstraint(
"ck_tenant_business_license_period", "ends_at is null or ends_at > starts_at"));
}
}
internal sealed class TenantLicensedTargetConfiguration : IEntityTypeConfiguration<TenantLicensedTarget>
{
public void Configure(EntityTypeBuilder<TenantLicensedTarget> builder)
{
builder.ConfigureTenantOwned("tenant_licensed_targets");
builder.HasIndex(entity => new
{
entity.TenantId,
entity.TenantBusinessLicenseId,
entity.ExamTargetProfileVersionId
}).IsUnique();
builder.HasOne<TenantBusinessLicense>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.TenantBusinessLicenseId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<ExamTargetProfileVersion>().WithMany()
.HasForeignKey(entity => entity.ExamTargetProfileVersionId)
.OnDelete(DeleteBehavior.Restrict);
builder.ToTable(table => table.HasCheckConstraint(
"ck_tenant_licensed_target_period", "ends_at is null or ends_at > starts_at"));
}
}
internal sealed class ProductAccessManifestConfiguration : IEntityTypeConfiguration<ProductAccessManifest>
{
public void Configure(EntityTypeBuilder<ProductAccessManifest> builder)
{
builder.ConfigureTenantEntity("product_access_manifests");
builder.ConfigureTimestamps();
builder.HasIndex(entity => new { entity.TenantId, entity.LearningProductId }).IsUnique();
builder.HasOne<LearningProduct>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.LearningProductId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<ProductAccessManifestVersion>().WithMany()
.HasForeignKey(entity => new
{
entity.TenantId,
ProductAccessManifestId = entity.Id,
Id = entity.CurrentVersionId
})
.HasPrincipalKey(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
}
internal sealed class ProductAccessManifestVersionConfiguration :
IEntityTypeConfiguration<ProductAccessManifestVersion>
{
public void Configure(EntityTypeBuilder<ProductAccessManifestVersion> builder)
{
builder.ConfigureTenantOwned("product_access_manifest_versions");
builder.HasAlternateKey(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.Id });
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.VersionNo }).IsUnique();
builder.HasOne<ProductAccessManifest>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.ToTable(table =>
{
table.HasCheckConstraint("ck_product_manifest_active_targets", "max_active_targets between 1 and 16");
table.HasCheckConstraint("ck_product_manifest_alternate_targets", "max_alternate_targets between 0 and max_active_targets");
table.HasCheckConstraint("ck_product_manifest_daily_question_limit",
"daily_question_limit is null or daily_question_limit >= 0");
table.HasCheckConstraint("ck_product_manifest_total_question_limit",
"total_question_limit is null or total_question_limit >= 0");
});
}
}
internal sealed class ProductManifestReleaseConfiguration : IEntityTypeConfiguration<ProductManifestRelease>
{
public void Configure(EntityTypeBuilder<ProductManifestRelease> builder)
{
builder.ConfigureTenantOwned("product_manifest_releases");
builder.HasIndex(entity => new
{
entity.TenantId,
entity.ProductAccessManifestVersionId,
entity.ContentOwnerTenantId,
entity.ContentReleaseId
}).IsUnique();
builder.HasOne<ProductAccessManifestVersion>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<ContentRelease>().WithMany()
.HasForeignKey(entity => new { TenantId = entity.ContentOwnerTenantId, Id = entity.ContentReleaseId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<PlatformContentPackageVersion>().WithMany()
.HasForeignKey(entity => new
{
TenantId = entity.PlatformContentPackageOwnerTenantId,
Id = entity.PlatformContentPackageVersionId
})
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.ToTable(table => table.HasCheckConstraint(
"ck_product_manifest_release_platform_pair",
"(platform_content_package_owner_tenant_id is null) = (platform_content_package_version_id is null)"));
}
}
internal sealed class ProductManifestTargetConfiguration : IEntityTypeConfiguration<ProductManifestTarget>
{
public void Configure(EntityTypeBuilder<ProductManifestTarget> builder)
{
builder.ConfigureTenantOwned("product_manifest_targets");
builder.HasIndex(entity => new
{
entity.TenantId,
entity.ProductAccessManifestVersionId,
entity.ExamTargetProfileVersionId
}).IsUnique();
builder.HasOne<ProductAccessManifestVersion>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<ExamTargetProfileVersion>().WithMany()
.HasForeignKey(entity => entity.ExamTargetProfileVersionId)
.OnDelete(DeleteBehavior.Restrict);
builder.ToTable(table => table.HasCheckConstraint(
"ck_product_manifest_target_role", "may_be_primary or may_be_alternate"));
}
}
internal sealed class ProductManifestResourceConfiguration : IEntityTypeConfiguration<ProductManifestResource>
{
public void Configure(EntityTypeBuilder<ProductManifestResource> builder)
{
builder.ConfigureTenantOwned("product_manifest_resources");
builder.Property(entity => entity.ResourceType).HasSnakeCaseEnum();
builder.Property(entity => entity.DisplayName).HasMaxLength(300);
builder.HasIndex(entity => new
{
entity.TenantId,
entity.ProductAccessManifestVersionId,
entity.ResourceType,
entity.ResourceId
}).HasDatabaseName("ux_product_manifest_resource").IsUnique();
builder.HasIndex(entity => new
{
entity.TenantId,
entity.ProductAccessManifestVersionId,
entity.SortOrder,
entity.Id
}).HasDatabaseName("ix_product_manifest_resource_order");
builder.HasOne<ProductAccessManifestVersion>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
}
}
internal sealed class StudentEntitlementConfiguration : IEntityTypeConfiguration<StudentEntitlement>
{
public void Configure(EntityTypeBuilder<StudentEntitlement> builder)
{
builder.ConfigureTenantEntity("student_entitlements_v2");
builder.ConfigureTimestamps();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.SourceType).HasMaxLength(100);
builder.Property(entity => entity.RevokedReason).HasMaxLength(1000);
builder.ToTable(table => table.HasCheckConstraint(
"ck_student_entitlements_v2_used_question_count", "used_question_count >= 0"));
builder.HasIndex(entity => new
{
entity.TenantId,
entity.UserId,
entity.Status,
entity.StartsAt,
entity.EndsAt
});
builder.HasIndex(entity => new { entity.TenantId, entity.SourceType, entity.SourceId })
.HasFilter("source_id is not null");
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne<LearningProduct>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.LearningProductId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<ProductAccessManifestVersion>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.ToTable(table => table.HasCheckConstraint(
"ck_student_entitlement_v2_period", "ends_at is null or ends_at > starts_at"));
}
}
internal sealed class StudentTargetSelectionHistoryConfiguration :
IEntityTypeConfiguration<StudentTargetSelectionHistory>
{
public void Configure(EntityTypeBuilder<StudentTargetSelectionHistory> builder)
{
builder.ConfigureTenantEntity("student_target_selection_history");
builder.ConfigureTimestamps();
builder.Property(entity => entity.Role).HasSnakeCaseEnum();
builder.Property(entity => entity.SourceType).HasMaxLength(100);
builder.Property(entity => entity.Reason).HasMaxLength(1000);
builder.HasIndex(entity => new
{
entity.TenantId,
entity.UserId,
entity.BusinessLineId,
entity.ExamTargetProfileVersionId
}).IsUnique().HasFilter("is_current");
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.BusinessLineId })
.IsUnique()
.HasDatabaseName("ux_student_target_primary")
.HasFilter("is_current and role = 'primary'");
builder.HasIndex(entity => new
{
entity.TenantId,
entity.UserId,
entity.BusinessLineId,
entity.EffectiveAt
}).HasDatabaseName("ix_student_target_selection_history");
builder.HasIndex(entity => new { entity.TenantId, entity.SourceType, entity.SourceId });
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne<BusinessLine>().WithMany().HasForeignKey(entity => entity.BusinessLineId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<ExamTargetProfileVersion>().WithMany()
.HasForeignKey(entity => entity.ExamTargetProfileVersionId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<StudentEntitlement>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.EntitlementId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.ToTable(table => table.HasCheckConstraint(
"ck_student_target_selection_source",
"(source_type = 'student_entitlement' and entitlement_id = source_id) or " +
"(source_type = 'class_assignment_grant' and entitlement_id is null)"));
}
}
internal sealed class ClassAssignmentGrantConfiguration : IEntityTypeConfiguration<ClassAssignmentGrant>
{
public void Configure(EntityTypeBuilder<ClassAssignmentGrant> builder)
{
builder.ConfigureTenantEntity("class_assignment_grants");
builder.ConfigureTimestamps();
builder.Property(entity => entity.ResourceType).HasSnakeCaseEnum();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.RevokedReason).HasMaxLength(1000);
builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.Status, entity.StartsAt });
builder.HasIndex(entity => new { entity.TenantId, entity.ResourceType, entity.ResourceId, entity.Status });
builder.HasOne<TenantClass>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.ClassId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<ProductAccessManifestVersion>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.ToTable(table => table.HasCheckConstraint(
"ck_class_assignment_grant_period", "ends_at is null or ends_at > starts_at"));
}
}
internal sealed class EffectiveAccessProjectionConfiguration : IEntityTypeConfiguration<EffectiveAccessProjection>
{
public void Configure(EntityTypeBuilder<EffectiveAccessProjection> builder)
{
builder.ConfigureTenantEntity("effective_access_projections");
builder.Property(entity => entity.AlternateProfileVersionIds).HasColumnType("uuid[]");
builder.Property(entity => entity.ContentReleaseIds).HasColumnType("uuid[]");
builder.Property(entity => entity.AudienceSegmentIds).HasColumnType("uuid[]");
builder.Property(entity => entity.ManifestVersionIds).HasColumnType("uuid[]");
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.BusinessLineId }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.ValidUntil });
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne<BusinessLine>().WithMany().HasForeignKey(entity => entity.BusinessLineId)
.OnDelete(DeleteBehavior.Restrict);
}
}