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

@@ -0,0 +1,37 @@
using System.Text.Json;
using Tiku.Domain.Common;
namespace Tiku.Domain.Catalog;
public sealed class TaxonomyNode : AuditableTenantEntity
{
public Guid? ParentOwnerTenantId { get; set; }
public Guid? ParentId { get; set; }
public TaxonomyNodeType NodeType { get; set; } = TaxonomyNodeType.KnowledgePoint;
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Path { get; set; }
public int Depth { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class QuestionTaxonomyAssignment : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid QuestionId { get; set; }
public Guid TaxonomyOwnerTenantId { get; set; }
public Guid TaxonomyNodeId { get; set; }
public bool IsPrimary { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public enum TaxonomyNodeType
{
Subject,
Chapter,
KnowledgePoint,
Paper,
Custom
}

View File

@@ -89,7 +89,7 @@ public sealed class Payment : AuditableTenantEntity
public JsonElement RawPayload { get; set; } = JsonDefaults.Object();
}
public sealed class PaymentEvent : Entity
public sealed class PaymentEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid? PaymentId { get; set; }
@@ -103,7 +103,7 @@ public sealed class PaymentEvent : Entity
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class Entitlement : Entity
public sealed class Entitlement : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid UserId { get; set; }
@@ -213,7 +213,7 @@ public sealed class TenantSubscription : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class TenantUsageRecord : Entity
public sealed class TenantUsageRecord : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public string MetricKey { get; set; } = string.Empty;
@@ -248,7 +248,7 @@ public sealed class CommerceRefundRequest : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class CommerceRefundEvent : Entity
public sealed class CommerceRefundEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid RefundRequestId { get; set; }
@@ -285,7 +285,7 @@ public sealed class CommerceReconciliationBatch : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class CommerceReconciliationItem : Entity
public sealed class CommerceReconciliationItem : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid BatchId { get; set; }
@@ -344,7 +344,7 @@ public sealed class CommerceReconciliationIssue : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class CommerceReconciliationIssueEvent : Entity
public sealed class CommerceReconciliationIssueEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid IssueId { get; set; }

View File

@@ -13,13 +13,18 @@ public interface IHasTimestamps
DateTimeOffset UpdatedAt { get; set; }
}
public interface ITenantOwned
{
Guid TenantId { get; set; }
}
public abstract class AuditableEntity : Entity, IHasTimestamps
{
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public abstract class TenantEntity : Entity
public abstract class TenantEntity : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
}

View File

@@ -141,6 +141,8 @@ public enum ContentStatus
public sealed class QuestionCollectionItem : AuditableTenantEntity
{
public Guid CollectionId { get; set; }
public Guid QuestionReferenceId { get; set; }
public Guid QuestionOwnerTenantId { get; set; }
public Guid QuestionId { get; set; }
public string? SectionKey { get; set; }
public int SortOrder { get; set; }

View File

@@ -3,7 +3,7 @@ using Tiku.Domain.Common;
namespace Tiku.Domain.Content;
public sealed class ContentAssetAccessEvent : Entity
public sealed class ContentAssetAccessEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid? AssetId { get; set; }
@@ -24,7 +24,7 @@ public sealed class ContentAssetAccessEvent : Entity
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class ContentAssetSecurityScanEvent : Entity
public sealed class ContentAssetSecurityScanEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid? AssetId { get; set; }
@@ -36,38 +36,25 @@ public sealed class ContentAssetSecurityScanEvent : Entity
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class QuestionBankGrant : AuditableEntity
public sealed class TenantQuestionBankPreference : AuditableTenantEntity
{
public Guid SourceQuestionBankId { get; set; }
public Guid QuestionBankOwnerTenantId { get; set; }
public Guid QuestionBankId { get; set; }
public bool IsVisible { get; set; } = true;
public string? Alias { get; set; }
public int SortOrder { get; set; }
public string? NavigationLocation { get; set; }
public Guid? CreatedBy { get; set; }
public Guid? UpdatedBy { get; set; }
public QuestionBankGrantScope GrantScope { get; set; } = QuestionBankGrantScope.Plans;
public string[] AllowedPlanCodes { get; set; } = [];
public Guid[] AllowedTenantIds { get; set; } = [];
public Guid[] AllowedRegionIds { get; set; } = [];
public Guid[] AllowedSubjectIds { get; set; } = [];
public QuestionBankGrantStatus Status { get; set; } = QuestionBankGrantStatus.Active;
public DateTimeOffset? StartsAt { get; set; }
public DateTimeOffset? ExpiresAt { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class TenantQuestionBankAdoption : AuditableTenantEntity
public sealed class TenantQuestionReference : AuditableTenantEntity
{
public Guid SourceQuestionBankId { get; set; }
public Guid? GrantId { get; set; }
public Guid? TargetQuestionBankId { get; set; }
public Guid? TargetEntryId { get; set; }
public Guid? TargetCollectionId { get; set; }
public Guid QuestionOwnerTenantId { get; set; }
public Guid QuestionId { get; set; }
public QuestionSource Source { get; set; } = QuestionSource.Tenant;
public Guid? CreatedBy { get; set; }
public Guid? UpdatedBy { get; set; }
public QuestionBankAdoptionMode AdoptionMode { get; set; } = QuestionBankAdoptionMode.CopiedSnapshot;
public QuestionBankAdoptionStatus Status { get; set; } = QuestionBankAdoptionStatus.Active;
public QuestionBankAdoptionSyncStatus SyncStatus { get; set; } = QuestionBankAdoptionSyncStatus.Synced;
public JsonElement SourceSnapshot { get; set; } = JsonDefaults.Object();
public int CopiedQuestionCount { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public DateTimeOffset? LastSyncedAt { get; set; }
}
public sealed class AiRecommendationReport : AuditableTenantEntity
@@ -90,9 +77,5 @@ public enum AssetAccessType { Download, Preview, AdminDownload, AdminPreview, Up
public enum AssetAccessDisposition { Attachment, Inline }
public enum AssetAccessResult { Granted, Denied }
public enum AssetSecurityRiskLevel { None, Low, Medium, High, Critical }
public enum QuestionBankGrantScope { AllActiveTenants, Plans, Tenants, Mixed }
public enum QuestionBankGrantStatus { Active, Disabled, Expired }
public enum QuestionBankAdoptionMode { CopiedSnapshot, Reference }
public enum QuestionBankAdoptionStatus { Active, SyncPending, Suspended, Archived }
public enum QuestionBankAdoptionSyncStatus { Pending, Synced, Failed }
public enum QuestionSource { Platform, Tenant }
public enum AiRecommendationReportStatus { Draft, Generated, Failed }

View File

@@ -188,7 +188,7 @@ public sealed class CommissionSettlementProof : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class CommissionSettlementExportEvent : Entity
public sealed class CommissionSettlementExportEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid SettlementId { get; set; }

View File

@@ -3,7 +3,7 @@ using Tiku.Domain.Common;
namespace Tiku.Domain.Import;
public sealed class PocketBaseImportRun : Entity
public sealed class PocketBaseImportRun : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public string SourceName { get; set; } = string.Empty;
@@ -14,7 +14,7 @@ public sealed class PocketBaseImportRun : Entity
public DateTimeOffset? FinishedAt { get; set; }
}
public sealed class PocketBaseRawRecord
public sealed class PocketBaseRawRecord : ITenantOwned
{
public Guid RunId { get; set; }
public Guid TenantId { get; set; }
@@ -26,7 +26,7 @@ public sealed class PocketBaseRawRecord
public DateTimeOffset ImportedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PocketBaseImportIssue : Entity
public sealed class PocketBaseImportIssue : Entity, ITenantOwned
{
public Guid? RunId { get; set; }
public Guid TenantId { get; set; }

View File

@@ -15,7 +15,6 @@ public sealed class PracticeSession : TenantEntity
public Guid? TargetId { get; set; }
public DateTimeOffset StartedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? FinishedAt { get; set; }
public JsonElement QuestionIds { get; set; } = JsonDefaults.Array();
public int QuestionCount { get; set; }
public int? DurationMinutes { get; set; }
public decimal? TotalScore { get; set; }
@@ -27,6 +26,17 @@ public sealed class PracticeSession : TenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class PracticeSessionQuestion : TenantEntity
{
public Guid PracticeSessionId { get; set; }
public Guid QuestionReferenceId { get; set; }
public Guid QuestionOwnerTenantId { get; set; }
public Guid QuestionId { get; set; }
public Guid QuestionVersionId { get; set; }
public int Position { get; set; }
public decimal? Score { get; set; }
}
public enum PracticeAccessMode
{
Free,
@@ -37,9 +47,8 @@ public enum PracticeAccessMode
public sealed class AnswerRecord : TenantEntity
{
public Guid UserId { get; set; }
public Guid? QuestionId { get; set; }
public Guid? QuestionVersionId { get; set; }
public Guid? PracticeSessionId { get; set; }
public Guid PracticeSessionId { get; set; }
public Guid SessionQuestionId { get; set; }
public string? LegacyId { get; set; }
public string? LegacyQuestionId { get; set; }
public string? LegacyCategoryId { get; set; }
@@ -50,19 +59,23 @@ public sealed class AnswerRecord : TenantEntity
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class FavoriteQuestion
public sealed class FavoriteQuestion : ITenantOwned
{
public Guid TenantId { get; set; }
public Guid UserId { get; set; }
public Guid QuestionReferenceId { get; set; }
public Guid QuestionOwnerTenantId { get; set; }
public Guid QuestionId { get; set; }
public string Source { get; set; } = "imported";
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class WrongQuestion
public sealed class WrongQuestion : ITenantOwned
{
public Guid TenantId { get; set; }
public Guid UserId { get; set; }
public Guid QuestionReferenceId { get; set; }
public Guid QuestionOwnerTenantId { get; set; }
public Guid QuestionId { get; set; }
public int WrongCount { get; set; } = 1;
public DateTimeOffset LastWrongAt { get; set; } = DateTimeOffset.UtcNow;

View File

@@ -36,7 +36,7 @@ public sealed class Report : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class ReportStatusEvent : Entity
public sealed class ReportStatusEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid ReportId { get; set; }
@@ -48,7 +48,7 @@ public sealed class ReportStatusEvent : Entity
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class UserScoreEvent : Entity
public sealed class UserScoreEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid UserId { get; set; }
@@ -73,7 +73,7 @@ public sealed class PracticeDailyUsage : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class PracticeAccessEvent : Entity
public sealed class PracticeAccessEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid? UserId { get; set; }

View File

@@ -18,7 +18,7 @@ public sealed class PlatformSaasPlan : AuditableEntity
public int SortOrder { get; set; }
}
public sealed class TenantBillingProfile : IHasTimestamps
public sealed class TenantBillingProfile : IHasTimestamps, ITenantOwned
{
public Guid TenantId { get; set; }
public string? BillingName { get; set; }
@@ -58,7 +58,7 @@ public sealed class TenantInvoice : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class TenantInvoiceItem : Entity
public sealed class TenantInvoiceItem : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid InvoiceId { get; set; }

View File

@@ -7,17 +7,10 @@ public sealed class QuestionBank : AuditableTenantEntity
{
public Guid? RegionId { get; set; }
public string Name { get; set; } = string.Empty;
public QuestionBankScope SourceScope { get; set; } = QuestionBankScope.Tenant;
public QuestionBankStatus Status { get; set; } = QuestionBankStatus.Active;
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public enum QuestionBankScope
{
Platform,
Tenant
}
public enum QuestionBankStatus
{
Active,
@@ -55,7 +48,7 @@ public enum QuestionStatus
Archived
}
public sealed class QuestionVersion : Entity
public sealed class QuestionVersion : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid QuestionId { get; set; }

View File

@@ -11,6 +11,10 @@ public sealed class TenantDomain : AuditableTenantEntity
public bool IsPrimary { get; set; }
public string? VerificationToken { get; set; }
public DateTimeOffset? VerifiedAt { get; set; }
public DateTimeOffset? LastCheckedAt { get; set; }
public DateTimeOffset? DnsVerifiedAt { get; set; }
public DateTimeOffset? TlsReadyAt { get; set; }
public string? LastFailureReason { get; set; }
}
public enum TenantDomainType
@@ -28,7 +32,7 @@ public enum TenantDomainStatus
Disabled
}
public sealed class TenantBranding : IHasTimestamps
public sealed class TenantBranding : IHasTimestamps, ITenantOwned
{
public Guid TenantId { get; set; }
public string BrandName { get; set; } = string.Empty;
@@ -45,7 +49,7 @@ public sealed class TenantBranding : IHasTimestamps
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class TenantSettings : IHasTimestamps
public sealed class TenantSettings : IHasTimestamps, ITenantOwned
{
public Guid TenantId { get; set; }
public JsonElement FeatureFlags { get; set; } = JsonDefaults.Object();
@@ -54,3 +58,20 @@ public sealed class TenantSettings : IHasTimestamps
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class TenantFrontendConfig : AuditableTenantEntity
{
public int SchemaVersion { get; set; } = 1;
public int ConfigVersion { get; set; } = 1;
public JsonElement PublishedBranding { get; set; } = JsonDefaults.Object();
public JsonElement PublishedTheme { get; set; } = JsonDefaults.Object();
public JsonElement PublishedFeatures { get; set; } = JsonDefaults.Object();
public JsonElement PublishedNavigation { get; set; } = JsonDefaults.Array();
public JsonElement PublishedHomeModules { get; set; } = JsonDefaults.Array();
public JsonElement DraftBranding { get; set; } = JsonDefaults.Object();
public JsonElement DraftTheme { get; set; } = JsonDefaults.Object();
public JsonElement DraftFeatures { get; set; } = JsonDefaults.Object();
public JsonElement DraftNavigation { get; set; } = JsonDefaults.Array();
public JsonElement DraftHomeModules { get; set; } = JsonDefaults.Array();
public DateTimeOffset? PublishedAt { get; set; }
}

View File

@@ -26,7 +26,7 @@ public sealed class TenantSecret : AuditableTenantEntity
public DateTimeOffset? ExpiresAt { get; set; }
}
public sealed class SmsVerificationCode : Entity
public sealed class SmsVerificationCode : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public string Phone { get; set; } = string.Empty;
@@ -43,7 +43,7 @@ public sealed class SmsVerificationCode : Entity
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class AuthLoginEvent : Entity
public sealed class AuthLoginEvent : Entity, ITenantOwned
{
public Guid TenantId { get; set; }
public Guid? UserId { get; set; }
@@ -69,7 +69,7 @@ public sealed class AuthSession : AuditableTenantEntity
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
}
public sealed class SmsSendRateLimit
public sealed class SmsSendRateLimit : ITenantOwned
{
public Guid TenantId { get; set; }
public SmsRateLimitDimension Dimension { get; set; } = SmsRateLimitDimension.Phone;