diff --git a/Tiku.Api/Configuration/ApiPresentationExtensions.cs b/Tiku.Api/Configuration/ApiPresentationExtensions.cs index 1631c50..0a2193a 100644 --- a/Tiku.Api/Configuration/ApiPresentationExtensions.cs +++ b/Tiku.Api/Configuration/ApiPresentationExtensions.cs @@ -26,6 +26,7 @@ internal static class ApiPresentationExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/Tiku.Api/Controllers/PlatformAdminActorResolver.cs b/Tiku.Api/Controllers/PlatformAdminActorResolver.cs new file mode 100644 index 0000000..cd26d42 --- /dev/null +++ b/Tiku.Api/Controllers/PlatformAdminActorResolver.cs @@ -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); + } +} diff --git a/Tiku.Api/Controllers/PlatformAdminController.cs b/Tiku.Api/Controllers/PlatformAdminController.cs deleted file mode 100644 index ff734c4..0000000 --- a/Tiku.Api/Controllers/PlatformAdminController.cs +++ /dev/null @@ -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(StatusCodes.Status200OK)] - public async Task> Overview(CancellationToken cancellationToken) - { - return Ok(await platformAdminService.GetOverviewAsync(ResolveActor(), cancellationToken)); - } - - [HttpGet("tenants")] - [Authorize(Policy = BackendPermissions.PlatformTenantManage)] - [EndpointSummary("查询平台租户列表")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> Tenants( - [FromQuery] PlatformAdminQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await platformAdminService.GetTenantsAsync(ResolveActor(), query.ToQuery(), cancellationToken)); - } - - [HttpPost("tenants")] - [Authorize(Policy = BackendPermissions.PlatformTenantManage)] - [EndpointSummary("创建平台租户")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> 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 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 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 GetBillingPolicy(Guid tenantId, CancellationToken cancellationToken) - { - return platformAdminService.GetTenantBillingPolicyAsync(ResolveActor(), tenantId, cancellationToken); - } - - [HttpPut("tenants/{tenantId:guid}/billing-policy")] - [Authorize(Policy = BackendPermissions.PlatformTenantManage)] - [EndpointSummary("更新租户收款策略")] - public Task UpsertBillingPolicy( - Guid tenantId, - UpsertTenantBillingPolicyDto request, - CancellationToken cancellationToken) - { - return platformAdminService.UpsertTenantBillingPolicyAsync(ResolveActor(), request.ToCommand(tenantId), - cancellationToken); - } - - [HttpGet("tenants/detail")] - [Authorize(Policy = BackendPermissions.PlatformTenantManage)] - [EndpointSummary("查询平台租户详情")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> 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(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public async Task> 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(StatusCodes.Status200OK)] - public async Task> BillingProfile( - UpsertPlatformTenantBillingProfileDto request, - CancellationToken cancellationToken) - { - return Ok(await platformAdminService.UpsertTenantBillingProfileAsync(ResolveActor(), request.ToCommand(), - cancellationToken)); - } - - [HttpGet("domains")] - [Authorize(Policy = BackendPermissions.PlatformTenantManage)] - [EndpointSummary("查询租户域名状态")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> 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(StatusCodes.Status200OK)] - public async Task> RecheckDomain( - Guid domainId, - CancellationToken cancellationToken) - { - return Ok(await platformAdminService.RecheckDomainAsync(ResolveActor(), domainId, cancellationToken)); - } - - [HttpGet("staff")] - [Authorize(Policy = BackendPermissions.PlatformStaffManage)] - [EndpointSummary("查询平台员工列表")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> Staff( - [FromQuery] PlatformAdminQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await platformAdminService.GetStaffAsync(ResolveActor(), query.ToQuery(), cancellationToken)); - } - - [HttpPut("staff")] - [Authorize(Policy = BackendPermissions.PlatformStaffManage)] - [EndpointSummary("创建或更新平台员工")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpsertStaff( - UpsertPlatformStaffDto request, - CancellationToken cancellationToken) - { - return Ok(await platformAdminService.UpsertStaffAsync(ResolveActor(), request.ToCommand(), cancellationToken)); - } - - [HttpPatch("staff/status")] - [Authorize(Policy = BackendPermissions.PlatformStaffManage)] - [EndpointSummary("启用或禁用平台员工")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> 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 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(StatusCodes.Status200OK)] - public async Task> AuditLogs( - [FromQuery] PlatformAdminQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await platformAdminService.GetAuditLogsAsync(ResolveActor(), query.ToQuery(), cancellationToken)); - } - - [HttpGet("audit-alerts")] - [Authorize(Policy = BackendPermissions.PlatformAuditView)] - [EndpointSummary("查询平台审计告警")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> 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(StatusCodes.Status200OK)] - public async Task> AuditAlertStatus( - UpdatePlatformAuditAlertStatusDto request, - CancellationToken cancellationToken) - { - return Ok(await platformAdminService.UpdateAuditAlertStatusAsync(ResolveActor(), request.ToCommand(), - cancellationToken)); - } - - [HttpGet("saas/dunning/channels")] - [Authorize(Policy = BackendPermissions.PlatformBillingNotification)] - [EndpointSummary("查询平台催缴通知渠道")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> 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(StatusCodes.Status200OK)] - public async Task> 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(StatusCodes.Status200OK)] - public async Task> DisableBillingDunningChannel( - DisablePlatformBillingDunningChannelDto request, - CancellationToken cancellationToken) - { - return Ok(await platformAdminService.DisableBillingDunningChannelAsync(ResolveActor(), request.ToCommand(), - cancellationToken)); - } - - [HttpGet("saas/dunning/events")] - [Authorize(Policy = BackendPermissions.PlatformBillingNotification)] - [EndpointSummary("查询平台催缴通知事件")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> 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(StatusCodes.Status200OK)] - public async Task> 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(StatusCodes.Status200OK)] - public async Task> 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> 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> 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); - } -} \ No newline at end of file diff --git a/Tiku.Api/Controllers/PlatformAuditAlertController.cs b/Tiku.Api/Controllers/PlatformAuditAlertController.cs new file mode 100644 index 0000000..2b42f98 --- /dev/null +++ b/Tiku.Api/Controllers/PlatformAuditAlertController.cs @@ -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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> AuditAlertStatus( + UpdatePlatformAuditAlertStatusDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpdateAuditAlertStatusAsync(actorResolver.Resolve(), request.ToCommand(), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/PlatformDashboardController.cs b/Tiku.Api/Controllers/PlatformDashboardController.cs new file mode 100644 index 0000000..2613483 --- /dev/null +++ b/Tiku.Api/Controllers/PlatformDashboardController.cs @@ -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(StatusCodes.Status200OK)] + public async Task> Overview(CancellationToken cancellationToken) + { + return Ok(await service.GetOverviewAsync(actorResolver.Resolve(), cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/PlatformDunningController.cs b/Tiku.Api/Controllers/PlatformDunningController.cs new file mode 100644 index 0000000..7931b63 --- /dev/null +++ b/Tiku.Api/Controllers/PlatformDunningController.cs @@ -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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> 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> 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> IgnoreBillingDunningEvent( + ResolvePlatformBillingDunningEventDto request, + CancellationToken cancellationToken) + { + return Ok(await service.IgnoreBillingDunningEventAsync(actorResolver.Resolve(), request.ToCommand(), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/PlatformStaffAccessController.cs b/Tiku.Api/Controllers/PlatformStaffAccessController.cs new file mode 100644 index 0000000..edc8405 --- /dev/null +++ b/Tiku.Api/Controllers/PlatformStaffAccessController.cs @@ -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(StatusCodes.Status200OK)] + public async Task> Staff( + [FromQuery] PlatformAdminQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetStaffAsync(actorResolver.Resolve(), query.ToQuery(), cancellationToken)); + } + + [HttpPut("staff")] + [Authorize(Policy = BackendPermissions.PlatformStaffManage)] + [EndpointSummary("创建或更新平台员工")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpsertStaff( + UpsertPlatformStaffDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpsertStaffAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken)); + } + + [HttpPatch("staff/status")] + [Authorize(Policy = BackendPermissions.PlatformStaffManage)] + [EndpointSummary("启用或禁用平台员工")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> 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 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(); + } +} diff --git a/Tiku.Api/Controllers/PlatformTenantDomainController.cs b/Tiku.Api/Controllers/PlatformTenantDomainController.cs new file mode 100644 index 0000000..586bf4d --- /dev/null +++ b/Tiku.Api/Controllers/PlatformTenantDomainController.cs @@ -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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> RecheckDomain( + Guid domainId, + CancellationToken cancellationToken) + { + return Ok(await service.RecheckDomainAsync(actorResolver.Resolve(), domainId, cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/TenantProvisioningAdministrationController.cs b/Tiku.Api/Controllers/TenantProvisioningAdministrationController.cs new file mode 100644 index 0000000..86c66aa --- /dev/null +++ b/Tiku.Api/Controllers/TenantProvisioningAdministrationController.cs @@ -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(StatusCodes.Status200OK)] + public async Task> Tenants( + [FromQuery] PlatformAdminQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetTenantsAsync(actorResolver.Resolve(), query.ToQuery(), cancellationToken)); + } + + [HttpPost("tenants")] + [Authorize(Policy = BackendPermissions.PlatformTenantManage)] + [EndpointSummary("创建平台租户")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> 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 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 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 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 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(StatusCodes.Status200OK)] + public async Task> 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(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public async Task> 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(StatusCodes.Status200OK)] + public async Task> BillingProfile( + UpsertPlatformTenantBillingProfileDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpsertTenantBillingProfileAsync(actorResolver.Resolve(), request.ToCommand(), + cancellationToken)); + } +} diff --git a/Tiku.Application/PlatformAdmin/PlatformAdminModels.cs b/Tiku.Application/PlatformAdmin/PlatformAdminModels.cs index 5ccf781..f665bef 100644 --- a/Tiku.Application/PlatformAdmin/PlatformAdminModels.cs +++ b/Tiku.Application/PlatformAdmin/PlatformAdminModels.cs @@ -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 GetOverviewAsync(PlatformAdminActor actor, CancellationToken cancellationToken = default); +} + +public interface ITenantProvisioningAdministrationService +{ + Task GetTenantsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default); @@ -355,12 +360,20 @@ public interface IPlatformAdminService Task UpsertTenantBillingPolicyAsync(PlatformAdminActor actor, UpsertTenantBillingPolicyCommand command, CancellationToken cancellationToken = default); +} + +public interface IPlatformTenantDomainService +{ Task GetDomainsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default); Task RecheckDomainAsync(PlatformAdminActor actor, Guid domainId, CancellationToken cancellationToken = default); +} + +public interface IPlatformStaffAccessService +{ Task GetStaffAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default); @@ -370,6 +383,10 @@ public interface IPlatformAdminService Task UpdateStaffStatusAsync(PlatformAdminActor actor, UpdatePlatformStaffStatusCommand command, CancellationToken cancellationToken = default); +} + +public interface IPlatformAuditAlertService +{ Task GetAuditLogsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default); @@ -379,6 +396,10 @@ public interface IPlatformAdminService Task UpdateAuditAlertStatusAsync(PlatformAdminActor actor, UpdatePlatformAuditAlertStatusCommand command, CancellationToken cancellationToken = default); +} + +public interface IPlatformDunningService +{ Task GetBillingDunningChannelsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default); @@ -407,4 +428,4 @@ public interface IPlatformAdminService public sealed class PlatformAdminException(string message, string code) : Exception(message) { public string Code { get; } = code; -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Modules/PlatformModule.cs b/Tiku.Infrastructure/Modules/PlatformModule.cs index b716b58..6113ff1 100644 --- a/Tiku.Infrastructure/Modules/PlatformModule.cs +++ b/Tiku.Infrastructure/Modules/PlatformModule.cs @@ -14,7 +14,13 @@ internal static class PlatformModule { internal static IServiceCollection AddPlatformModule(this IServiceCollection services) { - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddOptions(); diff --git a/Tiku.Infrastructure/PlatformAdmin/AuditAndAlerts/PlatformAdminService.AuditAndAlerts.cs b/Tiku.Infrastructure/PlatformAdmin/AuditAndAlerts/PlatformAuditAlertService.cs similarity index 95% rename from Tiku.Infrastructure/PlatformAdmin/AuditAndAlerts/PlatformAdminService.AuditAndAlerts.cs rename to Tiku.Infrastructure/PlatformAdmin/AuditAndAlerts/PlatformAuditAlertService.cs index a607139..978acee 100644 --- a/Tiku.Infrastructure/PlatformAdmin/AuditAndAlerts/PlatformAdminService.AuditAndAlerts.cs +++ b/Tiku.Infrastructure/PlatformAdmin/AuditAndAlerts/PlatformAuditAlertService.cs @@ -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 GetAuditLogsAsync( PlatformAdminActor actor, @@ -91,4 +92,4 @@ internal sealed partial class PlatformAdminService return alert; }, cancellationToken); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/PlatformAdmin/Dashboard/PlatformAdminService.Dashboard.cs b/Tiku.Infrastructure/PlatformAdmin/Dashboard/PlatformDashboardService.cs similarity index 94% rename from Tiku.Infrastructure/PlatformAdmin/Dashboard/PlatformAdminService.Dashboard.cs rename to Tiku.Infrastructure/PlatformAdmin/Dashboard/PlatformDashboardService.cs index 174a9a5..a6ddcec 100644 --- a/Tiku.Infrastructure/PlatformAdmin/Dashboard/PlatformAdminService.Dashboard.cs +++ b/Tiku.Infrastructure/PlatformAdmin/Dashboard/PlatformDashboardService.cs @@ -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 GetOverviewAsync( PlatformAdminActor actor, @@ -50,4 +51,4 @@ internal sealed partial class PlatformAdminService public int QuestionCount { get; init; } public int LearningActiveUserCount { get; init; } } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/PlatformAdmin/Dunning/PlatformAdminService.Dunning.cs b/Tiku.Infrastructure/PlatformAdmin/Dunning/PlatformDunningService.cs similarity index 98% rename from Tiku.Infrastructure/PlatformAdmin/Dunning/PlatformAdminService.Dunning.cs rename to Tiku.Infrastructure/PlatformAdmin/Dunning/PlatformDunningService.cs index 2e3ce6b..c0a8599 100644 --- a/Tiku.Infrastructure/PlatformAdmin/Dunning/PlatformAdminService.Dunning.cs +++ b/Tiku.Infrastructure/PlatformAdmin/Dunning/PlatformDunningService.cs @@ -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 GetBillingDunningChannelsAsync( PlatformAdminActor actor, @@ -224,4 +225,4 @@ internal sealed partial class PlatformAdminService return ToDunningEventItem(item); }, cancellationToken); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdminService.Foundation.cs b/Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdministrationFoundation.Helpers.cs similarity index 84% rename from Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdminService.Foundation.cs rename to Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdministrationFoundation.Helpers.cs index 5c4ae52..9f777b0 100644 --- a/Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdminService.Foundation.cs +++ b/Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdministrationFoundation.Helpers.cs @@ -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 ProvisioningReplayResultAsync( + protected async Task ProvisioningReplayResultAsync( TikuDbContext dbContext, Guid tenantId, CancellationToken cancellationToken) @@ -65,7 +65,7 @@ internal sealed partial class PlatformAdminService true); } - private static async Task OwnerActivationReplayResultAsync( + protected static async Task 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 OwnerActivationStatusAsync( + protected async Task 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 ExecuteSystemAsync( + protected Task ExecuteSystemAsync( string reason, Func> operation, CancellationToken cancellationToken) @@ -109,7 +109,7 @@ internal sealed partial class PlatformAdminService return ExecuteSystemAsync(reason, (_, dbContext) => operation(dbContext), cancellationToken); } - private Task ExecuteSystemAsync( + protected Task ExecuteSystemAsync( string reason, Func> 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 roleCodes) + protected static PlatformStaffItem ToStaffItem(User user, IReadOnlyCollection 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 values, string[] fallback) + protected static string[] NormalizeArray(IReadOnlyCollection 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(status, true, out var value) ? value : throw InvalidStatus(status); } - private static TenantDomainStatus ParseDomainStatus(string status) + protected static TenantDomainStatus ParseDomainStatus(string status) { return Enum.TryParse(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(status, true, out var value) ? value : throw InvalidStatus(status); } - private static PlatformBillingDunningNotificationStatus ParseDunningEventStatus(string status) + protected static PlatformBillingDunningNotificationStatus ParseDunningEventStatus(string status) { return Enum.TryParse(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"); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdministrationFoundation.cs b/Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdministrationFoundation.cs new file mode 100644 index 0000000..9c9f96e --- /dev/null +++ b/Tiku.Infrastructure/PlatformAdmin/Foundation/PlatformAdministrationFoundation.cs @@ -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 ProvisioningOptions, + IOptions 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; +} diff --git a/Tiku.Infrastructure/PlatformAdmin/PlatformAdminService.cs b/Tiku.Infrastructure/PlatformAdmin/PlatformAdminService.cs deleted file mode 100644 index b07ece6..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/PlatformAdminService.cs +++ /dev/null @@ -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 provisioningOptions, - IOptions domainOptions) : IPlatformAdminService -{ - private readonly DomainLifecycleOptions domains = domainOptions.Value; - private readonly TenantProvisioningOptions provisioning = provisioningOptions.Value; -} \ No newline at end of file diff --git a/Tiku.Infrastructure/PlatformAdmin/PlatformApprovalService.cs b/Tiku.Infrastructure/PlatformAdmin/PlatformApprovalService.cs index bd1a324..fda7630 100644 --- a/Tiku.Infrastructure/PlatformAdmin/PlatformApprovalService.cs +++ b/Tiku.Infrastructure/PlatformAdmin/PlatformApprovalService.cs @@ -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(item), cancellationToken), nameof(UpdatePlatformTenantStatusCommand) => await scope.ServiceProvider - .GetRequiredService() + .GetRequiredService() .UpdateTenantStatusAsync(new PlatformAdminActor(item.RequestedBy), Deserialize(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); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/PlatformAdmin/StaffAndAccess/PlatformAdminService.StaffAndAccess.cs b/Tiku.Infrastructure/PlatformAdmin/StaffAndAccess/PlatformStaffAccessService.cs similarity index 97% rename from Tiku.Infrastructure/PlatformAdmin/StaffAndAccess/PlatformAdminService.StaffAndAccess.cs rename to Tiku.Infrastructure/PlatformAdmin/StaffAndAccess/PlatformStaffAccessService.cs index 711d86e..92d242b 100644 --- a/Tiku.Infrastructure/PlatformAdmin/StaffAndAccess/PlatformAdminService.StaffAndAccess.cs +++ b/Tiku.Infrastructure/PlatformAdmin/StaffAndAccess/PlatformStaffAccessService.cs @@ -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 GetStaffAsync( PlatformAdminActor actor, @@ -132,4 +133,4 @@ internal sealed partial class PlatformAdminService return ToStaffItem(user, roleCodes); }, cancellationToken); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/PlatformAdmin/TenantDomains/PlatformAdminService.TenantDomains.cs b/Tiku.Infrastructure/PlatformAdmin/TenantDomains/PlatformTenantDomainService.cs similarity index 94% rename from Tiku.Infrastructure/PlatformAdmin/TenantDomains/PlatformAdminService.TenantDomains.cs rename to Tiku.Infrastructure/PlatformAdmin/TenantDomains/PlatformTenantDomainService.cs index ef95350..05b91e3 100644 --- a/Tiku.Infrastructure/PlatformAdmin/TenantDomains/PlatformAdminService.TenantDomains.cs +++ b/Tiku.Infrastructure/PlatformAdmin/TenantDomains/PlatformTenantDomainService.cs @@ -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 GetDomainsAsync( PlatformAdminActor actor, @@ -64,4 +65,4 @@ internal sealed partial class PlatformAdminService return new PlatformDomainRecheckResult(domain.Id, domain.Status, domain.LastCheckedAt.Value); }, cancellationToken); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/PlatformAdmin/TenantProvisioning/PlatformAdminService.TenantProvisioning.cs b/Tiku.Infrastructure/PlatformAdmin/TenantProvisioning/TenantProvisioningAdministrationService.cs similarity index 99% rename from Tiku.Infrastructure/PlatformAdmin/TenantProvisioning/PlatformAdminService.TenantProvisioning.cs rename to Tiku.Infrastructure/PlatformAdmin/TenantProvisioning/TenantProvisioningAdministrationService.cs index cccbc1c..0d12eb0 100644 --- a/Tiku.Infrastructure/PlatformAdmin/TenantProvisioning/PlatformAdminService.TenantProvisioning.cs +++ b/Tiku.Infrastructure/PlatformAdmin/TenantProvisioning/TenantProvisioningAdministrationService.cs @@ -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 GetTenantsAsync( PlatformAdminActor actor, @@ -584,4 +585,4 @@ internal sealed partial class PlatformAdminService return ToBillingPolicyItem(policy); }, cancellationToken); } -} \ No newline at end of file +} diff --git a/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs b/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs index c081139..e9fca64 100644 --- a/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs +++ b/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs @@ -35,7 +35,6 @@ public sealed class ArchitectureBoundaryTests var root = FindRepositoryRoot(); var legacyLineBudgets = new Dictionary(StringComparer.Ordinal) { - ["PlatformAdminService"] = 1719, ["ContentManagementService"] = 1096, ["PlatformQuestionBankService"] = 1033, ["CommerceService"] = 983,