using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Tiku.Api.Contracts; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Domain.Tenancy; namespace Tiku.Api.Controllers; [ApiController] [Tags("租户端-公开配置")] [AllowAnonymous] [Produces("application/json")] [Route("api/public/tenant")] public sealed class TenantPublicController( ITenantContext tenantContext, ITenantContextInitializer tenantContextInitializer, ITenantDirectory tenantDirectory, IPublicTenantConfigurationQuery publicConfiguration) : ControllerBase { [HttpGet("resolve")] [EndpointSummary("解析当前租户")] [EndpointDescription("根据访问域名、host 参数、x-tenant-code 或 tenantCode 查询启用中的租户及公开品牌配置。")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Resolve( [FromQuery] TenantResolveQueryDto query, CancellationToken cancellationToken) { var tenantCode = NormalizeTenantCode(query.TenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault()); var host = NormalizeHost(query.Host ?? Request.Host.Host); var directoryEntry = tenantContext.ResolutionSource == TenantResolutionSource.Host ? await tenantDirectory.FindByHostAsync(Request.Host.Host, cancellationToken) : !string.IsNullOrWhiteSpace(tenantCode) ? await tenantDirectory.FindByCodeAsync(tenantCode, cancellationToken) : string.IsNullOrWhiteSpace(host) ? null : await tenantDirectory.FindByHostAsync(host, cancellationToken); var tenant = directoryEntry is null ? null : new TenantLookupResult( directoryEntry.TenantId, directoryEntry.TenantCode, directoryEntry.Name, directoryEntry.Status, directoryEntry.Mode, directoryEntry.Host); if (tenant is null) return NotFound(new ProblemDetails { Title = "Tenant was not found.", Status = StatusCodes.Status404NotFound, Detail = "No active tenant matches the supplied host or tenantCode." }); tenantContextInitializer.Initialize( tenant.Id, tenant.Slug, tenant.Host is null ? TenantResolutionSource.TenantCode : TenantResolutionSource.Host); return Ok(await BuildResponseAsync(tenant, tenant.Host, cancellationToken)); } [HttpGet("current-public")] [EndpointSummary("获取公开租户配置")] [EndpointDescription("公开页面使用的租户品牌、功能开关和 public config。和 resolve 返回结构一致。")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public Task> CurrentPublic( [FromQuery] TenantResolveQueryDto query, CancellationToken cancellationToken) { return Resolve(query, cancellationToken); } private async Task BuildResponseAsync( TenantLookupResult tenant, string? host, CancellationToken cancellationToken) { var configuration = await publicConfiguration.GetAsync(tenant.Id, cancellationToken); return new TenantResolveResponseDto( new PublicTenantDto( tenant.Id, tenant.Slug, tenant.Name, tenant.Status, tenant.Mode, host), new PublicTenantBrandingDto( configuration.BrandName, configuration.ShortName, configuration.Slogan, configuration.LogoUrl, configuration.FaviconUrl, configuration.ServiceWechat, configuration.ServiceAccountName, configuration.Theme, configuration.PublicAssets), configuration.FeatureFlags, configuration.AdminFeatureFlags, configuration.PublicConfig); } private static string? NormalizeTenantCode(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } private static string? NormalizeHost(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var host = value.Trim().ToLowerInvariant(); return host.Split(':', 2)[0]; } private sealed record TenantLookupResult( Guid Id, string Slug, string Name, TenantStatus Status, TenantMode Mode, string? Host); }