feat: улучшена информативность ошибок при импорте CSV
- В сообщениях об ошибках теперь отображается содержимое проблемной строки - Добавлена специальная обработка ошибки Data truncated (обрезание данных) - Показывается конкретное значение поля, вызвавшего ошибку - Добавлена информация о типе и размере столбца при truncation - Улучшено форматирование вывода ошибок (JSON для больших данных) - Номер строки теперь соответствует номеру в исходном CSV файле (+2 для учета заголовка)
This commit is contained in:
@@ -791,13 +791,36 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
|
|||||||
rows: records
|
rows: records
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.errorMessages && result.errorMessages.length > 0) {
|
if (result.errorMessages && result.errorMessages.length > 0) {
|
||||||
|
// Показываем только первые 10 ошибок
|
||||||
const errorsToShow = result.errorMessages.slice(0, 10);
|
const errorsToShow = result.errorMessages.slice(0, 10);
|
||||||
const moreErrors = result.errorMessages.length > 10 ? `\n... и еще ${result.errorMessages.length - 10} ошибок` : '';
|
const moreErrors = result.errorMessages.length > 10
|
||||||
alert(`Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`);
|
? `\n... и еще ${result.errorMessages.length - 10} ошибок`
|
||||||
} else {
|
: '';
|
||||||
|
|
||||||
|
// Создаем детальное сообщение
|
||||||
|
let errorDetails = `Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\n`;
|
||||||
|
errorDetails += 'ПЕРВЫЕ ОШИБКИ:\n';
|
||||||
|
errorDetails += '═══════════════════════════════════════\n';
|
||||||
|
errorDetails += errorsToShow.join('\n\n');
|
||||||
|
errorDetails += moreErrors;
|
||||||
|
|
||||||
|
// Выводим в alert (для больших сообщений можно использовать модальное окно)
|
||||||
|
alert(errorDetails);
|
||||||
|
|
||||||
|
// Также выводим в консоль для детального анализа
|
||||||
|
console.group('📋 Детали импорта CSV');
|
||||||
|
console.log(`✅ Импортировано: ${result.inserted}`);
|
||||||
|
console.log(`❌ Ошибок: ${result.errors}`);
|
||||||
|
console.log('\nВсе ошибки:');
|
||||||
|
result.errorMessages.forEach((msg, idx) => {
|
||||||
|
console.log(`${idx + 1}. ${msg}`);
|
||||||
|
});
|
||||||
|
console.groupEnd();
|
||||||
|
} else {
|
||||||
alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`);
|
alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await table.replaceData();
|
await table.replaceData();
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
|
|||||||
@@ -129,11 +129,34 @@ class DataService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatPDOError(\PDOException $e, string $schema, string $table, array $params = []): string
|
private function formatPDOError(\PDOException $e, string $schema, string $table, array $params = []): string
|
||||||
{
|
{
|
||||||
$code = $e->getCode();
|
$code = $e->getCode();
|
||||||
$message = $e->getMessage();
|
$message = $e->getMessage();
|
||||||
|
|
||||||
|
// Data truncated (обрезание данных)
|
||||||
|
if ($code === '01000' && str_contains($message, 'Data truncated')) {
|
||||||
|
if (preg_match('/column \'([^\']+)\'/', $message, $matches)) {
|
||||||
|
$field = $matches[1];
|
||||||
|
$value = 'N/A';
|
||||||
|
|
||||||
|
// Пытаемся найти значение в параметрах
|
||||||
|
foreach ($params as $paramName => $paramValue) {
|
||||||
|
if (str_contains($paramName, $field)) {
|
||||||
|
$value = $paramValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return "Ошибка: поле '{$field}' - значение '{$value}' слишком большое или имеет неверный формат.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Ошибка: поле '{$field}' - данные обрезаны для значения '{$value}'.";
|
||||||
|
}
|
||||||
|
return "Ошибка: данные обрезаны при вставке.";
|
||||||
|
}
|
||||||
|
|
||||||
if ($code === '23000' && str_contains($message, 'foreign key constraint')) {
|
if ($code === '23000' && str_contains($message, 'foreign key constraint')) {
|
||||||
if (preg_match('/FOREIGN KEY $$`([^`]+)`$$/', $message, $matches)) {
|
if (preg_match('/FOREIGN KEY $$`([^`]+)`$$/', $message, $matches)) {
|
||||||
$field = $matches[1];
|
$field = $matches[1];
|
||||||
@@ -160,7 +183,8 @@ class DataService
|
|||||||
}
|
}
|
||||||
|
|
||||||
return "Ошибка БД: " . $message;
|
return "Ошибка БД: " . $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function updateRow(string $schema, string $table, array $row, array $columns, array $pk): array
|
public function updateRow(string $schema, string $table, array $row, array $columns, array $pk): array
|
||||||
{
|
{
|
||||||
@@ -235,8 +259,8 @@ class DataService
|
|||||||
return ['deleted' => $stmt->rowCount()];
|
return ['deleted' => $stmt->rowCount()];
|
||||||
}
|
}
|
||||||
|
|
||||||
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: Schema=$schema, Table=$table, Строк=" . count($rows) . " ===");
|
error_log("=== ИМПОРТ CSV: Schema=$schema, Table=$table, Строк=" . count($rows) . " ===");
|
||||||
|
|
||||||
if (empty($rows)) {
|
if (empty($rows)) {
|
||||||
@@ -249,6 +273,36 @@ class DataService
|
|||||||
|
|
||||||
// Собираем информацию о всех столбцах таблицы
|
// Собираем информацию о всех столбцах таблицы
|
||||||
$validColumns = [];
|
$validColumns = [];
|
||||||
|
$columnTypes = []; // Для детальной информации об ошибках
|
||||||
|
|
||||||
|
// Предварительная проверка данных
|
||||||
|
$warnings = [];
|
||||||
|
foreach ($rows as $index => $row) {
|
||||||
|
foreach ($row as $field => $value) {
|
||||||
|
if (isset($columnTypes[$field]) && is_numeric($value)) {
|
||||||
|
$type = $columnTypes[$field]['data_type'];
|
||||||
|
|
||||||
|
// Проверка для INT типов
|
||||||
|
if (str_contains($type, 'int')) {
|
||||||
|
if ($value > 2147483647) { // INT max value
|
||||||
|
$warnings[] = "Строка CSV #" . ($index + 2) . ": поле '$field' = '$value' превышает максимальное значение для типа INT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка для DECIMAL/FLOAT
|
||||||
|
if (str_contains($type, 'decimal') || str_contains($type, 'float')) {
|
||||||
|
if (strlen((string)$value) > 15) {
|
||||||
|
$warnings[] = "Строка CSV #" . ($index + 2) . ": поле '$field' = '$value' может быть слишком большим";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($warnings) && count($warnings) <= 20) {
|
||||||
|
error_log("Предупреждения перед импортом:\n" . implode("\n", $warnings));
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($columns as $c) {
|
foreach ($columns as $c) {
|
||||||
$name = $c['COLUMN_NAME'];
|
$name = $c['COLUMN_NAME'];
|
||||||
$extra = $c['EXTRA'] ?? '';
|
$extra = $c['EXTRA'] ?? '';
|
||||||
@@ -256,6 +310,12 @@ class DataService
|
|||||||
|
|
||||||
$isNullable = ($c['IS_NULLABLE'] === true || $c['IS_NULLABLE'] === 'YES');
|
$isNullable = ($c['IS_NULLABLE'] === true || $c['IS_NULLABLE'] === 'YES');
|
||||||
|
|
||||||
|
// Сохраняем информацию о типе столбца
|
||||||
|
$columnTypes[$name] = [
|
||||||
|
'type' => $c['COLUMN_TYPE'] ?? $dataType,
|
||||||
|
'data_type' => $dataType
|
||||||
|
];
|
||||||
|
|
||||||
// Пропускаем только auto_increment поля
|
// Пропускаем только auto_increment поля
|
||||||
if (!str_contains($extra, 'auto_increment')) {
|
if (!str_contains($extra, 'auto_increment')) {
|
||||||
$validColumns[$name] = [
|
$validColumns[$name] = [
|
||||||
@@ -326,12 +386,16 @@ class DataService
|
|||||||
$inserted++;
|
$inserted++;
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
$errors++;
|
$errors++;
|
||||||
$errorMsg = "Строка " . ($index + 1) . ": " . $this->formatPDOError($e, $schema, $table);
|
|
||||||
|
// ✅ Формируем детальное сообщение об ошибке с данными строки
|
||||||
|
$csvLineNumber = $index + 2; // +2 потому что: +1 для человеческой нумерации, +1 для строки заголовка
|
||||||
|
$errorMsg = $this->formatImportError($e, $csvLineNumber, $row, $columnTypes);
|
||||||
$errorMessages[] = $errorMsg;
|
$errorMessages[] = $errorMsg;
|
||||||
|
|
||||||
// Логируем только первые 10 ошибок
|
// Логируем только первые 10 ошибок
|
||||||
if ($errors <= 10) {
|
if ($errors <= 10) {
|
||||||
error_log("Ошибка импорта строки " . ($index + 1) . ": " . $e->getMessage());
|
error_log("Ошибка импорта строки CSV #$csvLineNumber: " . $e->getMessage());
|
||||||
|
error_log("Данные строки: " . json_encode($row, JSON_UNESCAPED_UNICODE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,8 +413,96 @@ class DataService
|
|||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
'errorMessages' => $errorMessages
|
'errorMessages' => $errorMessages
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматирует ошибку импорта с детальной информацией
|
||||||
|
*/
|
||||||
|
private function formatImportError(\PDOException $e, int $lineNumber, array $rowData, array $columnTypes): string
|
||||||
|
{
|
||||||
|
$code = $e->getCode();
|
||||||
|
$message = $e->getMessage();
|
||||||
|
|
||||||
|
$baseMsg = "Строка CSV #{$lineNumber}: ";
|
||||||
|
|
||||||
|
// Обработка Data truncated (обрезание данных)
|
||||||
|
if ($code === '01000' && str_contains($message, 'Data truncated')) {
|
||||||
|
if (preg_match('/column \'([^\']+)\'/', $message, $matches)) {
|
||||||
|
$field = $matches[1];
|
||||||
|
$value = $rowData[$field] ?? 'N/A';
|
||||||
|
$columnType = $columnTypes[$field]['type'] ?? 'unknown';
|
||||||
|
|
||||||
|
// Проверяем, не слишком ли большое число
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
$valueLength = strlen((string)$value);
|
||||||
|
return $baseMsg . "Поле '{$field}' ({$columnType}): значение '{$value}' (длина: {$valueLength}) слишком большое или имеет неверный формат. " .
|
||||||
|
"Данные строки: " . $this->formatRowData($rowData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $baseMsg . "Поле '{$field}' ({$columnType}): данные обрезаны для значения '{$value}'. " .
|
||||||
|
"Данные строки: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
return $baseMsg . "Данные обрезаны. Строка: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foreign Key constraint
|
||||||
|
if ($code === '23000' && str_contains($message, 'foreign key constraint')) {
|
||||||
|
if (preg_match('/FOREIGN KEY $$`([^`]+)`$$/', $message, $matches)) {
|
||||||
|
$field = $matches[1];
|
||||||
|
$value = $rowData[$field] ?? 'N/A';
|
||||||
|
return $baseMsg . "Поле '{$field}' = '{$value}': значение не существует в связанной таблице. " .
|
||||||
|
"Данные: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
return $baseMsg . "Нарушена связь с другой таблицей. Данные: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate key
|
||||||
|
if ($code === '23000' && str_contains($message, 'Duplicate entry')) {
|
||||||
|
if (preg_match('/Duplicate entry \'([^\']+)\' for key \'([^\']+)\'/', $message, $matches)) {
|
||||||
|
$value = $matches[1];
|
||||||
|
$key = $matches[2];
|
||||||
|
return $baseMsg . "Дубликат: значение '{$value}' (ключ '{$key}') уже существует. " .
|
||||||
|
"Данные: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
return $baseMsg . "Дубликат записи. Данные: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOT NULL constraint
|
||||||
|
if (str_contains($message, "cannot be null") || str_contains($message, "doesn't have a default value")) {
|
||||||
|
if (preg_match('/Column \'([^\']+)\'/', $message, $matches)) {
|
||||||
|
$field = $matches[1];
|
||||||
|
return $baseMsg . "Поле '{$field}' обязательно для заполнения. " .
|
||||||
|
"Данные: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
return $baseMsg . "Не заполнены обязательные поля. Данные: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Общая ошибка БД
|
||||||
|
return $baseMsg . "Ошибка БД: " . $message . ". Данные: " . $this->formatRowData($rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматирует данные строки для вывода (сокращает длинные значения)
|
||||||
|
*/
|
||||||
|
private function formatRowData(array $rowData): string
|
||||||
|
{
|
||||||
|
$formatted = [];
|
||||||
|
foreach ($rowData as $key => $value) {
|
||||||
|
if ($value === null) {
|
||||||
|
$formatted[] = "$key=NULL";
|
||||||
|
} else {
|
||||||
|
// Ограничиваем длину значения для удобства чтения
|
||||||
|
$strValue = (string)$value;
|
||||||
|
if (strlen($strValue) > 50) {
|
||||||
|
$strValue = substr($strValue, 0, 50) . '...';
|
||||||
|
}
|
||||||
|
$formatted[] = "$key='$strValue'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode(', ', $formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Конвертирует различные форматы дат в формат MySQL (YYYY-MM-DD)
|
* Конвертирует различные форматы дат в формат MySQL (YYYY-MM-DD)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user