feat: enforce tenant isolation and shared question bank

This commit is contained in:
2026-07-27 16:59:12 +08:00
parent 28e9a9fa41
commit db4c7b4496
137 changed files with 6402 additions and 112274 deletions

View File

@@ -31,8 +31,11 @@ public sealed class PersistenceModelTests
.Select(entity => entity.GetTableName())
.ToHashSet(StringComparer.Ordinal);
Assert.Equal(133, tableNames.Count);
Assert.Contains("tenants", tableNames);
Assert.Contains("tenant_frontend_configs", tableNames);
Assert.Contains("tenant_question_references", tableNames);
Assert.Contains("taxonomy_nodes", tableNames);
Assert.Contains("practice_session_questions", tableNames);
Assert.Contains("tenant_settings", tableNames);
Assert.Contains("content_entries", tableNames);
Assert.Contains("content_nodes", tableNames);
@@ -102,8 +105,7 @@ public sealed class PersistenceModelTests
Assert.Contains("point_exchange_orders", tableNames);
Assert.Contains("content_asset_access_events", tableNames);
Assert.Contains("content_asset_security_scan_events", tableNames);
Assert.Contains("question_bank_grants", tableNames);
Assert.Contains("tenant_question_bank_adoptions", tableNames);
Assert.Contains("tenant_question_bank_preferences", tableNames);
Assert.Contains("ai_recommendation_reports", tableNames);
Assert.Contains("referral_tracks", tableNames);
Assert.Contains("referral_codes", tableNames);
@@ -361,9 +363,7 @@ public sealed class PersistenceModelTests
[InlineData(typeof(ContentImportIssue), nameof(ContentImportIssue.Details), "'{}'::jsonb")]
[InlineData(typeof(VideoExplanation), nameof(VideoExplanation.KnowledgeTags), "'[]'::jsonb")]
[InlineData(typeof(QuestionVideo), nameof(QuestionVideo.Metadata), "'{}'::jsonb")]
[InlineData(typeof(QuestionBankGrant), nameof(QuestionBankGrant.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantQuestionBankAdoption), nameof(TenantQuestionBankAdoption.SourceSnapshot), "'{}'::jsonb")]
[InlineData(typeof(TenantQuestionBankAdoption), nameof(TenantQuestionBankAdoption.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantQuestionBankPreference), nameof(TenantQuestionBankPreference.Metadata), "'{}'::jsonb")]
[InlineData(typeof(AiRecommendationReport), nameof(AiRecommendationReport.InputPayload), "'{}'::jsonb")]
[InlineData(typeof(AiRecommendationReport), nameof(AiRecommendationReport.ContextPayload), "'{}'::jsonb")]
[InlineData(typeof(AiRecommendationReport), nameof(AiRecommendationReport.ResultPayload), "'{}'::jsonb")]
@@ -388,6 +388,7 @@ public sealed class PersistenceModelTests
using var context = new TikuDbContext(Options);
AssertHasUniqueIndex<ContentImportItem>(
nameof(ContentImportItem.TenantId),
nameof(ContentImportItem.JobId),
nameof(ContentImportItem.RowNo));
AssertHasUniqueIndex<QuestionVideo>(
@@ -395,17 +396,10 @@ public sealed class PersistenceModelTests
nameof(QuestionVideo.QuestionId),
nameof(QuestionVideo.VideoId),
nameof(QuestionVideo.VideoType));
AssertHasUniqueIndex<TenantQuestionBankAdoption>(
nameof(TenantQuestionBankAdoption.TenantId),
nameof(TenantQuestionBankAdoption.SourceQuestionBankId));
var allowedPlanCodes = context.Model.FindEntityType(typeof(QuestionBankGrant))!
.FindProperty(nameof(QuestionBankGrant.AllowedPlanCodes))!;
Assert.Equal("text[]", allowedPlanCodes.GetColumnType());
var allowedTenantIds = context.Model.FindEntityType(typeof(QuestionBankGrant))!
.FindProperty(nameof(QuestionBankGrant.AllowedTenantIds))!;
Assert.Equal("uuid[]", allowedTenantIds.GetColumnType());
AssertHasUniqueIndex<TenantQuestionBankPreference>(
nameof(TenantQuestionBankPreference.TenantId),
nameof(TenantQuestionBankPreference.QuestionBankOwnerTenantId),
nameof(TenantQuestionBankPreference.QuestionBankId));
void AssertHasUniqueIndex<TEntity>(params string[] propertyNames)
{
@@ -635,6 +629,7 @@ public sealed class PersistenceModelTests
nameof(CommerceRefundRequest.TenantId),
nameof(CommerceRefundRequest.RefundNo));
AssertHasUniqueIndex<CommerceReconciliationItem>(
nameof(CommerceReconciliationItem.TenantId),
nameof(CommerceReconciliationItem.BatchId),
nameof(CommerceReconciliationItem.RowNo));
AssertHasUniqueIndex<CommerceReconciliationIssue>(
@@ -848,7 +843,9 @@ public sealed class PersistenceModelTests
using var context = new TikuDbContext(Options);
AssertHasUniqueIndex<PlatformSaasPlan>(nameof(PlatformSaasPlan.Code));
AssertHasUniqueIndex<TenantInvoice>(nameof(TenantInvoice.InvoiceNo));
AssertHasUniqueIndex<TenantInvoice>(
nameof(TenantInvoice.TenantId),
nameof(TenantInvoice.InvoiceNo));
AssertHasUniqueIndex<TenantInvoice>(
nameof(TenantInvoice.TenantId),
nameof(TenantInvoice.BillingPeriodStart),
@@ -863,6 +860,7 @@ public sealed class PersistenceModelTests
AssertHasUniqueIndex<PlatformAuditAlert>(nameof(PlatformAuditAlert.RuleId), nameof(PlatformAuditAlert.AuditLogId));
AssertHasUniqueIndex<PlatformDunningNotificationChannel>(nameof(PlatformDunningNotificationChannel.ChannelCode));
AssertHasUniqueIndex<PlatformDunningNotificationEvent>(
nameof(PlatformDunningNotificationEvent.TenantId),
nameof(PlatformDunningNotificationEvent.ChannelId),
nameof(PlatformDunningNotificationEvent.ReminderId));