Почему перестала работать ЮКасса? На двух проектах одновременно перестали обновляться данные в базе данных 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
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",
"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(),
"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 OK. Проблема возникла одновременно на двух проектах, размещенных на одном хостинге. В чём может быть причина?
Проблема с YooKassa, когда вебхуки доставляются успешно (200 OK), но база данных MySQL не обновляется, особенно при одновременном возникновении на двух проектах, указывает на системную проблему. Вот основные возможные причины и решения:
Содержание
- Основные причины проблемы
- Диагностика и проверка
- Решения и исправления
- Оптимизация кода обработчика
- Профилактические меры
- Заключение
Основные причины проблемы
1. Проблемы с соединением с базой данных
Наиболее вероятная причина - проблемы с файлом подключения к базе данных /inc/connect.php. В производственной среде могут возникать:
- Временные сбои соединения с MySQL
- Ограничения на количество одновременных подключений
- Таймауты соединений при высокой нагрузке
2. Отсутствие обработки ошибок
В вашем коде нет обработки ошибок MySQL. Если возникает ошибка при выполнении запроса, скрипт продолжит работу и вернет 200 OK, но база данных не будет обновлена.
3. Проблемы с правами доступа
Пользователь MySQL может не иметь достаточных прав на выполнение операций UPDATE и INSERT в нужных таблицах.
4. Конфликты при одновременной обработке
Если два вебхука обрабатываются одновременно для одного пользователя, может возникать состояние гонки (race condition).
5. Проблемы с таймингом
YooKassa может отправлять вебхуки до того, как платеж полностью обработан в их системе, что приводит к несоответствию данных.
Диагностика и проверка
1. Проверка файла подключения
Добавьте логирование в файл /inc/connect.php:
// В начале файла подключения
error_log("Database connection attempt at: " . date('Y-m-d H:i:s'));
// После установки соединения
if (!$db) {
error_log("Database connection failed: " . mysqli_connect_error());
die("Database connection error");
}
error_log("Database connection successful");
2. Логирование всех операций в callback
Модифицируйте ваш обработчик с подробным логированием:
// Включите логирование в начале скрипта
file_put_contents('yookassa_log.txt', date('Y-m-d H:i:s') . " - Received data: " . $input . "\n", FILE_APPEND);
// После каждого запроса к БД
file_put_contents('yookassa_log.txt', date('Y-m-d H:i:s') . " - DB query executed: $sql\n", FILE_APPEND);
// При ошибках
if ($stmt->error) {
file_put_contents('yookassa_log.txt', date('Y-m-d H:i:s') . " - DB Error: " . $stmt->error . "\n", FILE_APPEND);
}
3. Проверка прав доступа
Выполните проверку прав пользователя MySQL:
SHOW GRANTS FOR 'ваш_пользователь'@'localhost';
Убедитесь, что пользователь имеет права SELECT, INSERT, UPDATE на нужные таблицы.
Решения и исправления
1. Добавление обработки ошибок
Модифицируйте ваш код с полноценной обработкой ошибок:
// Обработка соединения с БД
if (!$db) {
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Database connection failed\n", FILE_APPEND);
exit('Database error');
}
// Обработка каждого запроса
$stmt = $db->prepare("SELECT id FROM payments WHERE payment_id = ?");
if (!$stmt) {
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Prepare failed: " . $db->error . "\n", FILE_APPEND);
exit('Database error');
}
if (!$stmt->execute()) {
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Execute failed: " . $stmt->error . "\n", FILE_APPEND);
exit('Database error');
}
2. Использование транзакций
Оберните операции с базой данных в транзакцию:
$db->begin_transaction();
try {
// Все операции с БД здесь
$db->commit();
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
$db->rollback();
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Transaction failed: " . $e->getMessage() . "\n", FILE_APPEND);
exit('Transaction error');
}
3. Проверка версии PHP
Согласно документации YooKassa SDK, требуется PHP 8.0+. Проверьте версию PHP на хостинге:
echo 'PHP Version: ' . phpversion();
4. Оптимизация подключения к БД
Добавьте параметры соединения для повышения стабильности:
// В файле подключения
$db = new mysqli($host, $user, $pass, $db_name);
// Установка параметров
$db->set_charset("utf8mb4");
$db->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
$db->options(MYSQLI_OPT_READ_TIMEOUT, 10);
Оптимизация кода обработчика
1. Безопасное получение данных
// Проверка валидности JSON
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - JSON decode error\n", FILE_APPEND);
exit('Invalid JSON');
}
// Безопасное получение значений
$payment_id = $data['object']['id'] ?? null;
$status = $data['object']['status'] ?? null;
$nickname = $data['object']['metadata']['nickname'] ?? null;
if (!$payment_id || !$status || !$nickname) {
http_response_code(400);
exit('Missing required fields');
}
2. Проверка суммы платежа
// Проверка и нормализация суммы
$amount = floatval($data['object']['amount']['value']);
if ($amount <= 0) {
http_response_code(400);
exit('Invalid amount');
}
3. Улучшенная обработка подписок
// Безопасная работа с датами
try {
$now = new DateTime('Europe/Moscow');
$current_subscribe_date = new DateTime($current_subscribe_date);
if ($now > $current_subscribe_date) {
$new_subscribe_date = $now->modify("+$subscribe_days days")->format('Y-m-d H:i:s');
} else {
$new_subscribe_date = $current_subscribe_date->modify("+$subscribe_days days")->format('Y-m-d H:i:s');
}
} catch (Exception $e) {
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Date error: " . $e->getMessage() . "\n", FILE_APPEND);
exit('Date processing error');
}
Профилактические меры
1. Мониторинг и алерты
Настройте мониторинг успешности обработки вебхуков:
// Отправка уведомлений при ошибках
if (http_response_code() !== 200) {
// Отправка уведомления в Telegram или Email
$message = "YooKassa webhook error at " . date('Y-m-d H:i:s') . "\n";
$message .= "Response code: " . http_response_code() . "\n";
$message .= "Error: " . $error;
// Используйте ваш метод отправки уведомлений
sendNotification($message);
}
2. Ретраи-механизм
Для временных сбоев реализуйте очередь с повторными попытками:
// Проверка существования платежа с задержкой
$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) {
$stmt->close();
http_response_code(200);
exit('Already processed');
}
$stmt->close();
// Если ошибка, добавить в очередь повторной обработки
if ($db->error) {
// Сохранить в таблицу retry_queue
$retry_stmt = $db->prepare("INSERT INTO retry_queue (payment_id, data, attempts, created_at) VALUES (?, ?, ?, NOW())");
$retry_stmt->bind_param("ssi", $payment_id, $input, $attempts);
$retry_stmt->execute();
$retry_stmt->close();
}
3. Регулярная проверка данных
Настройте ежедневную проверку данных:
-- Скрипт для проверки несоответствий
SELECT p.* FROM payments p
LEFT JOIN users u ON p.nickname = u.nickname
WHERE p.status = 'succeeded' AND (u.subscribe_date IS NULL OR u.subscribe_rate IS NULL);
Заключение
Проблема с YooKassa на двух проектах одновременно указывает на системную ошибку, скорее всего связанную с:
- Конфигурацией хостинга - проверьте лимиты MySQL и параметры PHP
- Файлом подключения к БД - добавьте логирование для диагностики
- Правами доступа - убедитесь, что пользователь MySQL имеет все необходимые права
- Обработкой ошибок - модифицируйте код с полной обработкой исключений
- Временными сбоями - реализуйте механизм повторных попыток
Рекомендуемые действия:
- Добавьте подробное логирование во всех точках взаимодействия с БД
- Проверьте права доступа пользователя MySQL
- Реализуйте обработку ошибок и транзакции
- Настройте мониторинг и уведомления о сбоях
- Контактуйте техподдержку хостинга с запросом логов MySQL
Проблема, возникшая одновременно на двух проектах, скорее всего связана с изменениями на уровне хостинга или общими настройками базы данных, а не с логикой вашего кода.