feat(saas): implement marketplace and tenant onboarding

This commit is contained in:
2026-07-29 13:58:59 +08:00
parent 76606029e2
commit 6db200a2fc
145 changed files with 16862 additions and 94529 deletions

View File

@@ -20,8 +20,19 @@ namespace Tiku.Infrastructure.Content;
public sealed class DirectContentService(
TikuDbContext dbContext,
IQuestionReferenceService questionReferenceService,
ICurrentAccessContext currentAccessContext) : IDirectContentService
ICurrentAccessContext currentAccessContext,
IFeatureAccessService featureAccessService) : IDirectContentService
{
private static readonly string[] ContentPermissions =
[
BackendPermissions.TenantContentManage,
BackendPermissions.TenantVocabularyManage,
BackendPermissions.TenantHandbookManage,
BackendPermissions.TenantVideoManage,
BackendPermissions.TenantScorelineManage,
BackendPermissions.TenantSiteContentManage,
BackendPermissions.TenantJobManage
];
private const int DefaultLimit = 100;
private const int MaxLimit = 1000;
private static readonly Regex ScorelineFieldKeyRegex = new("^[A-Za-z][A-Za-z0-9_]{0,63}$", RegexOptions.Compiled);
@@ -50,6 +61,13 @@ public sealed class DirectContentService(
TenantId = actor.TenantId
};
ApplyQuestion(question, command);
if (question.Status != QuestionStatus.Archived)
{
await featureAccessService.ConsumeQuotaIfConfiguredAsync(
actor.TenantId,
SaasQuotaMetricCatalog.PrivateQuestionCount,
cancellationToken: cancellationToken);
}
dbContext.Questions.Add(question);
await dbContext.SaveChangesAsync(cancellationToken);
@@ -85,7 +103,19 @@ public sealed class DirectContentService(
}
await AssertQuestionReferencesAsync(actor.TenantId, command, cancellationToken);
await using var transaction = dbContext.Database.CurrentTransaction is null
? await dbContext.Database.BeginTransactionAsync(cancellationToken)
: null;
var wasCounted = question.Status != QuestionStatus.Archived;
ApplyQuestion(question, command);
var isCounted = question.Status != QuestionStatus.Archived;
if (!wasCounted && isCounted)
{
await featureAccessService.ConsumeQuotaIfConfiguredAsync(
actor.TenantId,
SaasQuotaMetricCatalog.PrivateQuestionCount,
cancellationToken: cancellationToken);
}
QuestionVersion? version;
if (command.CreateVersion || !question.CurrentVersionId.HasValue)
{
@@ -120,6 +150,18 @@ public sealed class DirectContentService(
await SyncPrimaryCollectionItemAsync(actor, question, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
if (wasCounted && !isCounted)
{
await featureAccessService.ReleaseQuotaAsync(
actor.TenantId,
SaasQuotaMetricCatalog.PrivateQuestionCount,
1,
cancellationToken);
}
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
return new ContentManagementResult<QuestionManagementItem>(ToQuestionItem(question, version));
}
@@ -1815,7 +1857,7 @@ public sealed class DirectContentService(
if (!access.IsCurrentTenantMember ||
access.UserId != actor.UserId ||
access.TenantId != actor.TenantId ||
!access.HasTenantPermission(BackendPermissions.TenantContentManage))
!ContentPermissions.Any(access.HasTenantPermission))
{
throw new ContentManagementException("Tenant content access was denied.", "content_access_denied");
}