feat(backend): ranking 100% Elasticsearch e critérios do PDF

This commit is contained in:
Frederico Castro
2025-12-15 00:13:12 -03:00
parent 70787fbb51
commit 2a0dc1a652
25 changed files with 522 additions and 263 deletions

View File

@@ -1,9 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
import asyncio
from fastapi import APIRouter, Depends, HTTPException, Query
from typing import Optional, List
from ...application.use_cases.obter_ranking import ObterRankingUseCase
from ...application.use_cases.obter_consultor import ObterConsultorUseCase
from ...application.mappers import RankingMapper
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
from ..schemas.consultor_schema import (
RankingResponseSchema,
@@ -20,7 +21,7 @@ from ..schemas.ranking_schema import (
ProcessarRankingResponseSchema,
ConsultaNomeSchema,
)
from .dependencies import get_repository, get_ranking_repository, get_processar_job
from .dependencies import get_repository, get_ranking_store, get_processar_job
from ...application.jobs.job_status import job_status
router = APIRouter(prefix="/api/v1", tags=["ranking"])
@@ -34,19 +35,34 @@ async def obter_ranking(
default=None, description="Filtrar por bloco (a, c, d)"
),
repository: ConsultorRepositoryImpl = Depends(get_repository),
store = Depends(get_ranking_store),
):
if store.is_ready():
total, entries = store.get_slice(offset=offset, limit=limite)
consultores_schema = [
ConsultorResumoSchema(
id_pessoa=e.id_pessoa,
nome=e.nome,
anos_atuacao=e.anos_atuacao,
ativo=e.ativo,
veterano=e.anos_atuacao >= 10,
pontuacao_total=e.pontuacao_total,
bloco_a=e.bloco_a,
bloco_c=e.bloco_c,
bloco_d=e.bloco_d,
rank=e.posicao,
)
for e in entries
]
return RankingResponseSchema(
total=total, limite=limite, offset=offset, consultores=consultores_schema
)
use_case = ObterRankingUseCase(repository=repository)
consultores_dto = await use_case.executar(limite=limite, componente=componente)
total = await repository.contar_total()
consultores_schema = [
ConsultorResumoSchema(**vars(dto)) for dto in consultores_dto
]
return RankingResponseSchema(
total=total, limite=limite, offset=offset, consultores=consultores_schema
)
consultores_schema = [ConsultorResumoSchema(**vars(dto)) for dto in consultores_dto]
return RankingResponseSchema(total=total, limite=limite, offset=offset, consultores=consultores_schema)
@router.get("/ranking/detalhado", response_model=RankingDetalhadoResponseSchema)
@@ -73,9 +89,15 @@ async def obter_ranking_detalhado(
async def obter_consultor(
id_pessoa: int,
repository: ConsultorRepositoryImpl = Depends(get_repository),
store = Depends(get_ranking_store),
):
use_case = ObterConsultorUseCase(repository=repository)
consultor = await use_case.executar(id_pessoa=id_pessoa)
rank = None
if store.is_ready():
found = store.get_by_id(id_pessoa)
rank = found.posicao if found else None
consultor = await use_case.executar(id_pessoa=id_pessoa, rank=rank)
if not consultor:
raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado")
@@ -93,14 +115,46 @@ async def ranking_paginado(
page: int = Query(default=1, ge=1, description="Número da página"),
size: int = Query(default=50, ge=1, le=1000, description="Tamanho da página (máx 1000)"),
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
ranking_repo = Depends(get_ranking_repository),
store = Depends(get_ranking_store),
):
total = ranking_repo.contar_total(filtro_ativo=ativo)
consultores = ranking_repo.buscar_paginado(page=page, size=size, filtro_ativo=ativo)
if not store.is_ready():
raise HTTPException(
status_code=503,
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
)
total, entries = store.get_page(page=page, size=size, filtro_ativo=ativo)
total_pages = (total + size - 1) // size
consultores_schema = [RankingMapper.consultor_ranking_to_schema(c) for c in consultores]
consultores_schema = []
for e in entries:
d = e.detalhes
consultores_schema.append(
ConsultorRankingResumoSchema(
id_pessoa=e.id_pessoa,
nome=e.nome,
posicao=e.posicao,
pontuacao_total=float(e.pontuacao_total),
bloco_a=float(e.bloco_a),
bloco_b=float(e.bloco_b),
bloco_c=float(e.bloco_c),
bloco_d=float(e.bloco_d),
ativo=e.ativo,
anos_atuacao=float(e.anos_atuacao),
coordenador_ppg=bool(d.get("coordenador_ppg", False)),
consultoria=d.get("consultoria"),
coordenacoes_capes=d.get("coordenacoes_capes"),
inscricoes=d.get("inscricoes"),
avaliacoes_comissao=d.get("avaliacoes_comissao"),
premiacoes=d.get("premiacoes"),
bolsas_cnpq=d.get("bolsas_cnpq"),
participacoes=d.get("participacoes"),
orientacoes=d.get("orientacoes"),
membros_banca=d.get("membros_banca"),
pontuacao=d.get("pontuacao"),
)
)
return RankingPaginadoResponseSchema(
total=total,
@@ -115,9 +169,15 @@ async def ranking_paginado(
async def buscar_por_nome(
nome: str = Query(..., min_length=3, description="Nome (ou parte) para buscar"),
limit: int = Query(default=5, ge=1, le=20, description="Limite de resultados"),
ranking_repo = Depends(get_ranking_repository),
store = Depends(get_ranking_store),
):
resultados = ranking_repo.buscar_por_nome(nome=nome, limit=limit)
if not store.is_ready():
raise HTTPException(
status_code=503,
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
)
resultados = store.buscar_por_nome(nome=nome, limit=limit)
return [
ConsultaNomeSchema(
id_pessoa=r["ID_PESSOA"],
@@ -131,10 +191,53 @@ async def buscar_por_nome(
@router.get("/ranking/estatisticas", response_model=EstatisticasRankingSchema)
async def ranking_estatisticas(
ranking_repo = Depends(get_ranking_repository),
store = Depends(get_ranking_store),
):
estatisticas = ranking_repo.obter_estatisticas()
distribuicao = ranking_repo.obter_distribuicao()
if not store.is_ready():
raise HTTPException(
status_code=503,
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
)
total = store.total()
ativos = store.total(filtro_ativo=True)
inativos = total - ativos
entries = store.get_page(page=1, size=total)[1] if total else []
totais = [e.pontuacao_total for e in entries]
distribuicao = []
if total:
buckets = [
("800+", lambda x: x >= 800),
("600-799", lambda x: 600 <= x < 800),
("400-599", lambda x: 400 <= x < 600),
("200-399", lambda x: 200 <= x < 400),
("0-199", lambda x: x < 200),
]
for faixa, pred in buckets:
qtd = sum(1 for x in totais if pred(x))
distribuicao.append(
{
"faixa": faixa,
"quantidade": qtd,
"percentual": round((qtd * 100.0 / total), 2) if total else 0,
}
)
estatisticas = {
"total_consultores": total,
"total_ativos": ativos,
"total_inativos": inativos,
"ultima_atualizacao": store.last_update.isoformat() if store.last_update else None,
"pontuacao_media": (sum(totais) / total) if total else 0,
"pontuacao_maxima": max(totais) if totais else 0,
"pontuacao_minima": min(totais) if totais else 0,
"media_componentes": {
"a": (sum(e.bloco_a for e in entries) / total) if total else 0,
"b": (sum(e.bloco_b for e in entries) / total) if total else 0,
"c": (sum(e.bloco_c for e in entries) / total) if total else 0,
"d": (sum(e.bloco_d for e in entries) / total) if total else 0,
},
}
return EstatisticasRankingSchema(
total_consultores=estatisticas.get("total_consultores", 0),
@@ -156,14 +259,13 @@ async def status_processamento():
@router.post("/ranking/processar", response_model=ProcessarRankingResponseSchema)
async def processar_ranking(
background_tasks: BackgroundTasks,
request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(),
job = Depends(get_processar_job),
):
if job_status.is_running:
raise HTTPException(status_code=409, detail="Job já está em execução")
background_tasks.add_task(job.executar, limpar_antes=request.limpar_antes)
asyncio.create_task(job.executar(limpar_antes=request.limpar_antes))
return ProcessarRankingResponseSchema(
sucesso=True,