refactor(platform): split control plane services

This commit is contained in:
2026-08-04 09:37:49 +08:00
parent 087ca78af5
commit c06cf4a4f7
22 changed files with 586 additions and 437 deletions

View File

@@ -14,7 +14,13 @@ internal static class PlatformModule
{
internal static IServiceCollection AddPlatformModule(this IServiceCollection services)
{
services.AddScoped<IPlatformAdminService, PlatformAdminService>();
services.AddScoped<PlatformAdministrationDependencies>();
services.AddScoped<IPlatformDashboardService, PlatformDashboardService>();
services.AddScoped<ITenantProvisioningAdministrationService, TenantProvisioningAdministrationService>();
services.AddScoped<IPlatformTenantDomainService, PlatformTenantDomainService>();
services.AddScoped<IPlatformStaffAccessService, PlatformStaffAccessService>();
services.AddScoped<IPlatformAuditAlertService, PlatformAuditAlertService>();
services.AddScoped<IPlatformDunningService, PlatformDunningService>();
services.AddScoped<IBackgroundJobHandler, TenantExportJobHandler>();
services.AddScoped<IPlatformOperationsQueryService, PlatformOperationsQueryService>();
services.AddOptions<TenantProvisioningOptions>();

View File

@@ -5,7 +5,8 @@ using Tiku.Domain.Platform;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed partial class PlatformAdminService
internal sealed class PlatformAuditAlertService(PlatformAdministrationDependencies dependencies)
: PlatformAdministrationServiceBase(dependencies), IPlatformAuditAlertService
{
public async Task<PlatformAuditLogList> GetAuditLogsAsync(
PlatformAdminActor actor,
@@ -91,4 +92,4 @@ internal sealed partial class PlatformAdminService
return alert;
}, cancellationToken);
}
}
}

View File

@@ -4,7 +4,8 @@ using Tiku.Application.Security;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed partial class PlatformAdminService
internal sealed class PlatformDashboardService(PlatformAdministrationDependencies dependencies)
: PlatformAdministrationServiceBase(dependencies), IPlatformDashboardService
{
public async Task<PlatformOverview> GetOverviewAsync(
PlatformAdminActor actor,
@@ -50,4 +51,4 @@ internal sealed partial class PlatformAdminService
public int QuestionCount { get; init; }
public int LearningActiveUserCount { get; init; }
}
}
}

View File

@@ -5,7 +5,8 @@ using Tiku.Domain.Platform;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed partial class PlatformAdminService
internal sealed class PlatformDunningService(PlatformAdministrationDependencies dependencies)
: PlatformAdministrationServiceBase(dependencies), IPlatformDunningService
{
public async Task<PlatformBillingDunningChannelList> GetBillingDunningChannelsAsync(
PlatformAdminActor actor,
@@ -224,4 +225,4 @@ internal sealed partial class PlatformAdminService
return ToDunningEventItem(item);
}, cancellationToken);
}
}
}

View File

@@ -15,9 +15,9 @@ using Tiku.Infrastructure.Tenancy;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed partial class PlatformAdminService
internal abstract partial class PlatformAdministrationServiceBase
{
private async Task AssertPlatformPermissionAsync(
protected async Task AssertPlatformPermissionAsync(
PlatformAdminActor actor,
string permissionCode,
CancellationToken cancellationToken)
@@ -27,7 +27,7 @@ internal sealed partial class PlatformAdminService
throw new PlatformAdminException("Platform admin access is required.", "platform_access_denied");
}
private static bool IsPlatformOperationIdempotencyConflict(DbUpdateException exception)
protected static bool IsPlatformOperationIdempotencyConflict(DbUpdateException exception)
{
return exception.InnerException is PostgresException
{
@@ -38,7 +38,7 @@ internal sealed partial class PlatformAdminService
StringComparison.Ordinal);
}
private async Task<PlatformTenantProvisioningResult> ProvisioningReplayResultAsync(
protected async Task<PlatformTenantProvisioningResult> ProvisioningReplayResultAsync(
TikuDbContext dbContext,
Guid tenantId,
CancellationToken cancellationToken)
@@ -65,7 +65,7 @@ internal sealed partial class PlatformAdminService
true);
}
private static async Task<PlatformOwnerActivationLinkResult> OwnerActivationReplayResultAsync(
protected static async Task<PlatformOwnerActivationLinkResult> OwnerActivationReplayResultAsync(
TikuDbContext dbContext,
Guid activationId,
CancellationToken cancellationToken)
@@ -75,7 +75,7 @@ internal sealed partial class PlatformAdminService
return new PlatformOwnerActivationLinkResult(grant.Id, null, grant.ExpiresAt, true);
}
private async Task<PlatformOwnerActivationStatus> OwnerActivationStatusAsync(
protected async Task<PlatformOwnerActivationStatus> OwnerActivationStatusAsync(
TikuDbContext dbContext,
Tenant tenant,
CancellationToken cancellationToken)
@@ -101,7 +101,7 @@ internal sealed partial class PlatformAdminService
return new PlatformOwnerActivationStatus(domainActive ? "ready_to_issue" : "domain_pending", null, null);
}
private Task<TResult> ExecuteSystemAsync<TResult>(
protected Task<TResult> ExecuteSystemAsync<TResult>(
string reason,
Func<TikuDbContext, Task<TResult>> operation,
CancellationToken cancellationToken)
@@ -109,7 +109,7 @@ internal sealed partial class PlatformAdminService
return ExecuteSystemAsync(reason, (_, dbContext) => operation(dbContext), cancellationToken);
}
private Task<TResult> ExecuteSystemAsync<TResult>(
protected Task<TResult> ExecuteSystemAsync<TResult>(
string reason,
Func<IServiceProvider, TikuDbContext, Task<TResult>> operation,
CancellationToken cancellationToken)
@@ -118,7 +118,7 @@ internal sealed partial class PlatformAdminService
new SystemScopeRequest(
null,
SystemScopeCallerType.Platform,
nameof(PlatformAdminService),
nameof(PlatformAdministrationServiceBase),
reason,
Guid.NewGuid().ToString("N"),
true),
@@ -126,7 +126,7 @@ internal sealed partial class PlatformAdminService
cancellationToken);
}
private static async Task RequireTenantAsync(TikuDbContext dbContext, Guid tenantId,
protected static async Task RequireTenantAsync(TikuDbContext dbContext, Guid tenantId,
CancellationToken cancellationToken)
{
var exists =
@@ -135,7 +135,7 @@ internal sealed partial class PlatformAdminService
if (!exists) throw new PlatformAdminException("Tenant was not found.", "tenant_not_found");
}
private static void AddAudit(TikuDbContext dbContext, PlatformAdminActor actor, string action, Guid targetId,
protected static void AddAudit(TikuDbContext dbContext, PlatformAdminActor actor, string action, Guid targetId,
object details)
{
dbContext.AuditLogs.Add(new AuditLog
@@ -148,7 +148,7 @@ internal sealed partial class PlatformAdminService
});
}
private static PlatformTenantItem ToTenantItem(Tenant tenant, int domainCount,
protected static PlatformTenantItem ToTenantItem(Tenant tenant, int domainCount,
DateTimeOffset? subscriptionExpiresAt)
{
return new PlatformTenantItem(
@@ -165,7 +165,7 @@ internal sealed partial class PlatformAdminService
tenant.UpdatedAt);
}
private static PlatformTenantDomainItem ToDomainItem(TenantDomain domain)
protected static PlatformTenantDomainItem ToDomainItem(TenantDomain domain)
{
return new PlatformTenantDomainItem(
domain.Id,
@@ -184,7 +184,7 @@ internal sealed partial class PlatformAdminService
null);
}
private PlatformTenantDomainItem ToDomainItemWithInstructions(TenantDomain domain)
protected PlatformTenantDomainItem ToDomainItemWithInstructions(TenantDomain domain)
{
return ToDomainItem(domain) with
{
@@ -194,7 +194,7 @@ internal sealed partial class PlatformAdminService
};
}
private static PlatformTenantSubscriptionItem ToSubscriptionItem(TenantSaasSubscription subscription)
protected static PlatformTenantSubscriptionItem ToSubscriptionItem(TenantSaasSubscription subscription)
{
return new PlatformTenantSubscriptionItem(
subscription.Id,
@@ -208,7 +208,7 @@ internal sealed partial class PlatformAdminService
subscription.Metadata);
}
private static TenantBillingProfileItem ToBillingProfileItem(TenantBillingProfile profile)
protected static TenantBillingProfileItem ToBillingProfileItem(TenantBillingProfile profile)
{
return new TenantBillingProfileItem(
profile.TenantId,
@@ -225,7 +225,7 @@ internal sealed partial class PlatformAdminService
profile.Metadata);
}
private static TenantBillingPolicyItem ToBillingPolicyItem(TenantBillingPolicy policy)
protected static TenantBillingPolicyItem ToBillingPolicyItem(TenantBillingPolicy policy)
{
return new TenantBillingPolicyItem(
policy.TenantId,
@@ -237,7 +237,7 @@ internal sealed partial class PlatformAdminService
policy.UpdatedAt);
}
private static PlatformBillingDunningChannelItem ToDunningChannelItem(
protected static PlatformBillingDunningChannelItem ToDunningChannelItem(
PlatformBillingDunningNotificationChannel channel)
{
return new PlatformBillingDunningChannelItem(
@@ -259,7 +259,7 @@ internal sealed partial class PlatformAdminService
channel.UpdatedAt);
}
private static PlatformBillingDunningEventItem ToDunningEventItem(PlatformBillingDunningNotificationEvent item)
protected static PlatformBillingDunningEventItem ToDunningEventItem(PlatformBillingDunningNotificationEvent item)
{
return new PlatformBillingDunningEventItem(
item.Id,
@@ -283,7 +283,7 @@ internal sealed partial class PlatformAdminService
item.UpdatedAt);
}
private static PlatformStaffItem ToStaffItem(User user, IReadOnlyCollection<string> roleCodes)
protected static PlatformStaffItem ToStaffItem(User user, IReadOnlyCollection<string> roleCodes)
{
return new PlatformStaffItem(
user.Id,
@@ -296,29 +296,29 @@ internal sealed partial class PlatformAdminService
user.UpdatedAt);
}
private static int Limit(int? limit)
protected static int Limit(int? limit)
{
return Math.Clamp(limit ?? 50, 1, 200);
}
private static string NormalizeCode(string value)
protected static string NormalizeCode(string value)
{
return value.Trim().ToLowerInvariant();
}
private static string? Normalize(string? value)
protected static string? Normalize(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
private static string Required(string? value, string name)
protected static string Required(string? value, string name)
{
return string.IsNullOrWhiteSpace(value)
? throw new PlatformAdminException($"{name} is required.", "idempotency_key_required")
: value.Trim();
}
private static string ProvisioningRequestHash(CreatePlatformTenantCommand command)
protected static string ProvisioningRequestHash(CreatePlatformTenantCommand command)
{
var payload = JsonSerializer.Serialize(new
{
@@ -342,7 +342,7 @@ internal sealed partial class PlatformAdminService
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload))).ToLowerInvariant();
}
private static string OwnerActivationRequestHash(IssuePlatformOwnerActivationLinkCommand command)
protected static string OwnerActivationRequestHash(IssuePlatformOwnerActivationLinkCommand command)
{
var payload = JsonSerializer.Serialize(new
{
@@ -353,7 +353,7 @@ internal sealed partial class PlatformAdminService
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload))).ToLowerInvariant();
}
private static TenantDomain CreatePrimaryDomain(Guid tenantId, string host)
protected static TenantDomain CreatePrimaryDomain(Guid tenantId, string host)
{
try
{
@@ -365,22 +365,22 @@ internal sealed partial class PlatformAdminService
}
}
private static string Base64Url(byte[] value)
protected static string Base64Url(byte[] value)
{
return Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
private string BuildOwnerActivationUrl(string host, Guid activationId, string token)
protected string BuildOwnerActivationUrl(string host, Guid activationId, string token)
{
return TenantOwnerActivationUrlPolicy.Build(provisioning, host, activationId, token);
}
private static JsonElement JsonObjectOrDefault(JsonElement value)
protected static JsonElement JsonObjectOrDefault(JsonElement value)
{
return value.ValueKind == JsonValueKind.Object ? value.Clone() : JsonDocument.Parse("{}").RootElement.Clone();
}
private static string? MaskPhone(string? phone)
protected static string? MaskPhone(string? phone)
{
var value = Normalize(phone);
return value is { Length: >= 7 }
@@ -388,7 +388,7 @@ internal sealed partial class PlatformAdminService
: value;
}
private static string? MaskBankAccount(string? account)
protected static string? MaskBankAccount(string? account)
{
var value = Normalize(account);
return value is { Length: > 8 }
@@ -396,7 +396,7 @@ internal sealed partial class PlatformAdminService
: value;
}
private static string MaskWebhook(string webhookUrl)
protected static string MaskWebhook(string webhookUrl)
{
var value = Normalize(webhookUrl) ?? string.Empty;
if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
@@ -405,7 +405,7 @@ internal sealed partial class PlatformAdminService
return $"{uri.Scheme}://{uri.Host}/****";
}
private static string[] NormalizeArray(IReadOnlyCollection<string> values, string[] fallback)
protected static string[] NormalizeArray(IReadOnlyCollection<string> values, string[] fallback)
{
var normalized = values
.Select(Normalize)
@@ -416,7 +416,7 @@ internal sealed partial class PlatformAdminService
return normalized.Length == 0 ? fallback : normalized;
}
private static bool ParseEnabledStatus(string status)
protected static bool ParseEnabledStatus(string status)
{
return NormalizeCode(status) switch
{
@@ -426,17 +426,17 @@ internal sealed partial class PlatformAdminService
};
}
private static TenantStatus ParseTenantStatus(string status)
protected static TenantStatus ParseTenantStatus(string status)
{
return Enum.TryParse<TenantStatus>(status, true, out var value) ? value : throw InvalidStatus(status);
}
private static TenantDomainStatus ParseDomainStatus(string status)
protected static TenantDomainStatus ParseDomainStatus(string status)
{
return Enum.TryParse<TenantDomainStatus>(status, true, out var value) ? value : throw InvalidStatus(status);
}
private static async Task EnsureTenantOwnerRoleAsync(
protected static async Task EnsureTenantOwnerRoleAsync(
TikuDbContext dbContext,
Guid tenantId,
Guid ownerUserId,
@@ -519,22 +519,22 @@ internal sealed partial class PlatformAdminService
});
}
private static PlatformAuditAlertStatus ParseAuditAlertStatus(string status)
protected static PlatformAuditAlertStatus ParseAuditAlertStatus(string status)
{
return Enum.TryParse<PlatformAuditAlertStatus>(status, true, out var value)
? value
: throw InvalidStatus(status);
}
private static PlatformBillingDunningNotificationStatus ParseDunningEventStatus(string status)
protected static PlatformBillingDunningNotificationStatus ParseDunningEventStatus(string status)
{
return Enum.TryParse<PlatformBillingDunningNotificationStatus>(status, true, out var value)
? value
: throw InvalidStatus(status);
}
private static PlatformAdminException InvalidStatus(string status)
protected static PlatformAdminException InvalidStatus(string status)
{
return new PlatformAdminException($"Unsupported status '{status}'.", "invalid_status");
}
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.Extensions.Options;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed record PlatformAdministrationDependencies(
ICurrentAccessContext CurrentAccessContext,
ITenantExecutionScope TenantExecutionScope,
IOptions<TenantProvisioningOptions> ProvisioningOptions,
IOptions<DomainLifecycleOptions> DomainOptions);
internal abstract partial class PlatformAdministrationServiceBase(PlatformAdministrationDependencies dependencies)
{
protected ICurrentAccessContext currentAccessContext { get; } = dependencies.CurrentAccessContext;
protected ITenantExecutionScope tenantExecutionScope { get; } = dependencies.TenantExecutionScope;
protected DomainLifecycleOptions domains { get; } = dependencies.DomainOptions.Value;
protected TenantProvisioningOptions provisioning { get; } = dependencies.ProvisioningOptions.Value;
}

View File

@@ -1,16 +0,0 @@
using Microsoft.Extensions.Options;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed partial class PlatformAdminService(
ICurrentAccessContext currentAccessContext,
ITenantExecutionScope tenantExecutionScope,
IOptions<TenantProvisioningOptions> provisioningOptions,
IOptions<DomainLifecycleOptions> domainOptions) : IPlatformAdminService
{
private readonly DomainLifecycleOptions domains = domainOptions.Value;
private readonly TenantProvisioningOptions provisioning = provisioningOptions.Value;
}

View File

@@ -22,7 +22,7 @@ internal sealed class PlatformApprovalService(
ITenantExecutionScope tenantExecutionScope,
IOperationAuditService auditService,
IPlatformBillingAdminService billingService,
IPlatformAdminService platformAdminService,
ITenantProvisioningAdministrationService tenantProvisioningService,
IPlatformPaymentSettingsService paymentSettingsService,
IBackofficeService backofficeService) : IPlatformApprovalService
{
@@ -177,11 +177,11 @@ internal sealed class PlatformApprovalService(
{
if (command.Status != TenantStatus.Archived)
return ExecuteImmediateAsync(() =>
platformAdminService.UpdateTenantStatusAsync(actor, command, cancellationToken));
tenantProvisioningService.UpdateTenantStatusAsync(actor, command, cancellationToken));
return SubmitAsync(actor.UserId, PlatformApprovalPolicyCodes.TenantArchive,
nameof(UpdatePlatformTenantStatusCommand), "tenants", command.TenantId.ToString("N"), null,
idempotencyKey, command.Reason, command,
async token => await platformAdminService.UpdateTenantStatusAsync(actor, command, token),
async token => await tenantProvisioningService.UpdateTenantStatusAsync(actor, command, token),
cancellationToken);
}
@@ -385,7 +385,7 @@ internal sealed class PlatformApprovalService(
.ConfirmManualPaymentAsync(new SaasCatalogActor(item.RequestedBy),
Deserialize<ConfirmManualPaymentCommand>(item), cancellationToken),
nameof(UpdatePlatformTenantStatusCommand) => await scope.ServiceProvider
.GetRequiredService<IPlatformAdminService>()
.GetRequiredService<ITenantProvisioningAdministrationService>()
.UpdateTenantStatusAsync(new PlatformAdminActor(item.RequestedBy),
Deserialize<UpdatePlatformTenantStatusCommand>(item), cancellationToken),
nameof(UpsertPlatformPaymentChannelCommand) => await scope.ServiceProvider
@@ -508,4 +508,4 @@ internal sealed class PlatformApprovalService(
item.Enabled, item.AlwaysRequireApproval, item.AmountThresholdCents, item.Version, item.ExpiresAfterHours,
item.Conditions);
}
}
}

View File

@@ -8,7 +8,8 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed partial class PlatformAdminService
internal sealed class PlatformStaffAccessService(PlatformAdministrationDependencies dependencies)
: PlatformAdministrationServiceBase(dependencies), IPlatformStaffAccessService
{
public async Task<PlatformStaffList> GetStaffAsync(
PlatformAdminActor actor,
@@ -132,4 +133,4 @@ internal sealed partial class PlatformAdminService
return ToStaffItem(user, roleCodes);
}, cancellationToken);
}
}
}

View File

@@ -7,7 +7,8 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed partial class PlatformAdminService
internal sealed class PlatformTenantDomainService(PlatformAdministrationDependencies dependencies)
: PlatformAdministrationServiceBase(dependencies), IPlatformTenantDomainService
{
public async Task<PlatformDomainList> GetDomainsAsync(
PlatformAdminActor actor,
@@ -64,4 +65,4 @@ internal sealed partial class PlatformAdminService
return new PlatformDomainRecheckResult(domain.Id, domain.Status, domain.LastCheckedAt.Value);
}, cancellationToken);
}
}
}

View File

@@ -13,7 +13,8 @@ using Tiku.Infrastructure.Tenancy;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed partial class PlatformAdminService
internal sealed class TenantProvisioningAdministrationService(PlatformAdministrationDependencies dependencies)
: PlatformAdministrationServiceBase(dependencies), ITenantProvisioningAdministrationService
{
public async Task<PlatformTenantList> GetTenantsAsync(
PlatformAdminActor actor,
@@ -584,4 +585,4 @@ internal sealed partial class PlatformAdminService
return ToBillingPolicyItem(policy);
}, cancellationToken);
}
}
}