НейроАгент

Почему не работает ЮКасса: решение проблем с вебхуками

Проблемы с ЮКассой на shared хостинге: не обновляется база данных при оплате. Узнайте причины и решения для таймаутов MySQL и оптимизации вебхуков.

Вопрос

Почему перестала работать ЮКасса? На двух проектах одновременно перестали обновляться данные в базе данных MySQL при оплате через ЮКассу, хотя обработчик возвращает ответ 200 OK. Поддержка ЮКассы утверждает, что уведомления доходят успешно, но данные в базе не обновляются и не создаются записи о платежах.

Вот код обработчика callback от ЮКассы:

php
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/connect.php';
session_start();

// Устанавливаем часовой пояс Москвы (UTC+3)
date_default_timezone_set('Europe/Moscow');

$input = file_get_contents('php://input');
$data = json_decode($input, true);

// Проверка необходимых полей
if (!isset($data['event'], $data['object']['id'], $data['object']['status'], $data['object']['metadata']['nickname'])) {
    http_response_code(400);
    exit('Некорректный запрос');
}

$payment_id = $data['object']['id'];
$status = $data['object']['status'];
$nickname = $data['object']['metadata']['nickname'];

// Обработка только успешных платежей
if ($data['event'] !== 'payment.succeeded' || $status !== 'succeeded') {
    http_response_code(200);
    exit('Не требуется обработка');
}

// Защита от повторной обработки
$stmt = $db->prepare("SELECT id FROM payments WHERE payment_id = ?");
$stmt->bind_param("s", $payment_id);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
    http_response_code(200);
    exit('Уже обработано');
}
$stmt->close();

// Получение суммы
$amount = $data['object']['amount']['value'];

// Привязка тарифа по сумме
$tariffs = [
    '39.00' => ['subscribe_days' => 7, 'subscribe_rates' => 2],
    '97.00' => ['subscribe_days' => 7, 'subscribe_rates' => 3],
    '69.00' => ['subscribe_days' => 14, 'subscribe_rates' => 2],
    '176.00' => ['subscribe_days' => 14, 'subscribe_rates' => 3],
    '118.00' => ['subscribe_days' => 30, 'subscribe_rates' => 2],
    '249.00' => ['subscribe_days' => 30, 'subscribe_rates' => 3],
    '290.00' => ['subscribe_days' => 90, 'subscribe_rates' => 2],
    '689.00' => ['subscribe_days' => 90, 'subscribe_rates' => 3],
];

if (!isset($tariffs[$amount])) {
    http_response_code(400);
    exit('Неизвестная сумма');
}

$subscribe_days = $tariffs[$amount]['subscribe_days'];
$subscribe_rates = $tariffs[$amount]['subscribe_rates'];

// Получение текущей подписки пользователя
$stmt = $db->prepare("SELECT subscribe_date FROM users WHERE nickname = ?");
$stmt->bind_param("s", $nickname);
$stmt->execute();
$stmt->bind_result($current_subscribe_date);
$stmt->fetch();
$stmt->close();

// Вычисление новой даты подписки с временем
$now = new DateTime();
$subscribe_end = new DateTime($current_subscribe_date);
if ($now > $subscribe_end) {
    $new_subscribe_date = $now->modify("+$subscribe_days days")->format('Y-m-d H:i:s');
} else {
    $new_subscribe_date = $subscribe_end->modify("+$subscribe_days days")->format('Y-m-d H:i:s');
}

// Обновление данных пользователя
$stmt = $db->prepare("UPDATE users SET subscribe_date = ?, subscribe_rate = ? WHERE nickname = ?");
$stmt->bind_param("sis", $new_subscribe_date, $subscribe_rates, $nickname);
$stmt->execute();
$stmt->close();

// Сохранение информации о платеже
$stmt = $db->prepare("INSERT INTO payments (payment_id, nickname, amount, status) VALUES (?, ?, ?, ?)");
$stmt->bind_param("ssds", $payment_id, $nickname, $amount, $status);
$stmt->execute();
$stmt->close();

http_response_code(200);
echo 'OK';
?>

Тестовый файл, который корректно обрабатывает запросы:

php
<?php
/**
 * ТЕСТОВЫЙ ЭМУЛЯТОР CALLBACK YOOKASSA
 */

header('Content-Type: text/plain; charset=utf-8');

// URL куда отправляем "вебхук" — ваш обработчик Юкассы
$callbackUrl = 'https://site.ru/pay.php';

// тестовый JSON полностью повторяет структуру YooKassa
$fakeCallbackData = [
    "event"  => "payment.succeeded",
    "object" => [
        "id"     => "2e4f82c2-000f-5010-9000-29fca13db031", // любой ID (уникальный для теста)
        "status" => "succeeded",
        "paid"   => true,
        "amount" => [
            "value"    => "39.00",
            "currency" => "RUB"
        ],
        "metadata" => [
            "nickname" => "legolas"
        ],
        "description" => "Test payment",
    ]
];

// JSON в нужном виде как у Юкассы
$jsonData = json_encode($fakeCallbackData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

// Инициируем CURL
$ch = curl_init($callbackUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Content-Type: application/json",
    "Idempotence-Key: " . time(), // как у YooKassa
    "User-Agent: AHC/2.1",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HEADER, true); // чтобы получить заголовки ответа

// Выполняем запрос
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);

$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$responseHeaders = substr($response, 0, $headerSize);
$responseBody    = substr($response, $headerSize);

$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME);

curl_close($ch);

// ---- Вывод результатов ----
echo "===== ОТПРАВЛЕННЫЙ JSON =====\n$jsonData\n\n";
echo "===== URL КУДА ОТПРАВЛЕНО =====\n$callbackUrl\n\n";
echo "===== HTTP CODE =====\n$httpCode\n\n";
echo "===== ВРЕМЯ ЗАПРОСА =====\n{$totalTime} сек.\n\n";

echo "===== ОТВЕТНЫЕ ЗАГОЛОВКИ =====\n$responseHeaders\n\n";
echo "===== ТЕЛО ОТВЕТА PAY-NOTICE =====\n$responseBody\n\n";

if ($errno) {
    echo "===== CURL ERROR =====\n$errno: $error\n\n";
}

echo "===== ТЕСТ ЗАВЕРШЁН =====\n";
?>

При ручном тестировании всё работает корректно, но при реальных платежах данные не обновляются. Оба проекта находятся на хостинге Джино. Поддержка ЮКассы подтверждает получение уведомлений с кодом 200, но проблема сохраняется. В чём может быть причина?

НейроАгент

Проблема с ЮКассой на двух проектах одновременно на хостинге Джино, при обработчике, который возвращает 200 OK, но не обновляет базу данных, скорее всего связана с особенностями работы вебхуков на shared хостинге. Вот основные возможные причины и решения:

Содержание

Основные проблемы на shared хостинге

На shared хостингах, таких как Джино, вебхуки сталкиваются с несколькими типичными проблемами:

1. Тайм-ауты MySQL подключений

Исходя из исследований, самая вероятная причина - тайм-ауты соединений с MySQL. На shared хостингах серверы часто закрывают неактивные соединения для экономии ресурсов.

Проблема: Ваш обработчик выполняет несколько операций с базой данных, и если между ними проходит больше времени, чем разрешено MySQL-сервером, соединение разрывается. Последующие запросы выполняются в “слепую”, и скрипт завершается с кодом 200, но без фактического выполнения операций.

Решение: Добавьте проверку соединения перед каждым запросом:

php
// Функция проверки соединения
function check_connection($db) {
    try {
        if (!$db->ping()) {
            $db->close();
            require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/connect.php';
        }
    } catch (Exception $e) {
        require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/connect.php';
    }
}

// Использование в обработчике
check_connection($db);

2. Ограничения на выполнение скриптов

Shared хостинг часто имеет ограничения по времени выполнения и памяти. Вебхуки от ЮКассы могут вызываться с задержками, что увеличивает время выполнения.


Настройка таймаутов и подключений

1. Увеличение таймаутов в PHP

Добавьте в начало скрипта обработчика установку таймаутов:

php
// Установка таймаутов
set_time_limit(60); // 60 секунд вместо стандартных 30
ini_set('max_execution_time', 60);
ini_set('memory_limit', '256M');

2. Использование persistent соединений

Модифицируйте файл подключения к базе данных:

php
// В файле /inc/connect.php
$db = new mysqli('p:' . $db_host, $db_user, $db_password, $db_name, $db_port);

Примечание: Параметр p: перед hostname включаетpersistent соединения, которые не закрываются после завершения скрипта.

3. Настройка wait_timeout в MySQL

Если у вас есть доступ к phpMyAdmin или SSH, увеличьте wait_timeout:

sql
-- Временное решение на время отладки
SET GLOBAL wait_timeout = 300;

Оптимизация обработчика вебхуков

1. Упрощение логики

Разделите логику на более мелкие операции с проверками соединения:

php
// После проверки полей и перед любыми операциями с БД
check_connection($db);

// Проверка дубликатов
$stmt = $db->prepare("SELECT id FROM payments WHERE payment_id = ?");
if (!$stmt) {
    log_error("Prepare failed: " . $db->error);
    http_response_code(500);
    exit('Database error');
}
// ... остальной код с проверками

2. Обработка ошибок

Добавьте детальное логирование ошибок:

php
// Функция логирования ошибок
function log_error($message) {
    $log_file = $_SERVER['DOCUMENT_ROOT'] . '/logs/webhook_errors.log';
    $timestamp = date('Y-m-d H:i:s');
    $log_message = "[$timestamp] ERROR: $message\n";
    file_put_contents($log_file, $log_message, FILE_APPEND);
}

// Пример использования
try {
    $stmt = $db->prepare("UPDATE users SET subscribe_date = ? WHERE nickname = ?");
    $stmt->bind_param("sis", $new_subscribe_date, $nickname);
    $stmt->execute();
} catch (Exception $e) {
    log_error("Database update failed: " . $e->getMessage());
    http_response_code(500);
    exit('Database error');
}

3. Использование транзакций

Оберните все операции с базой данных в транзакцию:

php
$db->begin_transaction();

try {
    // Все операции с БД
    $db->commit();
} catch (Exception $e) {
    $db->rollback();
    log_error("Transaction failed: " . $e->getMessage());
    http_response_code(500);
    exit('Transaction error');
}

Диагностика и логирование

1. Добавление подробного логирования

Модифицируйте обработчик для логирования всех операций:

php
// В начале обработчика
$log_data = [
    'timestamp' => date('Y-m-d H:i:s'),
    'payment_id' => $payment_id ?? 'N/A',
    'status' => $status ?? 'N/A',
    'nickname' => $nickname ?? 'N/A',
    'amount' => $amount ?? 'N/A',
    'ip' => $_SERVER['REMOTE_ADDR'],
    'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'N/A'
];

file_put_contents($_SERVER['DOCUMENT_ROOT'] . '/logs/webhook.log', json_encode($log_data) . "\n", FILE_APPEND);

// После каждой операции с БД
file_put_contents($_SERVER['DOCUMENT_ROOT'] . '/logs/db_operations.log', 
    date('Y-m-d H:i:s') . " - Executing: " . $stmt->error . "\n", FILE_APPEND);

2. Проверка состояния соединения

Добавьте диагностику соединения:

php
// Проверка состояния соединения с БД
if (!$db->ping()) {
    file_put_contents($_SERVER['DOCUMENT_ROOT'] . '/logs/connection_errors.log', 
        date('Y-m-d H:i:s') . " - Connection lost\n", FILE_APPEND);
    http_response_code(500);
    exit('Connection lost');
}

Решения для Jino хостинга

1. Обращение в поддержку Джино

Свяжитесь с поддержкой Джино и попросите:

  • Проверить настройки MySQL timeout
  • Увеличить лимиты выполнения PHP скриптов
  • Проверить логи ошибок веб-сервера

Важно: Укажите, что проблема возникает именно при обработке вебхуков от внешних сервисов.

2. Альтернативный подход: асинхронная обработка

Если проблема в тайм-аутах, рассмотрите асинхронную обработку:

php
// Быстрый ответ ЮКассе
http_response_code(200);
echo 'OK';
exit;

// Далее выполняем фоновую обработку
ignore_user_abort(true);
set_time_limit(0);

// Основная логика обработки
// ...

3. Использование очередей

Реализуйте простую очередь для обработки платежей:

php
// Сначала записываем в очередь
$stmt = $db->prepare("INSERT INTO payment_queue (payment_id, nickname, amount, status, processed) VALUES (?, ?, ?, ?, 0)");
$stmt->bind_param("ssds", $payment_id, $nickname, $amount, $status);
$stmt->execute();

// Отдельный cron-скрипт для обработки очереди

4. Переключение на другой хостинг

Если проблема системная и не решается, рассмотрите перенос на хостинг с лучшей поддержкой вебхуков, например:

  • VPS или выделенный сервер
  • Хостинги, специализирующиеся на e-commerce
  • Облачные решения (AWS, Google Cloud)

Источники

  1. YooKassa - Notifications — Payment acceptance via the YooMoney API
  2. Connection timed out when trying to connect to MySQL database with PHP - Hosting Support
  3. php - mysqli_connect(): (HY000/2002): Connection timed out on hosting - Stack Overflow
  4. How to solve PHP database link timeout issue? (php database connection timeout) - USAVPS.COM
  5. Set the connection timeout when using PHP | Cloud SQL for MySQL | Google Cloud
  6. MySQL connection timeouts - Database Administrators Stack Exchange

Заключение

  1. Наиболее вероятная причина - тайм-ауты MySQL соединений на shared хостинге Джино
  2. Первый шаг - добавьте проверку соединений и подробное логирование для диагностики
  3. Второй шаг - настройте persistent соединения и увеличьте PHP тайм-ауты
  4. Если проблема сохраняется - обратитесь в поддержку Джино с детальными логами
  5. Долгосрочное решение - рассмотрите переход на VPS или облачный хостинг для стабильной работы вебхуков

Начните с добавления проверки соединений и логирования - это поможет точно определить, где происходит сбой в цепочке обработки платежей.