Compare commits
2 Commits
a1440baf3d
...
579517a1d4
Author | SHA1 | Date | |
---|---|---|---|
579517a1d4 | |||
d4ce3b2577 |
@@ -51,6 +51,11 @@ namespace OpenCand.API.Config
|
||||
|
||||
options.OnRejected = async (context, token) =>
|
||||
{
|
||||
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("RateLimitingConfig");
|
||||
var clientIdentifier = GetClientIdentifier(context.HttpContext);
|
||||
logger.LogWarning("Rate limit exceeded for client {ClientIdentifier}", clientIdentifier);
|
||||
|
||||
context.HttpContext.Response.StatusCode = 429;
|
||||
|
||||
var retryAfter = GetRetryAfter(context);
|
||||
|
@@ -21,7 +21,7 @@ namespace OpenCand.API.Controllers
|
||||
[EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)]
|
||||
public async Task<CandidatoSearchResult> CandidatoSearch([FromQuery] string q)
|
||||
{
|
||||
if (string.IsNullOrEmpty(q) || q.Length == 1)
|
||||
if (string.IsNullOrEmpty(q) || q.Length < 3)
|
||||
{
|
||||
throw new ArgumentException("Query parameter 'q' cannot be null/empty.", nameof(q));
|
||||
}
|
||||
|
@@ -21,9 +21,9 @@ namespace OpenCand.API.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("enriquecimento")]
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos()
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos([FromQuery] GetValueSumRequestFilter requestFilter)
|
||||
{
|
||||
return await estatisticaService.GetMaioresEnriquecimentos();
|
||||
return await estatisticaService.GetMaioresEnriquecimentos(requestFilter);
|
||||
}
|
||||
|
||||
[HttpPost("values-sum")]
|
||||
|
@@ -102,6 +102,16 @@ namespace OpenCand.Repository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task IncreaseCandidatoPopularity(Guid idcandidato)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(ConnectionString);
|
||||
await connection.ExecuteAsync(@"
|
||||
UPDATE candidato
|
||||
SET popularidade = popularidade + 1
|
||||
WHERE idcandidato = @idcandidato;
|
||||
", new { idcandidato });
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetRandomCandidatoIdAsync()
|
||||
{
|
||||
using var connection = new NpgsqlConnection(ConnectionString);
|
||||
|
@@ -2,8 +2,9 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Npgsql;
|
||||
using OpenCand.API.Model;
|
||||
using OpenCand.Core.Models;
|
||||
using OpenCand.Repository;
|
||||
using System.Text.Json;
|
||||
using static OpenCand.API.Model.GetValueSumRequest;
|
||||
|
||||
namespace OpenCand.API.Repository
|
||||
{
|
||||
@@ -16,9 +17,39 @@ namespace OpenCand.API.Repository
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos()
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null)
|
||||
{
|
||||
string cacheKey = GenerateCacheKey("GetMaioresEnriquecimento");
|
||||
var joinBase = string.Empty;
|
||||
if (requestFilter == null) requestFilter = new GetValueSumRequestFilter();
|
||||
else
|
||||
{
|
||||
joinBase = " JOIN candidato_mapping cm ON ed.idcandidato = cm.idcandidato ";
|
||||
var whereConditions = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(requestFilter.Partido))
|
||||
{
|
||||
whereConditions.Add($"cm.sgpartido = '{requestFilter.Partido}'");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(requestFilter.Uf))
|
||||
{
|
||||
whereConditions.Add($"cm.siglauf = '{requestFilter.Uf.ToUpper()}'");
|
||||
}
|
||||
if (requestFilter.Ano != null)
|
||||
{
|
||||
whereConditions.Add($"cm.ano = '{requestFilter.Ano}'");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(requestFilter.Cargo))
|
||||
{
|
||||
whereConditions.Add($"cm.cargo = '{requestFilter.Cargo}'");
|
||||
}
|
||||
if (whereConditions.Count > 0)
|
||||
{
|
||||
joinBase += " WHERE " + string.Join(" AND ", whereConditions);
|
||||
}
|
||||
}
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(requestFilter);
|
||||
|
||||
string cacheKey = GenerateCacheKey("GetMaioresEnriquecimento", requestJson);
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
@@ -46,6 +77,7 @@ namespace OpenCand.API.Repository
|
||||
JOIN candidato c ON ed.idcandidato = c.idcandidato
|
||||
JOIN mv_bem_candidato pi ON ed.idcandidato = pi.idcandidato AND ed.anoInicial = pi.ano
|
||||
JOIN mv_bem_candidato pf ON ed.idcandidato = pf.idcandidato AND ed.anoFinal = pf.ano
|
||||
" + joinBase + @"
|
||||
ORDER BY
|
||||
enriquecimento DESC
|
||||
LIMIT 25;")
|
||||
|
@@ -58,13 +58,14 @@ namespace OpenCand.API.Services
|
||||
await PerformPreLoad("GetValueSum", () => estatisticaService.GetValueSum(request));
|
||||
}
|
||||
}
|
||||
|
||||
await PerformPreLoad("GetMaioresEnriquecimentos", () => estatisticaService.GetMaioresEnriquecimentos(null));
|
||||
}
|
||||
|
||||
private async Task PreloadSingleEndpoints(EstatisticaService estatisticaService, OpenCandService openCandService)
|
||||
{
|
||||
logger.LogInformation("Preloading single-call endpoints...");
|
||||
|
||||
await PerformPreLoad("GetOpenCandStatsAsync", estatisticaService.GetMaioresEnriquecimentos);
|
||||
await PerformPreLoad("GetOpenCandStatsAsync", openCandService.GetOpenCandStatsAsync);
|
||||
await PerformPreLoad("GetDatabaseTechStatsAsync", openCandService.GetDatabaseTechStatsAsync);
|
||||
await PerformPreLoad("GetDataAvailabilityStatsAsync", openCandService.GetDataAvailabilityStatsAsync);
|
||||
|
@@ -1,56 +1,54 @@
|
||||
using OpenCand.API.Model;
|
||||
using OpenCand.API.Repository;
|
||||
using OpenCand.Repository;
|
||||
using static OpenCand.API.Model.GetValueSumRequest;
|
||||
|
||||
namespace OpenCand.API.Services
|
||||
{
|
||||
public class EstatisticaService
|
||||
{
|
||||
private readonly OpenCandRepository openCandRepository;
|
||||
private readonly CandidatoRepository candidatoRepository;
|
||||
private readonly BemCandidatoRepository bemCandidatoRepository;
|
||||
private readonly DespesaReceitaRepository despesaReceitaRepository;
|
||||
private readonly EstatisticaRepository estatisticaRepository;
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly ILogger<OpenCandService> logger;
|
||||
|
||||
public EstatisticaService(
|
||||
OpenCandRepository openCandRepository,
|
||||
CandidatoRepository candidatoRepository,
|
||||
BemCandidatoRepository bemCandidatoRepository,
|
||||
DespesaReceitaRepository despesaReceitaRepository,
|
||||
EstatisticaRepository estatisticaRepository,
|
||||
IConfiguration configuration,
|
||||
ILogger<OpenCandService> logger)
|
||||
{
|
||||
this.openCandRepository = openCandRepository;
|
||||
this.candidatoRepository = candidatoRepository;
|
||||
this.bemCandidatoRepository = bemCandidatoRepository;
|
||||
this.despesaReceitaRepository = despesaReceitaRepository;
|
||||
this.estatisticaRepository = estatisticaRepository;
|
||||
this.configuration = configuration;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ConfigurationModel> GetConfigurationModel()
|
||||
{
|
||||
logger.LogInformation("Getting configuration model");
|
||||
|
||||
return await estatisticaRepository.GetConfiguration();
|
||||
}
|
||||
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos()
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null)
|
||||
{
|
||||
return await estatisticaRepository.GetMaioresEnriquecimentos();
|
||||
logger.LogInformation($"Getting maiores enriquecimentos. Filters: Partido={requestFilter?.Partido}, Uf={requestFilter?.Uf}, Ano={requestFilter?.Ano}, Cargo={requestFilter?.Cargo}");
|
||||
|
||||
return await estatisticaRepository.GetMaioresEnriquecimentos(requestFilter);
|
||||
}
|
||||
|
||||
public async Task<List<GetValueSumResponse>> GetValueSum(GetValueSumRequest request)
|
||||
{
|
||||
logger.LogInformation($"Getting value sum for {request.Type} grouped by {request.GroupBy}. Filters: Partido={request.Filter?.Partido}, Uf={request.Filter?.Uf}, Ano={request.Filter?.Ano}, Cargo={request.Filter?.Cargo}");
|
||||
// count exec time
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
ValidateRequest(request);
|
||||
|
||||
var sqlBuilder = new SqlQueryBuilder();
|
||||
var query = sqlBuilder.BuildQuery(request);
|
||||
var parameters = sqlBuilder.GetParameters();
|
||||
|
||||
return await estatisticaRepository.GetValueSum(query, parameters);
|
||||
var result = await estatisticaRepository.GetValueSum(query, parameters);
|
||||
|
||||
stopwatch.Stop();
|
||||
logger.LogInformation($"GetValueSum - Execution time: {stopwatch.ElapsedMilliseconds} ms");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ValidateRequest(GetValueSumRequest request)
|
||||
|
@@ -37,11 +37,15 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting OpenCand stats");
|
||||
|
||||
return await openCandRepository.GetOpenCandStatsAsync();
|
||||
}
|
||||
|
||||
public async Task<DataAvailabilityStats> GetDataAvailabilityStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting data availability stats");
|
||||
|
||||
var stats = await openCandRepository.GetDataAvailabilityAsync();
|
||||
|
||||
return stats;
|
||||
@@ -49,6 +53,8 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting database tech stats");
|
||||
|
||||
var stats = await openCandRepository.GetDatabaseTechStatsAsync();
|
||||
|
||||
stats.Tables = stats.Tables.OrderBy(t => t.Name).ToList();
|
||||
@@ -59,14 +65,18 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<CandidatoSearchResult> SearchCandidatosAsync(string query)
|
||||
{
|
||||
logger.LogInformation($"Searching candidatos with query: {query}");
|
||||
|
||||
return new CandidatoSearchResult()
|
||||
{
|
||||
Candidatos = await candidatoRepository.SearchCandidatosAsync(query)
|
||||
Candidatos = await candidatoRepository.SearchCandidatosAsync(query) ?? new List<Candidato>()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetRandomCandidato()
|
||||
{
|
||||
logger.LogInformation("Getting random candidato");
|
||||
|
||||
return await candidatoRepository.GetRandomCandidatoIdAsync();
|
||||
}
|
||||
|
||||
@@ -76,6 +86,15 @@ namespace OpenCand.API.Services
|
||||
var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato);
|
||||
var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato);
|
||||
|
||||
try
|
||||
{
|
||||
await candidatoRepository.IncreaseCandidatoPopularity(idcandidato);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"Error increasing popularity for Candidato ID {idcandidato}");
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"Candidato with ID {idcandidato} not found.");
|
||||
@@ -84,6 +103,10 @@ namespace OpenCand.API.Services
|
||||
{
|
||||
throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found.");
|
||||
}
|
||||
if (candidatoExt == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"CandidatoExt for ID {idcandidato} not found.");
|
||||
}
|
||||
|
||||
foreach (var eleicao in eleicoes)
|
||||
{
|
||||
@@ -96,6 +119,8 @@ namespace OpenCand.API.Services
|
||||
result.Eleicoes = eleicoes.OrderByDescending(e => e.Ano).ToList();
|
||||
result.CandidatoExt = candidatoExt.OrderByDescending(ce => ce.Ano).ToList();
|
||||
|
||||
logger.LogDebug($"Found Candidato: {result.Nome}, ID: {result.IdCandidato}, CPF: {result.Cpf}, FotoUrl: {result.FotoUrl}");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -107,6 +132,8 @@ namespace OpenCand.API.Services
|
||||
result = new List<BemCandidato>();
|
||||
}
|
||||
|
||||
logger.LogInformation($"Found {result.Count} bens for Candidato ID {idcandidato}");
|
||||
|
||||
return new BemCandidatoResult()
|
||||
{
|
||||
Bens = result.OrderByDescending(r => r.TipoBem).ThenByDescending(r => r.Valor).ToList()
|
||||
@@ -115,12 +142,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<RedeSocialResult> GetCandidatoRedeSocialById(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting redes sociais for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
result = new List<RedeSocial>();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} redes sociais for Candidato ID {idcandidato}");
|
||||
|
||||
return new RedeSocialResult()
|
||||
{
|
||||
RedesSociais = result.OrderByDescending(r => r.Ano).ToList()
|
||||
@@ -129,12 +160,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<CpfRevealResult> GetCandidatoCpfById(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting CPF for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await candidatoRepository.GetCandidatoCpfAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new CpfRevealResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found CPF {result} for Candidato ID {idcandidato}");
|
||||
|
||||
return new CpfRevealResult()
|
||||
{
|
||||
Cpf = result
|
||||
@@ -143,12 +178,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<DespesasResult> GetDespesasByIdAndYear(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting despesas for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await despesaReceitaRepository.GetDespesasByCandidatoIdYearAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new DespesasResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} despesas for Candidato ID {idcandidato}");
|
||||
|
||||
return new DespesasResult()
|
||||
{
|
||||
Despesas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
||||
@@ -157,12 +196,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<ReceitaResult> GetReceitasByIdAndYear(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting receitas for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await despesaReceitaRepository.GetReceitasByCandidatoIdYearAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new ReceitaResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} receitas for Candidato ID {idcandidato}");
|
||||
|
||||
return new ReceitaResult()
|
||||
{
|
||||
Receitas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
||||
|
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Information"
|
||||
}
|
||||
},
|
||||
"DatabaseSettings": {
|
||||
"ConnectionString": "Host=localhost;Database=opencand;Username=root;Password=root;Include Error Detail=true;CommandTimeout=300"
|
||||
"ConnectionString": "Host=localhost;Database=opencand;Username=root;Password=root;Pooling=true;Minimum Pool Size=1;Maximum Pool Size=20;Connection Lifetime=300;Command Timeout=30;Application Name=OpenCand.API;Include Error Detail=true"
|
||||
},
|
||||
"FotosSettings": {
|
||||
"Path": "./fotos_cand",
|
||||
|
Reference in New Issue
Block a user