Busca de motoristas com raio crescente e notificações inteligentes
Sistema completo para encontrar o motorista ideal em tempo real. Expande o raio de busca a cada minuto, notifica cada motorista no máximo duas vezes e integra Firebase Realtime Database, GeoFire, Google Maps Platform e Expo Notifications.
Arquitetura
Toda a stack é integrada: Firebase Realtime Database guarda posições em tempo real via GeoFire, o backend PHP persiste corridas e tokens no MySQL, e o Expo Notifications entrega push para os dispositivos.
Armazena posições dos motoristas online com geohash, corridas pendentes e controle de notificações.
Persistência de corridas, motoristas, tokens de push e histórico financeiro. Endpoints REST para o app.
Consultas geográficas eficientes via geohash. Encontra motoristas dentro de um raio específico.
Envio de notificações push para iOS e Android via Expo Push API com suporte a FCM v1.
Infraestrutura
Serviços base que sustentam o sistema. Todos já configurados no projeto.
| Serviço | Onde | Estado |
|---|---|---|
| Firebase Realtime DB | Projeto federal-driver |
Ativo |
| Regras com índice | Realtime DB → Rules | Precisa adicionar |
Tabela corridas |
MySQL · cPanel | Adicionar colunas |
Tabela corridas_notificacoes |
MySQL · cPanel | Criar |
| Backend PHP | /backend/api/ |
Existe |
| Chave REST Google Maps | app.config.ts → extra.google |
Configurada |
| FCM v1 (Service Account) | EAS Credentials | Configurada |
Bibliotecas
Pacotes npm necessários no app React Native. Todos compatíveis com Expo SDK 57.
| Biblioteca | Propósito | Estado |
|---|---|---|
geofire-common |
Funções puras: geohashForLocation, distanceBetween, geohashQueryBounds |
Instalado |
geofire |
Wrapper para queries prontas com new GeoFire(ref).query() — opcional |
Recomendado |
firebase (v9+) |
SDK modular para Realtime DB, Auth, Functions | Instalado |
expo-location |
Obter GPS do dispositivo em foreground e background | Instalado |
expo-notifications |
Push notifications nativas com suporte a FCM v1 | Instalado |
react-native-maps |
Renderizar mapa nativo (alternativa ao Leaflet/WebView) | Instalado |
geofire-common fornece apenas as funções de cálculo (geohash, distância, bounds).
Para usar a API new GeoFire(ref).query(...), instale também o pacote geofire.
Google Maps Platform
As APIs REST do Google complementam o GeoFire. O GeoFire encontra motoristas por raio; as APIs do Google calculam rotas, endereços e distâncias.
| API | Função | Free tier / mês |
|---|---|---|
| Geocoding API | Converter endereço ↔ coordenadas (reverse geocode) | 10.000 chamadas |
| Directions API | Calcular rota A→B com polyline codificada | 10.000 chamadas |
| Distance Matrix API | Distância e tempo entre múltiplos pontos | 10.000 chamadas |
| Places API | Autocomplete de endereços, detalhes de locais | 10.000 chamadas |
| Static Maps API | Imagem do mapa com rota (usado no Confirmar Viagem) | 100.000 chamadas |
console.cloud.google.com/billing para evitar surpresas.
Tabelas SQL
Ajustes necessários na base MySQL para suportar o sistema de raio crescente.
-- Adicionar colunas de controle à tabela corridas ALTER TABLE corridas ADD COLUMN raio_atual INT DEFAULT 2000, ADD COLUMN rodada_atual INT DEFAULT 0, ADD COLUMN ultima_expansao DATETIME NULL, ADD COLUMN aceita_em DATETIME NULL; -- Tabela de controle de notificações (máx 2x por motorista) CREATE TABLE corridas_notificacoes ( id INT AUTO_INCREMENT PRIMARY KEY, corrida_id VARCHAR(50) NOT NULL, motorista_id VARCHAR(50) NOT NULL, vezes TINYINT DEFAULT 1, primeira_em DATETIME NOT NULL, ultima_em DATETIME NOT NULL, UNIQUE KEY uniq_corrida_motorista (corrida_id, motorista_id), INDEX idx_corrida (corrida_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Índices para performance CREATE INDEX idx_motoristas_online ON motoristas (online, ativo, status);
Regras Firebase
Adicione o índice .indexOn no nó de motoristas online. Sem isto, o Firebase baixa
todos os dados para o cliente e filtra localmente — lento e caro.
{
"rules": {
"motoristas_online_geofire": {
".indexOn": "g"
},
"corridas": {
".indexOn": ["status", "criadoEm"]
}
}
}
g (geohash) e l (array [lat, lng]).
O índice .indexOn: "g" permite queries por prefixo de geohash.
Fluxo Completo
Do momento em que o passageiro pede uma corrida até o motorista aceitar.
salvar_corridas.php → MySQL grava → Firebase recebe nó corridas/{id}
status: "aceita" → loop para automaticamente
Lógica PHP
Alternativa sem Cloud Functions. Executa dentro da requisição HTTP com set_time_limit().
Ideal para baixo volume de corridas simultâneas.
// Após gravar a corrida e obter $corrida_id function haversineMetros($lat1, $lon1, $lat2, $lon2) { $R = 6371000; $dLat = deg2rad($lat2 - $lat1); $dLon = deg2rad($lon2 - $lon1); $a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon/2)**2; return 2 * $R * asin(sqrt($a)); } $tempo_inicial = time(); $tempo_maximo = 420; // 7 minutos $raio_inicial = 2000; // 2 km $incremento = 2000; // +2 km por minuto while ((time() - $tempo_inicial) < $tempo_maximo) { // Verifica se a corrida já foi aceita $stmt = $pdo->prepare("SELECT status FROM corridas WHERE id = ?"); $stmt->execute([$corrida_id]); if ($stmt->fetchColumn() !== 'buscando_motorista') break; // Calcula raio atual $decorrido = time() - $tempo_inicial; $raio_atual = $raio_inicial + floor($decorrido / 60) * $incremento; // Busca motoristas online e filtra por raio $motoristas = $pdo->query( "SELECT user_id, expo, fcm_token, latitude, longitude FROM motoristas WHERE online = 1 AND ativo = 1" )->fetchAll(); foreach ($motoristas as $m) { $d = haversineMetros($origem_lat, $origem_lng, $m['latitude'], $m['longitude']); if ($d > $raio_atual) continue; // Verifica quantas vezes já foi notificado $st = $pdo->prepare("SELECT vezes FROM corridas_notificacoes WHERE corrida_id = ? AND motorista_id = ?"); $st->execute([$corrida_id, $m['user_id']]); $vezes = (int) $st->fetchColumn(); if ($vezes >= 2) continue; // Envia push via Expo enviarPushMotorista( $m['expo'] ?: $m['fcm_token'], $corrida_id, $vezes === 0 ? 'Nova corrida' : 'Corrida ainda disponível', 'A ' . round($d) . 'm de você' ); // Registra no MySQL $pdo->prepare("INSERT INTO corridas_notificacoes (corrida_id, motorista_id, vezes, primeira_em, ultima_em) VALUES (?, ?, 1, NOW(), NOW()) ON DUPLICATE KEY UPDATE vezes = vezes + 1, ultima_em = NOW()") ->execute([$corrida_id, $m['user_id']]); } sleep(60); }
App React Native
Código do motorista para salvar a posição com geohash e do passageiro para consultar motoristas próximos.
import * as Location from 'expo-location'; import { ref, set } from 'firebase/database'; import * as geofire from 'geofire-common'; import { database } from './firebaseConnection'; export async function atualizarPosicao(userId) { const { coords } = await Location.getCurrentPositionAsync({}); const { latitude, longitude, heading } = coords; // Gera o geohash para busca por raio const geohash = geofire.geohashForLocation([latitude, longitude]); await set(ref(database, `motoristas_online_geofire/${userId}`), { g: geohash, // geohash (indexado) l: [latitude, longitude], // [lat, lng] heading, updatedAt: Date.now(), }); }
import { getDatabase, ref, get } from 'firebase/database'; import * as geofire from 'geofire-common'; async function buscarMotoristasProximos(origemLat, origemLng, raioKm) { const center = [origemLat, origemLng]; const radiusInM = raioKm * 1000; // Gera bounds de geohash para o raio const bounds = geofire.geohashQueryBounds(center, radiusInM); const db = getDatabase(); const promises = []; for (const b of bounds) { const q = query( ref(db, 'motoristas_online_geofire'), orderByChild('g'), startAt(b[0]), endAt(b[1]) ); promises.push(get(q)); } const snapshots = await Promise.all(promises); const motoristas = []; for (const snap of snapshots) { snap.forEach((child) => { const data = child.val(); const [lat, lng] = data.l; const distanceKm = geofire.distanceBetween(center, [lat, lng]); if (distanceKm <= raioKm) { motoristas.push({ id: child.key, latitude: lat, longitude: lng, distanciaKm: distanceKm, }); } }); } // Ordena do mais próximo ao mais distante motoristas.sort((a, b) => a.distanciaKm - b.distanciaKm); return motoristas; }
Checklist de Implementação
Ordem recomendada para implementar tudo do zero.
federal-driverexpo na tabela usuarios com o token ExpoPush.indexOn: "g" nas regras do Firebasecorridas_notificacoes no MySQLraio_atual, rodada_atual, ultima_expansao na tabela corridasgeofire no app (opcional, se quiser usar a API pronta)atualizarPosicao() no app do motorista com geohashbuscarMotoristasProximos() no app do passageiro