diff --git a/services/BotService.php b/services/BotService.php index b2b6291..70572bf 100644 --- a/services/BotService.php +++ b/services/BotService.php @@ -21,9 +21,29 @@ class BotService { if ($messageType !== 'text') { return; } - + $messageText = trim($messageText); $phoneNumber = $user['phone_number']; + $userId = $user['id'] ?? null; + + // ── Advisory lock por usuario ───────────────────────────────────────── + // Serializar el procesamiento para el mismo usuario y evitar race conditions + // (WhatsApp puede entregar el mismo webhook dos veces en paralelo). + $lockAcquired = false; + if ($userId) { + $lockKey = 'bot_user_' . $userId; + $lockRow = $this->db->fetch("SELECT GET_LOCK(:k, 10) as ok", ['k' => $lockKey]); + $lockAcquired = !empty($lockRow['ok']); + if (!$lockAcquired) { + error_log("[BotService] processMessage - lock timeout para user_id={$userId}, ignorando llamada concurrente"); + return; + } + // Recargar datos frescos del usuario después de adquirir el lock + $fresh = $this->db->fetch("SELECT * FROM users WHERE id = :id", ['id' => $userId]); + if ($fresh) $user = $fresh; + } + + try { // Comprobar en la base de datos el estado más reciente de 'in_service' para evitar condiciones de carrera try { @@ -393,6 +413,13 @@ class BotService { // ignore } } + + } finally { + // Liberar advisory lock si fue adquirido + if ($lockAcquired && isset($lockKey)) { + try { $this->db->query("DO RELEASE_LOCK(:k)", ['k' => $lockKey]); } catch (Exception $e) {} + } + } } /**