docs: Adiciona resumo completo do projeto de ranking

Documenta:
- Arquitetura implementada (4 componentes)
- Estrutura de código (Clean Architecture)
- Problema atual (Componente B = 0 por rede)
- Soluções propostas (script standalone)
- Comandos úteis e validações
- Status: 95% completo, falta resolver acesso rede CAPES
This commit is contained in:
Frederico Castro
2025-12-10 05:28:34 -03:00
parent 178fc2ad53
commit d1379b4f5c
4 changed files with 619 additions and 0 deletions

473
RESUMO_PROJETO.md Normal file
View File

@@ -0,0 +1,473 @@
# Resumo do Projeto - Ranking de 350k Consultores CAPES
## O QUE FOI FEITO
### ✅ Sistema de Ranking Completo Implementado
**Objetivo:** Classificar TODOS os 300k+ consultores CAPES (governo federal, vai para auditoria)
**Arquitetura implementada:**
```
Elasticsearch (ATUACAPES) → Busca 350k consultores com atuações
Oracle REMOTO (CAPES) → Busca coordenações PPG (SUCUPIRA_PAINEL)
Backend Python → Calcula pontuação A+B+C+D
Oracle LOCAL (Docker) → Salva em TB_RANKING_CONSULTOR (cache)
Frontend → Lê tabela paginada (50 por página)
```
---
## COMPONENTES DE PONTUAÇÃO (Documento Oficial CAPES)
### Componente A - Coordenação CAPES (máx 450 pts)
- CA, CAJ, CAJ-MP, CAM
- Base + tempo + áreas extras + bônus ativo + retorno
- **✅ IMPLEMENTADO E FUNCIONANDO**
### Componente B - Coordenação de Programa PPG (máx 180 pts)
- Base: 70 pts
- Tempo: 5 pts/ano (máx 50)
- Programas extras: 20 pts/programa (máx 40)
- Nota PPG: 7=20, 6=15, 5=10, 4=5, 3=0 (máx 20)
- **✅ CÓDIGO IMPLEMENTADO MAS NÃO FUNCIONA** (problema de rede)
### Componente C - Consultoria (máx 230 pts)
- Consultor ativo/histórico + tempo + eventos + áreas
- **✅ IMPLEMENTADO E FUNCIONANDO**
### Componente D - Premiações (máx 180 pts)
- Premiações recebidas + avaliações + inscrições
- **✅ IMPLEMENTADO E FUNCIONANDO**
---
## ARQUITETURA DO CÓDIGO
### Camadas (Clean Architecture)
```
backend/
├── src/
│ ├── domain/ # Entidades e regras de negócio
│ │ ├── entities/
│ │ │ └── consultor.py # Consultor, Coordenação, Consultoria, etc
│ │ ├── services/
│ │ │ └── calculador_pontuacao.py # CÁLCULO DOS 4 COMPONENTES
│ │ └── value_objects/
│ │ └── pontuacao.py # ComponentePontuacao, PontuacaoCompleta
│ │
│ ├── application/ # Casos de uso e jobs
│ │ ├── use_cases/
│ │ │ └── obter_ranking.py
│ │ └── jobs/
│ │ ├── processar_ranking.py # JOB PRINCIPAL (350k)
│ │ ├── job_status.py # Status em tempo real
│ │ └── scheduler.py # Loop asyncio (sem cron)
│ │
│ ├── infrastructure/ # Acesso a dados
│ │ ├── elasticsearch/
│ │ │ └── client.py # Scroll API para 350k
│ │ ├── oracle/
│ │ │ ├── client.py # OracleClient genérico
│ │ │ └── ranking_repository.py # CRUD da TB_RANKING_CONSULTOR
│ │ └── repositories/
│ │ └── consultor_repository_impl.py # Constrói consultores
│ │
│ └── interface/ # API REST
│ └── api/
│ ├── app.py # FastAPI + lifespan (conecta Oracles)
│ ├── routes.py # Endpoints
│ ├── dependencies.py # DUAS CONEXÕES ORACLE
│ └── config.py # Settings (lê .env)
├── sql/
│ ├── schema_ranking.sql # TB_RANKING_CONSULTOR + SP_ATUALIZAR_POSICOES
│ └── schema_ppg.sql # TB_COORDENACAO_PROGRAMA (não usado)
└── scripts/
└── popular_componente_b.py # Script emergencial (roda no host)
```
---
## BANCO DE DADOS
### TB_RANKING_CONSULTOR (Oracle LOCAL)
```sql
CREATE TABLE TB_RANKING_CONSULTOR (
ID_PESSOA NUMBER(10) PRIMARY KEY,
NOME VARCHAR2(200),
POSICAO NUMBER(10),
PONTUACAO_TOTAL NUMBER(10,2),
COMPONENTE_A NUMBER(10,2),
COMPONENTE_B NUMBER(10,2), PROBLEMA: Está zerado
COMPONENTE_C NUMBER(10,2),
COMPONENTE_D NUMBER(10,2),
ATIVO CHAR(1),
ANOS_ATUACAO NUMBER(5,1),
DT_CALCULO TIMESTAMP,
JSON_DETALHES CLOB
);
```
**Status atual:** 350.215 registros com A, C, D calculados, mas B=0
---
## CONFIGURAÇÃO (.env)
### Desenvolvimento (atual)
```bash
# Elasticsearch
ES_URL=http://elastic-atuacapes.hom.capes.gov.br:9200
ES_INDEX=atuacapes
ES_USER=admin-atuacapes
ES_PASSWORD=O}!S0bj%FhJ:
# Oracle LOCAL (Docker) - Para salvar ranking
ORACLE_LOCAL_USER=local123
ORACLE_LOCAL_PASSWORD=local123
ORACLE_LOCAL_DSN=oracle18c:1521/XEPDB1
# Oracle REMOTO (CAPES) - Para ler SUCUPIRA_PAINEL
ORACLE_REMOTE_USER=FREDERICOAC
ORACLE_REMOTE_PASSWORD=FREDEricoac
ORACLE_REMOTE_DSN=oracledhtsrv02.hom.capes.gov.br:1521/hom_dr
```
### Produção (futuro)
```bash
# Mesmo Oracle para tudo
ORACLE_LOCAL_DSN=oracle-prod.capes:1521/PROD
ORACLE_REMOTE_DSN=oracle-prod.capes:1521/PROD # Mesmo!
```
---
## COMMITS IMPORTANTES
```
f69bcd9 - feat: Implementa job de ranking para 300k consultores
c6aaf66 - refactor: Substitui APScheduler por asyncio nativo para OCP
e11cdcd - feat: Implementa duas conexões Oracle simultâneas
57ef5a7 - fix: Corrige cálculo de pontuação da nota do PPG no Componente B
178fc2a - docs: Adiciona documentação completa do Componente B (PPG)
```
---
## O PROBLEMA ATUAL
### Componente B = 0 para todos os 350k consultores
**Causa raiz:**
Container Docker **não consegue acessar** `oracledhtsrv02.hom.capes.gov.br` porque:
- VPN CAPES está no HOST
- Container está em rede isolada
- DNS não resolve hostname interno da CAPES
**Evidências:**
```bash
# Dentro do container:
oracle_remote_client.is_connected = False
# Log do erro:
AVISO Oracle: ORA-12154: TNS:could not resolve the connect identifier specified
```
**Resultado:**
- Job processa 350k consultores ✅
- Componente A, C, D calculados ✅
- Componente B = 0 (não consulta Oracle REMOTO) ❌
---
## SOLUÇÕES POSSÍVEIS
### ⭐ SOLUÇÃO 1: Script Standalone (RECOMENDADA - RÁPIDA)
**Arquivo criado:** `backend/scripts/popular_componente_b.py`
**Como funciona:**
- Roda DIRETO NO HOST (não no Docker)
- Usa VPN do host para acessar Oracle CAPES
- Lê 350k IDs da tabela
- Busca PPG do SUCUPIRA_PAINEL
- Atualiza COMPONENTE_B em batch de 1000
- Atualiza posições
**Executar:**
```bash
cd /home/fred/projetos/ranking/backend
pip3 install cx-Oracle # Se não tiver
python3 scripts/popular_componente_b.py
```
**Tempo estimado:** 20-40 minutos para 350k consultores
**Vantagens:**
- ✅ Resolve AGORA sem mexer em Docker
- ✅ Usa VPN que já está funcionando
- ✅ Roda 1x para corrigir os dados
- ✅ Depois o job normal funciona (quando tiver em produção)
---
### SOLUÇÃO 2: Extra Hosts no Docker
```yaml
# docker-compose.yml
backend:
extra_hosts:
- "oracledhtsrv02.hom.capes.gov.br:IP_DO_SERVIDOR"
```
**Precisa:** Descobrir o IP real de `oracledhtsrv02.hom.capes.gov.br`
```bash
nslookup oracledhtsrv02.hom.capes.gov.br
```
---
### SOLUÇÃO 3: Network Mode Host
```yaml
# docker-compose.yml
backend:
network_mode: "host" # Usa rede do host
```
**Problema:** Perde isolamento, pode conflitar portas
---
## FRONTEND
### Funcionalidades Implementadas
**2 Modos de visualização:**
1. **Top N (Rápido)**
- Endpoint: `/api/v1/ranking/detalhado?limite=100`
- Busca do Elasticsearch + calcula na hora
- Bom para top 10/50/100/500
2. **Ranking Completo (300k)**
- Endpoint: `/api/v1/ranking/paginado?page=1&size=50`
-`TB_RANKING_CONSULTOR` direto
- Paginação: 350k consultores, 70k páginas
**Componentes:**
-`RankingPaginado.jsx` - Tabela com paginação
-`App.jsx` - Seletor de modo
- ✅ Barra de progresso do job em tempo real
- ✅ Estatísticas (total, ativos, média, distribuição)
- ✅ Botão reprocessar
**Acesso:** http://localhost:5173
---
## JOB DE PROCESSAMENTO
### Scheduler (asyncio nativo - sem cron)
**Arquivo:** `backend/src/application/jobs/scheduler.py`
**Como funciona:**
```python
# Calcula tempo até 3h da manhã
proxima_execucao = datetime.now().replace(hour=3, minute=0)
await asyncio.sleep(segundos_ate_proxima)
await job.executar(limpar_antes=True)
# Loop infinito
```
**Execução:**
- Automática: Diariamente às 3h
- Manual: `POST /api/v1/ranking/processar`
**Compatível com OCP/Kubernetes** (não usa cron do sistema)
---
## PERFORMANCE
**Máquina atual:**
- CPU: 5.8GHz boost
- RAM: 64GB DDR5
- Processamento: ~350k consultores em 25-30 minutos
**Batch:**
- 1.000 consultores por batch
- ~350 batches total
- MERGE (upsert) em batch
---
## O QUE PRECISA SER FEITO PARA CONCLUIR
### 🎯 ÚNICO PROBLEMA: Componente B zerado
**Situação:**
- ✅ Código do Componente B está correto e implementado
- ✅ Duas conexões Oracle configuradas
- ✅ Query SUCUPIRA_PAINEL pronta
- ❌ Container Docker não acessa rede CAPES
**Solução mais rápida:**
1. **Rodar script standalone:**
```bash
cd /home/fred/projetos/ranking/backend
python3 scripts/popular_componente_b.py
```
2. **Ou configurar extra_hosts no docker-compose.yml:**
```bash
# Descobrir IP:
nslookup oracledhtsrv02.hom.capes.gov.br
# Adicionar ao docker-compose.yml:
backend:
extra_hosts:
- "oracledhtsrv02.hom.capes.gov.br:IP_AQUI"
```
3. **Depois rodar job completo:**
```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"limpar_antes": true}' \
http://localhost:8000/api/v1/ranking/processar
```
---
## VALIDAÇÃO FINAL
Quando o Componente B funcionar, verificar:
```sql
-- Deve ter consultores com B > 0
SELECT
COUNT(*) AS TOTAL,
SUM(CASE WHEN COMPONENTE_B > 0 THEN 1 ELSE 0 END) AS COM_PPG,
MAX(COMPONENTE_B) AS MAX_B,
AVG(COMPONENTE_B) AS MEDIA_B
FROM TB_RANKING_CONSULTOR;
-- Ver top 5 com PPG
SELECT ID_PESSOA, NOME, COMPONENTE_A, COMPONENTE_B, COMPONENTE_C, COMPONENTE_D, PONTUACAO_TOTAL
FROM TB_RANKING_CONSULTOR
WHERE POSICAO <= 5
ORDER BY POSICAO;
```
**Esperado:** Alguns consultores com B entre 70-180 pts
---
## DOCUMENTAÇÃO GERADA
- `COMPONENTE_B_STATUS.md` - Implementação completa do Componente B
- `SCHEDULER.md` - Como funciona o scheduler sem cron (OCP)
- `backend/sql/schema_ranking.sql` - DDL da tabela principal
- `.claude/rules/ranking-*.md` - Regras oficiais e queries
---
## COMANDOS ÚTEIS
### Verificar tabela
```bash
echo "SELECT COUNT(*), AVG(COMPONENTE_B) FROM TB_RANKING_CONSULTOR;" | \
sqlplus -S local123/local123@127.0.0.1:1521/XEPDB1
```
### Rodar job manualmente
```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"limpar_antes": true}' \
http://localhost:8000/api/v1/ranking/processar
```
### Ver status
```bash
curl -s http://localhost:8000/api/v1/ranking/status | python3 -m json.tool
```
### Ver estatísticas
```bash
curl -s http://localhost:8000/api/v1/ranking/estatisticas | python3 -m json.tool
```
---
## BRANCH E COMMITS
**Branch:** `develop`
**Últimos commits:**
```
178fc2a - docs: Adiciona documentação completa do Componente B (PPG)
57ef5a7 - fix: Corrige cálculo de pontuação da nota do PPG no Componente B
e11cdcd - feat: Implementa duas conexões Oracle simultâneas
f69bcd9 - feat: Implementa job de ranking para 300k consultores
c6aaf66 - refactor: Substitui APScheduler por asyncio nativo para OCP
```
---
## RESUMO EXECUTIVO
**Status Geral:** 95% completo
**Funciona:**
- ✅ 350.215 consultores processados
- ✅ Componentes A, C, D calculados corretamente
- ✅ Paginação de 350k consultores
- ✅ Frontend com 2 modos (Top N + Completo)
- ✅ Scheduler asyncio para OCP
- ✅ Duas conexões Oracle configuradas
- ✅ Performance excelente (~30min para 350k)
**Não funciona (1 problema):**
- ❌ Componente B = 0 (Container não acessa rede CAPES)
**Para resolver:**
- Rodar `scripts/popular_componente_b.py` no host (20-40min)
- OU configurar DNS/extra_hosts no docker-compose.yml
- OU deploy em servidor dentro da rede CAPES
**Depois disso:** Sistema 100% funcional e pronto para produção.
---
## CONTEXTO IMPORTANTE
- Sistema para GOVERNO FEDERAL
- Vai passar por AUDITORIA
- Dados REAIS obrigatórios (zero mocks/fakes)
- Recursos CAPES intocáveis
- Documento técnico oficial deve ser seguido à risca
- Ambiente: OCP (sem cron disponível nos PODs)
---
## PRÓXIMA SESSÃO - O QUE FAZER
1. **Verificar VPN CAPES ativa**
2. **Rodar script:** `python3 backend/scripts/popular_componente_b.py`
3. **Validar:** Componente B > 0 em alguns consultores
4. **Testar frontend:** http://localhost:5173
5. **Commit final e push**
**Tempo estimado:** 30-60 minutos total

View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python3
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
import cx_Oracle
from datetime import datetime
ORACLE_LOCAL_USER = "local123"
ORACLE_LOCAL_PASSWORD = "local123"
ORACLE_LOCAL_DSN = "127.0.0.1:1521/XEPDB1"
ORACLE_REMOTE_USER = "FREDERICOAC"
ORACLE_REMOTE_PASSWORD = "FREDEricoac"
ORACLE_REMOTE_DSN = "oracledhtsrv02.hom.capes.gov.br:1521/hom_dr"
def calcular_componente_b(coordenacoes):
if not coordenacoes:
return 0
base = 70
anos_totais = 0
for c in coordenacoes:
inicio = c[7]
fim = c[8]
if fim:
anos = (fim - inicio).days // 365
else:
anos = (datetime.now() - inicio).days // 365
anos_totais += anos
tempo = min(int(anos_totais * 5), 50)
programas_distintos = len(set(c[1] for c in coordenacoes))
extras = min((programas_distintos - 1) * 20, 40) if programas_distintos > 1 else 0
maior_nota = 0
for c in coordenacoes:
nota_str = str(c[4]).strip() if c[4] else ""
if nota_str == '7':
maior_nota = max(maior_nota, 7)
elif nota_str == '6':
maior_nota = max(maior_nota, 6)
elif nota_str == '5':
maior_nota = max(maior_nota, 5)
elif nota_str == '4':
maior_nota = max(maior_nota, 4)
elif nota_str == '3':
maior_nota = max(maior_nota, 3)
mapa_nota = {7: 20, 6: 15, 5: 10, 4: 5, 3: 0}
bonus = mapa_nota.get(maior_nota, 0)
return base + tempo + extras + bonus
print("Conectando Oracle LOCAL...")
conn_local = cx_Oracle.connect(ORACLE_LOCAL_USER, ORACLE_LOCAL_PASSWORD, ORACLE_LOCAL_DSN)
print("Conectando Oracle REMOTO...")
conn_remote = cx_Oracle.connect(ORACLE_REMOTE_USER, ORACLE_REMOTE_PASSWORD, ORACLE_REMOTE_DSN)
cursor_local = conn_local.cursor()
cursor_remote = conn_remote.cursor()
print("Buscando consultores...")
cursor_local.execute("SELECT ID_PESSOA FROM TB_RANKING_CONSULTOR")
ids_pessoas = [row[0] for row in cursor_local.fetchall()]
print(f"Total: {len(ids_pessoas)} consultores")
processados = 0
com_ppg = 0
batch_updates = []
for id_pessoa in ids_pessoas:
try:
cursor_remote.execute("""
SELECT
c.ID_PESSOA,
c.ID_PROGRAMA_SNPG,
p.NM_PROGRAMA,
p.CD_PROGRAMA_PPG,
p.DS_CONCEITO,
p.NM_PROGRAMA_MODALIDADE,
aa.NM_AREA_AVALIACAO,
c.DT_INICIO_VIGENCIA,
c.DT_FIM_VIGENCIA
FROM SUCUPIRA_PAINEL.VM_COORDENADOR c
INNER JOIN SUCUPIRA_PAINEL.VM_PROGRAMA_SUCUPIRA p ON c.ID_PROGRAMA_SNPG = p.ID_PROGRAMA
LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_CONHECIMENTO ac ON p.ID_AREA_CONHECIMENTO_ATUAL = ac.ID_AREA_CONHECIMENTO
LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_AVALIACAO aa ON ac.ID_AREA_AVALIACAO = aa.ID_AREA_AVALIACAO
WHERE c.ID_PESSOA = :id_pessoa
""", {"id_pessoa": id_pessoa})
coordenacoes = cursor_remote.fetchall()
if coordenacoes:
comp_b = calcular_componente_b(coordenacoes)
batch_updates.append((comp_b, id_pessoa))
com_ppg += 1
processados += 1
if len(batch_updates) >= 1000:
cursor_local.executemany(
"UPDATE TB_RANKING_CONSULTOR SET COMPONENTE_B = :comp_b, PONTUACAO_TOTAL = COMPONENTE_A + :comp_b + COMPONENTE_C + COMPONENTE_D WHERE ID_PESSOA = :id_pessoa",
batch_updates
)
conn_local.commit()
print(f"Processados: {processados}/{len(ids_pessoas)} | Com PPG: {com_ppg}")
batch_updates = []
except Exception as e:
print(f"Erro no consultor {id_pessoa}: {e}")
if batch_updates:
cursor_local.executemany(
"UPDATE TB_RANKING_CONSULTOR SET COMPONENTE_B = :comp_b, PONTUACAO_TOTAL = COMPONENTE_A + :comp_b + COMPONENTE_C + COMPONENTE_D WHERE ID_PESSOA = :id_pessoa",
batch_updates
)
conn_local.commit()
print(f"\nFinalizado!")
print(f"Total processados: {processados}")
print(f"Com PPG: {com_ppg}")
print("\nAtualizando posições...")
cursor_local.callproc("SP_ATUALIZAR_POSICOES")
conn_local.commit()
cursor_local.close()
cursor_remote.close()
conn_local.close()
conn_remote.close()
print("Concluído!")

View File

@@ -395,8 +395,15 @@ class ConsultorRepositoryImpl(ConsultorRepository):
if self.oracle_client and self.oracle_client.is_connected: if self.oracle_client and self.oracle_client.is_connected:
try: try:
coordenacoes_programas_raw = self.oracle_client.buscar_coordenacoes_programa(int(id_pessoa)) coordenacoes_programas_raw = self.oracle_client.buscar_coordenacoes_programa(int(id_pessoa))
if coordenacoes_programas_raw:
print(f"DEBUG: Consultor {id_pessoa} tem {len(coordenacoes_programas_raw)} coordenações PPG")
except Exception as e: except Exception as e:
print(f"AVISO Oracle: erro ao buscar coordenacoes do programa para {id_pessoa}: {e}") print(f"AVISO Oracle: erro ao buscar coordenacoes do programa para {id_pessoa}: {e}")
else:
if not self.oracle_client:
print(f"DEBUG: oracle_client é None para consultor {id_pessoa}")
elif not self.oracle_client.is_connected:
print(f"DEBUG: oracle_client NÃO está conectado para consultor {id_pessoa}")
coordenacoes_programas = [ coordenacoes_programas = [
CoordenacaoPrograma( CoordenacaoPrograma(
id_programa=c["ID_PROGRAMA_SNPG"], id_programa=c["ID_PROGRAMA_SNPG"],

View File

@@ -17,6 +17,10 @@ services:
volumes: volumes:
- ./backend/src:/app/src - ./backend/src:/app/src
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
dns:
- 172.19.100.16
- 172.19.100.17
- 8.8.8.8
depends_on: depends_on:
oracle18c: oracle18c:
condition: service_healthy condition: service_healthy