Initial commit
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
# LeadScraper — Arquitetura e Schema (v1.1)
|
||||
|
||||
Documento de arquitetura de referência. Qualquer mudança de schema ou de stack deve ser refletida aqui primeiro (versionar para v1.1, v1.2...) antes de alterar código.
|
||||
|
||||
---
|
||||
|
||||
## 1. Stack tecnológica
|
||||
|
||||
### Backend
|
||||
- **Java 21**
|
||||
- **Spring Boot 3.x** — REST API, injeção de dependência, configuração
|
||||
- **Spring Data JPA + Hibernate** — persistência
|
||||
- **Flyway** — migrations versionadas do banco
|
||||
- **PostgreSQL 16** — banco relacional principal
|
||||
- **Playwright for Java** — automação de navegador (Chromium headless) para o scraping do Google Maps
|
||||
- **Spring Security + JWT** — autenticação da API (dashboard interno, não público)
|
||||
- **Bean Validation (Jakarta Validation)** — validação de entrada (CNPJ, telefone, etc.)
|
||||
- **Lombok** — redução de boilerplate
|
||||
- **Maven** — build e gerenciamento de dependências
|
||||
|
||||
### Frontend
|
||||
- **React 18 + Vite + TypeScript**
|
||||
- **Tailwind CSS + shadcn/ui** — componentes e estilo
|
||||
- **TanStack Query** — cache e sincronização com a API
|
||||
- **React Hook Form + Zod** — formulários e validação
|
||||
- **Recharts** — gráficos simples (ex: leads por ramo, buscas por dia)
|
||||
|
||||
### Infraestrutura
|
||||
- **Docker + Docker Compose** — orquestração de todos os serviços
|
||||
- **Nginx** — servir o build estático do frontend dentro do container
|
||||
- **Volumes Docker** — persistência de dados do PostgreSQL entre reinicializações
|
||||
|
||||
---
|
||||
|
||||
## 2. Arquitetura em módulos (backend)
|
||||
|
||||
```
|
||||
com.seconectai.leadscraper
|
||||
├── config/ → segurança, CORS, configuração do Playwright
|
||||
├── domain/ → entidades JPA (Lead, Ramo, Busca, Proxy, Telefone, HistoricoContato)
|
||||
├── repository/ → interfaces Spring Data JPA
|
||||
├── scraper/ → motor de automação do navegador (busca, scroll, extração bruta)
|
||||
├── normalizer/ → padronização de nome, telefone, CNPJ, ramo
|
||||
├── proxy/ → gestão do pool de proxies, rotação, backoff
|
||||
├── service/ → orquestração das regras de negócio
|
||||
├── controller/ → endpoints REST
|
||||
├── security/ → autenticação JWT
|
||||
└── worker/ → processamento assíncrono das buscas em fila (status pendente → em_execucao → concluida)
|
||||
```
|
||||
|
||||
## 3. Fluxo de execução de uma busca
|
||||
|
||||
1. Usuário informa `termo_busca` + `localizacao` pelo frontend
|
||||
2. Backend cria um registro em `buscas` com status `pendente`
|
||||
3. `worker` pega a busca pendente, muda status para `em_execucao`, seleciona um proxy disponível no pool
|
||||
4. `scraper` abre o navegador com o proxy configurado, executa a busca no Maps, faz scroll gradual com delays humanizados até esgotar os resultados, extrai dados brutos de cada card
|
||||
5. `normalizer` padroniza cada lead extraído (nome, telefone, CNPJ, ramo) e calcula `hash_dedup`
|
||||
6. `service` verifica duplicidade (por `hash_dedup` e `place_id`); se já existe, atualiza vínculo com a nova busca em vez de duplicar
|
||||
7. Leads novos são persistidos; `busca.total_encontrados` é atualizado; status muda para `concluida` (ou `erro` se algo falhar, com detalhe do erro registrado)
|
||||
8. Frontend consulta o status via polling (TanStack Query) e exibe os leads assim que disponíveis
|
||||
|
||||
## 4. Schema do banco de dados
|
||||
|
||||
### 4.1 `usuarios` — acesso ao dashboard
|
||||
|
||||
```sql
|
||||
CREATE TABLE usuarios (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nome VARCHAR(150) NOT NULL,
|
||||
email VARCHAR(150) NOT NULL UNIQUE,
|
||||
senha_hash VARCHAR(255) NOT NULL,
|
||||
papel VARCHAR(20) NOT NULL DEFAULT 'admin', -- admin | operador
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
### 4.2 `ramos` — taxonomia normalizada de categorias
|
||||
|
||||
```sql
|
||||
CREATE TABLE ramos (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nome_canonico VARCHAR(100) NOT NULL UNIQUE,
|
||||
categoria_pai_id BIGINT REFERENCES ramos(id),
|
||||
aliases JSONB NOT NULL DEFAULT '[]',
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
### 4.3 `proxies` — pool de proxies para rotação
|
||||
|
||||
```sql
|
||||
CREATE TABLE proxies (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
endereco VARCHAR(255) NOT NULL,
|
||||
tipo VARCHAR(20) DEFAULT 'residencial', -- residencial | datacenter
|
||||
status VARCHAR(20) DEFAULT 'ativo', -- ativo | degradado | bloqueado
|
||||
total_bloqueios INTEGER DEFAULT 0,
|
||||
em_uso BOOLEAN NOT NULL DEFAULT false,
|
||||
ultima_utilizacao TIMESTAMPTZ,
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
### 4.4 `buscas` — cada execução do motor de busca
|
||||
|
||||
```sql
|
||||
CREATE TABLE buscas (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
termo_busca VARCHAR(150) NOT NULL,
|
||||
localizacao VARCHAR(150) NOT NULL,
|
||||
ramo_id BIGINT REFERENCES ramos(id),
|
||||
proxy_id BIGINT REFERENCES proxies(id),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pendente', -- pendente | em_execucao | concluida | erro
|
||||
total_encontrados INTEGER DEFAULT 0,
|
||||
detalhe_erro TEXT,
|
||||
iniciado_em TIMESTAMPTZ,
|
||||
finalizado_em TIMESTAMPTZ,
|
||||
criado_por BIGINT REFERENCES usuarios(id),
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
### 4.5 `leads` — tabela principal
|
||||
|
||||
```sql
|
||||
CREATE TABLE leads (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
place_id VARCHAR(150),
|
||||
razao_social VARCHAR(200),
|
||||
nome_fantasia VARCHAR(200) NOT NULL,
|
||||
cnpj CHAR(14),
|
||||
telefone_e164 VARCHAR(20),
|
||||
telefone_bruto VARCHAR(40),
|
||||
email VARCHAR(150),
|
||||
site VARCHAR(255),
|
||||
endereco_completo TEXT,
|
||||
logradouro VARCHAR(200),
|
||||
numero VARCHAR(20),
|
||||
bairro VARCHAR(100),
|
||||
cidade VARCHAR(100),
|
||||
uf CHAR(2),
|
||||
cep CHAR(8),
|
||||
latitude NUMERIC(10,7),
|
||||
longitude NUMERIC(10,7),
|
||||
ramo_id BIGINT REFERENCES ramos(id),
|
||||
categoria_bruta VARCHAR(150),
|
||||
fonte VARCHAR(30) NOT NULL DEFAULT 'google_maps',
|
||||
busca_id BIGINT REFERENCES buscas(id),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'novo', -- novo | contatado | qualificado | convertido | descartado
|
||||
hash_dedup VARCHAR(64) NOT NULL UNIQUE,
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_leads_ramo ON leads(ramo_id);
|
||||
CREATE INDEX idx_leads_cidade_uf ON leads(cidade, uf);
|
||||
CREATE INDEX idx_leads_busca ON leads(busca_id);
|
||||
CREATE UNIQUE INDEX idx_leads_cnpj_unique ON leads(cnpj) WHERE cnpj IS NOT NULL;
|
||||
```
|
||||
|
||||
### 4.6 `telefones` (opcional — múltiplos telefones por lead)
|
||||
|
||||
```sql
|
||||
CREATE TABLE telefones (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
lead_id BIGINT NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
|
||||
numero_e164 VARCHAR(20) NOT NULL,
|
||||
tipo VARCHAR(20) DEFAULT 'principal',
|
||||
UNIQUE (lead_id, numero_e164)
|
||||
);
|
||||
```
|
||||
|
||||
### 4.7 `historico_contato` — pipeline de acompanhamento
|
||||
|
||||
```sql
|
||||
CREATE TABLE historico_contato (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
lead_id BIGINT NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
|
||||
canal VARCHAR(30), -- whatsapp | ligacao | email
|
||||
resultado VARCHAR(50),
|
||||
observacao TEXT,
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
## 5. Regras de padronização (resumo — detalhado na etapa 04)
|
||||
|
||||
- **Nome:** trim, colapsar espaços, Title Case preservando siglas societárias (LTDA, ME, EIRELI, S/A)
|
||||
- **Telefone:** apenas dígitos, assumir DDI +55 se ausente, formato final E.164, manter bruto para auditoria
|
||||
- **CNPJ:** apenas dígitos, validar dígito verificador (módulo 11) antes de gravar
|
||||
- **Ramo:** nunca gravar categoria bruta como definitiva — mapear via tabela `ramos.aliases`; sem mapeamento conhecido, `ramo_id = NULL` e categoria fica em `categoria_bruta` para curadoria manual
|
||||
- **Deduplicação:** `hash_dedup = SHA-256(nome_normalizado + '|' + telefone_e164)`; `place_id` como critério secundário
|
||||
|
||||
## 6. Anti-bloqueio (resumo — detalhado na etapa 05)
|
||||
|
||||
- Pool de proxies residenciais com rotação por sessão de busca (não por requisição)
|
||||
- Delays humanizados aleatórios entre ações e entre buscas
|
||||
- Scroll gradual em passos pequenos, não pulo direto
|
||||
- Fingerprint variável (User-Agent, viewport, timezone) por contexto de navegador
|
||||
- Detecção de captcha/bloqueio com backoff exponencial e desativação temporária do proxy afetado
|
||||
- Limite de volume auto-imposto por proxy/hora
|
||||
|
||||
## 7. Docker Compose — visão geral dos serviços
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres: # banco de dados
|
||||
backend: # API Spring Boot + motor de scraping
|
||||
frontend: # build React servido via Nginx
|
||||
```
|
||||
|
||||
Detalhes completos do `docker-compose.yml` estão na etapa 09.
|
||||
|
||||
## 8. Histórico de versões deste documento
|
||||
|
||||
- **v1.1** (atual) — adiciona `proxies.em_uso` para reserva concorrente do pool de proxies na etapa 05
|
||||
- **v1.0** — schema inicial completo: leads, ramos, buscas, proxies, telefones, historico_contato, usuarios; stack backend/frontend/infra definida
|
||||
Reference in New Issue
Block a user