From 130f63f6b2620535e67162c92c83bfc2b380dddb Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Wed, 21 Jan 2026 04:04:56 +0300 Subject: [PATCH] =?UTF-8?q?perf:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=B1=D1=8B=D1=82=D0=BE=D1=87=D0=BD=D0=BE=D0=B5=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=B8=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D0=B5=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B8=D0=B7?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Удалено детальное логирование каждой строки импорта в PHP (error_log в циклах) - Убраны console.log при парсинге и обработке CSV на фронтенде - Оставлено только логирование начала/конца импорта и ошибок - Значительно улучшена производительность при импорте больших файлов (тысячи строк) --- public/app.js | 40 +++---- public/index.php | 22 +--- src/DataService.php | 254 +++++++++++++++++++++++--------------------- 3 files changed, 154 insertions(+), 162 deletions(-) diff --git a/public/app.js b/public/app.js index 38e1b16..86d6a0f 100644 --- a/public/app.js +++ b/public/app.js @@ -591,15 +591,9 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => const file = e.target.files[0]; if (!file) return; - console.log('=== НАЧАЛО ИМПОРТА CSV ==='); - console.log('Файл:', file.name, 'Размер:', file.size, 'байт'); - try { const text = await file.text(); - console.log('Содержимое файла (первые 500 символов):\n', text.substring(0, 500)); - const rows = parseCSV(text); - console.log('Всего строк после парсинга:', rows.length); if (rows.length === 0) { alert('CSV файл пуст'); @@ -607,38 +601,43 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => } const headers = rows[0]; - console.log('Заголовки CSV:', headers); - const dataRows = rows.slice(1); - console.log('Строк данных (без заголовка):', dataRows.length); if (dataRows.length === 0) { alert('Нет данных для импорта (только заголовки)'); return; } - const records = dataRows.map((row, idx) => { + // Показываем прогресс для больших файлов + if (dataRows.length > 100) { + const proceed = confirm(`Файл содержит ${dataRows.length} строк. Импорт может занять некоторое время. Продолжить?`); + if (!proceed) { + e.target.value = ''; + return; + } + } + + // Преобразуем в массив объектов + const records = dataRows.map(row => { const obj = {}; headers.forEach((header, i) => { - const value = row[i] || null; - obj[header] = value; + obj[header] = row[i] || null; }); return obj; }); - console.log('Финальные записи для отправки на сервер:', JSON.stringify(records, null, 2)); - + // Отправляем на сервер const result = await api('/api/table/import-csv', 'POST', { schema: currentSchema, table: currentTable, rows: records }); - console.log('Ответ от сервера:', result); - if (result.errorMessages && result.errorMessages.length > 0) { - console.error('Ошибки импорта:', result.errorMessages); - alert(`Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\nОшибки:\n${result.errorMessages.join('\n')}`); + // Показываем только первые 10 ошибок + const errorsToShow = result.errorMessages.slice(0, 10); + const moreErrors = result.errorMessages.length > 10 ? `\n... и еще ${result.errorMessages.length - 10} ошибок` : ''; + alert(`Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`); } else { alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`); } @@ -646,12 +645,13 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => await table.replaceData(); e.target.value = ''; } catch (err) { - console.error('=== КРИТИЧЕСКАЯ ОШИБКА ИМПОРТА ==='); - console.error('Ошибка:', err); + console.error('Ошибка импорта:', err); alert('Ошибка импорта: ' + err.message); + e.target.value = ''; } }); + // ========== ЭКСПОРТ CSV ========== document.getElementById('btnExportCSV').addEventListener('click', async () => { if (!currentSchema || !currentTable || !table) { diff --git a/public/index.php b/public/index.php index 2b8f8fa..eecde63 100644 --- a/public/index.php +++ b/public/index.php @@ -170,23 +170,18 @@ $app->post('/api/table/delete', function (Request $request, Response $response) return $response->withHeader('Content-Type', 'application/json'); }); +// API: импорт CSV (массовая вставка) // API: импорт CSV (массовая вставка) $app->post('/api/table/import-csv', function (Request $request, Response $response) use ($container) { - error_log("\n========== ЗАПРОС НА ИМПОРТ CSV =========="); - $body = (string)$request->getBody(); - error_log("Тело запроса (первые 1000 символов): " . substr($body, 0, 1000)); - $payload = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { - error_log("ОШИБКА JSON: " . json_last_error_msg()); + error_log("CSV Import: Invalid JSON - " . json_last_error_msg()); $response->getBody()->write(json_encode(['error' => 'Invalid JSON: ' . json_last_error_msg()])); return $response->withHeader('Content-Type', 'application/json')->withStatus(400); } - error_log("Декодированный payload: " . json_encode($payload, JSON_PRETTY_PRINT)); - $pdo = $container->get('db'); $meta = new \App\MetaService($pdo); $ds = new \App\DataService($pdo); @@ -195,29 +190,20 @@ $app->post('/api/table/import-csv', function (Request $request, Response $respon $table = $payload['table'] ?? ''; $rows = $payload['rows'] ?? []; - error_log("Schema: $schema"); - error_log("Table: $table"); - error_log("Количество строк: " . count($rows)); - if (empty($schema) || empty($table)) { - error_log("ОШИБКА: пустая schema или table"); + error_log("CSV Import: Missing schema or table"); $response->getBody()->write(json_encode(['error' => 'Schema and table are required'])); return $response->withHeader('Content-Type', 'application/json')->withStatus(400); } try { $metaArr = $meta->getTableMeta($schema, $table); - error_log("Метаданные таблицы получены, столбцов: " . count($metaArr['columns'])); - $result = $ds->insertMultipleRows($schema, $table, $rows, $metaArr['columns']); - error_log("Результат импорта: " . json_encode($result)); - $response->getBody()->write(json_encode($result)); return $response->withHeader('Content-Type', 'application/json'); } catch (\Exception $e) { - error_log("КРИТИЧЕСКАЯ ОШИБКА: " . $e->getMessage()); - error_log("Stack trace: " . $e->getTraceAsString()); + error_log("CSV Import: Critical error - " . $e->getMessage()); $response->getBody()->write(json_encode([ 'error' => $e->getMessage(), diff --git a/src/DataService.php b/src/DataService.php index 8f68322..0df7da5 100644 --- a/src/DataService.php +++ b/src/DataService.php @@ -235,147 +235,153 @@ class DataService return ['deleted' => $stmt->rowCount()]; } - public function insertMultipleRows(string $schema, string $table, array $rows, array $columns): array - { - error_log("=== НАЧАЛО ИМПОРТА CSV ==="); - error_log("Schema: $schema, Table: $table"); - error_log("Количество строк для импорта: " . count($rows)); +public function insertMultipleRows(string $schema, string $table, array $rows, array $columns): array +{ + error_log("=== ИМПОРТ CSV: Schema=$schema, Table=$table, Строк=" . count($rows) . " ==="); + + if (empty($rows)) { + return ['inserted' => 0, 'errors' => 0, 'message' => 'No rows provided']; + } + + $inserted = 0; + $errors = 0; + $errorMessages = []; + + // Собираем информацию о всех столбцах таблицы + $validColumns = []; + foreach ($columns as $c) { + $name = $c['COLUMN_NAME']; + $extra = $c['EXTRA'] ?? ''; + $dataType = strtolower($c['DATA_TYPE'] ?? ''); - if (empty($rows)) { - return ['inserted' => 0, 'errors' => 0, 'message' => 'No rows provided']; - } - - $inserted = 0; - $errors = 0; - $errorMessages = []; - - // ✅ Собираем информацию о всех столбцах таблицы - $validColumns = []; - foreach ($columns as $c) { - $name = $c['COLUMN_NAME']; - $extra = $c['EXTRA'] ?? ''; - $dataType = strtolower($c['DATA_TYPE'] ?? ''); - - // ✅ Правильная проверка IS_NULLABLE - $isNullable = ($c['IS_NULLABLE'] === true || $c['IS_NULLABLE'] === 'YES'); - - error_log("Столбец: $name, Type: $dataType, Extra: $extra, Nullable: " . ($isNullable ? 'YES' : 'NO') . ", Default: " . ($c['COLUMN_DEFAULT'] ?? 'NULL')); - - // Пропускаем только auto_increment поля - if (!str_contains($extra, 'auto_increment')) { - $validColumns[$name] = [ - 'nullable' => $isNullable, - 'has_default' => !empty($c['COLUMN_DEFAULT']) || $c['COLUMN_DEFAULT'] === '0', - 'default' => $c['COLUMN_DEFAULT'] ?? null, - 'data_type' => $dataType - ]; - } else { - error_log("Пропускаем auto_increment столбец: $name"); - } - } + $isNullable = ($c['IS_NULLABLE'] === true || $c['IS_NULLABLE'] === 'YES'); - error_log("Валидные столбцы для импорта: " . json_encode(array_keys($validColumns))); + // Пропускаем только auto_increment поля + if (!str_contains($extra, 'auto_increment')) { + $validColumns[$name] = [ + 'nullable' => $isNullable, + 'has_default' => !empty($c['COLUMN_DEFAULT']) || $c['COLUMN_DEFAULT'] === '0', + 'default' => $c['COLUMN_DEFAULT'] ?? null, + 'data_type' => $dataType + ]; + } + } - $this->pdo->beginTransaction(); + $this->pdo->beginTransaction(); - try { - foreach ($rows as $index => $row) { - error_log("\n--- Обработка строки " . ($index + 1) . " ---"); - error_log("Данные строки: " . json_encode($row)); - - try { - $insertCols = []; - $placeholders = []; - $params = []; + try { + foreach ($rows as $index => $row) { + try { + $insertCols = []; + $placeholders = []; + $params = []; - // ✅ Перебираем ВСЕ столбцы таблицы (кроме auto_increment) - foreach ($validColumns as $name => $info) { - $value = null; + // Перебираем ВСЕ столбцы таблицы (кроме auto_increment) + foreach ($validColumns as $name => $info) { + $value = null; + + // Если столбец есть в CSV строке + if (array_key_exists($name, $row)) { + $value = $row[$name]; - // Если столбец есть в CSV строке - if (array_key_exists($name, $row)) { - $value = $row[$name]; - error_log(" Столбец '$name' найден в CSV: '" . var_export($value, true) . "'"); - - // ✅ Обрабатываем пустые значения как NULL - if ($value === null || $value === '' || in_array($value, ['NULL', 'null'], true)) { - error_log(" Преобразуем '$value' в NULL"); - $value = null; - } else { - // ✅ Конвертируем форматы дат для date/datetime полей - if (in_array($info['data_type'], ['date', 'datetime', 'timestamp'])) { - $value = $this->convertDateFormat($value); - error_log(" Конвертация даты: результат = '$value'"); - } - } + // Обрабатываем пустые значения как NULL + if ($value === null || $value === '' || in_array($value, ['NULL', 'null'], true)) { + $value = null; } else { - error_log(" Столбец '$name' НЕ найден в CSV"); - // ✅ Столбца нет в CSV - устанавливаем NULL или пропускаем - // Если поле имеет значение по умолчанию - пропускаем (БД сама подставит) - if ($info['has_default']) { - error_log(" Пропускаем (есть default): " . $info['default']); - continue; - } - // Если поле nullable - ставим NULL - if ($info['nullable']) { - error_log(" Устанавливаем NULL (nullable)"); - $value = null; - } else { - // Поле обязательное и нет дефолта - ошибка - error_log(" ОШИБКА: обязательное поле без значения (nullable={$info['nullable']})"); - throw new \PDOException("Missing required field: $name"); + // Конвертируем форматы дат для date/datetime полей + if (in_array($info['data_type'], ['date', 'datetime', 'timestamp'])) { + $value = $this->convertDateFormat($value); } } - - $insertCols[] = "`$name`"; - $placeholders[] = ":$name"; - $params[":$name"] = $value; - error_log(" Добавлено в INSERT: '$name' = " . ($value === null ? 'NULL' : "'$value'")); + } else { + // Столбца нет в CSV + if ($info['has_default']) { + continue; + } + if ($info['nullable']) { + $value = null; + } else { + throw new \PDOException("Missing required field: $name"); + } } - - if (empty($insertCols)) { - error_log("Нет столбцов для вставки, пропускаем строку"); - continue; - } - - $sql = sprintf( - "INSERT INTO `%s`.`%s` (%s) VALUES (%s)", - $schema, $table, - implode(',', $insertCols), - implode(',', $placeholders) - ); - error_log("SQL: $sql"); - error_log("Параметры: " . json_encode($params)); - - $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); - $inserted++; - error_log("✓ Строка успешно вставлена, ID: " . $this->pdo->lastInsertId()); - } catch (\PDOException $e) { - $errors++; - $errorMsg = "Строка " . ($index + 1) . ": " . $this->formatPDOError($e, $schema, $table); - error_log("✗ ОШИБКА при вставке строки " . ($index + 1) . ": " . $e->getMessage()); - error_log(" SQL Code: " . $e->getCode()); - $errorMessages[] = $errorMsg; + $insertCols[] = "`$name`"; + $placeholders[] = ":$name"; + $params[":$name"] = $value; + } + + if (empty($insertCols)) { + continue; + } + + $sql = sprintf( + "INSERT INTO `%s`.`%s` (%s) VALUES (%s)", + $schema, $table, + implode(',', $insertCols), + implode(',', $placeholders) + ); + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + $inserted++; + } catch (\PDOException $e) { + $errors++; + $errorMsg = "Строка " . ($index + 1) . ": " . $this->formatPDOError($e, $schema, $table); + $errorMessages[] = $errorMsg; + + // Логируем только первые 10 ошибок, чтобы не засорять лог + if ($errors <= 10) { + error_log("Ошибка импорта строки " . ($index + 1) . ": " . $e->getMessage()); } } - - $this->pdo->commit(); - error_log("=== ИМПОРТ ЗАВЕРШЁН: вставлено=$inserted, ошибок=$errors ==="); - } catch (\Exception $e) { - $this->pdo->rollBack(); - error_log("=== КРИТИЧЕСКАЯ ОШИБКА ИМПОРТА: " . $e->getMessage() . " ==="); - throw new \RuntimeException('Import failed: ' . $e->getMessage()); } - return [ - 'inserted' => $inserted, - 'errors' => $errors, - 'errorMessages' => $errorMessages - ]; + $this->pdo->commit(); + error_log("=== ИМПОРТ ЗАВЕРШЁН: вставлено=$inserted, ошибок=$errors ==="); + } catch (\Exception $e) { + $this->pdo->rollBack(); + error_log("=== КРИТИЧЕСКАЯ ОШИБКА ИМПОРТА: " . $e->getMessage() . " ==="); + throw new \RuntimeException('Import failed: ' . $e->getMessage()); } + return [ + 'inserted' => $inserted, + 'errors' => $errors, + 'errorMessages' => $errorMessages + ]; +} + +/** + * Конвертирует различные форматы дат в формат MySQL (YYYY-MM-DD) + */ +private function convertDateFormat(string $date): string +{ + $date = trim($date); + + // Уже в нужном формате YYYY-MM-DD + if (preg_match('/^\d{4}-\d{2}-\d{2}/', $date)) { + return substr($date, 0, 10); + } + + // Формат DD.MM.YYYY или DD/MM/YYYY или DD-MM-YYYY + if (preg_match('/^(\d{1,2})[\.\/-](\d{1,2})[\.\/-](\d{4})/', $date, $matches)) { + $day = str_pad($matches[1], 2, '0', STR_PAD_LEFT); + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $year = $matches[3]; + return "$year-$month-$day"; + } + + // Пробуем распарсить через strtotime + $timestamp = strtotime($date); + if ($timestamp !== false) { + return date('Y-m-d', $timestamp); + } + + // Если не удалось распарсить - возвращаем как есть + return $date; +} + + /** * Конвертирует различные форматы дат в формат MySQL (YYYY-MM-DD) */