Почему перестала работать ЮКасса? На двух проектах одновременно перестали обновляться данные в базе данных MySQL при оплате через ЮКассу, хотя обработчик возвращает ответ 200 OK. Поддержка ЮКассы утверждает, что уведомления доходят успешно, но данные в базе не обновляются и не создаются записи о платежах.
Вот код обработчика callback от ЮКассы:
<?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
/**
* ТЕСТОВЫЙ ЭМУЛЯТОР 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 хостинге
- Настройка таймаутов и подключений
- Оптимизация обработчика вебхуков
- Диагностика и логирование
- Решения для Jino хостинга
Основные проблемы на shared хостинге
На shared хостингах, таких как Джино, вебхуки сталкиваются с несколькими типичными проблемами:
1. Тайм-ауты MySQL подключений
Исходя из исследований, самая вероятная причина - тайм-ауты соединений с MySQL. На shared хостингах серверы часто закрывают неактивные соединения для экономии ресурсов.
Проблема: Ваш обработчик выполняет несколько операций с базой данных, и если между ними проходит больше времени, чем разрешено MySQL-сервером, соединение разрывается. Последующие запросы выполняются в “слепую”, и скрипт завершается с кодом 200, но без фактического выполнения операций.
Решение: Добавьте проверку соединения перед каждым запросом:
// Функция проверки соединения
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
Добавьте в начало скрипта обработчика установку таймаутов:
// Установка таймаутов
set_time_limit(60); // 60 секунд вместо стандартных 30
ini_set('max_execution_time', 60);
ini_set('memory_limit', '256M');
2. Использование persistent соединений
Модифицируйте файл подключения к базе данных:
// В файле /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:
-- Временное решение на время отладки
SET GLOBAL wait_timeout = 300;
Оптимизация обработчика вебхуков
1. Упрощение логики
Разделите логику на более мелкие операции с проверками соединения:
// После проверки полей и перед любыми операциями с БД
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. Обработка ошибок
Добавьте детальное логирование ошибок:
// Функция логирования ошибок
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. Использование транзакций
Оберните все операции с базой данных в транзакцию:
$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. Добавление подробного логирования
Модифицируйте обработчик для логирования всех операций:
// В начале обработчика
$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. Проверка состояния соединения
Добавьте диагностику соединения:
// Проверка состояния соединения с БД
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. Альтернативный подход: асинхронная обработка
Если проблема в тайм-аутах, рассмотрите асинхронную обработку:
// Быстрый ответ ЮКассе
http_response_code(200);
echo 'OK';
exit;
// Далее выполняем фоновую обработку
ignore_user_abort(true);
set_time_limit(0);
// Основная логика обработки
// ...
3. Использование очередей
Реализуйте простую очередь для обработки платежей:
// Сначала записываем в очередь
$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)
Источники
- YooKassa - Notifications — Payment acceptance via the YooMoney API
- Connection timed out when trying to connect to MySQL database with PHP - Hosting Support
- php - mysqli_connect(): (HY000/2002): Connection timed out on hosting - Stack Overflow
- How to solve PHP database link timeout issue? (php database connection timeout) - USAVPS.COM
- Set the connection timeout when using PHP | Cloud SQL for MySQL | Google Cloud
- MySQL connection timeouts - Database Administrators Stack Exchange
Заключение
- Наиболее вероятная причина - тайм-ауты MySQL соединений на shared хостинге Джино
- Первый шаг - добавьте проверку соединений и подробное логирование для диагностики
- Второй шаг - настройте persistent соединения и увеличьте PHP тайм-ауты
- Если проблема сохраняется - обратитесь в поддержку Джино с детальными логами
- Долгосрочное решение - рассмотрите переход на VPS или облачный хостинг для стабильной работы вебхуков
Начните с добавления проверки соединений и логирования - это поможет точно определить, где происходит сбой в цепочке обработки платежей.