Rastreamento de Localização em Tempo Real com WebSockets, Redis e Mini Programas

Visão Geral da Arquitetura

Para construir um sistema de compartilhamento de localização em tempo real que exiba a posição dos usuários em um mapa, adotamos uma arquitetura distribuída. O front-end (Mini Programa) captura as coordenadas em segundo plano e as envia para uma API em PHP. O back end PHP publica essas atualizações em um canal Redis. Um serviço Python independente consome essas mensagens do Redis e as distribui para os painéis dos operadores através de WebSockets.

Configuração do Front-end (Mini Programa)

No lado do cliente, é necessário ativar o rastreamento em segundo plano. O aplicativo deve verificar as permissões do usuário e, em seguida, iniciar o listneer de mudanças de localização. Abaixo está uma implementação reestruturada utilizando promessas e nomenclatura alternativa:

methods: {
    async initializeBackgroundTracking() {
        try {
            const settings = await uni.getSetting();
            const hasBgPermission = settings.authSetting['scope.userLocationBackground'];
            
            if (!hasBgPermission) {
                await this.requestLocationPermission();
            } else {
                this.startTrackingListener();
            }
        } catch (error) {
            console.error('Erro ao verificar configurações:', error);
        }
    },

    async requestLocationPermission() {
        try {
            await uni.authorize({ scope: 'scope.userLocationBackground' });
            this.startTrackingListener();
        } catch (authError) {
            const modalRes = await uni.showModal({
                title: 'Permissão Necessária',
                content: 'Por favor, autorize o acesso à localização em segundo plano nas configurações.'
            });
            if (modalRes.confirm) {
                const openRes = await uni.openSetting();
                if (openRes.authSetting['scope.userLocationBackground']) {
                    this.startTrackingListener();
                }
            }
        }
    },

    startTrackingListener() {
        uni.startLocationUpdateBackground({
            success: () => {
                uni.onLocationChange((locationData) => {
                    this.$api.postData({
                        endpoint: '/api/tracking/update',
                        payload: locationData
                    });
                });
            },
            fail: (err) => {
                console.error('Falha ao iniciar rastreamento:', err);
            }
        });
    }
}

Declaração de Permissões

É obrigatório declarar as permissões de localização em segundo plano no arquivo de configuração do projeto (manifest.json):

"mp-weixin" : {
    "permissions": {
        "scope.userLocationBackground" : {
            "desc" : "Necessitamos de acesso contínuo à sua localização para fornecer serviços de rastreamento precisos."
        },
        "scope.userLocation" : {
            "desc" : "Permita o acesso à sua localização atual para funcionalidades básicas do mapa."
        }
    },
    "requiredPrivateInfos" : [
        "onLocationChange",
        "startLocationUpdateBackground"
    ],
    "requiredBackgroundModes" : [ "location" ]
}

Publicação de Dados via PHP

O endpoint da API recebe as coordenadas e as transmite para o barramento de mensagens. Utilizando o cliente Redis no PHP, a publicação é feita da seguinte forma:

$payload = json_encode([
    'user_id' => $userId,
    'lng' => $request->input('longitude'),
    'lat' => $request->input('latitude'),
    'timestamp' => time()
]);

Redis::connection('default')->publish('tracking_updates', $payload);

Servidor WebSocket em Python

O núcleo da distribuição em tempo real é um servidor assíncrono em Python. Ele mantém um mapeamento de conexões ativas por ID de usuário e escuta o canal Redis. Certifique-se de apontar o host do Redis para o IP correto do contêiner ou da máquina hospedeira.

#!/usr/bin/env python3
import asyncio
import json
import logging
from redis.asyncio import Redis
from websockets.server import serve
from typing import Dict, Set

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)

class RealTimeTracker:
    def __init__(self):
        self.active_clients: Dict[str, Set] = {}
        self.redis_client = None
        self.pubsub_channel = None
        self.redis_params = {
            'host': '172.17.0.1',
            'port': 6379,
            'decode_responses': True
        }

    async def connect_redis(self):
        for attempt in range(5):
            try:
                self.redis_client = Redis(**self.redis_params)
                await self.redis_client.ping()
                self.pubsub_channel = self.redis_client.pubsub()
                logger.info("Conexão com Redis estabelecida.")
                return True
            except Exception as e:
                logger.error(f"Tentativa {attempt + 1} falhou: {e}")
                await asyncio.sleep(3)
        return False

    async def listen_to_updates(self):
        while True:
            if not self.redis_client:
                if not await self.connect_redis():
                    continue
            
            try:
                await self.pubsub_channel.subscribe('tracking_updates')
                logger.info("Inscrito no canal tracking_updates.")
                
                async for msg in self.pubsub_channel.listen():
                    if msg['type'] == 'message':
                        await self.broadcast_to_clients(msg['data'])
            except Exception as e:
                logger.error(f"Erro no subscriber: {e}")
                await asyncio.sleep(5)

    async def broadcast_to_clients(self, raw_data):
        try:
            parsed_data = json.loads(raw_data)
            target_uid = str(parsed_data.get('user_id'))
            
            if target_uid in self.active_clients:
                disconnected_sockets = set()
                for ws in self.active_clients[target_uid]:
                    try:
                        await ws.send(json.dumps(parsed_data))
                    except Exception:
                        disconnected_sockets.add(ws)
                
                for dead_ws in disconnected_sockets:
                    self.active_clients[target_uid].discard(dead_ws)
                
                if not self.active_clients[target_uid]:
                    del self.active_clients[target_uid]
        except json.JSONDecodeError:
            logger.error("Mensagem JSON inválida recebida do Redis.")

    async def add_client(self, ws, uid):
        if uid not in self.active_clients:
            self.active_clients[uid] = set()
        self.active_clients[uid].add(ws)
        logger.info(f"Cliente registrado para UID: {uid}")

    async def remove_client(self, ws, uid):
        if uid in self.active_clients:
            self.active_clients[uid].discard(ws)
            if not self.active_clients[uid]:
                del self.active_clients[uid]
            logger.info(f"Cliente desconectado do UID: {uid}")

    async def handle_connection(self, ws):
        uid = None
        try:
            init_msg = await ws.recv()
            payload = json.loads(init_msg)
            uid = str(payload.get('user_id'))
            
            if not uid:
                await ws.close(1008, "user_id é obrigatório")
                return

            await self.add_client(ws, uid)
            async for _ in ws:
                pass
        except Exception as e:
            logger.error(f"Erro na conexão: {e}")
        finally:
            if uid:
                await self.remove_client(ws, uid)

async def start_server():
    tracker = RealTimeTracker()
    asyncio.create_task(tracker.listen_to_updates())
    
    async with serve(tracker.handle_connection, "0.0.0.0", 8765):
        logger.info("Servidor WebSocket rodando na porta 8765")
        await asyncio.Future()

if __name__ == "__main__":
    asyncio.run(start_server())

Integração no Painel do Operador

Por fim, a interface do operador estabelece uma conexão WebSocket para receber os fluxos de coordenadas e atualizar os marcadores no mapa:

class LocationSocketManager {
    constructor(url) {
        this.socket = new WebSocket(url);
        this.setupListeners();
    }

    setupListeners() {
        this.socket.addEventListener('open', () => {
            console.log('Conexão WebSocket estabelecida.');
            this.socket.send(JSON.stringify({ user_id: 'TARGET_USER_ID' }));
        });

        this.socket.addEventListener('message', (event) => {
            const coordinates = JSON.parse(event.data);
            this.updateMapMarker(coordinates);
        });

        this.socket.addEventListener('close', () => {
            console.log('Conexão encerrada.');
        });
    }

    updateMapMarker(data) {
        console.log(`Atualizando mapa: Lat ${data.lat}, Lng ${data.lng}`);
        // Lógica para atualizar o marcador no mapa (ex: Baidu Maps, Google Maps, etc.)
    }
}

const tracker = new LocationSocketManager('ws://api.exemplo.com:8765');

Tags: WebSockets Redis Python PHP WeChatMiniProgram

Publicado em 6-9 22:15 por Thomas