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

@@ -26,6 +26,7 @@ internal static class ApiPresentationExtensions
services.AddScoped<DirectContentActorResolver>();
services.AddScoped<CommerceAdminActorResolver>();
services.AddScoped<LearningActorResolver>();
services.AddScoped<PlatformAdminActorResolver>();
return services;
}

View File

@@ -0,0 +1,15 @@
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
namespace Tiku.Api.Controllers;
public sealed class PlatformAdminActorResolver(ICurrentUser currentUser)
{
internal PlatformAdminActor Resolve()
{
if (currentUser.UserId is not { } userId)
throw new PlatformAdminException("Platform admin actor was not resolved.", "platform_access_denied");
return new PlatformAdminActor(userId);
}
}

View File

@@ -1,358 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Api.OpenApi;
using Tiku.Application.Auth;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("平台端-平台管理")]
[Authorize(Policy = BackendPermissions.PlatformDashboardView)]
[Produces("application/json")]
[Route("api/platform")]
public sealed class PlatformAdminController(
IPlatformAdminService platformAdminService,
IPlatformApprovalService approvalService,
IAuthAdministrationService authAdministrationService,
ICurrentUser currentUser) : ControllerBase
{
[HttpGet("overview")]
[EndpointSummary("查询平台经营概览")]
[ProducesResponseType<PlatformOverview>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformOverview>> Overview(CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetOverviewAsync(ResolveActor(), cancellationToken));
}
[HttpGet("tenants")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询平台租户列表")]
[ProducesResponseType<PlatformTenantList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantList>> Tenants(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetTenantsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpPost("tenants")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("创建平台租户")]
[ProducesResponseType<PlatformTenantProvisioningResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantProvisioningResult>> CreateTenant(
CreatePlatformTenantDto request,
[FromHeader(Name = "Idempotency-Key")] [Required]
string idempotencyKey,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.CreateTenantAsync(
ResolveActor(),
request.ToCommand(idempotencyKey),
cancellationToken));
}
[HttpPut("tenants/{tenantId:guid}/primary-domain")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("更正租户主域名")]
public Task<PlatformTenantDomainItem> ReplacePrimaryDomain(
Guid tenantId,
ReplacePlatformPrimaryDomainDto request,
CancellationToken cancellationToken)
{
return platformAdminService.ReplacePrimaryDomainAsync(ResolveActor(), request.ToCommand(tenantId),
cancellationToken);
}
[HttpPost("tenants/{tenantId:guid}/owner-activation-links")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("一次性领取租户 Owner 激活链接")]
[EndpointDescription("仅在主域名和试用/订阅有效时签发;幂等重放不会再次返回明文链接。")]
public Task<PlatformOwnerActivationLinkResult> IssueOwnerActivationLink(
Guid tenantId,
IssuePlatformOwnerActivationLinkDto request,
[FromHeader(Name = "Idempotency-Key")] [Required]
string idempotencyKey,
CancellationToken cancellationToken)
{
return platformAdminService.IssueOwnerActivationLinkAsync(
ResolveActor(), request.ToCommand(tenantId, idempotencyKey), cancellationToken);
}
[HttpGet("tenants/{tenantId:guid}/billing-policy")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询租户收款策略")]
public Task<TenantBillingPolicyItem> GetBillingPolicy(Guid tenantId, CancellationToken cancellationToken)
{
return platformAdminService.GetTenantBillingPolicyAsync(ResolveActor(), tenantId, cancellationToken);
}
[HttpPut("tenants/{tenantId:guid}/billing-policy")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("更新租户收款策略")]
public Task<TenantBillingPolicyItem> UpsertBillingPolicy(
Guid tenantId,
UpsertTenantBillingPolicyDto request,
CancellationToken cancellationToken)
{
return platformAdminService.UpsertTenantBillingPolicyAsync(ResolveActor(), request.ToCommand(tenantId),
cancellationToken);
}
[HttpGet("tenants/detail")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询平台租户详情")]
[ProducesResponseType<PlatformTenantDetail>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantDetail>> TenantDetail(
[FromQuery] Guid tenantId,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetTenantDetailAsync(ResolveActor(), tenantId, cancellationToken));
}
[HttpPatch("tenants/status")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("更新租户业务状态")]
[PlatformOperationRisk("critical", PlatformApprovalPolicyCodes.TenantArchive)]
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status200OK)]
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status202Accepted)]
public async Task<ActionResult<PlatformCommandSubmission>> TenantStatus(
UpdatePlatformTenantStatusDto request,
[FromHeader(Name = "Idempotency-Key")] [Required]
string idempotencyKey,
CancellationToken cancellationToken)
{
var result = await approvalService.UpdateTenantStatusAsync(ResolveActor(), request.ToCommand(), idempotencyKey,
cancellationToken);
return result.ExecutionStatus == "pending_approval" ? Accepted(result) : Ok(result);
}
[HttpPut("tenants/billing-profile")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("保存租户账务与开票资料")]
[ProducesResponseType<TenantBillingProfileItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantBillingProfileItem>> BillingProfile(
UpsertPlatformTenantBillingProfileDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.UpsertTenantBillingProfileAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("domains")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询租户域名状态")]
[ProducesResponseType<PlatformDomainList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDomainList>> Domains(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetDomainsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpPost("domains/{domainId:guid}/recheck")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("重新触发租户域名 DNS/TLS 验证")]
[ProducesResponseType<PlatformDomainRecheckResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDomainRecheckResult>> RecheckDomain(
Guid domainId,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.RecheckDomainAsync(ResolveActor(), domainId, cancellationToken));
}
[HttpGet("staff")]
[Authorize(Policy = BackendPermissions.PlatformStaffManage)]
[EndpointSummary("查询平台员工列表")]
[ProducesResponseType<PlatformStaffList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformStaffList>> Staff(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetStaffAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpPut("staff")]
[Authorize(Policy = BackendPermissions.PlatformStaffManage)]
[EndpointSummary("创建或更新平台员工")]
[ProducesResponseType<PlatformStaffItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformStaffItem>> UpsertStaff(
UpsertPlatformStaffDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.UpsertStaffAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPatch("staff/status")]
[Authorize(Policy = BackendPermissions.PlatformStaffManage)]
[EndpointSummary("启用或禁用平台员工")]
[ProducesResponseType<PlatformStaffItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformStaffItem>> StaffStatus(
UpdatePlatformStaffStatusDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.UpdateStaffStatusAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpPost("staff/{userId:guid}/password-reset")]
[Authorize(Policy = BackendPermissions.PlatformStaffManage)]
[EndpointSummary("为平台员工设置一次性临时密码")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> ResetStaffPassword(
Guid userId,
AdministrativePasswordResetDto request,
CancellationToken cancellationToken)
{
var actor = ResolveActor();
await authAdministrationService.ResetPasswordAsync(
new AdministrativePasswordResetRequest(
actor.UserId,
userId,
null,
request.TemporaryPassword,
request.Reason),
cancellationToken);
return NoContent();
}
[HttpGet("audit-logs")]
[Authorize(Policy = BackendPermissions.PlatformAuditView)]
[EndpointSummary("查询平台审计日志")]
[ProducesResponseType<PlatformAuditLogList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformAuditLogList>> AuditLogs(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetAuditLogsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpGet("audit-alerts")]
[Authorize(Policy = BackendPermissions.PlatformAuditView)]
[EndpointSummary("查询平台审计告警")]
[ProducesResponseType<PlatformAuditAlertList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformAuditAlertList>> AuditAlerts(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetAuditAlertsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpPost("audit-alerts/status")]
[Authorize(Policy = BackendPermissions.PlatformAuditView)]
[EndpointSummary("更新平台审计告警状态")]
[ProducesResponseType<PlatformAuditAlert>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformAuditAlert>> AuditAlertStatus(
UpdatePlatformAuditAlertStatusDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.UpdateAuditAlertStatusAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("saas/dunning/channels")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知渠道")]
[ProducesResponseType<PlatformBillingDunningChannelList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelList>> BillingDunningChannels(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetBillingDunningChannelsAsync(ResolveActor(), query.ToQuery(),
cancellationToken));
}
[HttpPut("saas/dunning/channels")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("创建或更新平台催缴通知渠道")]
[ProducesResponseType<PlatformBillingDunningChannelItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelItem>> UpsertBillingDunningChannel(
UpsertPlatformBillingDunningChannelDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.UpsertBillingDunningChannelAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpPost("saas/dunning/channels/disable")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("禁用平台催缴通知渠道")]
[ProducesResponseType<PlatformBillingDunningChannelItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelItem>> DisableBillingDunningChannel(
DisablePlatformBillingDunningChannelDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.DisableBillingDunningChannelAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("saas/dunning/events")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知事件")]
[ProducesResponseType<PlatformBillingDunningEventList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventList>> BillingDunningEvents(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetBillingDunningEventsAsync(ResolveActor(), query.ToQuery(),
cancellationToken));
}
[HttpGet("saas/dunning/events/detail")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知事件详情")]
[ProducesResponseType<PlatformBillingDunningEventItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventItem>> BillingDunningEventDetail(
[FromQuery] Guid eventId,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetBillingDunningEventDetailAsync(ResolveActor(), eventId,
cancellationToken));
}
[HttpPost("saas/dunning/events/retry")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("重新标记平台催缴通知事件待发送")]
[ProducesResponseType<PlatformBillingDunningEventItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventItem>> RetryBillingDunningEvent(
RetryPlatformBillingDunningEventDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.RetryBillingDunningEventAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpPost("saas/dunning/events/acknowledge")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("人工确认平台催缴通知事件")]
public async Task<ActionResult<PlatformBillingDunningEventItem>> AcknowledgeBillingDunningEvent(
ResolvePlatformBillingDunningEventDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.AcknowledgeBillingDunningEventAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpPost("saas/dunning/events/ignore")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("人工忽略平台催缴通知事件")]
public async Task<ActionResult<PlatformBillingDunningEventItem>> IgnoreBillingDunningEvent(
ResolvePlatformBillingDunningEventDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.IgnoreBillingDunningEventAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
private PlatformAdminActor ResolveActor()
{
if (currentUser.UserId is not { } userId)
throw new PlatformAdminException("Platform admin actor was not resolved.", "platform_access_denied");
return new PlatformAdminActor(userId);
}
}

View File

@@ -0,0 +1,55 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Api.OpenApi;
using Tiku.Application.Auth;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("平台端-平台管理")]
[Authorize(Policy = BackendPermissions.PlatformDashboardView)]
[Produces("application/json")]
[Route("api/platform")]
public sealed class PlatformAuditAlertController(
IPlatformAuditAlertService service,
PlatformAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("audit-logs")]
[Authorize(Policy = BackendPermissions.PlatformAuditView)]
[EndpointSummary("查询平台审计日志")]
[ProducesResponseType<PlatformAuditLogList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformAuditLogList>> AuditLogs(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await service.GetAuditLogsAsync(actorResolver.Resolve(), query.ToQuery(), cancellationToken));
}
[HttpGet("audit-alerts")]
[Authorize(Policy = BackendPermissions.PlatformAuditView)]
[EndpointSummary("查询平台审计告警")]
[ProducesResponseType<PlatformAuditAlertList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformAuditAlertList>> AuditAlerts(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await service.GetAuditAlertsAsync(actorResolver.Resolve(), query.ToQuery(), cancellationToken));
}
[HttpPost("audit-alerts/status")]
[Authorize(Policy = BackendPermissions.PlatformAuditView)]
[EndpointSummary("更新平台审计告警状态")]
[ProducesResponseType<PlatformAuditAlert>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformAuditAlert>> AuditAlertStatus(
UpdatePlatformAuditAlertStatusDto request,
CancellationToken cancellationToken)
{
return Ok(await service.UpdateAuditAlertStatusAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
}

View File

@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Api.OpenApi;
using Tiku.Application.Auth;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("平台端-平台管理")]
[Authorize(Policy = BackendPermissions.PlatformDashboardView)]
[Produces("application/json")]
[Route("api/platform")]
public sealed class PlatformDashboardController(
IPlatformDashboardService service,
PlatformAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("overview")]
[EndpointSummary("查询平台经营概览")]
[ProducesResponseType<PlatformOverview>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformOverview>> Overview(CancellationToken cancellationToken)
{
return Ok(await service.GetOverviewAsync(actorResolver.Resolve(), cancellationToken));
}
}

View File

@@ -0,0 +1,115 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Api.OpenApi;
using Tiku.Application.Auth;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("平台端-平台管理")]
[Authorize(Policy = BackendPermissions.PlatformDashboardView)]
[Produces("application/json")]
[Route("api/platform")]
public sealed class PlatformDunningController(
IPlatformDunningService service,
PlatformAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("saas/dunning/channels")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知渠道")]
[ProducesResponseType<PlatformBillingDunningChannelList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelList>> BillingDunningChannels(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await service.GetBillingDunningChannelsAsync(actorResolver.Resolve(), query.ToQuery(),
cancellationToken));
}
[HttpPut("saas/dunning/channels")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("创建或更新平台催缴通知渠道")]
[ProducesResponseType<PlatformBillingDunningChannelItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelItem>> UpsertBillingDunningChannel(
UpsertPlatformBillingDunningChannelDto request,
CancellationToken cancellationToken)
{
return Ok(await service.UpsertBillingDunningChannelAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpPost("saas/dunning/channels/disable")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("禁用平台催缴通知渠道")]
[ProducesResponseType<PlatformBillingDunningChannelItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelItem>> DisableBillingDunningChannel(
DisablePlatformBillingDunningChannelDto request,
CancellationToken cancellationToken)
{
return Ok(await service.DisableBillingDunningChannelAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpGet("saas/dunning/events")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知事件")]
[ProducesResponseType<PlatformBillingDunningEventList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventList>> BillingDunningEvents(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await service.GetBillingDunningEventsAsync(actorResolver.Resolve(), query.ToQuery(),
cancellationToken));
}
[HttpGet("saas/dunning/events/detail")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知事件详情")]
[ProducesResponseType<PlatformBillingDunningEventItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventItem>> BillingDunningEventDetail(
[FromQuery] Guid eventId,
CancellationToken cancellationToken)
{
return Ok(await service.GetBillingDunningEventDetailAsync(actorResolver.Resolve(), eventId,
cancellationToken));
}
[HttpPost("saas/dunning/events/retry")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("重新标记平台催缴通知事件待发送")]
[ProducesResponseType<PlatformBillingDunningEventItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventItem>> RetryBillingDunningEvent(
RetryPlatformBillingDunningEventDto request,
CancellationToken cancellationToken)
{
return Ok(await service.RetryBillingDunningEventAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpPost("saas/dunning/events/acknowledge")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("人工确认平台催缴通知事件")]
public async Task<ActionResult<PlatformBillingDunningEventItem>> AcknowledgeBillingDunningEvent(
ResolvePlatformBillingDunningEventDto request,
CancellationToken cancellationToken)
{
return Ok(await service.AcknowledgeBillingDunningEventAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpPost("saas/dunning/events/ignore")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("人工忽略平台催缴通知事件")]
public async Task<ActionResult<PlatformBillingDunningEventItem>> IgnoreBillingDunningEvent(
ResolvePlatformBillingDunningEventDto request,
CancellationToken cancellationToken)
{
return Ok(await service.IgnoreBillingDunningEventAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
}

View File

@@ -0,0 +1,77 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Api.OpenApi;
using Tiku.Application.Auth;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("平台端-平台管理")]
[Authorize(Policy = BackendPermissions.PlatformDashboardView)]
[Produces("application/json")]
[Route("api/platform")]
public sealed class PlatformStaffAccessController(
IPlatformStaffAccessService service,
IAuthAdministrationService authAdministrationService,
PlatformAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("staff")]
[Authorize(Policy = BackendPermissions.PlatformStaffManage)]
[EndpointSummary("查询平台员工列表")]
[ProducesResponseType<PlatformStaffList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformStaffList>> Staff(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await service.GetStaffAsync(actorResolver.Resolve(), query.ToQuery(), cancellationToken));
}
[HttpPut("staff")]
[Authorize(Policy = BackendPermissions.PlatformStaffManage)]
[EndpointSummary("创建或更新平台员工")]
[ProducesResponseType<PlatformStaffItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformStaffItem>> UpsertStaff(
UpsertPlatformStaffDto request,
CancellationToken cancellationToken)
{
return Ok(await service.UpsertStaffAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpPatch("staff/status")]
[Authorize(Policy = BackendPermissions.PlatformStaffManage)]
[EndpointSummary("启用或禁用平台员工")]
[ProducesResponseType<PlatformStaffItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformStaffItem>> StaffStatus(
UpdatePlatformStaffStatusDto request,
CancellationToken cancellationToken)
{
return Ok(await service.UpdateStaffStatusAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpPost("staff/{userId:guid}/password-reset")]
[Authorize(Policy = BackendPermissions.PlatformStaffManage)]
[EndpointSummary("为平台员工设置一次性临时密码")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> ResetStaffPassword(
Guid userId,
AdministrativePasswordResetDto request,
CancellationToken cancellationToken)
{
var actor = actorResolver.Resolve();
await authAdministrationService.ResetPasswordAsync(
new AdministrativePasswordResetRequest(
actor.UserId,
userId,
null,
request.TemporaryPassword,
request.Reason),
cancellationToken);
return NoContent();
}
}

View File

@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Api.OpenApi;
using Tiku.Application.Auth;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("平台端-平台管理")]
[Authorize(Policy = BackendPermissions.PlatformDashboardView)]
[Produces("application/json")]
[Route("api/platform")]
public sealed class PlatformTenantDomainController(
IPlatformTenantDomainService service,
PlatformAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("domains")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询租户域名状态")]
[ProducesResponseType<PlatformDomainList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDomainList>> Domains(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await service.GetDomainsAsync(actorResolver.Resolve(), query.ToQuery(), cancellationToken));
}
[HttpPost("domains/{domainId:guid}/recheck")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("重新触发租户域名 DNS/TLS 验证")]
[ProducesResponseType<PlatformDomainRecheckResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDomainRecheckResult>> RecheckDomain(
Guid domainId,
CancellationToken cancellationToken)
{
return Ok(await service.RecheckDomainAsync(actorResolver.Resolve(), domainId, cancellationToken));
}
}

View File

@@ -0,0 +1,136 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Api.OpenApi;
using Tiku.Application.Auth;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("平台端-平台管理")]
[Authorize(Policy = BackendPermissions.PlatformDashboardView)]
[Produces("application/json")]
[Route("api/platform")]
public sealed class TenantProvisioningAdministrationController(
ITenantProvisioningAdministrationService service,
IPlatformApprovalService approvalService,
PlatformAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("tenants")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询平台租户列表")]
[ProducesResponseType<PlatformTenantList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantList>> Tenants(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await service.GetTenantsAsync(actorResolver.Resolve(), query.ToQuery(), cancellationToken));
}
[HttpPost("tenants")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("创建平台租户")]
[ProducesResponseType<PlatformTenantProvisioningResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantProvisioningResult>> CreateTenant(
CreatePlatformTenantDto request,
[FromHeader(Name = "Idempotency-Key")] [Required]
string idempotencyKey,
CancellationToken cancellationToken)
{
return Ok(await service.CreateTenantAsync(
actorResolver.Resolve(),
request.ToCommand(idempotencyKey),
cancellationToken));
}
[HttpPut("tenants/{tenantId:guid}/primary-domain")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("更正租户主域名")]
public Task<PlatformTenantDomainItem> ReplacePrimaryDomain(
Guid tenantId,
ReplacePlatformPrimaryDomainDto request,
CancellationToken cancellationToken)
{
return service.ReplacePrimaryDomainAsync(actorResolver.Resolve(), request.ToCommand(tenantId),
cancellationToken);
}
[HttpPost("tenants/{tenantId:guid}/owner-activation-links")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("一次性领取租户 Owner 激活链接")]
[EndpointDescription("仅在主域名和试用/订阅有效时签发;幂等重放不会再次返回明文链接。")]
public Task<PlatformOwnerActivationLinkResult> IssueOwnerActivationLink(
Guid tenantId,
IssuePlatformOwnerActivationLinkDto request,
[FromHeader(Name = "Idempotency-Key")] [Required]
string idempotencyKey,
CancellationToken cancellationToken)
{
return service.IssueOwnerActivationLinkAsync(
actorResolver.Resolve(), request.ToCommand(tenantId, idempotencyKey), cancellationToken);
}
[HttpGet("tenants/{tenantId:guid}/billing-policy")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询租户收款策略")]
public Task<TenantBillingPolicyItem> GetBillingPolicy(Guid tenantId, CancellationToken cancellationToken)
{
return service.GetTenantBillingPolicyAsync(actorResolver.Resolve(), tenantId, cancellationToken);
}
[HttpPut("tenants/{tenantId:guid}/billing-policy")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("更新租户收款策略")]
public Task<TenantBillingPolicyItem> UpsertBillingPolicy(
Guid tenantId,
UpsertTenantBillingPolicyDto request,
CancellationToken cancellationToken)
{
return service.UpsertTenantBillingPolicyAsync(actorResolver.Resolve(), request.ToCommand(tenantId),
cancellationToken);
}
[HttpGet("tenants/detail")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询平台租户详情")]
[ProducesResponseType<PlatformTenantDetail>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantDetail>> TenantDetail(
[FromQuery] Guid tenantId,
CancellationToken cancellationToken)
{
return Ok(await service.GetTenantDetailAsync(actorResolver.Resolve(), tenantId, cancellationToken));
}
[HttpPatch("tenants/status")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("更新租户业务状态")]
[PlatformOperationRisk("critical", PlatformApprovalPolicyCodes.TenantArchive)]
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status200OK)]
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status202Accepted)]
public async Task<ActionResult<PlatformCommandSubmission>> TenantStatus(
UpdatePlatformTenantStatusDto request,
[FromHeader(Name = "Idempotency-Key")] [Required]
string idempotencyKey,
CancellationToken cancellationToken)
{
var result = await approvalService.UpdateTenantStatusAsync(actorResolver.Resolve(), request.ToCommand(), idempotencyKey,
cancellationToken);
return result.ExecutionStatus == "pending_approval" ? Accepted(result) : Ok(result);
}
[HttpPut("tenants/billing-profile")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("保存租户账务与开票资料")]
[ProducesResponseType<TenantBillingProfileItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantBillingProfileItem>> BillingProfile(
UpsertPlatformTenantBillingProfileDto request,
CancellationToken cancellationToken)
{
return Ok(await service.UpsertTenantBillingProfileAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
}

View File

@@ -324,10 +324,15 @@ public sealed record RetryPlatformBillingDunningEventCommand(Guid EventId, strin
public sealed record ResolvePlatformBillingDunningEventCommand(Guid EventId, string Reason);
public interface IPlatformAdminService
public interface IPlatformDashboardService
{
Task<PlatformOverview> GetOverviewAsync(PlatformAdminActor actor, CancellationToken cancellationToken = default);
}
public interface ITenantProvisioningAdministrationService
{
Task<PlatformTenantList> GetTenantsAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
@@ -355,12 +360,20 @@ public interface IPlatformAdminService
Task<TenantBillingPolicyItem> UpsertTenantBillingPolicyAsync(PlatformAdminActor actor,
UpsertTenantBillingPolicyCommand command, CancellationToken cancellationToken = default);
}
public interface IPlatformTenantDomainService
{
Task<PlatformDomainList> GetDomainsAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
Task<PlatformDomainRecheckResult> RecheckDomainAsync(PlatformAdminActor actor, Guid domainId,
CancellationToken cancellationToken = default);
}
public interface IPlatformStaffAccessService
{
Task<PlatformStaffList> GetStaffAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
@@ -370,6 +383,10 @@ public interface IPlatformAdminService
Task<PlatformStaffItem> UpdateStaffStatusAsync(PlatformAdminActor actor, UpdatePlatformStaffStatusCommand command,
CancellationToken cancellationToken = default);
}
public interface IPlatformAuditAlertService
{
Task<PlatformAuditLogList> GetAuditLogsAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
@@ -379,6 +396,10 @@ public interface IPlatformAdminService
Task<PlatformAuditAlert> UpdateAuditAlertStatusAsync(PlatformAdminActor actor,
UpdatePlatformAuditAlertStatusCommand command, CancellationToken cancellationToken = default);
}
public interface IPlatformDunningService
{
Task<PlatformBillingDunningChannelList> GetBillingDunningChannelsAsync(PlatformAdminActor actor,
PlatformAdminQuery query, CancellationToken cancellationToken = default);

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,

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,

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,

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,21 +519,21 @@ 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

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,

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,

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,

View File

@@ -35,7 +35,6 @@ public sealed class ArchitectureBoundaryTests
var root = FindRepositoryRoot();
var legacyLineBudgets = new Dictionary<string, int>(StringComparer.Ordinal)
{
["PlatformAdminService"] = 1719,
["ContentManagementService"] = 1096,
["PlatformQuestionBankService"] = 1033,
["CommerceService"] = 983,