Correções de segurança: - SQL Injection: usar prepared statements em ranking_repository.py - Validação de entrada para parâmetros page/size Correções de bugs: - Bônus de continuidade: 15→20 pts para 8+ anos (conforme especificação) - Memory leak: limpar _consultores após processamento do ranking Melhorias de robustez: - Substituir bare except por exceções específicas - Threading.Lock para padrão singleton thread-safe - Pool Oracle com configuração otimizada (timeout/getmode) - ES client com timeouts diferenciados e verificação is_closed - Logging para tipos de coordenação desconhecidos Correções frontend: - Polling com timeout máximo de 5 minutos - useEffect cleanup para setTimeout - React.memo e useMemo para otimização de performance
386 lines
14 KiB
Python
386 lines
14 KiB
Python
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime
|
|
import json
|
|
|
|
from ...domain.entities.consultor_ranking import ConsultorRanking
|
|
from .client import OracleClient
|
|
|
|
|
|
class RankingOracleRepository:
|
|
def __init__(self, oracle_client: OracleClient):
|
|
self.client = oracle_client
|
|
|
|
def inserir_batch(self, consultores: List[Dict[str, Any]]) -> int:
|
|
"""
|
|
Insere ou atualiza um batch de consultores usando MERGE.
|
|
Retorna o número de registros processados.
|
|
"""
|
|
import oracledb
|
|
|
|
if not consultores:
|
|
return 0
|
|
|
|
merge_sql = """
|
|
MERGE INTO TB_RANKING_CONSULTOR t
|
|
USING (
|
|
SELECT
|
|
:id_pessoa AS ID_PESSOA,
|
|
:nome AS NOME,
|
|
:pontuacao_total AS PONTUACAO_TOTAL,
|
|
:componente_a AS COMPONENTE_A,
|
|
:componente_b AS COMPONENTE_B,
|
|
:componente_c AS COMPONENTE_C,
|
|
:componente_d AS COMPONENTE_D,
|
|
:ativo AS ATIVO,
|
|
:anos_atuacao AS ANOS_ATUACAO,
|
|
TO_CLOB(:json_detalhes) AS JSON_DETALHES
|
|
FROM DUAL
|
|
) s
|
|
ON (t.ID_PESSOA = s.ID_PESSOA)
|
|
WHEN MATCHED THEN
|
|
UPDATE SET
|
|
t.NOME = s.NOME,
|
|
t.PONTUACAO_TOTAL = s.PONTUACAO_TOTAL,
|
|
t.COMPONENTE_A = s.COMPONENTE_A,
|
|
t.COMPONENTE_B = s.COMPONENTE_B,
|
|
t.COMPONENTE_C = s.COMPONENTE_C,
|
|
t.COMPONENTE_D = s.COMPONENTE_D,
|
|
t.ATIVO = s.ATIVO,
|
|
t.ANOS_ATUACAO = s.ANOS_ATUACAO,
|
|
t.DT_CALCULO = CURRENT_TIMESTAMP,
|
|
t.JSON_DETALHES = s.JSON_DETALHES
|
|
WHEN NOT MATCHED THEN
|
|
INSERT (
|
|
ID_PESSOA, NOME, PONTUACAO_TOTAL,
|
|
COMPONENTE_A, COMPONENTE_B, COMPONENTE_C, COMPONENTE_D,
|
|
ATIVO, ANOS_ATUACAO, JSON_DETALHES, DT_CALCULO
|
|
)
|
|
VALUES (
|
|
s.ID_PESSOA, s.NOME, s.PONTUACAO_TOTAL,
|
|
s.COMPONENTE_A, s.COMPONENTE_B, s.COMPONENTE_C, s.COMPONENTE_D,
|
|
s.ATIVO, s.ANOS_ATUACAO, s.JSON_DETALHES, CURRENT_TIMESTAMP
|
|
)
|
|
"""
|
|
|
|
with self.client.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
try:
|
|
for consultor in consultores:
|
|
json_str = json.dumps(consultor, ensure_ascii=False)
|
|
cursor.setinputsizes(json_detalhes=oracledb.DB_TYPE_CLOB)
|
|
params = {
|
|
"id_pessoa": consultor["id_pessoa"],
|
|
"nome": consultor["nome"],
|
|
"pontuacao_total": consultor["pontuacao_total"],
|
|
"componente_a": consultor.get("bloco_a") or consultor.get("componente_a", 0),
|
|
"componente_b": consultor.get("bloco_b") or consultor.get("componente_b", 0),
|
|
"componente_c": consultor.get("bloco_c") or consultor.get("componente_c", 0),
|
|
"componente_d": consultor.get("bloco_d") or consultor.get("componente_d", 0),
|
|
"ativo": "S" if consultor.get("ativo") else "N",
|
|
"anos_atuacao": consultor.get("anos_atuacao", 0),
|
|
"json_detalhes": json_str
|
|
}
|
|
cursor.execute(merge_sql, params)
|
|
|
|
conn.commit()
|
|
return len(consultores)
|
|
except Exception as e:
|
|
conn.rollback()
|
|
raise RuntimeError(f"Erro ao inserir batch no Oracle: {e}")
|
|
finally:
|
|
cursor.close()
|
|
|
|
def buscar_paginado(
|
|
self,
|
|
page: int = 1,
|
|
size: int = 50,
|
|
filtro_ativo: Optional[bool] = None
|
|
) -> List[ConsultorRanking]:
|
|
"""
|
|
Busca ranking paginado ordenado por posição.
|
|
"""
|
|
if page < 1:
|
|
page = 1
|
|
if size < 1 or size > 10000:
|
|
size = 50
|
|
|
|
offset = (page - 1) * size
|
|
limit_end = offset + size
|
|
|
|
where_clause = ""
|
|
params = {
|
|
"offset": offset,
|
|
"limit_end": limit_end,
|
|
}
|
|
|
|
if filtro_ativo is not None:
|
|
where_clause = "AND ATIVO = :ativo"
|
|
params["ativo"] = "S" if filtro_ativo else "N"
|
|
|
|
query = f"""
|
|
SELECT * FROM (
|
|
SELECT
|
|
ID_PESSOA,
|
|
NOME,
|
|
POSICAO,
|
|
PONTUACAO_TOTAL,
|
|
COMPONENTE_A,
|
|
COMPONENTE_B,
|
|
COMPONENTE_C,
|
|
COMPONENTE_D,
|
|
ATIVO,
|
|
ANOS_ATUACAO,
|
|
DT_CALCULO,
|
|
JSON_DETALHES,
|
|
ROW_NUMBER() OVER (ORDER BY POSICAO NULLS LAST, PONTUACAO_TOTAL DESC) AS RN
|
|
FROM TB_RANKING_CONSULTOR
|
|
WHERE 1=1 {where_clause}
|
|
)
|
|
WHERE RN > :offset AND RN <= :limit_end
|
|
"""
|
|
|
|
results = self.client.executar_query(query, params)
|
|
|
|
consultores = []
|
|
for r in results:
|
|
json_det = r["JSON_DETALHES"]
|
|
if hasattr(json_det, "read"):
|
|
json_det = json_det.read()
|
|
else:
|
|
json_det = str(json_det) if json_det else "{}"
|
|
|
|
consultores.append(
|
|
ConsultorRanking(
|
|
id_pessoa=r["ID_PESSOA"],
|
|
nome=r["NOME"],
|
|
posicao=r["POSICAO"],
|
|
pontuacao_total=float(r["PONTUACAO_TOTAL"]),
|
|
componente_a=float(r["COMPONENTE_A"]),
|
|
componente_b=float(r["COMPONENTE_B"]),
|
|
componente_c=float(r["COMPONENTE_C"]),
|
|
componente_d=float(r["COMPONENTE_D"]),
|
|
ativo=r["ATIVO"] == "S",
|
|
anos_atuacao=float(r["ANOS_ATUACAO"]),
|
|
dt_calculo=r["DT_CALCULO"],
|
|
json_detalhes=json_det
|
|
)
|
|
)
|
|
|
|
return consultores
|
|
|
|
def contar_total(self, filtro_ativo: Optional[bool] = None) -> int:
|
|
"""
|
|
Conta total de consultores no ranking.
|
|
"""
|
|
where_clause = ""
|
|
params = {}
|
|
|
|
if filtro_ativo is not None:
|
|
where_clause = "WHERE ATIVO = :ativo"
|
|
params["ativo"] = "S" if filtro_ativo else "N"
|
|
|
|
query = f"SELECT COUNT(*) AS TOTAL FROM TB_RANKING_CONSULTOR {where_clause}"
|
|
results = self.client.executar_query(query, params)
|
|
|
|
return results[0]["TOTAL"] if results else 0
|
|
|
|
def buscar_por_nome(self, nome: str, limit: int = 5) -> List[Dict[str, Any]]:
|
|
"""
|
|
Busca consultores por nome, procurando cada palavra separadamente.
|
|
"""
|
|
palavras = [p.strip() for p in nome.upper().split() if len(p.strip()) >= 2]
|
|
|
|
if not palavras:
|
|
return []
|
|
|
|
conditions = []
|
|
params = {"limit": limit}
|
|
|
|
for i, palavra in enumerate(palavras):
|
|
param_name = f"p{i}"
|
|
conditions.append(f"UPPER(NOME) LIKE :{param_name}")
|
|
params[param_name] = f"%{palavra}%"
|
|
|
|
where_clause = " AND ".join(conditions)
|
|
|
|
query = f"""
|
|
SELECT
|
|
ID_PESSOA,
|
|
NOME,
|
|
POSICAO,
|
|
PONTUACAO_TOTAL
|
|
FROM TB_RANKING_CONSULTOR
|
|
WHERE {where_clause}
|
|
ORDER BY POSICAO NULLS LAST
|
|
FETCH FIRST :limit ROWS ONLY
|
|
"""
|
|
return self.client.executar_query(query, params)
|
|
|
|
def buscar_por_id(self, id_pessoa: int) -> Optional[ConsultorRanking]:
|
|
"""
|
|
Busca consultor específico com sua posição no ranking.
|
|
"""
|
|
query = """
|
|
SELECT
|
|
ID_PESSOA,
|
|
NOME,
|
|
POSICAO,
|
|
PONTUACAO_TOTAL,
|
|
COMPONENTE_A,
|
|
COMPONENTE_B,
|
|
COMPONENTE_C,
|
|
COMPONENTE_D,
|
|
ATIVO,
|
|
ANOS_ATUACAO,
|
|
DT_CALCULO,
|
|
JSON_DETALHES
|
|
FROM TB_RANKING_CONSULTOR
|
|
WHERE ID_PESSOA = :id_pessoa
|
|
"""
|
|
|
|
results = self.client.executar_query(query, {"id_pessoa": id_pessoa})
|
|
|
|
if not results:
|
|
return None
|
|
|
|
r = results[0]
|
|
|
|
json_det = r["JSON_DETALHES"]
|
|
if hasattr(json_det, "read"):
|
|
json_det = json_det.read()
|
|
else:
|
|
json_det = str(json_det) if json_det else "{}"
|
|
|
|
return ConsultorRanking(
|
|
id_pessoa=r["ID_PESSOA"],
|
|
nome=r["NOME"],
|
|
posicao=r["POSICAO"],
|
|
pontuacao_total=float(r["PONTUACAO_TOTAL"]),
|
|
componente_a=float(r["COMPONENTE_A"]),
|
|
componente_b=float(r["COMPONENTE_B"]),
|
|
componente_c=float(r["COMPONENTE_C"]),
|
|
componente_d=float(r["COMPONENTE_D"]),
|
|
ativo=r["ATIVO"] == "S",
|
|
anos_atuacao=float(r["ANOS_ATUACAO"]),
|
|
dt_calculo=r["DT_CALCULO"],
|
|
json_detalhes=json_det
|
|
)
|
|
|
|
def atualizar_posicoes(self) -> None:
|
|
"""
|
|
Chama a procedure SP_ATUALIZAR_POSICOES para recalcular as posições.
|
|
"""
|
|
with self.client.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
try:
|
|
cursor.callproc("SP_ATUALIZAR_POSICOES")
|
|
conn.commit()
|
|
except Exception as e:
|
|
conn.rollback()
|
|
raise RuntimeError(f"Erro ao atualizar posições: {e}")
|
|
finally:
|
|
cursor.close()
|
|
|
|
def obter_estatisticas(self) -> Dict[str, Any]:
|
|
"""
|
|
Retorna estatísticas do ranking.
|
|
"""
|
|
query = """
|
|
SELECT
|
|
COUNT(*) AS TOTAL_CONSULTORES,
|
|
COUNT(CASE WHEN ATIVO = 'S' THEN 1 END) AS TOTAL_ATIVOS,
|
|
COUNT(CASE WHEN ATIVO = 'N' THEN 1 END) AS TOTAL_INATIVOS,
|
|
MAX(DT_CALCULO) AS ULTIMA_ATUALIZACAO,
|
|
AVG(PONTUACAO_TOTAL) AS PONTUACAO_MEDIA,
|
|
MAX(PONTUACAO_TOTAL) AS PONTUACAO_MAXIMA,
|
|
MIN(PONTUACAO_TOTAL) AS PONTUACAO_MINIMA,
|
|
AVG(COMPONENTE_A) AS MEDIA_COMP_A,
|
|
AVG(COMPONENTE_B) AS MEDIA_COMP_B,
|
|
AVG(COMPONENTE_C) AS MEDIA_COMP_C,
|
|
AVG(COMPONENTE_D) AS MEDIA_COMP_D
|
|
FROM TB_RANKING_CONSULTOR
|
|
"""
|
|
|
|
results = self.client.executar_query(query)
|
|
|
|
if not results:
|
|
return {}
|
|
|
|
r = results[0]
|
|
return {
|
|
"total_consultores": r["TOTAL_CONSULTORES"],
|
|
"total_ativos": r["TOTAL_ATIVOS"],
|
|
"total_inativos": r["TOTAL_INATIVOS"],
|
|
"ultima_atualizacao": r["ULTIMA_ATUALIZACAO"].isoformat() if r["ULTIMA_ATUALIZACAO"] else None,
|
|
"pontuacao_media": float(r["PONTUACAO_MEDIA"]) if r["PONTUACAO_MEDIA"] else 0,
|
|
"pontuacao_maxima": float(r["PONTUACAO_MAXIMA"]) if r["PONTUACAO_MAXIMA"] else 0,
|
|
"pontuacao_minima": float(r["PONTUACAO_MINIMA"]) if r["PONTUACAO_MINIMA"] else 0,
|
|
"media_componentes": {
|
|
"a": float(r["MEDIA_COMP_A"]) if r["MEDIA_COMP_A"] else 0,
|
|
"b": float(r["MEDIA_COMP_B"]) if r["MEDIA_COMP_B"] else 0,
|
|
"c": float(r["MEDIA_COMP_C"]) if r["MEDIA_COMP_C"] else 0,
|
|
"d": float(r["MEDIA_COMP_D"]) if r["MEDIA_COMP_D"] else 0
|
|
}
|
|
}
|
|
|
|
def obter_distribuicao(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
Retorna distribuição de consultores por faixa de pontuação.
|
|
"""
|
|
query = """
|
|
SELECT
|
|
CASE
|
|
WHEN PONTUACAO_TOTAL >= 800 THEN '800+'
|
|
WHEN PONTUACAO_TOTAL >= 600 THEN '600-799'
|
|
WHEN PONTUACAO_TOTAL >= 400 THEN '400-599'
|
|
WHEN PONTUACAO_TOTAL >= 200 THEN '200-399'
|
|
ELSE '0-199'
|
|
END AS FAIXA,
|
|
COUNT(*) AS QUANTIDADE,
|
|
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM TB_RANKING_CONSULTOR), 2) AS PERCENTUAL
|
|
FROM TB_RANKING_CONSULTOR
|
|
GROUP BY
|
|
CASE
|
|
WHEN PONTUACAO_TOTAL >= 800 THEN '800+'
|
|
WHEN PONTUACAO_TOTAL >= 600 THEN '600-799'
|
|
WHEN PONTUACAO_TOTAL >= 400 THEN '400-599'
|
|
WHEN PONTUACAO_TOTAL >= 200 THEN '200-399'
|
|
ELSE '0-199'
|
|
END
|
|
ORDER BY
|
|
CASE
|
|
WHEN FAIXA = '800+' THEN 1
|
|
WHEN FAIXA = '600-799' THEN 2
|
|
WHEN FAIXA = '400-599' THEN 3
|
|
WHEN FAIXA = '200-399' THEN 4
|
|
ELSE 5
|
|
END
|
|
"""
|
|
|
|
results = self.client.executar_query(query)
|
|
|
|
return [
|
|
{
|
|
"faixa": r["FAIXA"],
|
|
"quantidade": r["QUANTIDADE"],
|
|
"percentual": float(r["PERCENTUAL"])
|
|
}
|
|
for r in results
|
|
]
|
|
|
|
def limpar_tabela(self) -> None:
|
|
"""
|
|
Limpa todos os registros da tabela de ranking.
|
|
Usar apenas quando for reprocessar do zero.
|
|
"""
|
|
with self.client.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
try:
|
|
cursor.execute("DELETE FROM TB_RANKING_CONSULTOR")
|
|
conn.commit()
|
|
except Exception as e:
|
|
conn.rollback()
|
|
raise RuntimeError(f"Erro ao limpar tabela: {e}")
|
|
finally:
|
|
cursor.close()
|