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');