fix: resolver problemas identificados no code review
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
This commit is contained in:
@@ -51,7 +51,7 @@ class ProcessarRankingJob:
|
|||||||
|
|
||||||
job_status.finalizar(sucesso=True)
|
job_status.finalizar(sucesso=True)
|
||||||
|
|
||||||
return {
|
resultado_final = {
|
||||||
"sucesso": True,
|
"sucesso": True,
|
||||||
"total_processados": resultado.get("processados", len(entries)),
|
"total_processados": resultado.get("processados", len(entries)),
|
||||||
"total_batches": resultado.get("batches", 0),
|
"total_batches": resultado.get("batches", 0),
|
||||||
@@ -59,9 +59,15 @@ class ProcessarRankingJob:
|
|||||||
"estatisticas": estatisticas
|
"estatisticas": estatisticas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self._consultores = []
|
||||||
|
|
||||||
|
return resultado_final
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._consultores = []
|
||||||
job_status.finalizar(sucesso=False, erro=str(e))
|
job_status.finalizar(sucesso=False, erro=str(e))
|
||||||
raise RuntimeError(f"Erro ao processar ranking: {e}")
|
logger.error(f"Erro ao processar ranking: {e}", exc_info=True)
|
||||||
|
raise RuntimeError(f"Erro ao processar ranking: {e}") from e
|
||||||
|
|
||||||
async def _processar_batch(self, docs: list, progress: dict) -> None:
|
async def _processar_batch(self, docs: list, progress: dict) -> None:
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
|
|||||||
@@ -91,9 +91,10 @@ class CalculadorPontuacao:
|
|||||||
bonus = 0
|
bonus = 0
|
||||||
|
|
||||||
# Bônus de continuidade (escalonado, não cumulativo) - apenas CONS_ATIVO
|
# Bônus de continuidade (escalonado, não cumulativo) - apenas CONS_ATIVO
|
||||||
|
# Spec: 8+ anos = 20 pts (era 15, corrigido conforme especificação)
|
||||||
if codigo == "CONS_ATIVO":
|
if codigo == "CONS_ATIVO":
|
||||||
if consultoria.anos_consecutivos >= 8:
|
if consultoria.anos_consecutivos >= 8:
|
||||||
bonus += 15
|
bonus += 20
|
||||||
elif consultoria.anos_consecutivos >= 5:
|
elif consultoria.anos_consecutivos >= 5:
|
||||||
bonus += 10
|
bonus += 10
|
||||||
elif consultoria.anos_consecutivos >= 3:
|
elif consultoria.anos_consecutivos >= 3:
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ class ElasticsearchClient:
|
|||||||
"Accept": "application/json"
|
"Accept": "application/json"
|
||||||
},
|
},
|
||||||
verify=self.verify_ssl,
|
verify=self.verify_ssl,
|
||||||
timeout=120.0
|
timeout=httpx.Timeout(
|
||||||
|
connect=10.0,
|
||||||
|
read=60.0,
|
||||||
|
write=30.0,
|
||||||
|
pool=10.0,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
@@ -39,8 +44,8 @@ class ElasticsearchClient:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self) -> httpx.AsyncClient:
|
def client(self) -> httpx.AsyncClient:
|
||||||
if not self._client:
|
if not self._client or self._client.is_closed:
|
||||||
raise RuntimeError("Cliente Elasticsearch não conectado. Execute connect() primeiro.")
|
raise RuntimeError("Cliente Elasticsearch não conectado ou fechado. Execute connect() primeiro.")
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
async def buscar_por_id(self, id_pessoa: int) -> Optional[dict]:
|
async def buscar_por_id(self, id_pessoa: int) -> Optional[dict]:
|
||||||
|
|||||||
@@ -21,13 +21,20 @@ class OracleClient:
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
dsn=self.dsn,
|
dsn=self.dsn,
|
||||||
min=2,
|
min=1,
|
||||||
max=10,
|
max=20,
|
||||||
increment=1,
|
increment=5,
|
||||||
|
timeout=30,
|
||||||
|
wait_timeout=10,
|
||||||
|
getmode=oracledb.POOL_GETMODE_TIMEDWAIT,
|
||||||
)
|
)
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
logger.info("Pool Oracle conectado com sucesso")
|
||||||
|
except oracledb.Error as e:
|
||||||
|
logger.error(f"Oracle database error: {e}", exc_info=True)
|
||||||
|
self._connected = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Oracle: {e}")
|
logger.error(f"Oracle connection error: {e}", exc_info=True)
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
@@ -45,11 +52,13 @@ class OracleClient:
|
|||||||
def get_connection(self):
|
def get_connection(self):
|
||||||
if not self._pool:
|
if not self._pool:
|
||||||
raise RuntimeError("Pool Oracle não conectado. Execute connect() primeiro.")
|
raise RuntimeError("Pool Oracle não conectado. Execute connect() primeiro.")
|
||||||
conn = self._pool.acquire()
|
conn = None
|
||||||
try:
|
try:
|
||||||
|
conn = self._pool.acquire()
|
||||||
yield conn
|
yield conn
|
||||||
finally:
|
finally:
|
||||||
self._pool.release(conn)
|
if conn:
|
||||||
|
self._pool.release(conn)
|
||||||
|
|
||||||
def executar_query(self, query: str, params: Optional[dict] = None) -> List[Dict[str, Any]]:
|
def executar_query(self, query: str, params: Optional[dict] = None) -> List[Dict[str, Any]]:
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
|
|||||||
@@ -99,11 +99,19 @@ class RankingOracleRepository:
|
|||||||
"""
|
"""
|
||||||
Busca ranking paginado ordenado por posição.
|
Busca ranking paginado ordenado por posição.
|
||||||
"""
|
"""
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
if size < 1 or size > 10000:
|
||||||
|
size = 50
|
||||||
|
|
||||||
offset = (page - 1) * size
|
offset = (page - 1) * size
|
||||||
limit_end = offset + size
|
limit_end = offset + size
|
||||||
|
|
||||||
where_clause = ""
|
where_clause = ""
|
||||||
params = {}
|
params = {
|
||||||
|
"offset": offset,
|
||||||
|
"limit_end": limit_end,
|
||||||
|
}
|
||||||
|
|
||||||
if filtro_ativo is not None:
|
if filtro_ativo is not None:
|
||||||
where_clause = "AND ATIVO = :ativo"
|
where_clause = "AND ATIVO = :ativo"
|
||||||
@@ -128,7 +136,7 @@ class RankingOracleRepository:
|
|||||||
FROM TB_RANKING_CONSULTOR
|
FROM TB_RANKING_CONSULTOR
|
||||||
WHERE 1=1 {where_clause}
|
WHERE 1=1 {where_clause}
|
||||||
)
|
)
|
||||||
WHERE RN > {offset} AND RN <= {limit_end}
|
WHERE RN > :offset AND RN <= :limit_end
|
||||||
"""
|
"""
|
||||||
|
|
||||||
results = self.client.executar_query(query, params)
|
results = self.client.executar_query(query, params)
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
return "CAJ_MP"
|
return "CAJ_MP"
|
||||||
elif "adjunt" in texto:
|
elif "adjunt" in texto:
|
||||||
return "CAJ"
|
return "CAJ"
|
||||||
|
elif "coordenador" in texto or "área" in texto or "area" in texto:
|
||||||
|
return "CA"
|
||||||
|
|
||||||
|
logger.warning(f"Tipo de coordenação desconhecido, inferindo CA: tipo='{tipo_coord}', texto='{texto[:100]}'")
|
||||||
return "CA"
|
return "CA"
|
||||||
|
|
||||||
def _extrair_coordenacoes_capes(self, atuacoes: List[Dict[str, Any]]) -> List[CoordenacaoCapes]:
|
def _extrair_coordenacoes_capes(self, atuacoes: List[Dict[str, Any]]) -> List[CoordenacaoCapes]:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ async def carregar_ranking_do_oracle() -> int:
|
|||||||
for c in consultores:
|
for c in consultores:
|
||||||
try:
|
try:
|
||||||
detalhes = json.loads(c.json_detalhes) if isinstance(c.json_detalhes, str) else c.json_detalhes or {}
|
detalhes = json.loads(c.json_detalhes) if isinstance(c.json_detalhes, str) else c.json_detalhes or {}
|
||||||
except:
|
except (json.JSONDecodeError, TypeError, ValueError):
|
||||||
detalhes = {}
|
detalhes = {}
|
||||||
|
|
||||||
entries.append(
|
entries.append(
|
||||||
@@ -120,13 +120,13 @@ async def lifespan(app: FastAPI):
|
|||||||
if scheduler:
|
if scheduler:
|
||||||
try:
|
try:
|
||||||
scheduler.parar()
|
scheduler.parar()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if oracle_client:
|
if oracle_client:
|
||||||
try:
|
try:
|
||||||
oracle_client.close()
|
oracle_client.close()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
await es_client.close()
|
await es_client.close()
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import threading
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from ...infrastructure.elasticsearch.client import ElasticsearchClient
|
from ...infrastructure.elasticsearch.client import ElasticsearchClient
|
||||||
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
||||||
from ...infrastructure.oracle.client import OracleClient
|
from ...infrastructure.oracle.client import OracleClient
|
||||||
@@ -15,22 +18,25 @@ es_client = ElasticsearchClient(
|
|||||||
verify_ssl=settings.ES_VERIFY_SSL,
|
verify_ssl=settings.ES_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
|
|
||||||
oracle_client = OracleClient(
|
oracle_client: Optional[OracleClient] = OracleClient(
|
||||||
user=settings.ORACLE_LOCAL_USER,
|
user=settings.ORACLE_LOCAL_USER,
|
||||||
password=settings.ORACLE_LOCAL_PASSWORD,
|
password=settings.ORACLE_LOCAL_PASSWORD,
|
||||||
dsn=settings.ORACLE_LOCAL_DSN,
|
dsn=settings.ORACLE_LOCAL_DSN,
|
||||||
) if settings.ORACLE_LOCAL_USER and settings.ORACLE_LOCAL_DSN else None
|
) if settings.ORACLE_LOCAL_USER and settings.ORACLE_LOCAL_DSN else None
|
||||||
|
|
||||||
ranking_oracle_repo = RankingOracleRepository(oracle_client) if oracle_client else None
|
ranking_oracle_repo: Optional[RankingOracleRepository] = RankingOracleRepository(oracle_client) if oracle_client else None
|
||||||
|
|
||||||
_repository: ConsultorRepositoryImpl = None
|
_repository: Optional[ConsultorRepositoryImpl] = None
|
||||||
_processar_job: ProcessarRankingJob = None
|
_processar_job: Optional[ProcessarRankingJob] = None
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def get_repository() -> ConsultorRepositoryImpl:
|
def get_repository() -> ConsultorRepositoryImpl:
|
||||||
global _repository
|
global _repository
|
||||||
if _repository is None:
|
if _repository is None:
|
||||||
_repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=oracle_client)
|
with _lock:
|
||||||
|
if _repository is None:
|
||||||
|
_repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=oracle_client)
|
||||||
return _repository
|
return _repository
|
||||||
|
|
||||||
|
|
||||||
@@ -49,9 +55,11 @@ def get_ranking_oracle_repo() -> RankingOracleRepository:
|
|||||||
def get_processar_job() -> ProcessarRankingJob:
|
def get_processar_job() -> ProcessarRankingJob:
|
||||||
global _processar_job
|
global _processar_job
|
||||||
if _processar_job is None:
|
if _processar_job is None:
|
||||||
_processar_job = ProcessarRankingJob(
|
with _lock:
|
||||||
es_client=es_client,
|
if _processar_job is None:
|
||||||
ranking_store=ranking_store,
|
_processar_job = ProcessarRankingJob(
|
||||||
ranking_oracle_repo=ranking_oracle_repo,
|
es_client=es_client,
|
||||||
)
|
ranking_store=ranking_store,
|
||||||
|
ranking_oracle_repo=ranking_oracle_repo,
|
||||||
|
)
|
||||||
return _processar_job
|
return _processar_job
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ function App() {
|
|||||||
setProcessMessage('Processamento do ranking já iniciado. Aguardando...');
|
setProcessMessage('Processamento do ranking já iniciado. Aguardando...');
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
const MAX_POLLING_TIME = 5 * 60 * 1000;
|
||||||
|
const POLLING_INTERVAL = 4000;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < MAX_POLLING_TIME) {
|
||||||
try {
|
try {
|
||||||
const st = await rankingService.getStatus();
|
const st = await rankingService.getStatus();
|
||||||
setProcessMessage(st.mensagem || `Processando... ${st.progress || 0}%`);
|
setProcessMessage(st.mensagem || `Processando... ${st.progress || 0}%`);
|
||||||
@@ -83,7 +87,11 @@ function App() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
setProcessMessage('Aguardando status do processamento...');
|
setProcessMessage('Aguardando status do processamento...');
|
||||||
}
|
}
|
||||||
await new Promise((r) => setTimeout(r, 4000));
|
await new Promise((r) => setTimeout(r, POLLING_INTERVAL));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - startTime >= MAX_POLLING_TIME) {
|
||||||
|
throw new Error('Timeout: processamento demorou mais que 5 minutos');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await rankingService.getRanking(page, pageSize);
|
const response = await rankingService.getRanking(page, pageSize);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, useMemo, memo } from 'react';
|
||||||
import './ConsultorCard.css';
|
import './ConsultorCard.css';
|
||||||
|
|
||||||
const SELOS = {
|
const SELOS = {
|
||||||
@@ -123,7 +123,7 @@ const FORMULAS = {
|
|||||||
},
|
},
|
||||||
bloco_c: {
|
bloco_c: {
|
||||||
titulo: 'Consultoria',
|
titulo: 'Consultoria',
|
||||||
descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade 8a+=+20 (escalonado)\nRetorno (reativação): +15 (uma vez)',
|
descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade: 3a=+5, 5a=+10, 8a+=+20 (escalonado)\nRetorno (reativação): +15 (uma vez)',
|
||||||
},
|
},
|
||||||
bloco_d: {
|
bloco_d: {
|
||||||
titulo: 'Premiacoes/Avaliacoes',
|
titulo: 'Premiacoes/Avaliacoes',
|
||||||
@@ -183,15 +183,16 @@ const ScoreItemWithTooltip = ({ value, label, formula, style }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado }) => {
|
const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecionado }) => {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const cardRef = useRef(null);
|
const cardRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (highlight && cardRef.current) {
|
if (highlight && cardRef.current) {
|
||||||
setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
cardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
cardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
}, 100);
|
}, 100);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}, [highlight]);
|
}, [highlight]);
|
||||||
|
|
||||||
@@ -219,7 +220,7 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
|
|||||||
const blocoD = pontuacao?.bloco_d || { total: consultor.bloco_d || 0 };
|
const blocoD = pontuacao?.bloco_d || { total: consultor.bloco_d || 0 };
|
||||||
const pontuacaoTotal = (blocoA.total || 0) + (blocoB.total || 0) + (blocoC.total || 0) + (blocoD.total || 0);
|
const pontuacaoTotal = (blocoA.total || 0) + (blocoB.total || 0) + (blocoC.total || 0) + (blocoD.total || 0);
|
||||||
|
|
||||||
const selos = gerarSelos(consultor);
|
const selos = useMemo(() => gerarSelos(consultor), [consultor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={cardRef} className={`ranking-card ${expanded ? 'expanded' : ''} ${highlight ? 'highlight' : ''} ${selecionado ? 'selecionado' : ''}`} onClick={() => setExpanded(!expanded)}>
|
<div ref={cardRef} className={`ranking-card ${expanded ? 'expanded' : ''} ${highlight ? 'highlight' : ''} ${selecionado ? 'selecionado' : ''}`} onClick={() => setExpanded(!expanded)}>
|
||||||
@@ -421,9 +422,11 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
const BlocoDetalhes = ({ titulo, bloco, cor }) => (
|
ConsultorCard.displayName = 'ConsultorCard';
|
||||||
|
|
||||||
|
const BlocoDetalhes = memo(({ titulo, bloco, cor }) => (
|
||||||
<div className="detail-section">
|
<div className="detail-section">
|
||||||
<h4 style={{ color: cor }}>{titulo}</h4>
|
<h4 style={{ color: cor }}>{titulo}</h4>
|
||||||
<div className="score-breakdown">
|
<div className="score-breakdown">
|
||||||
@@ -468,6 +471,8 @@ const BlocoDetalhes = ({ titulo, bloco, cor }) => (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
));
|
||||||
|
|
||||||
|
BlocoDetalhes.displayName = 'BlocoDetalhes';
|
||||||
|
|
||||||
export default ConsultorCard;
|
export default ConsultorCard;
|
||||||
|
|||||||
Reference in New Issue
Block a user