using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Tiku.Api.Contracts; using Tiku.Application.Catalog; using Tiku.Application.Scoreline; using Tiku.Application.Security; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.Api.Controllers; [ApiController] [AllowAnonymous] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [Produces("application/json")] [Route("api/scoreline")] public sealed class ScorelineController( IScorelineQueryService scorelineQueryService, ITenantContext currentTenant, TikuDbContext dbContext) : ControllerBase { private static readonly string[] DynamicPrefixes = ["field.", "min.", "max."]; [HttpGet("fields")] [EndpointSummary("查询分数线字段配置")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetFields( [FromQuery] ScorelineQueryDto query, CancellationToken cancellationToken) { return Ok(await scorelineQueryService.GetFieldsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("records")] [EndpointSummary("分页查询分数线记录")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetRecords( [FromQuery] ScorelineQueryDto query, CancellationToken cancellationToken) { return Ok(await scorelineQueryService.GetRecordsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken), ResolveDynamicFilters()), cancellationToken)); } [HttpGet("trend")] [EndpointSummary("查询历年分数线趋势")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetTrend( [FromQuery] ScorelineQueryDto query, CancellationToken cancellationToken) { return Ok(await scorelineQueryService.GetTrendAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken), ResolveDynamicFilters()), cancellationToken)); } [HttpGet("years")] [EndpointSummary("查询分数线可用年份")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetYears( [FromQuery] ScorelineQueryDto query, CancellationToken cancellationToken) { return Ok(await scorelineQueryService.GetYearsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } private IReadOnlyCollection ResolveDynamicFilters() { var filters = new List(); foreach (var (key, value) in Request.Query) { var prefix = DynamicPrefixes.FirstOrDefault(key.StartsWith); if (prefix is null) { continue; } var raw = value.FirstOrDefault(); if (string.IsNullOrWhiteSpace(raw)) { continue; } filters.Add(new ScorelineDynamicFilter( prefix.TrimEnd('.'), key[prefix.Length..], raw.Trim())); } return filters; } private async Task ResolveTenantIdAsync( ScorelineQueryDto query, CancellationToken cancellationToken) { if (currentTenant.TenantId.HasValue) { return currentTenant.TenantId.Value; } var tenantCode = query.TenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(tenantCode)) { throw new TenantNotFoundException(); } var tenantId = await dbContext.Tenants .Where(tenant => tenant.Slug == tenantCode.Trim() && tenant.Status == TenantStatus.Active) .Select(tenant => (Guid?)tenant.Id) .SingleOrDefaultAsync(cancellationToken); return tenantId ?? throw new TenantNotFoundException(); } }