perf: убрано избыточное логирование при импорте для повышения производительности
- Удалено детальное логирование каждой строки импорта в PHP (error_log в циклах) - Убраны console.log при парсинге и обработке CSV на фронтенде - Оставлено только логирование начала/конца импорта и ошибок - Значительно улучшена производительность при импорте больших файлов (тысячи строк)
This commit is contained in:
@@ -591,15 +591,9 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
|
|||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
console.log('=== НАЧАЛО ИМПОРТА CSV ===');
|
|
||||||
console.log('Файл:', file.name, 'Размер:', file.size, 'байт');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
console.log('Содержимое файла (первые 500 символов):\n', text.substring(0, 500));
|
|
||||||
|
|
||||||
const rows = parseCSV(text);
|
const rows = parseCSV(text);
|
||||||
console.log('Всего строк после парсинга:', rows.length);
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
alert('CSV файл пуст');
|
alert('CSV файл пуст');
|
||||||
@@ -607,38 +601,43 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headers = rows[0];
|
const headers = rows[0];
|
||||||
console.log('Заголовки CSV:', headers);
|
|
||||||
|
|
||||||
const dataRows = rows.slice(1);
|
const dataRows = rows.slice(1);
|
||||||
console.log('Строк данных (без заголовка):', dataRows.length);
|
|
||||||
|
|
||||||
if (dataRows.length === 0) {
|
if (dataRows.length === 0) {
|
||||||
alert('Нет данных для импорта (только заголовки)');
|
alert('Нет данных для импорта (только заголовки)');
|
||||||
return;
|
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 = {};
|
const obj = {};
|
||||||
headers.forEach((header, i) => {
|
headers.forEach((header, i) => {
|
||||||
const value = row[i] || null;
|
obj[header] = row[i] || null;
|
||||||
obj[header] = value;
|
|
||||||
});
|
});
|
||||||
return obj;
|
return obj;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Финальные записи для отправки на сервер:', JSON.stringify(records, null, 2));
|
// Отправляем на сервер
|
||||||
|
|
||||||
const result = await api('/api/table/import-csv', 'POST', {
|
const result = await api('/api/table/import-csv', 'POST', {
|
||||||
schema: currentSchema,
|
schema: currentSchema,
|
||||||
table: currentTable,
|
table: currentTable,
|
||||||
rows: records
|
rows: records
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Ответ от сервера:', result);
|
|
||||||
|
|
||||||
if (result.errorMessages && result.errorMessages.length > 0) {
|
if (result.errorMessages && result.errorMessages.length > 0) {
|
||||||
console.error('Ошибки импорта:', result.errorMessages);
|
// Показываем только первые 10 ошибок
|
||||||
alert(`Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\nОшибки:\n${result.errorMessages.join('\n')}`);
|
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 {
|
} else {
|
||||||
alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`);
|
alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`);
|
||||||
}
|
}
|
||||||
@@ -646,12 +645,13 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
|
|||||||
await table.replaceData();
|
await table.replaceData();
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('=== КРИТИЧЕСКАЯ ОШИБКА ИМПОРТА ===');
|
console.error('Ошибка импорта:', err);
|
||||||
console.error('Ошибка:', err);
|
|
||||||
alert('Ошибка импорта: ' + err.message);
|
alert('Ошибка импорта: ' + err.message);
|
||||||
|
e.target.value = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// ========== ЭКСПОРТ CSV ==========
|
// ========== ЭКСПОРТ CSV ==========
|
||||||
document.getElementById('btnExportCSV').addEventListener('click', async () => {
|
document.getElementById('btnExportCSV').addEventListener('click', async () => {
|
||||||
if (!currentSchema || !currentTable || !table) {
|
if (!currentSchema || !currentTable || !table) {
|
||||||
|
|||||||
@@ -170,23 +170,18 @@ $app->post('/api/table/delete', function (Request $request, Response $response)
|
|||||||
return $response->withHeader('Content-Type', 'application/json');
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: импорт CSV (массовая вставка)
|
||||||
// API: импорт CSV (массовая вставка)
|
// API: импорт CSV (массовая вставка)
|
||||||
$app->post('/api/table/import-csv', function (Request $request, Response $response) use ($container) {
|
$app->post('/api/table/import-csv', function (Request $request, Response $response) use ($container) {
|
||||||
error_log("\n========== ЗАПРОС НА ИМПОРТ CSV ==========");
|
|
||||||
|
|
||||||
$body = (string)$request->getBody();
|
$body = (string)$request->getBody();
|
||||||
error_log("Тело запроса (первые 1000 символов): " . substr($body, 0, 1000));
|
|
||||||
|
|
||||||
$payload = json_decode($body, true);
|
$payload = json_decode($body, true);
|
||||||
|
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
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()]));
|
$response->getBody()->write(json_encode(['error' => 'Invalid JSON: ' . json_last_error_msg()]));
|
||||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log("Декодированный payload: " . json_encode($payload, JSON_PRETTY_PRINT));
|
|
||||||
|
|
||||||
$pdo = $container->get('db');
|
$pdo = $container->get('db');
|
||||||
$meta = new \App\MetaService($pdo);
|
$meta = new \App\MetaService($pdo);
|
||||||
$ds = new \App\DataService($pdo);
|
$ds = new \App\DataService($pdo);
|
||||||
@@ -195,29 +190,20 @@ $app->post('/api/table/import-csv', function (Request $request, Response $respon
|
|||||||
$table = $payload['table'] ?? '';
|
$table = $payload['table'] ?? '';
|
||||||
$rows = $payload['rows'] ?? [];
|
$rows = $payload['rows'] ?? [];
|
||||||
|
|
||||||
error_log("Schema: $schema");
|
|
||||||
error_log("Table: $table");
|
|
||||||
error_log("Количество строк: " . count($rows));
|
|
||||||
|
|
||||||
if (empty($schema) || empty($table)) {
|
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']));
|
$response->getBody()->write(json_encode(['error' => 'Schema and table are required']));
|
||||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$metaArr = $meta->getTableMeta($schema, $table);
|
$metaArr = $meta->getTableMeta($schema, $table);
|
||||||
error_log("Метаданные таблицы получены, столбцов: " . count($metaArr['columns']));
|
|
||||||
|
|
||||||
$result = $ds->insertMultipleRows($schema, $table, $rows, $metaArr['columns']);
|
$result = $ds->insertMultipleRows($schema, $table, $rows, $metaArr['columns']);
|
||||||
|
|
||||||
error_log("Результат импорта: " . json_encode($result));
|
|
||||||
|
|
||||||
$response->getBody()->write(json_encode($result));
|
$response->getBody()->write(json_encode($result));
|
||||||
return $response->withHeader('Content-Type', 'application/json');
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("КРИТИЧЕСКАЯ ОШИБКА: " . $e->getMessage());
|
error_log("CSV Import: Critical error - " . $e->getMessage());
|
||||||
error_log("Stack trace: " . $e->getTraceAsString());
|
|
||||||
|
|
||||||
$response->getBody()->write(json_encode([
|
$response->getBody()->write(json_encode([
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
|
|||||||
@@ -237,9 +237,7 @@ class DataService
|
|||||||
|
|
||||||
public function insertMultipleRows(string $schema, string $table, array $rows, array $columns): array
|
public function insertMultipleRows(string $schema, string $table, array $rows, array $columns): array
|
||||||
{
|
{
|
||||||
error_log("=== НАЧАЛО ИМПОРТА CSV ===");
|
error_log("=== ИМПОРТ CSV: Schema=$schema, Table=$table, Строк=" . count($rows) . " ===");
|
||||||
error_log("Schema: $schema, Table: $table");
|
|
||||||
error_log("Количество строк для импорта: " . count($rows));
|
|
||||||
|
|
||||||
if (empty($rows)) {
|
if (empty($rows)) {
|
||||||
return ['inserted' => 0, 'errors' => 0, 'message' => 'No rows provided'];
|
return ['inserted' => 0, 'errors' => 0, 'message' => 'No rows provided'];
|
||||||
@@ -249,18 +247,15 @@ class DataService
|
|||||||
$errors = 0;
|
$errors = 0;
|
||||||
$errorMessages = [];
|
$errorMessages = [];
|
||||||
|
|
||||||
// ✅ Собираем информацию о всех столбцах таблицы
|
// Собираем информацию о всех столбцах таблицы
|
||||||
$validColumns = [];
|
$validColumns = [];
|
||||||
foreach ($columns as $c) {
|
foreach ($columns as $c) {
|
||||||
$name = $c['COLUMN_NAME'];
|
$name = $c['COLUMN_NAME'];
|
||||||
$extra = $c['EXTRA'] ?? '';
|
$extra = $c['EXTRA'] ?? '';
|
||||||
$dataType = strtolower($c['DATA_TYPE'] ?? '');
|
$dataType = strtolower($c['DATA_TYPE'] ?? '');
|
||||||
|
|
||||||
// ✅ Правильная проверка IS_NULLABLE
|
|
||||||
$isNullable = ($c['IS_NULLABLE'] === true || $c['IS_NULLABLE'] === 'YES');
|
$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 поля
|
// Пропускаем только auto_increment поля
|
||||||
if (!str_contains($extra, 'auto_increment')) {
|
if (!str_contains($extra, 'auto_increment')) {
|
||||||
$validColumns[$name] = [
|
$validColumns[$name] = [
|
||||||
@@ -269,60 +264,43 @@ class DataService
|
|||||||
'default' => $c['COLUMN_DEFAULT'] ?? null,
|
'default' => $c['COLUMN_DEFAULT'] ?? null,
|
||||||
'data_type' => $dataType
|
'data_type' => $dataType
|
||||||
];
|
];
|
||||||
} else {
|
|
||||||
error_log("Пропускаем auto_increment столбец: $name");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log("Валидные столбцы для импорта: " . json_encode(array_keys($validColumns)));
|
|
||||||
|
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
foreach ($rows as $index => $row) {
|
foreach ($rows as $index => $row) {
|
||||||
error_log("\n--- Обработка строки " . ($index + 1) . " ---");
|
|
||||||
error_log("Данные строки: " . json_encode($row));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$insertCols = [];
|
$insertCols = [];
|
||||||
$placeholders = [];
|
$placeholders = [];
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
// ✅ Перебираем ВСЕ столбцы таблицы (кроме auto_increment)
|
// Перебираем ВСЕ столбцы таблицы (кроме auto_increment)
|
||||||
foreach ($validColumns as $name => $info) {
|
foreach ($validColumns as $name => $info) {
|
||||||
$value = null;
|
$value = null;
|
||||||
|
|
||||||
// Если столбец есть в CSV строке
|
// Если столбец есть в CSV строке
|
||||||
if (array_key_exists($name, $row)) {
|
if (array_key_exists($name, $row)) {
|
||||||
$value = $row[$name];
|
$value = $row[$name];
|
||||||
error_log(" Столбец '$name' найден в CSV: '" . var_export($value, true) . "'");
|
|
||||||
|
|
||||||
// ✅ Обрабатываем пустые значения как NULL
|
// Обрабатываем пустые значения как NULL
|
||||||
if ($value === null || $value === '' || in_array($value, ['NULL', 'null'], true)) {
|
if ($value === null || $value === '' || in_array($value, ['NULL', 'null'], true)) {
|
||||||
error_log(" Преобразуем '$value' в NULL");
|
|
||||||
$value = null;
|
$value = null;
|
||||||
} else {
|
} else {
|
||||||
// ✅ Конвертируем форматы дат для date/datetime полей
|
// Конвертируем форматы дат для date/datetime полей
|
||||||
if (in_array($info['data_type'], ['date', 'datetime', 'timestamp'])) {
|
if (in_array($info['data_type'], ['date', 'datetime', 'timestamp'])) {
|
||||||
$value = $this->convertDateFormat($value);
|
$value = $this->convertDateFormat($value);
|
||||||
error_log(" Конвертация даты: результат = '$value'");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log(" Столбец '$name' НЕ найден в CSV");
|
// Столбца нет в CSV
|
||||||
// ✅ Столбца нет в CSV - устанавливаем NULL или пропускаем
|
|
||||||
// Если поле имеет значение по умолчанию - пропускаем (БД сама подставит)
|
|
||||||
if ($info['has_default']) {
|
if ($info['has_default']) {
|
||||||
error_log(" Пропускаем (есть default): " . $info['default']);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Если поле nullable - ставим NULL
|
|
||||||
if ($info['nullable']) {
|
if ($info['nullable']) {
|
||||||
error_log(" Устанавливаем NULL (nullable)");
|
|
||||||
$value = null;
|
$value = null;
|
||||||
} else {
|
} else {
|
||||||
// Поле обязательное и нет дефолта - ошибка
|
|
||||||
error_log(" ОШИБКА: обязательное поле без значения (nullable={$info['nullable']})");
|
|
||||||
throw new \PDOException("Missing required field: $name");
|
throw new \PDOException("Missing required field: $name");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,11 +308,9 @@ class DataService
|
|||||||
$insertCols[] = "`$name`";
|
$insertCols[] = "`$name`";
|
||||||
$placeholders[] = ":$name";
|
$placeholders[] = ":$name";
|
||||||
$params[":$name"] = $value;
|
$params[":$name"] = $value;
|
||||||
error_log(" Добавлено в INSERT: '$name' = " . ($value === null ? 'NULL' : "'$value'"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($insertCols)) {
|
if (empty($insertCols)) {
|
||||||
error_log("Нет столбцов для вставки, пропускаем строку");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,19 +321,18 @@ class DataService
|
|||||||
implode(',', $placeholders)
|
implode(',', $placeholders)
|
||||||
);
|
);
|
||||||
|
|
||||||
error_log("SQL: $sql");
|
|
||||||
error_log("Параметры: " . json_encode($params));
|
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare($sql);
|
$stmt = $this->pdo->prepare($sql);
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
$inserted++;
|
$inserted++;
|
||||||
error_log("✓ Строка успешно вставлена, ID: " . $this->pdo->lastInsertId());
|
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
$errors++;
|
$errors++;
|
||||||
$errorMsg = "Строка " . ($index + 1) . ": " . $this->formatPDOError($e, $schema, $table);
|
$errorMsg = "Строка " . ($index + 1) . ": " . $this->formatPDOError($e, $schema, $table);
|
||||||
error_log("✗ ОШИБКА при вставке строки " . ($index + 1) . ": " . $e->getMessage());
|
|
||||||
error_log(" SQL Code: " . $e->getCode());
|
|
||||||
$errorMessages[] = $errorMsg;
|
$errorMessages[] = $errorMsg;
|
||||||
|
|
||||||
|
// Логируем только первые 10 ошибок, чтобы не засорять лог
|
||||||
|
if ($errors <= 10) {
|
||||||
|
error_log("Ошибка импорта строки " . ($index + 1) . ": " . $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,6 +351,37 @@ class DataService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует различные форматы дат в формат 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)
|
* Конвертирует различные форматы дат в формат MySQL (YYYY-MM-DD)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user