refactor(tenant-admin): split capability services

This commit is contained in:
2026-08-04 09:17:35 +08:00
parent 4f8e216282
commit 1b867cfa3a
28 changed files with 1008 additions and 765 deletions

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Tiku.Api.Controllers;
using Tiku.Api.OpenApi;
using Tiku.Api.Security;
@@ -21,6 +22,7 @@ internal static class ApiPresentationExtensions
options.AddOperationTransformer<AuthenticationOperationTagsTransformer>();
});
services.AddProblemDetails();
services.AddScoped<TenantAdminActorResolver>();
return services;
}

View File

@@ -0,0 +1,21 @@
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
public sealed class TenantAdminActorResolver(
ICurrentUser currentUser,
ITenantContext currentTenant)
{
internal TenantAdminActor Resolve()
{
try
{
return TenantAdminActor.FromResolvedIdentity(currentTenant.TenantId, currentUser.UserId);
}
catch (InvalidOperationException exception)
{
throw new TenantAdminDirectException(exception.Message, "tenant_admin_access_denied");
}
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantAdminDashboardController(
ITenantAdminDashboardService dashboardService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("overview")]
[Authorize(Policy = BackendPermissions.TenantDashboardView)]
[EndpointSummary("查询租户后台运营概览")]
[ProducesResponseType<TenantAdminOverviewItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminOverviewItem>> GetOverview(CancellationToken cancellationToken)
{
return Ok(await dashboardService.GetOverviewAsync(actorResolver.Resolve(), cancellationToken));
}
}

View File

@@ -1,568 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantAdminDirectController(
ITenantAdminDirectService tenantAdminService,
IAuthAdministrationService authAdministrationService,
ICurrentUser currentUser,
ITenantContext currentTenant) : ControllerBase
{
[HttpGet("overview")]
[Authorize(Policy = BackendPermissions.TenantDashboardView)]
[EndpointSummary("查询租户后台运营概览")]
[ProducesResponseType<TenantAdminOverviewItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminOverviewItem>> GetOverview(CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetOverviewAsync(ResolveActor(), cancellationToken));
}
[HttpGet("classes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户班级")]
[ProducesResponseType<TenantAdminClassList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminClassList>> GetClasses(
[FromQuery] TenantAdminClassQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetClassesAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("classes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户班级")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassItem>>> UpsertClass(
UpsertTenantAdminClassDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertClassAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("classes/disable")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("停用租户班级")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassItem>>> DisableClass(
[FromBody] Guid classId,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.DisableClassAsync(ResolveActor(), classId, cancellationToken));
}
[HttpGet("classes/members")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询班级成员")]
[ProducesResponseType<CatalogList<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminClassMemberItem>>> GetClassMembers(
[FromQuery] TenantAdminClassMemberQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetClassMembersAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("classes/members")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新班级成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassMemberItem>>> UpsertClassMember(
UpsertTenantAdminClassMemberDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertClassMemberAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpPost("classes/members/remove")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("移除班级成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassMemberItem>>> RemoveClassMember(
RemoveTenantAdminClassMemberDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.RemoveClassMemberAsync(ResolveActor(), request.ClassMemberId,
cancellationToken));
}
[HttpGet("students")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户学生")]
[ProducesResponseType<TenantAdminStudentList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminStudentList>> GetStudents(
[FromQuery] TenantAdminStudentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetStudentsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("students")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户学生档案")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentItem>>> UpsertStudent(
UpsertTenantAdminStudentDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertStudentAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("students/status")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("更新租户学生状态")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentStatusItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentStatusItem>>> UpdateStudentStatus(
UpdateTenantAdminStudentStatusDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpdateStudentStatusAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpPost("students/import/preview")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("预览学生批量导入")]
[ProducesResponseType<TenantAdminStudentImportPreview>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminStudentImportPreview>> PreviewStudentImport(
TenantAdminStudentImportDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.PreviewStudentImportAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpPost("students/import")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("提交学生批量导入")]
[ProducesResponseType<TenantAdminStudentImportResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminStudentImportResult>> ImportStudents(
TenantAdminStudentImportDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.ImportStudentsAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("students/bulk-assign-class")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("批量分班")]
[ProducesResponseType<TenantAdminBulkOperationResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminBulkOperationResult>> BulkAssignClass(
TenantAdminBulkAssignClassDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.BulkAssignClassAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpPost("students/bulk-status")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("批量调整学生状态")]
[ProducesResponseType<TenantAdminBulkOperationResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminBulkOperationResult>> BulkStudentStatus(
TenantAdminBulkStatusDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.BulkUpdateStudentStatusAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("students/supervision/rules")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学习督导规则")]
[ProducesResponseType<CatalogList<TenantSupervisionRuleItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantSupervisionRuleItem>>> SupervisionRules(
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetSupervisionRulesAsync(ResolveActor(), cancellationToken));
}
[HttpPut("students/supervision/rules")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新学习督导规则")]
[ProducesResponseType<ContentManagementResult<TenantSupervisionRuleItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantSupervisionRuleItem>>> UpsertSupervisionRule(
UpsertTenantSupervisionRuleDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertSupervisionRuleAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("students/supervision/preview")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("预览学习风险学生")]
[ProducesResponseType<TenantSupervisionPreview>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantSupervisionPreview>> PreviewSupervision(CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.PreviewSupervisionAsync(ResolveActor(), cancellationToken));
}
[HttpPost("students/supervision/generate")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("批量生成学习督导跟进任务")]
[ProducesResponseType<TenantSupervisionGenerateResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantSupervisionGenerateResult>> GenerateSupervision(
TenantSupervisionGenerateDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GenerateSupervisionFollowupsAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("student-followups/report")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学生跟进统计报表")]
[ProducesResponseType<TenantFollowupReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantFollowupReport>> FollowupReport(CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetFollowupReportAsync(ResolveActor(), cancellationToken));
}
[HttpGet("feedbacks/report")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询反馈处理统计报表")]
[ProducesResponseType<TenantFeedbackReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantFeedbackReport>> FeedbackReport(CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetFeedbackReportAsync(ResolveActor(), cancellationToken));
}
[HttpGet("points/risk-report")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询积分风险统计报表")]
[ProducesResponseType<TenantPointRiskReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantPointRiskReport>> PointRiskReport(CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetPointRiskReportAsync(ResolveActor(), cancellationToken));
}
[HttpGet("student-notes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学生备注")]
[ProducesResponseType<CatalogList<TenantAdminStudentNoteItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminStudentNoteItem>>> GetStudentNotes(
[FromQuery] TenantAdminStudentActivityQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetStudentNotesAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("student-notes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新学生备注")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentNoteItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentNoteItem>>> UpsertStudentNote(
UpsertTenantAdminStudentNoteDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertStudentNoteAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("student-followups")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学生跟进")]
[ProducesResponseType<CatalogList<TenantAdminStudentFollowupItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminStudentFollowupItem>>> GetStudentFollowups(
[FromQuery] TenantAdminStudentActivityQueryDto query,
CancellationToken cancellationToken)
{
return Ok(
await tenantAdminService.GetStudentFollowupsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("student-followups")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新学生跟进")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentFollowupItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentFollowupItem>>> UpsertStudentFollowup(
UpsertTenantAdminStudentFollowupDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertStudentFollowupAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("members")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("查询租户成员")]
[ProducesResponseType<CatalogList<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminMemberItem>>> GetMembers(
[FromQuery] TenantAdminMemberQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetMembersAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("members")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("新增或更新租户成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminMemberItem>>> UpsertMember(
UpsertTenantAdminMemberDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertMemberAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("members/disable")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("停用租户成员并撤销会话")]
[ProducesResponseType<ContentManagementResult<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminMemberItem>>> DisableMember(
DisableTenantAdminMemberDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.DisableMemberAsync(ResolveActor(), request.MembershipId, cancellationToken));
}
[HttpPost("members/{userId:guid}/password-reset")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("为租户成员设置一次性临时密码")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> ResetMemberPassword(
Guid userId,
AdministrativePasswordResetDto request,
CancellationToken cancellationToken)
{
var actor = ResolveActor();
await authAdministrationService.ResetPasswordAsync(
new AdministrativePasswordResetRequest(
actor.UserId,
userId,
actor.TenantId,
request.TemporaryPassword,
request.Reason),
cancellationToken);
return NoContent();
}
[HttpGet("audit-logs")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("查询租户审计日志")]
[ProducesResponseType<CatalogList<TenantAdminAuditLogItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminAuditLogItem>>> GetAuditLogs(
[FromQuery] TenantAdminAuditLogQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetAuditLogsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("branding")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("更新租户品牌信息")]
[ProducesResponseType<ContentManagementResult<TenantBrandingItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantBrandingItem>>> UpsertBranding(
UpsertTenantBrandingDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertBrandingAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPut("settings")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("更新租户公开设置与功能开关")]
[ProducesResponseType<ContentManagementResult<TenantSettingsItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantSettingsItem>>> UpsertSettings(
UpsertTenantSettingsDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertSettingsAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("theme-templates")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询可用租户主题模板")]
[ProducesResponseType<CatalogList<TenantThemeTemplateItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantThemeTemplateItem>>> GetThemeTemplates(
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetThemeTemplatesAsync(ResolveActor(), cancellationToken));
}
[HttpGet("theme")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询租户当前主题与草稿")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> GetTheme(
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetThemeAsync(ResolveActor(), cancellationToken));
}
[HttpPost("theme/preview")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("生成租户主题草稿")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> PreviewTheme(
PreviewTenantThemeDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.PreviewThemeAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("theme/publish")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("发布租户主题")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> PublishTheme(
PublishTenantThemeDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.PublishThemeAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("domains")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询租户域名")]
[ProducesResponseType<CatalogList<TenantDomainItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantDomainItem>>> GetDomains(CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetDomainsAsync(ResolveActor(), cancellationToken));
}
[HttpPost("domains")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("添加租户域名")]
[ProducesResponseType<ContentManagementResult<TenantDomainItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantDomainItem>>> CreateDomain(
CreateTenantDomainDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.CreateDomainAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("auth-providers")]
[Authorize(Policy = BackendPermissions.TenantProviderManage)]
[EndpointSummary("查询租户登录 Provider 公开配置")]
[ProducesResponseType<CatalogList<TenantIdentityProviderItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantIdentityProviderItem>>> GetAuthProviders(
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetAuthProvidersAsync(ResolveActor(), cancellationToken));
}
[HttpPut("auth-providers")]
[Authorize(Policy = BackendPermissions.TenantProviderManage)]
[EndpointSummary("新增或更新租户登录 Provider")]
[ProducesResponseType<ContentManagementResult<TenantIdentityProviderItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantIdentityProviderItem>>> UpsertAuthProvider(
UpsertTenantIdentityProviderDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertAuthProviderAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("badges")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户勋章")]
[ProducesResponseType<CatalogList<TenantAdminBadgeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminBadgeItem>>> GetBadges(
[FromQuery] TenantAdminBadgeQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetBadgesAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("badges")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户勋章")]
[ProducesResponseType<ContentManagementResult<TenantAdminBadgeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminBadgeItem>>> UpsertBadge(
UpsertTenantAdminBadgeDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertBadgeAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("badge-grants")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询勋章发放记录")]
[ProducesResponseType<CatalogList<TenantAdminBadgeGrantItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminBadgeGrantItem>>> GetBadgeGrants(
[FromQuery] TenantAdminBadgeGrantQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetBadgeGrantsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPost("badge-grants")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("向租户成员发放勋章")]
[ProducesResponseType<ContentManagementResult<TenantAdminBadgeGrantItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminBadgeGrantItem>>> GrantBadge(
GrantTenantAdminBadgeDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GrantBadgeAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("notifications")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询用户站内通知")]
[ProducesResponseType<CatalogList<TenantAdminNotificationItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminNotificationItem>>> GetNotifications(
[FromQuery] TenantAdminNotificationQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetNotificationsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("notifications")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新用户站内通知")]
[ProducesResponseType<ContentManagementResult<TenantAdminNotificationItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminNotificationItem>>> UpsertNotification(
UpsertTenantAdminNotificationDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertNotificationAsync(ResolveActor(), request.ToCommand(),
cancellationToken));
}
[HttpGet("feedbacks")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询用户反馈")]
[ProducesResponseType<CatalogList<TenantAdminFeedbackItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminFeedbackItem>>> GetFeedbacks(
[FromQuery] TenantAdminFeedbackQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetFeedbacksAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPost("feedbacks/status")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("处理用户反馈")]
[ProducesResponseType<ContentManagementResult<TenantAdminFeedbackItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminFeedbackItem>>> UpdateFeedback(
UpdateTenantAdminFeedbackDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpdateFeedbackAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
private TenantAdminActor ResolveActor()
{
try
{
return TenantAdminActor.FromResolvedIdentity(currentTenant.TenantId, currentUser.UserId);
}
catch (InvalidOperationException exception)
{
throw new TenantAdminDirectException(exception.Message, "tenant_admin_access_denied");
}
}
}

View File

@@ -0,0 +1,88 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantClassController(
ITenantClassService classService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("classes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户班级")]
[ProducesResponseType<TenantAdminClassList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminClassList>> GetClasses(
[FromQuery] TenantAdminClassQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await classService.GetClassesAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("classes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户班级")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassItem>>> UpsertClass(
UpsertTenantAdminClassDto request,
CancellationToken cancellationToken)
{
return Ok(await classService.UpsertClassAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpPost("classes/disable")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("停用租户班级")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassItem>>> DisableClass(
[FromBody] Guid classId,
CancellationToken cancellationToken)
{
return Ok(await classService.DisableClassAsync(actorResolver.Resolve(), classId, cancellationToken));
}
[HttpGet("classes/members")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询班级成员")]
[ProducesResponseType<CatalogList<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminClassMemberItem>>> GetClassMembers(
[FromQuery] TenantAdminClassMemberQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await classService.GetClassMembersAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("classes/members")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新班级成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassMemberItem>>> UpsertClassMember(
UpsertTenantAdminClassMemberDto request,
CancellationToken cancellationToken)
{
return Ok(await classService.UpsertClassMemberAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpPost("classes/members/remove")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("移除班级成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassMemberItem>>> RemoveClassMember(
RemoveTenantAdminClassMemberDto request,
CancellationToken cancellationToken)
{
return Ok(await classService.RemoveClassMemberAsync(actorResolver.Resolve(), request.ClassMemberId,
cancellationToken));
}
}

View File

@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantDomainProviderController(
ITenantDomainProviderService domainProviderService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("domains")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询租户域名")]
[ProducesResponseType<CatalogList<TenantDomainItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantDomainItem>>> GetDomains(CancellationToken cancellationToken)
{
return Ok(await domainProviderService.GetDomainsAsync(actorResolver.Resolve(), cancellationToken));
}
[HttpPost("domains")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("添加租户域名")]
[ProducesResponseType<ContentManagementResult<TenantDomainItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantDomainItem>>> CreateDomain(
CreateTenantDomainDto request,
CancellationToken cancellationToken)
{
return Ok(await domainProviderService.CreateDomainAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpGet("auth-providers")]
[Authorize(Policy = BackendPermissions.TenantProviderManage)]
[EndpointSummary("查询租户登录 Provider 公开配置")]
[ProducesResponseType<CatalogList<TenantIdentityProviderItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantIdentityProviderItem>>> GetAuthProviders(
CancellationToken cancellationToken)
{
return Ok(await domainProviderService.GetAuthProvidersAsync(actorResolver.Resolve(), cancellationToken));
}
[HttpPut("auth-providers")]
[Authorize(Policy = BackendPermissions.TenantProviderManage)]
[EndpointSummary("新增或更新租户登录 Provider")]
[ProducesResponseType<ContentManagementResult<TenantIdentityProviderItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantIdentityProviderItem>>> UpsertAuthProvider(
UpsertTenantIdentityProviderDto request,
CancellationToken cancellationToken)
{
return Ok(await domainProviderService.UpsertAuthProviderAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
}

View File

@@ -0,0 +1,109 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantEngagementController(
ITenantEngagementService engagementService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("badges")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户勋章")]
[ProducesResponseType<CatalogList<TenantAdminBadgeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminBadgeItem>>> GetBadges(
[FromQuery] TenantAdminBadgeQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await engagementService.GetBadgesAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("badges")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户勋章")]
[ProducesResponseType<ContentManagementResult<TenantAdminBadgeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminBadgeItem>>> UpsertBadge(
UpsertTenantAdminBadgeDto request,
CancellationToken cancellationToken)
{
return Ok(await engagementService.UpsertBadgeAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpGet("badge-grants")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询勋章发放记录")]
[ProducesResponseType<CatalogList<TenantAdminBadgeGrantItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminBadgeGrantItem>>> GetBadgeGrants(
[FromQuery] TenantAdminBadgeGrantQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await engagementService.GetBadgeGrantsAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPost("badge-grants")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("向租户成员发放勋章")]
[ProducesResponseType<ContentManagementResult<TenantAdminBadgeGrantItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminBadgeGrantItem>>> GrantBadge(
GrantTenantAdminBadgeDto request,
CancellationToken cancellationToken)
{
return Ok(await engagementService.GrantBadgeAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpGet("notifications")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询用户站内通知")]
[ProducesResponseType<CatalogList<TenantAdminNotificationItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminNotificationItem>>> GetNotifications(
[FromQuery] TenantAdminNotificationQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await engagementService.GetNotificationsAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("notifications")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新用户站内通知")]
[ProducesResponseType<ContentManagementResult<TenantAdminNotificationItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminNotificationItem>>> UpsertNotification(
UpsertTenantAdminNotificationDto request,
CancellationToken cancellationToken)
{
return Ok(await engagementService.UpsertNotificationAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpGet("feedbacks")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询用户反馈")]
[ProducesResponseType<CatalogList<TenantAdminFeedbackItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminFeedbackItem>>> GetFeedbacks(
[FromQuery] TenantAdminFeedbackQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await engagementService.GetFeedbacksAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPost("feedbacks/status")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("处理用户反馈")]
[ProducesResponseType<ContentManagementResult<TenantAdminFeedbackItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminFeedbackItem>>> UpdateFeedback(
UpdateTenantAdminFeedbackDto request,
CancellationToken cancellationToken)
{
return Ok(await engagementService.UpdateFeedbackAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
}

View File

@@ -0,0 +1,74 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantMemberAccessController(
ITenantMemberAccessService memberAccessService,
IAuthAdministrationService authAdministrationService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("members")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("查询租户成员")]
[ProducesResponseType<CatalogList<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminMemberItem>>> GetMembers(
[FromQuery] TenantAdminMemberQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await memberAccessService.GetMembersAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("members")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("新增或更新租户成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminMemberItem>>> UpsertMember(
UpsertTenantAdminMemberDto request,
CancellationToken cancellationToken)
{
return Ok(await memberAccessService.UpsertMemberAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpPost("members/disable")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("停用租户成员并撤销会话")]
[ProducesResponseType<ContentManagementResult<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminMemberItem>>> DisableMember(
DisableTenantAdminMemberDto request,
CancellationToken cancellationToken)
{
return Ok(await memberAccessService.DisableMemberAsync(actorResolver.Resolve(), request.MembershipId, cancellationToken));
}
[HttpPost("members/{userId:guid}/password-reset")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("为租户成员设置一次性临时密码")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> ResetMemberPassword(
Guid userId,
AdministrativePasswordResetDto request,
CancellationToken cancellationToken)
{
var actor = actorResolver.Resolve();
await authAdministrationService.ResetPasswordAsync(
new AdministrativePasswordResetRequest(
actor.UserId,
userId,
actor.TenantId,
request.TemporaryPassword,
request.Reason),
cancellationToken);
return NoContent();
}
}

View File

@@ -0,0 +1,95 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantSiteSettingsController(
ITenantSiteSettingsService siteSettingsService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("audit-logs")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("查询租户审计日志")]
[ProducesResponseType<CatalogList<TenantAdminAuditLogItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminAuditLogItem>>> GetAuditLogs(
[FromQuery] TenantAdminAuditLogQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await siteSettingsService.GetAuditLogsAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("branding")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("更新租户品牌信息")]
[ProducesResponseType<ContentManagementResult<TenantBrandingItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantBrandingItem>>> UpsertBranding(
UpsertTenantBrandingDto request,
CancellationToken cancellationToken)
{
return Ok(await siteSettingsService.UpsertBrandingAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpPut("settings")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("更新租户公开设置与功能开关")]
[ProducesResponseType<ContentManagementResult<TenantSettingsItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantSettingsItem>>> UpsertSettings(
UpsertTenantSettingsDto request,
CancellationToken cancellationToken)
{
return Ok(await siteSettingsService.UpsertSettingsAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpGet("theme-templates")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询可用租户主题模板")]
[ProducesResponseType<CatalogList<TenantThemeTemplateItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantThemeTemplateItem>>> GetThemeTemplates(
CancellationToken cancellationToken)
{
return Ok(await siteSettingsService.GetThemeTemplatesAsync(actorResolver.Resolve(), cancellationToken));
}
[HttpGet("theme")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询租户当前主题与草稿")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> GetTheme(
CancellationToken cancellationToken)
{
return Ok(await siteSettingsService.GetThemeAsync(actorResolver.Resolve(), cancellationToken));
}
[HttpPost("theme/preview")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("生成租户主题草稿")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> PreviewTheme(
PreviewTenantThemeDto request,
CancellationToken cancellationToken)
{
return Ok(await siteSettingsService.PreviewThemeAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpPost("theme/publish")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("发布租户主题")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> PublishTheme(
PublishTenantThemeDto request,
CancellationToken cancellationToken)
{
return Ok(await siteSettingsService.PublishThemeAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
}

View File

@@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantStudentController(
ITenantStudentService studentService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("students")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户学生")]
[ProducesResponseType<TenantAdminStudentList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminStudentList>> GetStudents(
[FromQuery] TenantAdminStudentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await studentService.GetStudentsAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("students")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户学生档案")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentItem>>> UpsertStudent(
UpsertTenantAdminStudentDto request,
CancellationToken cancellationToken)
{
return Ok(await studentService.UpsertStudentAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpPost("students/status")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("更新租户学生状态")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentStatusItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentStatusItem>>> UpdateStudentStatus(
UpdateTenantAdminStudentStatusDto request,
CancellationToken cancellationToken)
{
return Ok(await studentService.UpdateStudentStatusAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpPost("students/import/preview")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("预览学生批量导入")]
[ProducesResponseType<TenantAdminStudentImportPreview>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminStudentImportPreview>> PreviewStudentImport(
TenantAdminStudentImportDto request,
CancellationToken cancellationToken)
{
return Ok(await studentService.PreviewStudentImportAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpPost("students/import")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("提交学生批量导入")]
[ProducesResponseType<TenantAdminStudentImportResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminStudentImportResult>> ImportStudents(
TenantAdminStudentImportDto request,
CancellationToken cancellationToken)
{
return Ok(await studentService.ImportStudentsAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
}
[HttpPost("students/bulk-assign-class")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("批量分班")]
[ProducesResponseType<TenantAdminBulkOperationResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminBulkOperationResult>> BulkAssignClass(
TenantAdminBulkAssignClassDto request,
CancellationToken cancellationToken)
{
return Ok(await studentService.BulkAssignClassAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpPost("students/bulk-status")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("批量调整学生状态")]
[ProducesResponseType<TenantAdminBulkOperationResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminBulkOperationResult>> BulkStudentStatus(
TenantAdminBulkStatusDto request,
CancellationToken cancellationToken)
{
return Ok(await studentService.BulkUpdateStudentStatusAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
}

View File

@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantStudentEngagementController(
ITenantStudentEngagementService studentEngagementService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("student-notes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学生备注")]
[ProducesResponseType<CatalogList<TenantAdminStudentNoteItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminStudentNoteItem>>> GetStudentNotes(
[FromQuery] TenantAdminStudentActivityQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await studentEngagementService.GetStudentNotesAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("student-notes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新学生备注")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentNoteItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentNoteItem>>> UpsertStudentNote(
UpsertTenantAdminStudentNoteDto request,
CancellationToken cancellationToken)
{
return Ok(await studentEngagementService.UpsertStudentNoteAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpGet("student-followups")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学生跟进")]
[ProducesResponseType<CatalogList<TenantAdminStudentFollowupItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminStudentFollowupItem>>> GetStudentFollowups(
[FromQuery] TenantAdminStudentActivityQueryDto query,
CancellationToken cancellationToken)
{
return Ok(
await studentEngagementService.GetStudentFollowupsAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
}
[HttpPut("student-followups")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新学生跟进")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentFollowupItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentFollowupItem>>> UpsertStudentFollowup(
UpsertTenantAdminStudentFollowupDto request,
CancellationToken cancellationToken)
{
return Ok(await studentEngagementService.UpsertStudentFollowupAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
}

View File

@@ -0,0 +1,90 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("租户端-运营管理")]
[Produces("application/json")]
[Route("api/tenant")]
public sealed class TenantSupervisionController(
ITenantSupervisionService supervisionService,
TenantAdminActorResolver actorResolver) : ControllerBase
{
[HttpGet("students/supervision/rules")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学习督导规则")]
[ProducesResponseType<CatalogList<TenantSupervisionRuleItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantSupervisionRuleItem>>> SupervisionRules(
CancellationToken cancellationToken)
{
return Ok(await supervisionService.GetSupervisionRulesAsync(actorResolver.Resolve(), cancellationToken));
}
[HttpPut("students/supervision/rules")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新学习督导规则")]
[ProducesResponseType<ContentManagementResult<TenantSupervisionRuleItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantSupervisionRuleItem>>> UpsertSupervisionRule(
UpsertTenantSupervisionRuleDto request,
CancellationToken cancellationToken)
{
return Ok(await supervisionService.UpsertSupervisionRuleAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpGet("students/supervision/preview")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("预览学习风险学生")]
[ProducesResponseType<TenantSupervisionPreview>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantSupervisionPreview>> PreviewSupervision(CancellationToken cancellationToken)
{
return Ok(await supervisionService.PreviewSupervisionAsync(actorResolver.Resolve(), cancellationToken));
}
[HttpPost("students/supervision/generate")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("批量生成学习督导跟进任务")]
[ProducesResponseType<TenantSupervisionGenerateResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantSupervisionGenerateResult>> GenerateSupervision(
TenantSupervisionGenerateDto request,
CancellationToken cancellationToken)
{
return Ok(await supervisionService.GenerateSupervisionFollowupsAsync(actorResolver.Resolve(), request.ToCommand(),
cancellationToken));
}
[HttpGet("student-followups/report")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学生跟进统计报表")]
[ProducesResponseType<TenantFollowupReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantFollowupReport>> FollowupReport(CancellationToken cancellationToken)
{
return Ok(await supervisionService.GetFollowupReportAsync(actorResolver.Resolve(), cancellationToken));
}
[HttpGet("feedbacks/report")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询反馈处理统计报表")]
[ProducesResponseType<TenantFeedbackReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantFeedbackReport>> FeedbackReport(CancellationToken cancellationToken)
{
return Ok(await supervisionService.GetFeedbackReportAsync(actorResolver.Resolve(), cancellationToken));
}
[HttpGet("points/risk-report")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询积分风险统计报表")]
[ProducesResponseType<TenantPointRiskReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantPointRiskReport>> PointRiskReport(CancellationToken cancellationToken)
{
return Ok(await supervisionService.GetPointRiskReportAsync(actorResolver.Resolve(), cancellationToken));
}
}

View File

@@ -3,12 +3,15 @@ using Tiku.Application.Content;
namespace Tiku.Application.TenantAdmin;
public interface ITenantAdminDirectService
public interface ITenantAdminDashboardService
{
Task<TenantAdminOverviewItem> GetOverviewAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
}
public interface ITenantClassService
{
Task<TenantAdminClassList> GetClassesAsync(
TenantAdminActor actor,
TenantAdminClassFilter filter,
@@ -38,7 +41,10 @@ public interface ITenantAdminDirectService
TenantAdminActor actor,
Guid classMemberId,
CancellationToken cancellationToken = default);
}
public interface ITenantStudentService
{
Task<TenantAdminStudentList> GetStudentsAsync(
TenantAdminActor actor,
TenantAdminStudentFilter filter,
@@ -73,7 +79,10 @@ public interface ITenantAdminDirectService
TenantAdminActor actor,
TenantAdminBulkStatusCommand command,
CancellationToken cancellationToken = default);
}
public interface ITenantSupervisionService
{
Task<CatalogList<TenantSupervisionRuleItem>> GetSupervisionRulesAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
@@ -103,7 +112,10 @@ public interface ITenantAdminDirectService
Task<TenantPointRiskReport> GetPointRiskReportAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
}
public interface ITenantStudentEngagementService
{
Task<CatalogList<TenantAdminStudentNoteItem>> GetStudentNotesAsync(
TenantAdminActor actor,
TenantAdminStudentActivityFilter filter,
@@ -123,7 +135,10 @@ public interface ITenantAdminDirectService
TenantAdminActor actor,
UpsertTenantAdminStudentFollowupCommand command,
CancellationToken cancellationToken = default);
}
public interface ITenantMemberAccessService
{
Task<CatalogList<TenantAdminMemberItem>> GetMembersAsync(
TenantAdminActor actor,
TenantAdminMemberFilter filter,
@@ -138,7 +153,10 @@ public interface ITenantAdminDirectService
TenantAdminActor actor,
Guid membershipId,
CancellationToken cancellationToken = default);
}
public interface ITenantSiteSettingsService
{
Task<CatalogList<TenantAdminAuditLogItem>> GetAuditLogsAsync(
TenantAdminActor actor,
TenantAdminAuditLogFilter filter,
@@ -171,7 +189,10 @@ public interface ITenantAdminDirectService
TenantAdminActor actor,
PublishTenantThemeCommand command,
CancellationToken cancellationToken = default);
}
public interface ITenantDomainProviderService
{
Task<CatalogList<TenantDomainItem>> GetDomainsAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
@@ -189,7 +210,10 @@ public interface ITenantAdminDirectService
TenantAdminActor actor,
UpsertTenantIdentityProviderCommand command,
CancellationToken cancellationToken = default);
}
public interface ITenantEngagementService
{
Task<CatalogList<TenantAdminBadgeItem>> GetBadgesAsync(
TenantAdminActor actor,
TenantAdminBadgeFilter filter,
@@ -229,4 +253,4 @@ public interface ITenantAdminDirectService
TenantAdminActor actor,
UpdateTenantAdminFeedbackCommand command,
CancellationToken cancellationToken = default);
}
}

View File

@@ -12,7 +12,16 @@ internal static class TenantAdminModule
{
internal static IServiceCollection AddTenantAdminModule(this IServiceCollection services)
{
services.AddScoped<ITenantAdminDirectService, TenantAdminDirectService>();
services.AddScoped<TenantAdminServiceDependencies>();
services.AddScoped<ITenantAdminDashboardService, TenantAdminDashboardService>();
services.AddScoped<ITenantClassService, TenantClassService>();
services.AddScoped<ITenantStudentService, TenantStudentService>();
services.AddScoped<ITenantSupervisionService, TenantSupervisionService>();
services.AddScoped<ITenantMemberAccessService, TenantMemberAccessService>();
services.AddScoped<ITenantSiteSettingsService, TenantSiteSettingsService>();
services.AddScoped<ITenantDomainProviderService, TenantDomainProviderService>();
services.AddScoped<ITenantEngagementService, TenantEngagementService>();
services.AddScoped<ITenantStudentEngagementService, TenantStudentEngagementService>();
services.AddScoped<ITenantOnboardingService, TenantOnboardingService>();
services.AddScoped<ITenantLifecycleService, TenantLifecycleService>();
services.AddScoped<IBackgroundJobHandler, StatisticsAggregationJobHandler>();

View File

@@ -10,7 +10,8 @@ using Tiku.Infrastructure.Security;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal sealed class TenantClassService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantClassService
{
public async Task<TenantAdminClassList> GetClassesAsync(
TenantAdminActor actor,
@@ -261,4 +262,4 @@ public sealed partial class TenantAdminDirectService
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantAdminClassMemberItem>(ToClassMemberItem(item, ToUserSummary(user)));
}
}
}

View File

@@ -9,7 +9,8 @@ using ReconciliationIssueStatus = Tiku.Domain.Commerce.ReconciliationIssueStatus
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal sealed class TenantAdminDashboardService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantAdminDashboardService
{
public async Task<TenantAdminOverviewItem> GetOverviewAsync(
TenantAdminActor actor,
@@ -98,4 +99,4 @@ public sealed partial class TenantAdminDirectService
openReconciliationIssueCount,
now);
}
}
}

View File

@@ -0,0 +1,111 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Notifications;
using Tiku.Application.Tenancy;
using Tiku.Application.TenantAdmin;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Security;
using Tiku.Infrastructure.Tenancy;
namespace Tiku.Infrastructure.TenantAdmin;
internal sealed class TenantDomainProviderService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantDomainProviderService
{
public async Task<CatalogList<TenantDomainItem>> GetDomainsAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default)
{
await RequireAllDataScopeAsync(actor, cancellationToken);
var items = await dbContext.TenantDomains.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId)
.OrderByDescending(item => item.IsPrimary)
.ThenBy(item => item.CreatedAt)
.Select(item => ToDomainItem(item))
.ToArrayAsync(cancellationToken);
return new CatalogList<TenantDomainItem>(items);
}
public async Task<ContentManagementResult<TenantDomainItem>> CreateDomainAsync(
TenantAdminActor actor,
CreateTenantDomainCommand command,
CancellationToken cancellationToken = default)
{
await RequireAllDataScopeAsync(actor, cancellationToken);
TenantDomain generated;
try
{
generated = TenantDomainProvisioning.CreatePrimary(actor.TenantId, command.Host);
}
catch (ArgumentException exception)
{
throw new TenantAdminDirectException(exception.Message, "invalid_domain_host");
}
var host = generated.Host;
if (command.IsPrimary)
{
var primaryDomains = await dbContext.TenantDomains
.Where(item => item.TenantId == actor.TenantId && item.IsPrimary)
.ToArrayAsync(cancellationToken);
foreach (var domain in primaryDomains) domain.IsPrimary = false;
}
var item = new TenantDomain
{
TenantId = actor.TenantId,
Host = host,
DomainType = ParseEnum(command.DomainType, TenantDomainType.Custom, "invalid_domain_type"),
Status = TenantDomainStatus.Pending,
IsPrimary = command.IsPrimary,
VerificationToken = generated.VerificationToken
};
dbContext.TenantDomains.Add(item);
await AddAuditAsync(actor, "tenant.domain.created", "tenant_domains", item.Id, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantDomainItem>(ToDomainItem(item));
}
public async Task<CatalogList<TenantIdentityProviderItem>> GetAuthProvidersAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default)
{
await RequireAllDataScopeAsync(actor, cancellationToken);
var items = await providerConfigService.GetProvidersAsync(
actor.TenantId,
TenantExternalProviderCapability.Identity,
cancellationToken: cancellationToken);
return new CatalogList<TenantIdentityProviderItem>(items.Select(ToAuthProviderItem).ToArray());
}
public async Task<ContentManagementResult<TenantIdentityProviderItem>> UpsertAuthProviderAsync(
TenantAdminActor actor,
UpsertTenantIdentityProviderCommand command,
CancellationToken cancellationToken = default)
{
await RequireAllDataScopeAsync(actor, cancellationToken);
ArgumentException.ThrowIfNullOrWhiteSpace(command.Provider);
var item = await providerConfigService.UpsertProviderAsync(
actor.TenantId,
new UpsertTenantExternalProviderCommand(
TenantExternalProviderCapability.Identity,
command.Provider,
ParseEnum(command.Status, TenantExternalProviderStatus.Disabled, "invalid_auth_provider_status"),
command.DisplayName,
command.SecretRef,
command.Priority,
command.ConfigPublic,
JsonObjectOrDefault(default)),
cancellationToken);
await AddAuditAsync(actor, "tenant.auth_provider.upserted", "tenant_external_providers", item.Id,
cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantIdentityProviderItem>(ToAuthProviderItem(item));
}
}

View File

@@ -13,100 +13,9 @@ using Tiku.Infrastructure.Tenancy;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal sealed class TenantEngagementService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantEngagementService
{
public async Task<CatalogList<TenantDomainItem>> GetDomainsAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default)
{
await RequireAllDataScopeAsync(actor, cancellationToken);
var items = await dbContext.TenantDomains.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId)
.OrderByDescending(item => item.IsPrimary)
.ThenBy(item => item.CreatedAt)
.Select(item => ToDomainItem(item))
.ToArrayAsync(cancellationToken);
return new CatalogList<TenantDomainItem>(items);
}
public async Task<ContentManagementResult<TenantDomainItem>> CreateDomainAsync(
TenantAdminActor actor,
CreateTenantDomainCommand command,
CancellationToken cancellationToken = default)
{
await RequireAllDataScopeAsync(actor, cancellationToken);
TenantDomain generated;
try
{
generated = TenantDomainProvisioning.CreatePrimary(actor.TenantId, command.Host);
}
catch (ArgumentException exception)
{
throw new TenantAdminDirectException(exception.Message, "invalid_domain_host");
}
var host = generated.Host;
if (command.IsPrimary)
{
var primaryDomains = await dbContext.TenantDomains
.Where(item => item.TenantId == actor.TenantId && item.IsPrimary)
.ToArrayAsync(cancellationToken);
foreach (var domain in primaryDomains) domain.IsPrimary = false;
}
var item = new TenantDomain
{
TenantId = actor.TenantId,
Host = host,
DomainType = ParseEnum(command.DomainType, TenantDomainType.Custom, "invalid_domain_type"),
Status = TenantDomainStatus.Pending,
IsPrimary = command.IsPrimary,
VerificationToken = generated.VerificationToken
};
dbContext.TenantDomains.Add(item);
await AddAuditAsync(actor, "tenant.domain.created", "tenant_domains", item.Id, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantDomainItem>(ToDomainItem(item));
}
public async Task<CatalogList<TenantIdentityProviderItem>> GetAuthProvidersAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default)
{
await RequireAllDataScopeAsync(actor, cancellationToken);
var items = await providerConfigService.GetProvidersAsync(
actor.TenantId,
TenantExternalProviderCapability.Identity,
cancellationToken: cancellationToken);
return new CatalogList<TenantIdentityProviderItem>(items.Select(ToAuthProviderItem).ToArray());
}
public async Task<ContentManagementResult<TenantIdentityProviderItem>> UpsertAuthProviderAsync(
TenantAdminActor actor,
UpsertTenantIdentityProviderCommand command,
CancellationToken cancellationToken = default)
{
await RequireAllDataScopeAsync(actor, cancellationToken);
ArgumentException.ThrowIfNullOrWhiteSpace(command.Provider);
var item = await providerConfigService.UpsertProviderAsync(
actor.TenantId,
new UpsertTenantExternalProviderCommand(
TenantExternalProviderCapability.Identity,
command.Provider,
ParseEnum(command.Status, TenantExternalProviderStatus.Disabled, "invalid_auth_provider_status"),
command.DisplayName,
command.SecretRef,
command.Priority,
command.ConfigPublic,
JsonObjectOrDefault(default)),
cancellationToken);
await AddAuditAsync(actor, "tenant.auth_provider.upserted", "tenant_external_providers", item.Id,
cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantIdentityProviderItem>(ToAuthProviderItem(item));
}
public async Task<CatalogList<TenantAdminBadgeItem>> GetBadgesAsync(
TenantAdminActor actor,
TenantAdminBadgeFilter filter,
@@ -455,4 +364,5 @@ public sealed partial class TenantAdminDirectService
: null;
return new ContentManagementResult<TenantAdminFeedbackItem>(ToFeedbackItem(report, user));
}
}
}

View File

@@ -10,9 +10,9 @@ using Tiku.Infrastructure.Security;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal abstract partial class TenantAdminServiceBase
{
private async Task<User> ResolveUserAsync(UserLookupCommand command, string primaryRole,
protected async Task<User> ResolveUserAsync(UserLookupCommand command, string primaryRole,
CancellationToken cancellationToken)
{
User? user = null;
@@ -61,7 +61,7 @@ public sealed partial class TenantAdminDirectService
return user;
}
private async Task<TenantMembership> EnsureMembershipAsync(
protected async Task<TenantMembership> EnsureMembershipAsync(
Guid tenantId,
Guid userId,
TenantRole role,
@@ -105,14 +105,14 @@ public sealed partial class TenantAdminDirectService
return membership;
}
private static string QuotaMetricForRole(TenantRole role)
protected static string QuotaMetricForRole(TenantRole role)
{
return role == TenantRole.Student
? SaasQuotaMetricCatalog.StudentCount
: SaasQuotaMetricCatalog.StaffCount;
}
private Task<bool> IsUserCountedForMetricAsync(
protected Task<bool> IsUserCountedForMetricAsync(
Guid tenantId,
Guid userId,
string metricCode,
@@ -130,7 +130,7 @@ public sealed partial class TenantAdminDirectService
: query.AnyAsync(item => item.Role != TenantRole.Student, cancellationToken);
}
private async Task<StudentProfile> EnsureStudentProfileAsync(
protected async Task<StudentProfile> EnsureStudentProfileAsync(
Guid tenantId,
Guid userId,
Guid? regionId,
@@ -169,7 +169,7 @@ public sealed partial class TenantAdminDirectService
return profile;
}
private async Task<IReadOnlyCollection<TenantAdminStudentImportPreviewItem>> BuildStudentImportPreviewAsync(
protected async Task<IReadOnlyCollection<TenantAdminStudentImportPreviewItem>> BuildStudentImportPreviewAsync(
TenantAdminActor actor,
CurrentDataScope scope,
TenantAdminStudentImportCommand command,
@@ -215,7 +215,7 @@ public sealed partial class TenantAdminDirectService
return items;
}
private async Task<TenantClassMember> UpsertClassMemberCoreAsync(
protected async Task<TenantClassMember> UpsertClassMemberCoreAsync(
TenantAdminActor actor,
Guid classId,
Guid userId,
@@ -249,7 +249,7 @@ public sealed partial class TenantAdminDirectService
return item;
}
private async Task<IReadOnlyCollection<TenantSupervisionRuleItem>> GetSupervisionRulesCoreAsync(
protected async Task<IReadOnlyCollection<TenantSupervisionRuleItem>> GetSupervisionRulesCoreAsync(
Guid tenantId,
CancellationToken cancellationToken)
{
@@ -264,7 +264,7 @@ public sealed partial class TenantAdminDirectService
return JsonSerializer.Deserialize<TenantSupervisionRuleItem[]>(rulesElement.GetRawText()) ?? [];
}
private async Task SaveSupervisionRulesCoreAsync(
protected async Task SaveSupervisionRulesCoreAsync(
Guid tenantId,
IReadOnlyCollection<TenantSupervisionRuleItem> rules,
CancellationToken cancellationToken)
@@ -284,7 +284,7 @@ public sealed partial class TenantAdminDirectService
settings.AdminFeatureFlags = JsonSerializer.SerializeToElement(existing);
}
private async Task<IReadOnlyCollection<TenantSupervisionRiskStudentItem>> BuildSupervisionRiskStudentsAsync(
protected async Task<IReadOnlyCollection<TenantSupervisionRiskStudentItem>> BuildSupervisionRiskStudentsAsync(
TenantAdminActor actor,
CurrentDataScope scope,
CancellationToken cancellationToken)
@@ -348,7 +348,7 @@ public sealed partial class TenantAdminDirectService
return result;
}
private async Task AssertStudentAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken)
protected async Task AssertStudentAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken)
{
var exists = await dbContext.TenantMemberships.AnyAsync(
item => item.TenantId == tenantId && item.UserId == userId && item.Role == TenantRole.Student,
@@ -356,7 +356,7 @@ public sealed partial class TenantAdminDirectService
if (!exists) throw new TenantAdminDirectException("Student was not found.", "student_not_found");
}
private async Task AssertStudentAsync(
protected async Task AssertStudentAsync(
TenantAdminActor actor,
CurrentDataScope scope,
Guid userId,
@@ -383,7 +383,7 @@ public sealed partial class TenantAdminDirectService
if (!exists) throw new TenantAdminDirectException("Student was not found.", "student_not_found");
}
private async Task AssertTenantMemberAsync(Guid tenantId, Guid? userId, CancellationToken cancellationToken)
protected async Task AssertTenantMemberAsync(Guid tenantId, Guid? userId, CancellationToken cancellationToken)
{
if (!userId.HasValue) return;
@@ -393,13 +393,13 @@ public sealed partial class TenantAdminDirectService
if (!exists) throw new TenantAdminDirectException("Tenant member was not found.", "tenant_member_not_found");
}
private async Task RevokeSessionsAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken)
protected async Task RevokeSessionsAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken)
{
await sessionStore.RevokeRealmAsync(
userId, AuthRealm.Tenant, tenantId, "membership_disabled", cancellationToken);
}
private async Task EnsureTenantOwnerBackendRoleAsync(
protected async Task EnsureTenantOwnerBackendRoleAsync(
Guid tenantId,
Guid userId,
CancellationToken cancellationToken)
@@ -470,7 +470,7 @@ public sealed partial class TenantAdminDirectService
});
}
private async Task EnsureBrandingThemeAsync(
protected async Task EnsureBrandingThemeAsync(
Guid tenantId,
JsonElement theme,
JsonElement publicAssets,
@@ -496,7 +496,7 @@ public sealed partial class TenantAdminDirectService
branding.PublicAssets = MergeJsonObjects(branding.PublicAssets, publicAssets);
}
private async Task AssertClassAsync(Guid tenantId, Guid? classId, CancellationToken cancellationToken)
protected async Task AssertClassAsync(Guid tenantId, Guid? classId, CancellationToken cancellationToken)
{
if (!classId.HasValue) return;
@@ -506,7 +506,7 @@ public sealed partial class TenantAdminDirectService
if (!exists) throw new TenantAdminDirectException("Class was not found.", "class_not_found");
}
private async Task AssertClassAsync(
protected async Task AssertClassAsync(
TenantAdminActor actor,
CurrentDataScope scope,
Guid? classId,
@@ -527,7 +527,7 @@ public sealed partial class TenantAdminDirectService
if (!exists) throw new TenantAdminDirectException("Class was not found.", "class_not_found");
}
private async Task<CurrentDataScope> RequireDataScopeAsync(
protected async Task<CurrentDataScope> RequireDataScopeAsync(
TenantAdminActor actor,
CancellationToken cancellationToken)
{
@@ -538,7 +538,7 @@ public sealed partial class TenantAdminDirectService
return access.DataScope;
}
private async Task RequireAllDataScopeAsync(
protected async Task RequireAllDataScopeAsync(
TenantAdminActor actor,
CancellationToken cancellationToken)
{
@@ -547,7 +547,7 @@ public sealed partial class TenantAdminDirectService
throw new TenantAdminDirectException("Tenant-wide resource was not found.", "tenant_resource_not_found");
}
private async Task AssertReferenceAsync<TEntity>(
protected async Task AssertReferenceAsync<TEntity>(
Guid tenantId,
Guid? id,
string code,
@@ -561,7 +561,7 @@ public sealed partial class TenantAdminDirectService
if (!exists) throw new TenantAdminDirectException("Referenced entity was not found in this tenant.", code);
}
private async Task<TEntity?> ResolveTenantEntityAsync<TEntity>(
protected async Task<TEntity?> ResolveTenantEntityAsync<TEntity>(
DbSet<TEntity> set,
Guid tenantId,
Guid? id,
@@ -583,7 +583,7 @@ public sealed partial class TenantAdminDirectService
cancellationToken);
}
private async Task AddAuditAsync(
protected async Task AddAuditAsync(
TenantAdminActor actor,
string action,
string targetType,
@@ -601,4 +601,4 @@ public sealed partial class TenantAdminDirectService
});
await Task.CompletedTask.WaitAsync(cancellationToken);
}
}
}

View File

@@ -10,9 +10,9 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal abstract partial class TenantAdminServiceBase
{
private static TenantAdminClassItem ToClassItem(TenantClass item, string? regionName, int studentCount,
protected static TenantAdminClassItem ToClassItem(TenantClass item, string? regionName, int studentCount,
int staffCount)
{
return new TenantAdminClassItem(
@@ -32,7 +32,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantAdminClassMemberItem ToClassMemberItem(TenantClassMember item, TenantAdminUserSummary user)
protected static TenantAdminClassMemberItem ToClassMemberItem(TenantClassMember item, TenantAdminUserSummary user)
{
return new TenantAdminClassMemberItem(
item.Id,
@@ -46,7 +46,7 @@ public sealed partial class TenantAdminDirectService
user);
}
private static TenantAdminStudentItem ToStudentItem(
protected static TenantAdminStudentItem ToStudentItem(
TenantMembership membership,
User user,
StudentProfile? profile,
@@ -80,7 +80,7 @@ public sealed partial class TenantAdminDirectService
membership.UpdatedAt);
}
private static TenantAdminUserSummary ToUserSummary(User user)
protected static TenantAdminUserSummary ToUserSummary(User user)
{
return new TenantAdminUserSummary(
user.Id,
@@ -92,7 +92,7 @@ public sealed partial class TenantAdminDirectService
user.PrimaryRole);
}
private static TenantAdminStudentNoteItem ToNoteItem(TenantStudentNote item)
protected static TenantAdminStudentNoteItem ToNoteItem(TenantStudentNote item)
{
return new TenantAdminStudentNoteItem(
item.Id,
@@ -108,7 +108,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantAdminStudentFollowupItem ToFollowupItem(TenantStudentFollowup item)
protected static TenantAdminStudentFollowupItem ToFollowupItem(TenantStudentFollowup item)
{
return new TenantAdminStudentFollowupItem(
item.Id,
@@ -130,7 +130,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantAdminMemberItem ToMemberItem(TenantMembership membership, User user)
protected static TenantAdminMemberItem ToMemberItem(TenantMembership membership, User user)
{
return new TenantAdminMemberItem(
membership.Id,
@@ -143,7 +143,7 @@ public sealed partial class TenantAdminDirectService
membership.UpdatedAt);
}
private static TenantBrandingItem ToBrandingItem(TenantBranding item)
protected static TenantBrandingItem ToBrandingItem(TenantBranding item)
{
return new TenantBrandingItem(
item.TenantId,
@@ -160,7 +160,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantSettingsItem ToSettingsItem(TenantSettings item)
protected static TenantSettingsItem ToSettingsItem(TenantSettings item)
{
return new TenantSettingsItem(
item.TenantId,
@@ -170,7 +170,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantThemeItem ToThemeItem(TenantThemeConfig item)
protected static TenantThemeItem ToThemeItem(TenantThemeConfig item)
{
return new TenantThemeItem(
item.TenantId,
@@ -187,7 +187,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantDomainItem ToDomainItem(TenantDomain item)
protected static TenantDomainItem ToDomainItem(TenantDomain item)
{
return new TenantDomainItem(
item.Id,
@@ -205,7 +205,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantIdentityProviderItem ToAuthProviderItem(TenantExternalProviderItem item)
protected static TenantIdentityProviderItem ToAuthProviderItem(TenantExternalProviderItem item)
{
return new TenantIdentityProviderItem(
item.Id,
@@ -219,7 +219,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantAdminBadgeItem ToBadgeItem(Badge item)
protected static TenantAdminBadgeItem ToBadgeItem(Badge item)
{
return new TenantAdminBadgeItem(
item.Id,
@@ -240,7 +240,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantAdminBadgeGrantItem ToBadgeGrantItem(UserBadge grant, User? user, User? grantedBy,
protected static TenantAdminBadgeGrantItem ToBadgeGrantItem(UserBadge grant, User? user, User? grantedBy,
Badge? badge)
{
return new TenantAdminBadgeGrantItem(
@@ -262,7 +262,7 @@ public sealed partial class TenantAdminDirectService
grant.UpdatedAt);
}
private static TenantAdminNotificationItem ToNotificationItem(UserNotification item)
protected static TenantAdminNotificationItem ToNotificationItem(UserNotification item)
{
return new TenantAdminNotificationItem(
item.Id,
@@ -284,7 +284,7 @@ public sealed partial class TenantAdminDirectService
item.UpdatedAt);
}
private static TenantAdminFeedbackItem ToFeedbackItem(Report report, User? user)
protected static TenantAdminFeedbackItem ToFeedbackItem(Report report, User? user)
{
return new TenantAdminFeedbackItem(
report.Id,
@@ -308,22 +308,22 @@ public sealed partial class TenantAdminDirectService
report.UpdatedAt);
}
private static int ResolveLimit(int? limit)
protected static int ResolveLimit(int? limit)
{
return Math.Clamp(limit ?? 100, 1, 500);
}
private static string? Normalize(string? value)
protected static string? Normalize(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
private static string NormalizeCode(string value)
protected static string NormalizeCode(string value)
{
return value.Trim().ToLowerInvariant();
}
private static string? MaskPhone(string? phone)
protected static string? MaskPhone(string? phone)
{
var value = Normalize(phone);
return value is { Length: >= 7 }
@@ -331,12 +331,12 @@ public sealed partial class TenantAdminDirectService
: value;
}
private static JsonElement JsonObjectOrDefault(JsonElement value)
protected static JsonElement JsonObjectOrDefault(JsonElement value)
{
return value.ValueKind == JsonValueKind.Object ? value.Clone() : JsonDefaults.Object();
}
private static JsonElement PermissionObject(JsonElement value)
protected static JsonElement PermissionObject(JsonElement value)
{
var result = new Dictionary<string, bool>(StringComparer.Ordinal);
if (value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) return JsonDefaults.Object();
@@ -358,7 +358,7 @@ public sealed partial class TenantAdminDirectService
return JsonSerializer.SerializeToElement(result);
}
private static JsonElement AccessMap(JsonElement value, string codePrefix)
protected static JsonElement AccessMap(JsonElement value, string codePrefix)
{
var result = new Dictionary<string, bool>(StringComparer.Ordinal);
if (value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) return JsonDefaults.Object();
@@ -380,7 +380,7 @@ public sealed partial class TenantAdminDirectService
return JsonSerializer.SerializeToElement(result);
}
private static JsonElement DataScope(JsonElement value)
protected static JsonElement DataScope(JsonElement value)
{
if (value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) return JsonDefaults.Object();
@@ -404,7 +404,7 @@ public sealed partial class TenantAdminDirectService
return value.Clone();
}
private async Task AssertGrantableAsync(
protected async Task AssertGrantableAsync(
TenantAdminActor actor,
TenantRole role,
CancellationToken cancellationToken)
@@ -428,7 +428,7 @@ public sealed partial class TenantAdminDirectService
"tenant_owner_required");
}
private static string RoleToPrimaryRole(TenantRole role)
protected static string RoleToPrimaryRole(TenantRole role)
{
return role switch
{
@@ -443,7 +443,7 @@ public sealed partial class TenantAdminDirectService
};
}
private static string NormalizeRoleCode(string value)
protected static string NormalizeRoleCode(string value)
{
var code = new string(value.Trim().ToLowerInvariant()
.Select(character => char.IsAsciiLetterOrDigit(character) || character is '_' or '-' ? character : '-')
@@ -457,7 +457,7 @@ public sealed partial class TenantAdminDirectService
return code;
}
private static string NormalizeDomain(string value)
protected static string NormalizeDomain(string value)
{
var host = Normalize(value)?.ToLowerInvariant()
.TrimEnd('.') ?? throw new TenantAdminDirectException("Domain host is required.", "domain_host_required");
@@ -469,7 +469,7 @@ public sealed partial class TenantAdminDirectService
return host;
}
private static JsonElement MergeJsonObjects(JsonElement first, JsonElement second)
protected static JsonElement MergeJsonObjects(JsonElement first, JsonElement second)
{
var result = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
if (first.ValueKind == JsonValueKind.Object)
@@ -483,7 +483,7 @@ public sealed partial class TenantAdminDirectService
return JsonSerializer.SerializeToElement(result);
}
private static void AssertNoSecrets(JsonElement value, string code)
protected static void AssertNoSecrets(JsonElement value, string code)
{
if (value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) return;
@@ -503,14 +503,14 @@ public sealed partial class TenantAdminDirectService
}
}
private static bool IsPermissionKey(string key)
protected static bool IsPermissionKey(string key)
{
return key.Length <= 100 &&
key.Contains(':', StringComparison.Ordinal) &&
key.All(character => char.IsAsciiLetterOrDigit(character) || character is ':' or '*');
}
private static bool IsAccessKey(string key)
protected static bool IsAccessKey(string key)
{
return key.Length <= 100 &&
key.Length > 0 &&
@@ -518,13 +518,13 @@ public sealed partial class TenantAdminDirectService
key.All(character => char.IsAsciiLetterOrDigit(character) || character is '_' or '.' or ':' or '-');
}
private static TEnum ParseEnum<TEnum>(string? value, TEnum fallback, string code)
protected static TEnum ParseEnum<TEnum>(string? value, TEnum fallback, string code)
where TEnum : struct, Enum
{
return string.IsNullOrWhiteSpace(value) ? fallback : ParseEnum<TEnum>(value, code);
}
private static TEnum ParseEnum<TEnum>(string value, string code)
protected static TEnum ParseEnum<TEnum>(string value, string code)
where TEnum : struct, Enum
{
var normalized = value.Replace("_", string.Empty, StringComparison.Ordinal)
@@ -535,4 +535,4 @@ public sealed partial class TenantAdminDirectService
throw new TenantAdminDirectException("Enum value was invalid.", code);
}
}
}

View File

@@ -0,0 +1,29 @@
using Tiku.Application.Auth;
using Tiku.Application.Notifications;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.TenantAdmin;
internal sealed record TenantAdminServiceDependencies(
TikuDbContext DbContext,
ITenantExternalProviderConfigService ProviderConfigService,
INotificationProvider NotificationProvider,
ICurrentAccessContext CurrentAccessContext,
IAuthSessionStore SessionStore,
IFeatureAccessService FeatureAccessService,
IAuthorizationStateInvalidator AuthorizationStateInvalidator);
internal abstract partial class TenantAdminServiceBase(TenantAdminServiceDependencies dependencies)
{
protected TikuDbContext dbContext { get; } = dependencies.DbContext;
protected ITenantExternalProviderConfigService providerConfigService { get; } = dependencies.ProviderConfigService;
protected INotificationProvider notificationProvider { get; } = dependencies.NotificationProvider;
protected ICurrentAccessContext currentAccessContext { get; } = dependencies.CurrentAccessContext;
protected IAuthSessionStore sessionStore { get; } = dependencies.SessionStore;
protected IFeatureAccessService featureAccessService { get; } = dependencies.FeatureAccessService;
protected IAuthorizationStateInvalidator authorizationStateInvalidator { get; } =
dependencies.AuthorizationStateInvalidator;
}

View File

@@ -8,7 +8,8 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal sealed class TenantMemberAccessService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantMemberAccessService
{
public async Task<CatalogList<TenantAdminMemberItem>> GetMembersAsync(
TenantAdminActor actor,
@@ -195,4 +196,4 @@ public sealed partial class TenantAdminDirectService
.SingleAsync(item => item.Id == membership.UserId, cancellationToken);
return new ContentManagementResult<TenantAdminMemberItem>(ToMemberItem(membership, user));
}
}
}

View File

@@ -9,7 +9,8 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal sealed class TenantSiteSettingsService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantSiteSettingsService
{
public async Task<CatalogList<TenantAdminAuditLogItem>> GetAuditLogsAsync(
TenantAdminActor actor,
@@ -242,4 +243,4 @@ public sealed partial class TenantAdminDirectService
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantThemeItem>(ToThemeItem(item));
}
}
}

View File

@@ -7,7 +7,8 @@ using Tiku.Infrastructure.Security;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal sealed class TenantStudentEngagementService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantStudentEngagementService
{
public async Task<CatalogList<TenantAdminStudentNoteItem>> GetStudentNotesAsync(
TenantAdminActor actor,
@@ -208,4 +209,4 @@ public sealed partial class TenantAdminDirectService
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantAdminStudentFollowupItem>(ToFollowupItem(item));
}
}
}

View File

@@ -11,7 +11,8 @@ using Tiku.Infrastructure.Security;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal sealed class TenantStudentService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantStudentService
{
public async Task<TenantAdminStudentList> GetStudentsAsync(
TenantAdminActor actor,
@@ -373,4 +374,4 @@ public sealed partial class TenantAdminDirectService
await dbContext.SaveChangesAsync(cancellationToken);
return new TenantAdminBulkOperationResult(command.UserIds.Count, succeeded, failed.Count, failed);
}
}
}

View File

@@ -10,7 +10,8 @@ using PointExchangeOrderStatus = Tiku.Domain.Commerce.PointExchangeOrderStatus;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService
internal sealed class TenantSupervisionService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantSupervisionService
{
public async Task<CatalogList<TenantSupervisionRuleItem>> GetSupervisionRulesAsync(
TenantAdminActor actor,
@@ -173,4 +174,4 @@ public sealed partial class TenantAdminDirectService
cancellationToken);
return new TenantPointRiskReport(negativeScoreUsers, highClaimUsers, cancelledExchangeOrders);
}
}
}

View File

@@ -1,19 +0,0 @@
using Tiku.Application.Auth;
using Tiku.Application.Notifications;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Application.TenantAdmin;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.TenantAdmin;
public sealed partial class TenantAdminDirectService(
TikuDbContext dbContext,
ITenantExternalProviderConfigService providerConfigService,
INotificationProvider notificationProvider,
ICurrentAccessContext currentAccessContext,
IAuthSessionStore sessionStore,
IFeatureAccessService featureAccessService,
IAuthorizationStateInvalidator authorizationStateInvalidator) : ITenantAdminDirectService
{
}

View File

@@ -35,7 +35,6 @@ public sealed class ArchitectureBoundaryTests
var root = FindRepositoryRoot();
var legacyLineBudgets = new Dictionary<string, int>(StringComparer.Ordinal)
{
["TenantAdminDirectService"] = 3190,
["DirectContentService"] = 2015,
["CommerceAdminService"] = 1842,
["LearningActivityService"] = 1781,
@@ -168,11 +167,14 @@ public sealed class ArchitectureBoundaryTests
{
var root = FindRepositoryRoot();
var files = new[]
{
Path.Combine(root, "Tiku.Infrastructure", "Growth", "CommissionService.cs"),
Path.Combine(root, "Tiku.Infrastructure", "Growth", "ReferralService.cs"),
Path.Combine(root, "Tiku.Api", "Controllers", "TenantAdminDirectController.cs")
};
{
Path.Combine(root, "Tiku.Infrastructure", "Growth", "CommissionService.cs"),
Path.Combine(root, "Tiku.Infrastructure", "Growth", "ReferralService.cs")
}
.Concat(Directory.EnumerateFiles(
Path.Combine(root, "Tiku.Api", "Controllers"),
"Tenant*Controller.cs",
SearchOption.AllDirectories));
AssertNoForbiddenSymbols(
root,