# 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