fix: deduplicar webhooks duplicados con INSERT IGNORE + UNIQUE index en message_id

- conversations.message_id ahora tiene UNIQUE index (aplicado en BD prod)
- saveMessage() usa INSERT IGNORE: si el mismo wamid llega dos veces en
  paralelo solo una llamada procesa el mensaje y llama al bot
- La segunda llamada recibe rowCount=0 y hace continue sin pasar al bot
- Elimina race condition que enviaba bienvenida + términos simultáneamente
This commit is contained in:
Lizandro Guarnizo
2026-03-13 19:22:34 -05:00
parent 80e9cb43db
commit bcfaf62293
+17 -2
View File
@@ -368,8 +368,14 @@ class WhatsAppWebhook {
// Filtrar campos según columnas existentes para evitar errores en esquemas antiguos
$saveData = $this->filterColumns('conversations', $saveData);
// Guardar mensaje y conservar el id de la conversación para posibles encolamientos
// Guardar mensaje con INSERT IGNORE para deduplicar webhooks duplicados.
// Si el message_id ya existe (segunda llamada paralela), rowCount=0 y saltamos el bot.
$conversationId = $this->saveMessage($saveData);
if (!$conversationId) {
// Mensaje ya procesado por una llamada concurrente, ignorar
error_log("[webhook] Duplicate webhook ignorado para message_id={$messageId}");
continue;
}
// Si el media se encoló (no se pudo descargar), actualizar media_queue con el conversation_id
if (!empty($mediaUrl) && empty($localFile) && in_array($messageType, ['image', 'audio', 'video', 'document', 'sticker'])) {
@@ -469,7 +475,16 @@ class WhatsAppWebhook {
}
private function saveMessage($messageData) {
return $this->db->insert('conversations', $messageData);
// INSERT IGNORE: si message_id ya existe (webhook duplicado), no inserta y devuelve 0
$cols = implode(', ', array_map(fn($c) => "`{$c}`", array_keys($messageData)));
$placeholders = implode(', ', array_map(fn($c) => ":{$c}", array_keys($messageData)));
$sql = "INSERT IGNORE INTO `conversations` ({$cols}) VALUES ({$placeholders})";
$stmt = $this->db->getConnection()->prepare($sql);
$stmt->execute($messageData);
if ($stmt->rowCount() === 0) {
return 0; // duplicado
}
return $this->db->getConnection()->lastInsertId();
}
/**