НейроАгент

Почему YooKassa не обновляет базу данных?

Проблема с YooKassa: вебхуки доставляются успешно (200 OK), но 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
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:

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

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

php
// Включите логирование в начале скрипта
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:

sql
SHOW GRANTS FOR 'ваш_пользователь'@'localhost';

Убедитесь, что пользователь имеет права SELECT, INSERT, UPDATE на нужные таблицы.

Решения и исправления

1. Добавление обработки ошибок

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

php
// Обработка соединения с БД
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. Использование транзакций

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

php
$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 на хостинге:

php
echo 'PHP Version: ' . phpversion();

4. Оптимизация подключения к БД

Добавьте параметры соединения для повышения стабильности:

php
// В файле подключения
$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. Безопасное получение данных

php
// Проверка валидности 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. Проверка суммы платежа

php
// Проверка и нормализация суммы
$amount = floatval($data['object']['amount']['value']);
if ($amount <= 0) {
    http_response_code(400);
    exit('Invalid amount');
}

3. Улучшенная обработка подписок

php
// Безопасная работа с датами
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. Мониторинг и алерты

Настройте мониторинг успешности обработки вебхуков:

php
// Отправка уведомлений при ошибках
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. Ретраи-механизм

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

php
// Проверка существования платежа с задержкой
$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. Регулярная проверка данных

Настройте ежедневную проверку данных:

sql
-- Скрипт для проверки несоответствий
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 на двух проектах одновременно указывает на системную ошибку, скорее всего связанную с:

  1. Конфигурацией хостинга - проверьте лимиты MySQL и параметры PHP
  2. Файлом подключения к БД - добавьте логирование для диагностики
  3. Правами доступа - убедитесь, что пользователь MySQL имеет все необходимые права
  4. Обработкой ошибок - модифицируйте код с полной обработкой исключений
  5. Временными сбоями - реализуйте механизм повторных попыток

Рекомендуемые действия:

  1. Добавьте подробное логирование во всех точках взаимодействия с БД
  2. Проверьте права доступа пользователя MySQL
  3. Реализуйте обработку ошибок и транзакции
  4. Настройте мониторинг и уведомления о сбоях
  5. Контактуйте техподдержку хостинга с запросом логов MySQL

Проблема, возникшая одновременно на двух проектах, скорее всего связана с изменениями на уровне хостинга или общими настройками базы данных, а не с логикой вашего кода.

Источники

  1. YooKassa PHP SDK Documentation
  2. PHP MySQL Error Handling Best Practices
  3. Webhook to MySQL Integration Guide
  4. Database Transaction Management
  5. PHP Connection Timeout Settings