Battery Import Tool (Инструмент импорта данных батарей)

1. Введение

Battery Import Tool – это программный модуль для импорта данных об аккумуляторных батареях из CSV-файла в базу данных системы. Он разработан для облегчения массового добавления и обновления записей батарей посредством загрузки подготовленного файла с данными. Данный инструмент был изначально разработан в 2025 году и адаптирован для работы с PHP 8.3, что обеспечивает поддержку современных возможностей языка (например, enum и readonly-свойств).

В данной документации представлено подробное техническое описание работы Battery Import Tool, включая инструкции по использованию для различных типов пользователей, логику работы программы, особенности реализации, а также примеры кода. Статья структурирована по ролям пользователей – обычный пользователь, разработчик и администратор – чтобы каждому предоставить релевантную информацию. В конце статьи приведён глоссарий технических терминов со сносками на них в тексте.

Основные возможности инструмента:

  • Импорт данных батарей из файла в формате CSV[^1] через веб-интерфейс.
  • Автоматическое определение разделителя колонок (запятая или точка с запятой) и обработка как строк с заголовком, так и без него.
  • Проверка корректности и полноты данных на стороне сервера: обязательные поля, типы данных, допустимые значения.
  • Поддержка транзакций базы данных[^2] для групповой записи: данные либо вносятся целиком, либо при ошибке откатываются, что сохраняет целостность базы.
  • Разделение прав и интерфейсов: конечные пользователи имеют простой веб-интерфейс для импорта, разработчики могут расширять функциональность, а администраторы – настраивать систему и следить за её работой.

Далее документация разделена на несколько частей. В разделе 2. Руководство для обычного пользователя изложено, как пользоваться инструментом: подготовка CSV-файла и выполнение импорта. Раздел 3. Документация для разработчика описывает внутреннее устройство программного кода, архитектуру решения и пример расширения функционала. В разделе 4. Руководство для администратора рассматривается развёртывание, настройка и сопровождение инструмента. В заключение, 5. Глоссарий содержит определения ключевых терминов, используемых в тексте.

2. Руководство для обычного пользователя

2.1. Назначение и возможности инструмента

Battery Import Tool предназначен для быстрого внесения списка аккумуляторных батарей в систему. Это особенно полезно, когда необходимо одновременно добавить или обновить большое количество записей (например, при первоначальном заполнении базы или при импорте данных от производителя). Инструмент экономит время и снижает вероятность ошибок по сравнению с ручным вводом данных каждой батареи.

Основные возможности для пользователя:

  • Удобный веб-интерфейс: форма с выпадающим списком брендов и полем для вставки данных CSV, не требующая специальных знаний SQL или доступа к базе данных.
  • Автоматическое различение новых и существующих записей: при импорте система определяет, какая батарея уже есть в базе, и обновляет её, а какие батареи отсутствуют – и добавляет их как новые.
  • Понятные сообщения об успехе или ошибке импорта: после обработки файла отображается количество добавленных и обновлённых записей, либо детальная ошибка, если что-то пошло не так (например, неверный формат данных).
  • Безопасность данных: инструмент проверяет целостность входных данных (наличие необходимых полей, корректность числовых значений и т.д.) прежде чем сохранить их в базе, что предотвращает появление некорректных сведений.
  • Поддержка разных форматов CSV: в том числе с разделителями-запятыми или точками с запятой, с заголовком колонок или без него – программа адаптируется автоматически (см. раздел 2.3. Формат CSV-файла).

Таким образом, для обычного пользователя Battery Import Tool выглядит как простой механизм загрузки списка батарей. Всё, что требуется – выбрать бренд (производителя), вставить содержимое CSV-файла с характеристиками батарей и нажать кнопку импорта. Дальнейшая обработка происходит автоматически, а результат сразу отображается на экране.

2.2. Интерфейс пользователя

Инструмент интегрирован в веб-страницу, представляющую собой форму для импорта. Ниже приведено описание элементов интерфейса, с которыми взаимодействует пользователь:

  • Поле выбора бренда (производителя) – выпадающий список Select Brand с перечнем всех доступных брендов батарей. Пользователь должен выбрать один бренд, которому соответствуют импортируемые данные. Если батареи разных брендов, их нужно импортировать раздельно, по одному бренду за раз.
  • Текстовые поля «Brand» и «Series» – расположены над формой поля ввода. Brand отображает выбранный бренд, а Series может использоваться для указания серии или модели ряда батарей. Эти поля связаны со специальным текстовым блоком (см. Prompt, описано ниже) и обновляются в реальном времени при ручном вводе текста.
  • Поле ввода данных (textarea) – большая текстовая область с меткой Battery Data (CSV format), куда пользователь вставляет содержимое CSV-файла. Предполагается, что пользователь заранее открыл CSV-файл (например, в редакторе таблиц или текстовом редакторе) и скопировал оттуда все строки с данными.
  • Кнопка «Import» – кнопка запуска процесса импорта. Она становится активной после выбора бренда и ввода (вставки) данных. При нажатии происходит отправка формы на сервер для обработки.
  • Сообщение о результате импорта – после нажатия кнопки и обработки данных сервер возвращает страницу с сообщением вверху формы. Это сообщение окрашено в зелёный цвет при успешном импорте или в красный – при ошибке. В сообщении указывается итог: например, Import successful: 12 added, 3 updated. (импорт успешно выполнен: добавлено 12 записей, обновлено 3) или подробное описание ошибки, если импорт не удался (например, Import failed: Voltage is required (line 5). – ошибка: на строке 5 не указан обязательный параметр «Напряжение»).
  • Блок «Prompt» (сопроводительный текст) – дополнительная текстовая подсказка или шаблон, расположенная ниже формы (в оригинальном интерфейсе она находится внутри тега <summary id="Prompt">). Этот блок содержит заранее подготовленный текст на русском языке, в который динамически подставляются выбранный бренд и серия (через <strong id="Brand"> и <strong id="Series">). Текст представляет собой, судя по коду, некую инструкцию или описание, связанное с выбранным брендом и серией батарей. Рядом с ним есть кнопка «Скопировать Prompt» (идентификатор PromptCopy), при нажатии которой весь текст блока Prompt копируется в буфер обмена[^3]. Этот функционал служит удобству пользователя: например, после успешного импорта можно скопировать шаблон описания для отчёта или другого документа, уже содержащий актуальные названия бренда и серии.

Рисунок 2.1. Внешний вид формы импорта (схематично): выпадающий список брендов, поле для данных CSV и кнопка запуска импорта.

flowchart TD
    A["Выбор бренда (выпадающий список)"] --> B["Вставка данных CSV (текстовое поле)"]
    B --> C["Нажатие кнопки «Import»"]
    C --> D{Обработка на сервере}
    D -->|Успешно| E["Появляется сообщение об успехе: 'added X, updated Y'"]
    D -->|Ошибка| F["Появляется сообщение об ошибке с описанием проблемы"]
    E --> G["Данные добавлены в систему"]
    F --> G

Диаграмма 2.1. Пример последовательности действий пользователя при импорте данных.

На этой диаграмме показан процесс взаимодействия пользователя с инструментом импорта: пользователь выбирает бренд, вставляет CSV-данные, нажимает кнопку импорта. Далее сервер обрабатывает данные – если всё прошло успешно, пользователь видит зелёное сообщение о числе добавленных/обновлённых записей; если произошла ошибка, отображается красное сообщение с пояснением. В любом случае после выполнения импорта (успешного или нет) пользователь может скорректировать данные и повторить попытку, либо перейти к другим действиям.

2.3. Формат CSV-файла

Перед использованием инструмента необходимо подготовить CSV-файл с данными батарей. CSV (от англ. Comma-Separated Values – «значения, разделённые запятыми»[^1]) – это текстовый формат, предназначенный для представления табличных данных. Каждая строка CSV-файла соответствует одной записи (в нашем случае – одной батарее), а значения полей в строке отделены определённым символом-разделителем.

Поддерживаемый формат CSV для импорта батарей:

  • Разделитель колонок: инструмент поддерживает разделители как запятую (,) – стандартный для CSV, так и точку с запятой (;) – часто используемый в русскоязычных локалях, где запятая применяется в качестве десятичного разделителя. Программа автоматически определяет, какой символ-разделитель используется, на основе первой строки файла. Пользователю не нужно указывать это явно – достаточно, чтобы во всём файле consistently применялся один тип разделителя.
  • Строка заголовков: рекомендуется, чтобы первая строка CSV-файла содержала названия колонок (заголовок), такие как Manufacturer, Model, Type, Voltage и т.д. При наличии заголовка импортёр распознает названия полей и соотнесёт их с ожидаемыми данными. Если заголовка нет, инструмент попытается распознать формат по количеству столбцов:
  • Если колонок ровно 4 или меньше, и нет явного заголовка, предполагается упрощённый порядок полей по умолчанию (имя или модель, тип, напряжение, ёмкость – см. ниже).
  • Если же колонок больше 4 и заголовок отсутствует или не распознан, импорт прервётся с ошибкой. В таком случае необходимо либо добавить строку заголовков в CSV, либо убедиться, что файл соответствует ожидаемой структуре.
  • Кодировка файла: CSV-файл должен быть сохранён в кодировке UTF-8 (без BOM) для корректной обработки русских символов и специальных знаков. Другие кодировки могут привести к искажению данных при импорте.
  • Разделитель строк: каждая запись должна начинаться с новой строки. Инструмент поддерживает стандартные переводы строк \n (Unix) и \r\n (Windows); они автоматически нормализуются при обработке.

Обязательные и опциональные поля: Для успешного импорта каждой записи (строки) CSV требуется наличие ряда обязательных полей. Ниже перечислены поля, обрабатываемые системой, с указанием обязательности и ожидаемого типа данных:

  • Manufacturer (Производитель): название производителя батареи. Обязательное поле. Если CSV-файл не содержит отдельной колонки Manufacturer, то в качестве производителя будет автоматически подставлено название выбранного бренда из выпадающего списка (см. раздел 2.2). Таким образом, пользователь может либо вообще не включать колонку производителя в CSV (тогда все импортируемые батареи получат производителя, совпадающего с выбранным брендом), либо включить – например, если необходимо указать уточненное имя производителя/бренда вручную для каждой строки.
  • Model (Модель, обозначение батареи): текстовое название или модельный номер батареи. Обязательное поле. Может также именоваться Name – инструмент воспримет колонку с названием «Name» как эквивалент «Model». Это значение используется для идентификации батареи внутри выбранного бренда, поэтому важно, чтобы у одного бренда не было двух батарей с точно одинаковым названием модели (регистр символов не учитывается при сравнении имён).
  • Type (тип батареи): тип/технология аккумулятора. Обязательное поле. Может задаваться двумя способами:
  • Название типа (например, «Lead-Acid AGM» или «LiFePO4») – текстовое поле, нечувствительное к регистру. Инструмент попробует сопоставить введённое название с известными типами в системе.
  • Код типа (BatteryTypeID) – целочисленный идентификатор типа батареи, соответствующий записи в справочнике типов. Пользователь может указать непосредственно ID типа, если он известен. Например, 10 для LiFePO4. Если значение типа не распознано (ни по названию, ни по ID), импорт будет прерван с ошибкой. Примечание: справочник типов батарей и соответствующие ID перечислены в разделе 2.4 ниже для удобства пользователей.
  • Voltage (номинальное напряжение): числовое значение напряжения батареи в вольтах (V). Обязательное поле. Должно быть положительным числом. Может быть целым или дробным (десятичная точка . используется как разделитель целой и дробной части). При попытке импортировать отрицательное напряжение или нечисловое значение будет выдана ошибка.
  • Capacity (ёмкость): числовое значение ёмкости батареи (обычно в ампер-часах, Ah). Обязательное поле. Так же, как и напряжение, должно быть неотрицательным числом (в действительности ёмкость должна быть > 0 для реальных батарей). Если значение некорректно (например, текст вместо числа, отрицательное значение), импорт прервётся с ошибкой.

Кроме выше перечисленных обязательных полей, CSV может содержать дополнительные, опциональные поля, которые будут импортированы в базу данных, если присутствуют. Они не обязательны, но позволяют сразу заполнить расширенную информацию о батарее:

  • ReferenceValue (Референсное значение): целое число, дополнительный параметр для ссылки или категории батареи. Это поле используется в системе как дополнительный классификатор (например, может означать некий код модели или другое свойство). Если в CSV оно присутствует, оно должно быть целым числом. При неверном формате будет ошибка.
  • ChargeCurrent (максимальный ток заряда): допустимый ток заряда батареи (в амперах). В CSV должен быть числом (целым или дробным) ≥ 0. Отражает максимально рекомендуемый ток при зарядке.
  • DischargeCurrent (максимальный ток разряда): допустимый ток разряда батареи (в амперах). Также неотрицательное число.
  • LifeTime (срок службы): ожидаемый срок службы батареи в годах. Целое число ≥ 0 (как правило, указывается например 5, 10 лет и т.д.).
  • TemperatureCoefficient20 и TemperatureCoefficient25 (температурный коэффициент ёмкости при 20°C и 25°C): числовые коэффициенты (может быть дробное значение), показывающие относительное изменение ёмкости при отклонении температуры от нормы (обычно выражаются в долях единицы на градус Цельсия). Например, коэффициент -0.005 1/°C означает, что при понижении температуры на 1°С ёмкость уменьшается на 0.5%. Эти поля сугубо технические и обычно берутся из технических паспортов батарей. Они не обязательны; если не указаны, могут быть рассчитаны по умолчанию или не заданы.
  • C1, C5, C20 (ёмкость при 1-часовом, 5-часовом, 20-часовом разряде): значения ёмкости батареи (в Ah) при различных режимах разряда. Дело в том, что реальная ёмкость аккумулятора зависит от скорости разряда: чем быстрее разряд, тем меньше отдаваемая ёмкость. Эти три поля позволяют указать ёмкость, измеренную при разряде за 1 час, за 5 часов и за 20 часов соответственно. Если они есть в CSV, они должны быть неотрицательными числами. Необязательные поля, полезны для точного моделирования характеристик батареи.
  • Weight (масса батареи): вес батареи в килограммах (кг). Дробное или целое число ≥ 0.
  • Length, Width, Height (габариты: длина, ширина, высота): размеры батареи в миллиметрах. Указываются целыми числами (мм) ≥ 0. Опциональные поля, позволяют хранить информацию о размере устройства.
  • Terminals (тип выводов): текстовое поле для обозначения типа клемм/выводов батареи (например, «F1», «M6 bolt» и т.п.). Необязательное текстовое поле, не влияющее на логику, но сохраняемое в базе, если указано.

Именование колонок: Заголовки колонок CSV-файла могут быть указаны в произвольном регистре, пробелы вокруг названий игнорируются. Важно, чтобы они совпадали с ожидаемыми именами (или их синонимами) на английском языке, перечисленными выше. Допустимые варианты названий, которые распознаёт программа:

  • Manufacturer (производитель)
  • Model или Name (модель)
  • Type или BatteryTypeID (тип батареи или ID типа)
  • Voltage (напряжение)
  • Capacity (ёмкость)
  • ReferenceValue (референс, обратите внимание на написание)
  • ChargeCurrent (ток заряда)
  • DischargeCurrent (ток разряда)
  • LifeTime (срок службы)
  • TemperatureCoefficient20 / TemperatureCoefficient25 (температурные коэффициенты)
  • C1, C5, C20 (ёмкости при разных режимах)
  • Weight (масса)
  • Length, Width, Height (размеры)
  • Terminals (выводы)

Если ваш CSV-файл содержит эти колонки, инструмент правильно их идентифицирует. Колонки могут быть в любом порядке. Если каких-то колонок нет, просто будут использованы значения по умолчанию или они будут пропущены (для необязательных полей). Если же в CSV присутствуют дополнительные колонки, не предусмотренные системой, рекомендуется их удалить перед импортацией или убедиться, что их игнорирование не повлияет на формат (лучшая практика – приводить CSV строго к требуемому набору полей).

Пример CSV-файла: Ниже приведён небольшой пример содержимого CSV (с заголовком), соответствующий требованиям:

Manufacturer,Model,Type,Voltage,Capacity,Weight
ACME Corp,SuperBattery 100,LiFePO4,12.8,100,14.5
ACME Corp,UltraBattery 150,LiFePO4,12.8,150,18.2
ACME Corp,Oldie 12V,Lead-Acid AGM,12.0,50,10.0

В этом примере импортируются три батареи. Первые две – литий-железо-фосфатные (LiFePO4) с напряжением 12.8 V, ёмкостью 100 и 150 Ah и массой 14.5 кг и 18.2 кг. Третья – AGM (кислотная батарея) 12 V, 50 Ah, весом 10 кг. Manufacturer везде указан явно как ACME Corp, но если в файле Manufacturer не было бы, мы могли бы выбрать бренд ACME в интерфейсе, и тогда инструмент автоматически подставил бы ACME как производителя каждой батареи при сохранении.

2.4. Справочник типов батарей (BatteryTypeID)

При указании типа батареи в CSV (поле Type) пользователь может использовать либо имя типа, либо числовой идентификатор. Ниже приведён справочник доступных типов батарей, поддерживаемых системой, и их ID. Эти данные помогут правильно заполнить колонку Type или BatteryTypeID в CSV-файле:

  • 1 – Unknown (неизвестный тип, по умолчанию)
  • 2 – Lead-Acid AGM (VRLA) – свинцово-кислотная батарея AGM[^4] (герметичная, с Absorbent Glass Mat, разновидность VRLA[^5])
  • 3 – Lead-Acid VRLA (Generic) – свинцово-кислотная батарея VRLA (общий тип, Valve-Regulated Lead-Acid[^5])
  • 4 – Lead-Acid SLA – свинцово-кислотная батарея SLA (Sealed Lead Acid, герметичная свинцово-кислотная)
  • 5 – Lead-Acid AGM – свинцово-кислотная батарея AGM (Absorbent Glass Mat, возможно дублирующая категория №2)
  • 6 – Lead-Acid GroE (OPzS) – свинцово-кислотная батарея типа OPzS (долговечные стационарные батареи с жидким электролитом, т.н. GroE)
  • 7 – Lead-Acid Ca/Ca – свинцово-кислотная батарея с кальций-кальциевым сплавом пластин (Ca/Ca)
  • 8 – Lead-Acid Sb/Ca Hybrid – свинцово-кислотная гибридная (со сплавом сурьма/кальций)
  • 9 – Lead-Acid Gel – свинцово-кислотная гелевая батарея (электролит загущён в гель)
  • 10 – LiFePO4 – литий-железо-фосфатный аккумулятор (LiFePO4, LFP[^6], один из типов литий-ионных)
  • 11 – Lead-Acid AGM (PzS) – свинцово-кислотная батарея AGM типа PzS (тяговые батареи с трубчатыми пластинами)
  • 12 – Ni-MH – никель-металл-гидридный аккумулятор (Ni-MH)
  • 13 – Li-ion (NMC/NCA/LCO) – литий-ионный аккумулятор на основе оксидов NiMnCo / NiCoAl / LiCoO2 (распространённые химические составы литий-ионных)
  • 14 – Li-Polymer – литий-полимерный аккумулятор (Li-Poly)
  • 15 – Li-ion (LTO) – литий-ионный аккумулятор на основе литий-титаната (LTO)
  • 16 – Ni-Fe – никель-железный аккумулятор (NiFe, «аккумулятор Эдисона»)
  • 17 – Ni-Zn – никель-цинковый аккумулятор
  • 18 – Ni-H2 – никель-водородный аккумулятор (используется, например, в космической технике)
  • 19 – Ag-Zn – серебряно-цинковый аккумулятор (дорогой, высокоэнергоплотный, часто первичный либо с ограниченным числом циклов)
  • 20 – Na-S – натрий-серный аккумулятор (высокотемпературный)
  • 21 – Na-NiCl2 – натрий-никель-хлоридный аккумулятор (также известен как ZEBRA, высокотемпературный)
  • 22 – Flow Battery – проточный аккумулятор (редокс-аккумулятор)

При подготовке CSV-файла убедитесь, что в каждом ряду указано корректное название или ID типа батареи, соответствующее списку выше. Название можно писать с любым регистром, например: «LiFePO4» эквивалентно «lifepo4» при импорте – регистр не имеет значения. Если система не сможет сопоставить указанный тип ни по ID, ни по имени, она выдаст ошибку и укажет строку, на которой встретился неизвестный тип.

2.5. Пошаговая инструкция по импорту данных

Чтобы успешно загрузить данные об аккумуляторах в систему с помощью Battery Import Tool, выполните следующие шаги:

  1. Подготовьте CSV-файл с данными, убедившись, что он соответствует требованиям из раздела 2.3. Проверьте, что все обязательные поля присутствуют, данные валидны, а первая строка – это заголовок колонок (Name, Type, Voltage, Capacity, etc.), либо файл содержит только 4 колонки (Name,Type,Voltage,Capacity) без заголовка.
  2. Откройте веб-интерфейс импорта батарей. Вам должна быть доступна страница с заголовком «Import Battery Data (CSV)». Возможно, для доступа требуется авторизация под учётной записью с соответствующими правами (см. 4.4. Безопасность и доступ).
  3. Выберите бренд (производителя) из выпадающего списка Select Brand. Щёлкните по списку и выберите нужное название. Примечание: Если нужного бренда нет в списке, обратитесь к администратору системы для добавления нового бренда (см. 4.3. Управление справочником брендов). Пока бренд не выбран, вы не сможете продолжить импорт (кнопка будет неактивна).
  4. (Опционально) Уточните серию или модельный ряд. Если интерфейс содержит поле Series (серия) для дополнительной фильтрации или информации, вы можете заполнить его. Это не влияет на импортируемые данные, но подставляется в текст Prompt для удобства. Например, вы можете ввести в поле Series значение, соответствующее линейке моделей (как в примере [DT] для серии). Если это поле не требуется, его можно проигнорировать.
  5. Откройте CSV-файл в текстовом редакторе или программе для работы с таблицами (Excel, Google Sheets и т.д.) и скопируйте все строки с данными, которые хотите импортировать (включая строку заголовков, если она есть).
  6. Вставьте скопированные данные в большое текстовое поле Battery Data (CSV format) на странице. Убедитесь, что текст вставился полностью. Визуально проверьте, что строки разделяются правильным образом, и символы не исказились.
  7. Нажмите кнопку «Import». После нажатия страница отправит данные на сервер и начнётся процесс импорта. Дождитесь обновления страницы с результатом. Обычно импорт происходит быстро (доли секунды или несколько секунд, в зависимости от количества строк).
  8. Проанализируйте результат. В верхней части страницы, под заголовком, появится сообщение:
  • Если всё прошло успешно, сообщение зелёным цветом сообщит, сколько записей было добавлено (added) и сколько обновлено (updated). Например: Import successful: 5 added, 2 updated. Это означает, что 5 новых батарей добавлено в базу, а 2 записи соответствовали уже существующим батареям (с тем же именем в рамках выбранного бренда) и были обновлены новыми данными из CSV.
  • Если произошла ошибка, появится сообщение красного цвета, начинающееся с Import failed: с описанием проблемы. Например: Import failed: Model is required (line 3). – означает, что в строке 3 CSV-файла отсутствует значение в колонке Model, хотя оно обязательно. В случае ошибки ни одна запись из файла не будет сохранена в базе, даже если до ошибки некоторые строки были корректны. Программа выполняет все операции в транзакции и откатывает изменения при первой же ошибке, поэтому вам нужно исправить CSV-файл и повторить импорт, не беспокоясь о частичном добавлении.
  1. Если необходимо, скопируйте сопроводительный текст (Prompt). После успешного импорта пользователь может нажать кнопку «Скопировать Prompt» (расположенную под серой чертой внизу страницы) – это скопирует в буфер специальный текстовый блок, где автоматически подставлены текущие бренд и серия. Например, текст может выглядеть как описание: «Для бренда [DELTA] серии [DT] добавлены батареи … » и так далее. Этот текст можно использовать, например, в отчёте или при формировании документации, чтобы не набирать вручную. Обратите внимание, что поля Brand/Series в тексте обновляются в реальном времени, когда вы выбираете бренд и вводите серию (благодаря скрипту на странице).
  2. Проверка данных в системе. После успешного импорта вы можете в соответствующем разделе системы (например, в интерфейсе просмотра списка батарей для данного бренда) убедиться, что новые записи появились или обновились. Все импортированные значения должны отобразиться на своих местах (тип, напряжение, ёмкость и прочие поля).
  3. При необходимости повторите импорт для других брендов или после исправления ошибок. Инструмент позволяет выполнять импорт многократно. Если нужно обновить уже загруженные данные, вы можете снова импортировать CSV с теми же батареями: система распознает существующие по имени и вместо дублирования обновит их поля (поля, которые заданы в CSV, будут обновлены на новые значения, а те, которые не указаны – останутся прежними, так как используется принцип COALESCE для обновления только предоставленных данных). Если вы хотите добавить новые батареи помимо уже существующих, просто включите их как новые строки с уникальными именами – они будут добавлены.

2.6. Возможные ошибки при импорте и их устранение

Инструмент предусматривает детализированные сообщения об ошибках, чтобы пользователь мог понять причину неудачи и исправить проблему. Ниже перечислены распространённые ошибки при импорте и способы их решения:

  • «Invalid or missing brand selection.» – Вы не выбрали бренд из списка перед нажатием Import. Решение: выбрать нужный бренд и повторить попытку.
  • «No battery data provided. Please paste the CSV data into the text area.» – Поле с CSV-данными осталось пустым. Решение: вставить данные из CSV-файла в текстовую область.
  • «CSV contains only a header and no data.» – Вы вставили только строку заголовков без строк с данными. Решение: убедиться, что в буфере обмена скопированы все строки, а не только первая. Добавить данные или убрать лишнюю пустую строку, затем повторить импорт.
  • «CSV header missing or unrecognized. Please include a header row with field names.» – Вставленный текст не содержит корректной строки заголовков, и при этом система обнаружила больше 4 полей в строках. Вероятно, CSV-файл имеет нестандартный формат или первая строка не распознана как заголовок. Решение: проверить первую строку CSV. Возможно, она пуста или содержит непонятные названия. Добавьте/исправьте заголовок или удалите первую строку, если она лишняя. Если файл действительно не имеет заголовка, но содержит больше 4 столбцов, добавьте строку заголовков вручную.
  • «Manufacturer is required (line X).» – В строке X не указано название производителя (колонка Manufacturer пуста). Обычно это возникает, если вы включили колонку Manufacturer в CSV, но для данной строки не заполнили значение. Решение: либо заполнить производителя в этой строке, либо убрать колонку Manufacturer вообще (тогда будет использоваться бренд из формы для всех строк).
  • «Model is required (line X).» – В строке X отсутствует название модели батареи (колонка Model или Name пустая). Это поле обязательно, поэтому заполните уникальное название модели.
  • «Battery type is required (line X).» – В строке X не указан тип батареи (Type пустой). Заполните тип (именем или ID согласно справочнику).
  • «Battery type column is missing.» – В CSV-файле отсутствует колонка, указывающая тип батареи. Присутствие поля типа обязательно. Решение: добавить колонку Type или BatteryTypeID в CSV и заполнить её.
  • «Unknown battery type ‘…'(line X).» – Указан тип батареи, который не распознаётся системой. В сообщении будет приведено название/ID, который не найден. Решение: проверить опечатки в названии типа (сравнить со справочником из раздела 2.4). Если используется ID, убедиться, что он есть в списке. Если тип новый и его нет в системе, обратитесь к администратору для добавления нового типа батарей (см. 4.3).
  • «Voltage is required (line X).» – Не указано значение напряжения в строке X (пустая колонка Voltage). Добавьте числовое значение напряжения.
  • «Invalid voltage value (line X).» – В поле напряжения указано некорректное значение (например, текст или символы, или отрицательное число). Исправьте на положительное число (можно дробное с точкой).
  • «Capacity is required (line X).» – Не указано значение ёмкости (пустая колонка Capacity). Заполните числом.
  • «Invalid capacity value (line X).» – Некорректное значение ёмкости (см. описание напряжения). Сделайте аналогично – положительное число.
  • «BatteryTypeID must be a number (line X).» – Если вы используете колонку BatteryTypeID для указания типа, то значения в ней должны быть цифрами. Эта ошибка появляется, если в поле ожидается число, а встретился текст или пусто. Проверьте, что в колонке BatteryTypeID только номера (например, 10, 22 и т.д.). Если у вас названия типов, то колонку надо назвать Type, а не BatteryTypeID, чтобы система воспринимала текст как имя.
  • «Line X: expected Y columns, got Z.» – Количество значений в строке X не соответствует числу заголовков/столбцов (например, каких-то разделителей не хватает или лишние). Такое происходит, если CSV-файл повреждён или неаккуратно отредактирован – например, значения содержат запятую или точку с запятой без должного экранирования кавычками, из-за чего строка «ломается» на лишние колонки. Решение: открыть CSV в редакторе, проверить строку X – скорее всего, одно из полей содержит символ-разделитель. Добавьте кавычки вокруг текстового значения, если оно включает запятую или точку с запятой. Либо воспользуйтесь функцией экспорта в CSV из программы типа Excel, которая сделает правильное экранирование автоматически. После исправления повторите импорт.
  • «Failed to prepare database statements.» – Системная ошибка подготовки запросов к базе данных. Это нетипичная ошибка для конечного пользователя; она может указывать на проблему с сервером (например, несовместимая версия MySQL). Решение: сообщить администратору или разработчику о проблеме.
  • «CSV parsing error on line X.» – Общая ошибка парсинга CSV на указанной строке. Может быть связана с редким случаем, если строка имеет неправильный формат, не покрытый отдельными сообщениями выше. Решение: перепроверьте синтаксис CSV, правильность кавычек и разделителей в этой строке.
  • «Import failed: …» (любое другое сообщение) – Любая другая ошибка будет содержать пояснение. Чаще всего ошибки связаны с некорректными данными, поэтому внимательно читайте текст и исправляйте источник данных. После устранения всех выявленных проблем файл будет импортирован успешно.

При возникновении ошибок всегда корректируйте исходный CSV-файл, а не пытайтесь дозагрузить недостающие данные вторым файлом (это может привести к дублям или частичному обновлению). Инструмент разработан так, чтобы за один импорт применялись все изменения целиком. После исправления файла повторная попытка обычно приводит к успешному результату.

3. Документация для разработчика

3.1. Архитектура решения

Battery Import Tool реализован как серверный скрипт на PHP с веб-интерфейсом (HTML/JavaScript) и взаимодействует с базой данных MySQL. Ниже представлена схема архитектуры, показывающая основные компоненты и потоки данных:

flowchart LR
    subgraph "Веб-браузер (клиент)"
      UI[HTML-страница<br/>с формой импорта]
    end
    subgraph "Веб-сервер (PHP-приложение)"
      IMP[BatteryImporter<br/>(PHP-класс)]
      UI -->|HTTP POST<br/>(CSV данные)| IMP
    end
    subgraph "База данных MySQL"
      DB[(Таблицы:<br/>Brands,<br/>Battery,<br/>BatteryTypes)]
    end
    IMP -->|SELECT<br/>справочники| DB
    IMP -->|INSERT/UPDATE<br/>данные батарей| DB
    IMP -->|ответ: успех/ошибка| UI
    UI -->|результат<br/>импорта| Client[[Пользователь]]

Диаграмма 3.1. Архитектура Battery Import Tool и взаимодействие компонентов.

В данной архитектуре можно выделить следующие элементы:

  • Интерфейс пользователя (Frontend) – HTML-страница с формой. Она содержит необходимую разметку: поля ввода (выпадающий список брендов, текстовое поле для данных), кнопку отправки и скрипты на JavaScript для улучшения UX (например, обновление полей Brand/Series в реальном времени, копирование текста Prompt). При нажатии кнопки Import происходит отправка формы методом POST на этот же скрипт.
  • Скрипт импорта (Backend) – PHP-код, который принимает HTTP-запрос с данными формы. Он реализует логику обработки CSV: чтение данных, разбор CSV, валидация, запись в базу. Основная логика инкапсулирована в классе BatteryImporter (см. раздел 3.2). Скрипт также выводит обратно HTML-страницу с тем же интерфейсом, дополняя её сообщением о результате (ошибка или успех).
  • База данных (MySQL) – хранит все необходимые таблицы: Brands (бренды/производители батарей), Battery (сведения о батареях), BatteryTypes (справочник типов батарей), и возможно другие связанные таблицы. Инструмент производит запросы к БД:
  • Чтение списка брендов из таблицы Brands для заполнения выпадающего списка.
  • Чтение уже существующих батарей данного бренда из таблицы Battery (чтобы определять, добавить новую или обновить существующую запись).
  • Чтение справочника типов BatteryTypes для сопоставления текстового названия типа с его ID.
  • Вставка новых записей или обновление существующих записей в таблице Battery внутри транзакции.

При успешном выполнении транзакции изменения фиксируются (COMMIT), и скрипт формирует ответ с сообщением об успехе. Если возникает любая ошибка на этапе парсинга или записи, выполняется откат (ROLLBACK), и пользователю возвращается сообщение об ошибке без частичного сохранения данных.

Стоит отметить, что Battery Import Tool работает синхронно в рамках одного HTTP-запроса. Это означает, что при импорте больших файлов CSV сервер может затратить значительное время на обработку. Разработчику нужно учитывать ограничения по времени выполнения скрипта и объёму данных. В текущей реализации подходящими средствами управления являются настройки PHP (например, max_execution_time и memory_limit) и MySQL (например, размер транзакции). Однако, так как импорт обычно запускает ограниченный круг пользователей (администраторы или менеджеры, а не массовые пользователи), и файлы CSV – разумного размера, синхронный подход упрощает архитектуру. В случае необходимости обработки очень больших файлов или частого импорта, архитектура может быть улучшена (например, разбивкой на пакетную обработку, фоновые задания, отображение прогресса и т.п.).

3.2. Компоненты и структура кода

Код инструмента состоит из нескольких ключевых частей, объединённых в одном PHP-скрипте (файле). Для ясности разбиения логики, основные функции выделены в класс BatteryImporter. Ниже приведён обзор структуры кода:

  • Импорт настроек и подключение к БД:
    В начале скрипта выполняется require_once __DIR__ . '/../config.php'; – подключение конфигурационного файла. Предположительно, в нём устанавливается соединение с базой данных MySQL через объект $mysqlix (расширение MySQLi). Далее код проверяет успешность соединения:
  if (!isset($mysqlix) || $mysqlix->connect_error) {
      die("Database connection error.");
  }

Если соединения нет, скрипт завершается с сообщением об ошибке подключения к БД.

  • Загрузка списка брендов:
    После подключения к БД, скрипт сразу выбирает все доступные бренды:
  $brands = [];
  if ($stmt = $mysqlix->prepare("SELECT ID, Name FROM Brands ORDER BY Name")) {
      // выполнение и связывание результатов
      while ($stmt->fetch()) {
          $brands[$bid] = "$bname (ID: $bid)";
      }
      ...
  } else {
      die("Failed to fetch brands from the database.");
  }

Здесь результат сохраняется в массив $brands, где ключом является ID бренда, а значением – строка с названием бренда и его ID в скобках. Этот массив используется для наполнения <select> списка в HTML. Если запрос не удалось подготовить (что маловероятно), скрипт завершится с ошибкой.

  • Enum ImportOutcome:
    Определено перечисление (enum) ImportOutcome с двумя возможными значениями: ADDED и UPDATED. Это перечисление используется позже для удобного доступа к строковым константам ‘added’ и ‘updated’ (например, при формировании результата импорта).
  enum ImportOutcome: string {
      case ADDED = 'added';
      case UPDATED = 'updated';
  }

Примечание: enum – это возможность, появившаяся в PHP 8.1[^7]. Использование enum делает код более читаемым по сравнению с простыми строковыми константами.

  • Класс BatteryImporter:
    Основная логика импортирования содержится в классе BatteryImporter. Этот класс принимает подключение к базе данных в конструкторе и имеет один главный метод importFromCsv($brandId, $csvText): array. Рассмотрим детали этого метода в следующем разделе (3.3).
  • Обработка отправки формы:
    После определения класса, в скрипте создаётся его экземпляр:
  $importer = new BatteryImporter($mysqlix);

Затем идёт проверка if ($_SERVER['REQUEST_METHOD'] === 'POST') – то есть, выполнено ли отправление формы. Если да, то собираются введённые данные:

  $postedBrand = $_POST['brand'] ?? "";
  $postedData = $_POST['data'] ?? "";

Здесь brand – выбранный ID бренда, а data – содержимое текстового поля (CSV-данные). Далее происходит базовая валидация:

  • Проверяется, выбран ли бренд и действительно ли он есть в массиве $brands. Если ID некорректен или пуст, формируется сообщение $message с ошибкой (HTML-строка с красным текстом).
  • Проверяется, не пусты ли данные CSV (после trim). Если пусто – формируется сообщение об этом.
  • Если оба условия в порядке, начинается попытка импорта: try { $results = $importer->importFromCsv($brandId, $postedData); $added = $results[ImportOutcome::ADDED->value] ?? 0; $updated = $results[ImportOutcome::UPDATED->value] ?? 0; $message = "<p style='color:green;'>Import successful: <strong>{$added}</strong> " . ImportOutcome::ADDED->value . ", <strong>{$updated}</strong> " . ImportOutcome::UPDATED->value . ".</p>"; $postedData = ""; } catch (Exception $ex) { $errMsg = htmlspecialchars($ex->getMessage(), ...); $message = "<p style='color:red;'>Import failed: {$errMsg}</p>"; } Здесь вызывается метод импорта; если он возвращает результат, извлекается количество добавленных и обновлённых записей. Они помещаются в зеленое сообщение об успехе. В случае успеха также очищается $postedData (чтобы текстовое поле стало пустым после обновления страницы – во избежание повторного импорта тех же данных по F5, и чтобы не мешало).
    Если же выброшено исключение, его текст берётся и экранируется в $errMsg, а затем формируется сообщение об ошибке. Обратите внимание, что текст исключения содержателен (например, «Voltage is required (line 5)»), поэтому пользователь получит эту информацию.
  • Вывод HTML-страницы:
    Наконец, скрипт выводит основной HTML содержимого страницы: заголовок формы, само сообщение $message (если есть), затем разметку формы (select с брендами, textarea, кнопка). Также выводятся два текстовых инпута BrandInput и SeriesInput (видимые пользователю поля ввода с автозаполнением примерами «DELTA» и «DT») – они, как отмечалось ранее, используются для механизма Prompt. После формы следует блок <article> с готовым текстом Prompt, где вставлены placeholders [DELTA] и [DT] внутри <strong id="Brand"> и <strong id="Series">.
    В конце страницы подключён скрипт на JavaScript, который:
  • Реализует функцию bindInputToText(inputId, outputId), связывающую поле ввода с элементом текста (при вводе обновляет содержимое текста).
  • Вызывает bindInputToText('BrandInput','Brand') и аналогично для Series – тем самым, когда пользователь печатает что-либо в поле BrandInput или SeriesInput, соответствующие <strong id="Brand"> и <strong id="Series"> внутри Prompt обновляются сразу. Это позволяет сразу видеть подставленные значения в шаблоне Prompt.
  • Реализует функцию copyPromptTextToClipboard(), которая копирует видимый текст из блока Prompt (id Prompt) в буфер обмена. Она поддерживает два способа: через современный API navigator.clipboard.writeText, а для старых браузеров – через создание временного <textarea> и команду document.execCommand('copy'). Эта функция привязана к кнопке копирования PromptCopy по событию click. Таким образом, на выходе страница представляет интерактивную форму с динамическими элементами, но основной процесс импорта выполняется на сервере.

Итоговая структура файла выглядит так:

  1. Подключение конфига, проверка соединения.
  2. Получение списка брендов.
  3. Определение enum и класса BatteryImporter (с методом importFromCsv).
  4. Создание объекта импортера.
  5. Обработка $_POST: валидация входных параметров, вызов $importer->importFromCsv, формирование сообщения.
  6. Вывод HTML: форма и вспомогательные элементы (BrandInput, SeriesInput, Prompt), плюс скрипт для динамического обновления и копирования.

Следует отметить, что данная структура «в одном файле» характерна для простых скриптов. В более крупном приложении код, вероятно, был бы разложен по разным файлам (отдельно класс импортера, отдельно шаблон интерфейса, и т.д.). Однако, поскольку цель инструмента – относительно ограниченная функциональность, такой монолитный скрипт допустим и упрощает развёртывание.

3.3. Логика метода BatteryImporter::importFromCsv

Метод importFromCsv(int $brandId, string $csvText): array – сердце всего инструмента. Он принимает идентификатор выбранного бренда и строку с CSV-данными, а возвращает ассоциативный массив с двумя числами: сколько записей добавлено ('added') и сколько обновлено ('updated'). В случае проблем он выбрасывает исключение (Exception) с сообщением об ошибке, которое затем перехватывается внешним кодом.

Рассмотрим по шагам, что делает этот метод (сопоставляя с исходным кодом):

  1. Глобальный массив брендов: в начале метода используется global $brands;. Это нужно, чтобы метод мог получить доступ к названию бренда по его ID, для сценария, когда в CSV не указана колонка Manufacturer. Массив $brands был подготовлен в глобальной области (см. раздел 3.2). Это небольшое нарушение инкапсуляции (глобальные переменные внутри метода), но в контексте данного скрипта это упростило реализацию. Разработчику важно помнить об этом, если вдруг метод будет использоваться отдельно – необходимо иметь заполненный глобальный $brands.
  2. Предварительная обработка текста CSV:
   $csvText = trim($csvText);
   $csvText = str_replace("\r\n", "\n", $csvText);
   $lines   = array_filter(explode("\n", $csvText), fn($line) => trim($line) !== "");

Сначала отсекаются лишние пробелы в начале/конце всей строки. Затем все последовательности \r\n (Windows-строки) заменяются на \n – для унификации перевода строк. Потом строка разбивается на массив строк $lines по \n. array_filter с анонимной функцией удаляет все пустые строки (если пользователь случайно добавил пустую строку в конце файла, она будет отброшена). В результате $lines содержит все непустые строки CSV.

  • Если после этой обработки массив $lines пуст, значит пользователь не предоставил данных (например, в поле были только пробелы или переводы строк). В таком случае выбрасывается исключение: throw new Exception("No data provided for import."); – это приведёт к сообщению Import failed: No data provided for import. которое мы описали в разделе 2.6.
  1. Определение разделителя и заголовка:
   $firstLine    = $lines[0];
   $delimiter    = (str_contains($firstLine, ';') && substr_count($firstLine, ';') >= substr_count($firstLine, ',')) ? ';' : ',';
   $headerFields = str_getcsv($firstLine, $delimiter);

Берётся первая строка файла ($firstLine). Чтобы определить разделитель, код проверяет: если строка содержит ; и количество ; не меньше, чем количество ,, то вероятно ; – разделитель. Иначе в качестве разделителя берётся запятая. Логика здесь: если даже есть запятые, но точек с запятой столько же или больше, значит скорее всего запятая используется как часть данных (например, десятичная запятая), а разделитель – ;. Это довольно простой эвристический метод, но он должен корректно работать в большинстве случаев (предполагая, что заголовок или первая строка не содержит тонкостей).

Далее str_getcsv используется для разбора первой строки с выбранным разделителем – получается массив полей $headerFields. str_getcsv – встроенная PHP-функция, умеющая парсить строку CSV с учётом кавычек и экранирования[^8].

  • Затем код проверяет: если есть \$headerFields и первое поле на вид похоже на одно из известных названий колонок (проверяется через регулярное выражение /^(manufacturer|model|type|B8?)/i). Здесь можно заметить странность: шаблон B8? скорее всего результат некорректной кодировки (возможно, имелось в виду что-то вроде ^(manufacturer|model|type|battery) – например, чтобы захватить вариант «BatteryTypeID», но символы исказились). Тем не менее, идея: распознать, является ли первая строка заголовком. Если да, выполняется: array_shift($lines); if (empty($lines)) { throw new Exception("CSV contains only a header and no data."); } То есть, заголовок удаляется из массива $lines (чтобы далее обрабатывать только строки с данными). Если после удаления оказывается, что больше строк нет – бросается исключение об отсутствии данных (заголовок без данных).
  • Если первая строка не распознана как заголовок: } elseif (count(str_getcsv($firstLine, $delimiter)) > 4) { throw new Exception("CSV header missing or unrecognized. Please include a header row with field names."); } То есть, если строка не выглядит как заголовок, и при этом число колонок > 4 – считается, что, вероятно, пользователь забыл строку заголовков или формат неправильный, потому что без заголовка ожидается максимум 4 колонки (Name,Type,Voltage,Capacity). В такой ситуации выбрасывается исключение о том, что заголовок не распознан или отсутствует.
  • Если ни одно из условий не сработало (например, строка не распознана как заголовок, но и колонок не более 4), значит файл, видимо, без заголовка и содержит 4 или менее столбцов. Тогда код готовится воспринимать эти 4 столбца как фиксированные поля: $colIndex = [ 'name' => 0, 'type' => 1, 'voltage' => 2, 'capacity'=> 3 ]; Массив $colIndex будет отображать имена полей на индексы колонок. Если заголовок был распознан, немного ниже создаётся $colIndex на основе заголовков: if (!empty($headerFields)) { foreach ($headerFields as $idx => $colName) { $colName = trim($colName); if ($colName === "") continue; $colIndex[strtolower($colName)] = $idx; } } Таким образом, $colIndex получает в ключах имена колонок в нижнем регистре, а в значениях – соответствующие индексы колонок CSV. Если файл имел заголовок, там будут ключи «manufacturer», «model» и т.д. Если нет, то останется как задано (name, type, voltage, capacity).
  1. Подготовка SQL-запросов:
    Чтобы эффективно вставлять и обновлять записи, заранее подготавливаются два параметризированных SQL-запроса (prepared statements[^9]):
  • $insertSql – оператор INSERT для таблицы Battery, перечисляющий все столбцы, кроме первичного ключа (ID). В VALUES проставлено 20 знаков ? – плейсхолдеров для параметров (под каждое поле).
  • $updateSql – оператор UPDATE для таблицы Battery, который обновляет соответствующие поля. Он устроен особым образом: UPDATE Battery SET BatteryTypeID = COALESCE(?, BatteryTypeID), Manufacturer = COALESCE(?, Manufacturer), ... Terminals = COALESCE(?, Terminals) WHERE ID = ? Здесь для каждого поля используется функция COALESCE(new_value, old_value), что означает: если новый параметр не NULL, то поле будет обновлено на него, а если NULL, то останется старое значение. Это позволяет при обновлении существующей батареи задавать только те поля, которые мы хотим изменить, не затрагивая остальные (например, если в CSV не было какого-то столбца, мы не хотим его обнулить). Однако в реализации данного инструмента используется такой подход не для выборочного обновления от пользователя, а скорее для внутренней логики: все параметры передаются, но если в CSV поле пустое, то передаётся NULL (пропускается изменение). Таким образом, существующие значения сохранятся. В конце запроса WHERE ID = ? – ограничение, что обновляется запись с конкретным ID. То есть обновление идёт точечно по идентификатору найденной существующей батареи.
  • После формирования SQL, с помощью $this->db->prepare() подготавливаются два объекта $insertStmt и $updateStmt. Если по каким-то причинам подготовка не удалась (например, ошибка синтаксиса запроса или проблемы с соединением), бросается исключение «Failed to prepare database statements.».
  1. Запуск транзакции:
   $this->db->begin_transaction();
   mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

Стартует транзакция на соединении (все последующие операции с базой будут накоплены, но не зафиксированы, пока не вызван commit). Также устанавливается режим отчёта ошибок MySQLi: MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT. Это означает, что любые ошибки при выполнении SQL-запросов будут выбрасывать исключения (mysqli_sql_exception), что облегчит обработку – мы сможем перехватить их в блоке catch. Обратите внимание: начиная с PHP 8.1 MySQLi по умолчанию и так настроен на выброс исключений при ошибках соединения/запросов[^10], однако явный вызов mysqli_report улучшает совместимость и явно указывает намерение.

  1. Основной цикл обработки строк CSV:
    Теперь, внутри блока try { ... } catch, создаются вспомогательные структуры и производится перебор каждой строки CSV:
  • Словарь $existing: $existing = []; if ($existStmt = $this->db->prepare("SELECT Name, ID FROM Battery WHERE BrandID = ?")) { ... while ($existStmt->fetch()) { $existing[strtolower($existingName)] = $existingId; } } Здесь заранее выбираются все уже существующие батареи данного бренда (BrandID = ?). Предполагается, что комбинация (BrandID, Name) уникальна для таблицы Battery (то есть у одного бренда не может быть две батареи с одинаковым Name/Model). Выборка идёт через подготовленный запрос; результат сохраняется в массив $existing, где ключ – имя батареи в нижнем регистре, а значение – её ID. Это нужно, чтобы быстро проверить при обработке каждой строки CSV: существует ли батарея с таким именем для данного бренда. Если существует – мы будем делать UPDATE, если нет – INSERT. Зачем приводить к нижнему регистру? Чтобы сравнение имён было case-insensitive (не чувствительным к регистру). Таким образом, «BatteryX» и «batteryx» воспринимаются как одна запись. После использования, $existStmt закрывается (->close()), освобождая ресурс.
  • Словарь $typesMap: $typesMap = []; if ($typeStmt = $this->db->prepare("SELECT Name, Alternate, ID FROM BatteryTypes")) { ... while ($typeStmt->fetch()) { if ($typeName) $typesMap[strtolower($typeName)] = $typeId; if ($typeAlt) $typesMap[strtolower($typeAlt)] = $typeId; } } Это загрузка справочника типов батарей. Из таблицы BatteryTypes выбираются колонки Name, Alternate и ID. Видимо, Alternate – это альтернативное название типа (например, сокращение или синоним). Все названия приводятся к нижнему регистру и помещаются в $typesMap, указывая на соответствующий ID типа. Таким образом, при обработке CSV можно по строковому названию типа быстро найти нужный идентификатор. Если в CSV указан сразу ID (числом), эту мапу можно не использовать для той строки.
  • Перебор строк $lines:
    Далее foreach ($lines as $lineNum => $line) – цикл по каждой строке данных. $lineNum – индекс (начиная с 0) в массиве $lines. Чтобы сообщать пользователю понятный номер строки из исходного CSV, в коде используют ($lineNum + 1), а если была строка заголовка, то фактический номер строки в файле будет на 1 больше, но поскольку мы удалили заголовок, плюс 1 всё равно даёт правильное положение.
    Внутри цикла для каждой строки происходит:
    • Разбор CSV строки в массив $fields с использованием str_getcsv($line, $delimiter). Если возвращается null (что может случиться если, например, строка не удаётся корректно распарсить, хотя str_getcsv обычно просто вернёт массив), тогда бросается исключение «CSV parsing error on line X.».
    • array_map('trim', $fields) убирает пробелы вокруг каждого поля. Затем, если вдруг в строке меньше полей, чем ожидалось (меньше, чем count($colIndex)), выполняется array_pad для дополнения массива пустыми строками до нужной длины. Это сделано, чтобы если, например, последняя строка CSV заканчивается разделителем (пустое поле в конце), str_getcsv может игнорировать последний пустой элемент – а нам важно сохранить позицию под все ожидаемые колонки (включая пустое значение).
    • После этого, если количество полей всё ещё не равно ожидаемому, кидается исключение: «Line X: expected Y columns, got Z.» – это случай, когда строка имеет больше колонок, чем заголовков, либо непредвиденная ситуация (например, кавычки не закрыты в CSV, что привело к склейке полей).
    • Извлечение и проверка полей:
      Далее код извлекает значения из $fields по индексам, используя ранее собранный $colIndex. Каждое извлечённое значение валидируется:
    • Manufacturer: $manufacturer = null; if (isset($colIndex['manufacturer'])) { $manufacturer = $fields[$colIndex['manufacturer']]; if ($manufacturer === "") { throw new Exception("Manufacturer is required (line " . ($lineNum + 1) . ")."); } } else { $manufacturer = $brands[$brandId] ?? ""; $manufacturer = preg_replace('/\s+\(ID:.*\)$/', '', $manufacturer); } Если колонка Manufacturer присутствует в CSV, взять её значение, проверить, что не пустое. Если пустое – ошибка. Если колонки нет, тогда берётся название бренда из глобального массива $brands по \$brandId, и отрезается часть » (ID: X)» с помощью preg_replace, чтобы осталась только чистое название. Таким образом, $manufacturer всегда будет непустым после этого блока (если даже \$brands[\$brandId] отсутствует, подстраховка ?? "" дала бы «», но такого не бывает, потому что брендId проверен ранее).
    • Model: $model = null; if (isset($colIndex['model'])) { $model = $fields[$colIndex['model']]; if ($model === "") { throw new Exception("Model is required (line ...)"); } } if (!$model && isset($colIndex['name'])) { $model = $fields[$colIndex['name']]; if ($model === "") { throw new Exception("Battery name/model is required (line ...)"); } } if (!$model) { throw new Exception("Model is missing (line ...)."); } Здесь учитывается, что либо CSV содержит колонку Model, либо, как синоним, Name. Код сначала пытается взять Model; если его нет или он пуст, и есть Name, берёт Name. Если после этого $model всё ещё пуст (или ни одна колонка не была найдена) – бросается ошибка. В итоге к этому моменту мы имеем переменную $model – название модели батареи. Это будет использоваться и как основной ключ для определения уникальности.
    • Battery Type (ID): $typeId = null; if (isset($colIndex['batterytypeid'])) { $typeIdStr = $fields[$colIndex['batterytypeid']]; if ($typeIdStr === "" || !ctype_digit($typeIdStr)) { throw new Exception("BatteryTypeID must be a number (line ...)."); } $typeId = (int)$typeIdStr; } elseif (isset($colIndex['type'])) { $typeStr = strtolower($fields[$colIndex['type']]); if ($typeStr === "") { throw new Exception("Battery type is required (line ...)."); } if (ctype_digit($typeStr)) { $typeId = (int)$typeStr; } else { $typeId = $typesMap[$typeStr] ?? null; } if (!$typeId) { throw new Exception("Unknown battery type '{$fields[$colIndex['type']]}' (line ...)."); } } else { throw new Exception("Battery type column is missing (line ...)."); } Логика такая: если CSV даёт явный BatteryTypeID, проверяем, что поле не пустое и состоит только из цифр (ctype_digit). Если условие не выполняется – ошибка (возможно, пользователь смешал форматы). Если всё хорошо – приводим к integer.
      Иначе, если есть колонка Type (текстовое название типа): приводим его к нижнему регистру для сопоставления. Если пустой – ошибка «Battery type is required». Если строка состоит только из цифр (например, «10»), то, вероятно, пользователь случайно записал ID, но в колонку Type – мы поддерживаем это: ctype_digit($typeStr) вернёт true, тогда сделаем (int) и получим ID. Если строка не просто цифры, значит это текст – ищем его в $typesMap. Если не нашли – ошибка «Unknown battery type ‘…’.
      Если же ни ‘batterytypeid’, ни ‘type’ колонки не оказалось вообще (что маловероятно, так как на стадии заголовка мы проверяли наличие типа) – бросаем исключение, что колонка типа отсутствует.
      В итоге получаем $typeId – целочисленный идентификатор типа батареи, гарантированно валидный.
    • Voltage:
      Проверяем, что колонка ‘voltage’ присутствует и не пустая. Если пустая – ошибка «Voltage is required». Далее используем filter_var с флагом FILTER_VALIDATE_FLOAT для попытки извлечь число с плавающей точкой. Если === false или полученное число < 0 – ошибка «Invalid voltage value». Примечание: filter_var с FILTER_VALIDATE_FLOAT ожидает строку и возвращает число с плавающей точкой при успехе или false при неуспехе. В данном случае, если пользователь указал «12,8» (с запятой) вместо «12.8», filter_var вернёт false, потому что запятая не распознается как десятичная точка (нужно было либо сменить разделитель, либо использовать локализацию). В такой ситуации появится ошибка invalid value. Решение для пользователя – исправить на формат с точкой, как обычно в CSV требуют.
    • Capacity:
      Аналогично voltage: обязательное поле, проверяется на пустоту и валидируется как float ≥ 0. Ошибки: «Capacity is required» или «Invalid capacity value».
    • ReferenceValue:
      Не обязательное. Если колонка есть и значение не пустое: $referenceValue = filter_var(..., FILTER_VALIDATE_INT); if ($referenceValue === false) { throw new Exception("Invalid ReferenceValue (line ...)."); } То есть оно должно быть целым числом (можно отрицательное? По коду не запрещено явно, но логически вряд ли нужно отрицательное – однако, проверка только что это integer).
    • ChargeCurrent, DischargeCurrent:
      Если есть и не пусто: валидируем как float ≥ 0. Если не число или < 0 – ошибка.
    • LifeTime:
      Если есть и не пусто: как int ≥ 0 (логично, что отрицательного срока службы нет).
    • TemperatureCoefficient20, 25:
      Если есть и не пусто: валидируем как float (отрицательные значения тут в принципе возможны, так как при росте температуры ёмкость может снижаться, но сам по себе коэффициент обычно отрицательный для батарей – код не запрещает отрицательные, он только проверяет $... === false).
    • C1, C5, C20:
      Если есть и не пусто: float ≥ 0. (Как правило, они положительные или 0, отрицательных быть не должно).
    • Weight:
      Если есть и не пусто: float ≥ 0.
    • Length, Width, Height:
      Если есть и не пусто: int ≥ 0.
    • Terminals:
      Если есть и не пусто: просто записываем строку (не нужна спец. валидация). Для всех числовых полей, при ошибке выдаётся конкретное сообщение, какое поле неверно и в какой строке.
    • Формирование имени записи: $name = $model; Здесь решено использовать поле Model/Name как ключевое имя батареи (в самой таблице Battery, вероятно, есть колонка Name или Model; судя по SQL, там есть и Manufacturer, и Model отдельно, плюс BrandID). Поскольку BrandID и Manufacturer могут дублировать друг друга в большинстве случаев, уникальность определяется, видимо, BrandID + Name(Model). Поэтому на основе модели формируется $name. (Возможно, первоначально предполагалось, что Name = Manufacturer + Model, но реализовали иначе).
    • Проверка существования и выполнение INSERT/UPDATE:$existingId = $existing[strtolower($name)] ?? null; if ($existingId) { // выполняем $updateStmt с соответствующими параметрами $results['updated']++; } else { // выполняем $insertStmt с соответствующими параметрами $results['added']++; $newId = $this->db->insert_id; $existing[strtolower($name)] = $newId; } Тут происходит ключевой момент:
      Если батарея с таким именем уже есть ($existingId найден), то нужно обновить существующую запись:
      • Вызывается $updateStmt->bind_param() с перечислением всех 20 параметров согласно запросу (19 новых значений + ID). Код показан (хоть и в обрезанном виде): bind_param("issiddddi...i", $typeId, $manufacturer, $model, $referenceValue, $capacity, ... $terminals, $name, $existingId). Здесь важно отметить: предпоследним параметром перед \$existingId передают $name. Однако, в запросе UPDATE ... WHERE ID = ? нет использования имени. Вероятно, это ошибка кода (либо в запросе хотели добавить условие AND Name = ? для надёжности, но забыли). Тем не менее, \$name здесь скорее всего не нужен, т.к. ID уникален. В результирующем коде он не влияет. Но его всё же передают, что может вызвать несоответствие типов ($name – строка, а в формате bind_param последняя ожидается int для ID… Вероятно, это просто артефакт при адаптации кода). В идеале, должно быть без $name или с ним в условии.
      • Затем выполняется $updateStmt->execute(). Поскольку включён режим STRICT, любая ошибка (например, если ID не найден, или нарушение ограничений) привела бы к исключению. Но если логика правильна, то ID всегда найден.
      • Увеличивается счётчик обновлений $results['updated']++.
        Если батарея новая (имени нет в $existing):
      • Готовятся параметры для вставки и вызывается $insertStmt->execute().
      • После успешной вставки, счётчик $results['added']++.
      • Получается сгенерированный авто-инкремент ID через $this->db->insert_id и сохраняется в массив $existing под ключом нового имени. Это сделано на случай, если CSV содержит несколько строк с одинаковым именем батареи (дубликаты). Тогда первая встретится как новая и будет вставлена; вторая при проверке existing уже найдёт ключ и вместо второй вставки выполнит блок обновления. Таким образом, предотвращается создание дублей в рамках одного импорта – повторяющиеся строки с одним именем приведут к одной записи (первая создаст, последующие обновят её). Это тонкий момент, но очень полезный: зачастую большие CSV могут случайно иметь дубли, и без этого механизма получили бы повторяющиеся записи.
    • Цикл foreach продолжается для всех строк. Если хотя бы одна итерация вызовет throw Exception, управление перейдёт в catch. В этом случае транзакция будет отменена (см. ниже).
  1. Завершение транзакции:
    Если цикл прошёл без исключений, это означает, что все операции прошли успешно (вставки/обновления). Тогда:
   $this->db->commit();
   $insertStmt->close();
   $updateStmt->close();
   return $results;

Транзакция фиксируется (commit), подготовленные выражения закрываются (освобождая ресурсы), и метод возвращает массив результатов. Этот массив включает, например, ['added' => 5, 'updated' => 2].

  1. Обработка исключений:
    Блок catch (Exception $e) перехватывает любые исключения, случившиеся внутри try. Это может быть как наше собственное throw new Exception(...) при проверке данных, так и исключения MySQLi (например, нарушение ограничений или сбой SQL). В catch выполняется:
   $this->db->rollback();
   $insertStmt->close();
   $updateStmt->close();
   throw $e;

То есть, транзакция откатывается, параметры закрываются, а исключение снова бросается выше (чтобы внешний код узнал об ошибке). Таким образом, ни одна частичная операция не сохранится в базе при любых проблемах.

Вышеприведённый разбор отражает, как метод importFromCsv обеспечивает атомарность импорта (либо всё, либо ничего) и безопасность данных (путём валидации). Благодаря использованию параметризованных запросов (через bind_param) код защищён от SQL-инъекций[^11] – все значения экранируются драйвером автоматически. Также применение filter_var и других проверок исключает прямую вставку неподходящих типов данных, что уберегает от ошибок SQL и поддерживает целостность (например, не вставится текст в поле для числа и т.п.).

3.4. Особенности реализации и используемые технологии

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

  • Использование современных возможностей PHP: Инструмент требует PHP версии не ниже 8.1. Это обусловлено применением enum (ImportOutcome) и ключевого слова readonly в описании свойства конструктора (private readonly mysqli $db означает, что свойство задаётся только в конструкторе и далее не меняется). В PHP 8.3 код адаптирован, видимо, с учётом каких-то незначительных изменений синтаксиса (в комментарии к коду указано, что код адаптирован под 8.3, возможно исправлены некоторые устаревшие конструкции). В целом, чтобы разработчик мог запускать и модифицировать код, необходимо соответствие версии PHP.
  • Расширение MySQLi: Код использует объект $mysqlix (вероятно, созданный как $mysqlix = new mysqli($host, $user, $pass, $dbname)). Применяются методы $mysqlix->prepare, $stmt->bind_param, $stmt->execute, $stmt->bind_result, $stmt->fetch – т.е. API MySQLi в процедурно-объектном стиле. Такой подход с подготовленными выражениями, как уже отмечалось, защищает от SQL-инъекций и повышает эффективность при множественном выполнении запросов (в цикле обновления/вставки).
  • Транзакционность: Работа с таблицей Battery идёт в рамках транзакции. Это критически важно, так как мы хотим, чтобы все вставки/обновления из одного CSV-файла применились вместе. Представим, что произошло бы без транзакции: если в середине файла ошибка, первые несколько строк уже вставлены/обновлены, а следующие нет – база пришла бы к несогласованному состоянию. Благодаря транзакции, если что-то не так – все изменения откатываются. Это значит, что после сообщения об ошибке база остаётся в том же состоянии, как до импорта (никаких частичных изменений). Таблицы Battery и связанные не должны иметь триггеров, нарушающих эту логику, и должны поддерживать транзакции (например, если движок таблиц InnoDB, то всё ок; если бы был MyISAM – он не поддерживает транзакции, но в современных системах, как правило, InnoDB).
  • Валидация данных: Практически для каждого поля проводится проверка. Это предотвращает множество потенциальных проблем – от простых опечаток до злонамеренного ввода. Например, метод ctype_digit и FILTER_VALIDATE_INT/FLOAT отсекают некорректные символы. One subtlety: FILTER_VALIDATE_FLOAT не распознает числа в экспоненциальной форме или с запятой – но это, вероятно, сознательно, т.к. ожидается обычная нотация с точкой.
  • Обработка дублирующихся записей: Как отмечалось, механизм с обновлением $existing массива предотвращает создание дублей, если в одном CSV два раза упоминается одна и та же батарея. Это тонкая деталь, о которой разработчику стоит знать, если он будет менять логику. Например, если решат по-другому идентифицировать записи (не только по имени, а комбинации manufacturer+model), нужно будет скорректировать и этот участок.
  • Производительность: В текущей реализации, если CSV-файл очень большой, возникают некоторые накладные расходы:
  • Предварительная загрузка всех существующих батарей бренда может быть тяжёлой, если у бренда уже есть тысяч 100 записей – выборка их имён и хранение в массиве потребует памяти и времени. Однако это нужно для быстрого поиска. По сути, мы делаем O(N + M) операций: выборка N существующих + обработка M новых строк, вместо потенциально O(N * M) проверок, если бы мы искали существование через запрос для каждой строки. Поэтому текущий подход, хоть и может есть память, но масштабируется лучше, чем запрос на каждую строку.
  • Цикл вставки/обновления совершает одну операцию с базой на каждую строку CSV (INSERT или UPDATE). Если нужно импортировать, скажем, 10 тысяч строк, то будет выполнено 10k запросов. Это может быть не очень быстро, но допустимо для администраторской операции, происходящей редко. Оптимизация могла бы заключаться в пакетной вставке (INSERT ... VALUES (...), (...), ...), но тогда усложнилось бы обновление существующих записей. Поэтому выбран понятный и надёжный способ.
  • Использование str_getcsv на каждую строку – тоже операция, которая выполняется M раз (где M – число строк). Альтернативой был бы один вызов str_getcsv на весь текст с разбиением по строкам, или применение SPL CSV Reader. Но раз набор данных относительно небольшой обычно, этого не сделали.
  • Включение MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT на каждую транзакцию (в функции) – возможно, лишнее, так как можно было один раз глобально настроить. Но не критично.
  • Простота сопровождения: Код достаточно понятно структурирован – особенно в части сервера. Он самодостаточен: использует немного внешних зависимостей (только $mysqlix из конфига и существующие таблицы). Если разработчику нужно изменить логику (например, добавить новое поле для импорта), потребуется:
  • Добавить колонку в таблицу Battery в базе.
  • Обновить SQL-запросы $insertSql и $updateSql (добавив плейсхолдеры).
  • Добавить обработку нового поля в метод importFromCsv (извлечение из CSV, валидация).
  • Добавить поле в справку/формат (например, подсказать пользователю название новой колонки CSV).
    В целом, изменения понятны и локализованы.
  • Front-end (JavaScript) часть: Хотя небольшая, но полезная:
  • Функция bindInputToText позволяет легко добавлять подобный функционал для других полей, если нужно (например, если появится ещё поле SeriesInput – что уже есть, или что-то вроде ModelInput, можно привязать).
  • Функция копирования универсальна и учитывает старые браузеры, что говорит о внимании к деталям UX. Отдельно надо упомянуть, что интерфейс <input id="BrandInput" value="DELTA"> и <strong id="Brand">[DELTA]</strong> – это, вероятно, сделано, чтобы при первой загрузке страницы демонстрировать пример: стоит дефолтное значение «DELTA» и «[DELTA]» соответственно. Как только пользователь выберет другой бренд или начнёт вводить, [DELTA] заменится на новое. Это не критично, но служит примером.

3.5. Добавление функциональности: пример

Как разработчик, вы можете расширять возможности Battery Import Tool. Рассмотрим типичную задачу: пользователь просит, чтобы при выборе бренда из выпадающего списка название бренда автоматически подставлялось в поле BrandInput и отражалось в тексте Prompt, без необходимости вручную набирать его.

На момент написания, если пользователь выберет бренд из списка, поле BrandInput (которое участвует в Prompt) не меняется само – там остаётся либо дефолтное значение, либо то, что пользователь ввёл ранее. Однако реализовать авто-подстановку несложно.

Мы можем использовать JavaScript для отслеживания изменения выпадающего списка брендов и копирования выбранного текста в нужное место. Ниже приведён фрагмент кода, который можно добавить в скрипт (в раздел <script>...</script> после определения функции bindInputToText, либо в отдельный связанный JS-файл), чтобы достичь требуемого:

<script>
document.getElementById('brand').addEventListener('change', function() {
    // Получаем выбранный опцион из выпадающего списка
    const select = this;
    const selectedOptionText = select.options[select.selectedIndex].text;
    // Находим поле BrandInput и элемент Brand (в Prompt)
    const brandInput = document.getElementById('BrandInput');
    const brandOutput = document.getElementById('Brand');
    if (brandInput && brandOutput) {
        // Устанавливаем в текстовое поле новое значение
        brandInput.value = selectedOptionText;
        // Вызываем событие 'input' вручную, чтобы сработала привязка bindInputToText
        brandInput.dispatchEvent(new Event('input'));
        // Альтернативно, напрямую обновляем текст:
        // brandOutput.textContent = selectedOptionText;
    }
});
</script>

Что делает этот код: он привязывается к событию change элемента <select id="brand">. При каждом изменении (т.е. когда пользователь выбирает новый бренд), код получает текст выбранной опции (например, «ACME Corp (ID: 5)» – именно такой текст находится в выпадающем списке). Вероятно, лучше немного обработать строку, чтобы убрать » (ID: 5)» – но для простоты можно оставить, либо улучшить:

const selectedBrandName = selectedOptionText.replace(/\s*\(ID:.*\)$/, "");

Это регулярное выражение уберёт часть в скобках. Затем установка brandInput.value обновит поле ввода. После этого вызывается dispatchEvent(new Event('input')) – это вручную запускает событие ввода, которое у нас уже связан с обновлением <strong id="Brand"> через ранее инициализированный bindInputToText. В результате и само поле, и элемент Prompt обновятся новым названием бренда.

Такое изменение улучшит удобство: как только пользователь выбрал бренд, он сразу увидит его имя в красных скобках в Prompt, и если будет копировать Prompt, получит актуальные данные.

Обратите внимание: при отправке формы значение поля BrandInput не используется на сервере для импорта (там важен выбранный brand), поэтому это изменение не влияет на серверную логику. Это чисто косметическое/удобство для пользователя.

3.6. Возможные направления улучшений

Разработчику могут быть интересны следующие возможные доработки инструмента:

  • Валидация на стороне клиента: Сейчас все проверки происходят на сервере. Можно добавить JavaScript-валидацию, хотя бы базовую (например, проверять наличие данных в поле CSV перед отправкой, предупреждать об очень больших файлах и т.п.). Однако важно не дублировать полностью логику сервера – сервер всё равно должен оставаться источником истины.
  • Отчёт о пропущенных полях: Дополнительно к жёстким ошибкам, можно делать предупреждения. Например, если CSV не содержит необязательных, но важных полей (скажем, Weight, Dimensions), система могла бы сообщать: «импортировано, однако учтите, что не заполнены поля X и Y». Это больше относится к UI улучшениям.
  • Поддержка загрузки файла: Текущая реализация требует от пользователя самостоятельно открыть CSV и скопировать данные. Это не всегда удобно, особенно для больших файлов. В будущем можно реализовать <input type="file"> для загрузки CSV-файла напрямую. Это потребует чуть другой обработки (например, чтения файла на сервере через fgetcsv), но избавит пользователя от копирования. При этом нужно будет обрабатывать различные кодировки, разделители – но у нас уже есть соответствующие механизмы.
  • Локализация интерфейса: Сейчас текст интерфейса частично на английском (например, надпись кнопки «Import», заголовок «Import Battery Data (CSV)»). Если целевая аудитория русскоязычная, имеет смысл перевести эти элементы. Поскольку вопрос локализации не был основной целью, это остаётся как улучшение UI.
  • Расширение функциональности Prompt: Блок Prompt выглядит специфическим для задачи (возможно, формирует текст для дальнейшего использования). Разработчик мог бы сделать его содержимое динамическим, основанным на результатах импорта, например: подставлять количество добавленных/обновлённых записей, список моделей, или другие данные. Пока Prompt – статический шаблон с только Brand и Series.
  • Логирование операций: Полезно вести лог импорта (например, в отдельную таблицу или файл) с указанием времени, пользователя, количества импортированных записей, были ли ошибки. Это больше административная задача, но разработчик должен заложить такую возможность для удобства отладки и аудита.
  • Интеграция с интерфейсом администрирования: Если остальная система имеет админ-панель, целесообразно вписать Battery Import Tool в неё – например, сделать страницу в админке, где можно выбрать бренд и загрузить файл. Сейчас он работает как отдельная страница. Это скорее вопрос организации, но важно не забыть про проверку прав доступа: в идеале, только администратор или ответственный менеджер должны иметь доступ к этой функции, т.к. она изменяет данные массово.

Разработчику, продолжающему работу над инструментом, следует тщательно тестировать изменения на различных наборах данных: с разными разделителями, с/без заголовка, граничные случаи (пустые строки, максимальные значения). Особенно при добавлении новых полей нужно тестировать и случаи, когда они присутствуют, и когда их нет, чтобы убедиться, что COALESCE обновление отрабатывает правильно.

4. Руководство для администратора

4.1. Требования к окружению и установка

Для эксплуатации Battery Import Tool необходим следующий программно-технический стек:

  • Веб-сервер с поддержкой PHP (например, Apache, Nginx + PHP-FPM). Версия PHP должна быть 8.1 или выше (рекомендуется 8.3, на которой тестировалось и адаптировалось решение).
  • База данных MySQL (или совместимая, например, MariaDB) для хранения данных. Требуется наличие соответствующих таблиц (см. раздел 4.2 о структуре базы).
  • Браузер на стороне клиента – инструмент рассчитан на использование в современных браузерах (Chrome, Firefox, Edge, Safari). JavaScript используется для улучшения UI, поэтому желательно, чтобы у пользователя был включён JS, но базовая функция импорта сработает и без JS (просто не будет динамического обновления Prompt и автозаполнения поля бренда, но данные импортируются при отправке формы).

Установка инструмента обычно сводится к размещению PHP-скрипта (файла) на сервере и настройке доступа к нему:

  1. Размещение файлов: Убедитесь, что файл скрипта (например, battery_import.php) находится в каталоге, доступном через веб-сервер, но при этом защищённом от неавторизованного доступа (подробнее в пункте 4.4). Рядом или в указанном месте должен быть конфигурационный файл config.php, который скрипт подключает. Этот конфиг должен содержать настройки подключения к БД (mysqli объект \$mysqlix).
  2. Настройка конфигурации: Откройте файл config.php и пропишите в нём правильные параметры: хост БД, имя базы, логин/пароль. Также убедитесь, что подключение устанавливается с необходимой кодировкой (желательно utf8mb4 для поддержки Unicode). Если config.php содержит другие настройки (например, путь для автозагрузки, настройки отчёта об ошибках), убедитесь, что они совместимы с данным инструментом.
  3. Настройка PHP: Проверьте настройки PHP:
  • file_uploads – не обязательно (мы не загружаем файл напрямую, но если планируется добавление загрузки файла, нужно включить).
  • post_max_size и upload_max_filesize – значение должно быть достаточным, чтобы вместить самый большой CSV, который вы собираетесь импортировать. Например, если планируется импортировать до 5 МБ данных, установите хотя бы 6-8 МБ.
  • max_execution_time – по умолчанию 30 секунд. Если у вас очень большие CSV (сотни тысяч строк), может потребоваться увеличить этот лимит. Однако чаще всего, файлы по несколько тысяч строк импортируются меньше чем за 30 сек. Администратор должен оценить, насколько большие импорты ожидаются, и при необходимости увеличить лимит (или настроить команду CLI-импорта).
  • memory_limit – должен позволять загрузить и обработать файл. Внутри скрипта файл хранится как строка (в $_POST['data']), так что памяти нужно примерно в 2-3 раза больше размера файла (с учётом разборов). Например, для файла 5 МБ установите memory_limit хотя бы 16-32 МБ.
  • Локаль/кодировка: Убедитесь, что PHP сконфигурирован так, чтобы строки обрабатывались в UTF-8. В скрипте нет явных вызовов multibyte-функций, но на всякий случай, можно выставить mb_internal_encoding("UTF-8") в config, если будут проблемы с русскими символами.
  1. Развёртывание базы данных: Необходимо создать (если ещё не созданы) таблицы:
  • Brands – справочник брендов.
  • BatteryTypes – справочник типов батарей.
  • Battery – основная таблица с данными батарей.
    Также, если система имеет связанные таблицы (например, для серий или других параметров), их тоже стоит учесть, но данный инструмент работает непосредственно с этими тремя. Подробнее о структуре – в разделе 4.2.
  1. Тестирование установки: После настройки, администратор должен проверить работоспособность:
  • Перейти на URL скрипта (например, https://example.com/tools/battery_import.php).
  • Убедиться, что открывается форма, и в выпадающем списке присутствуют ожидаемые бренды. Если список пуст или возникает ошибка, проверить подключение к БД, наличие таблицы Brands и данных в ней.
  • Выполнить пробный импорт с небольшим CSV, содержащим 1-2 записи (можно создать тестовый бренд и тип заранее). Убедиться, что по нажатию Import появляются корректные сообщения, и данные появились в таблице Battery.
  • Проверить сценарии ошибок: например, попытаться импортировать с пустым полем, с заведомо неправильными данными – скрипт должен выдавать понятные сообщения, а база не должна получать лишних записей.
  1. Интеграция (если нужна): Если этот скрипт должен интегрироваться в существующий интерфейс, убедитесь, что:
  • Стили оформления страницы соответствуют остальному сайту (в исходном коде есть небольшой <style> – его можно подправить или вынести в общий файл CSS).
  • Переводы на нужный язык (в нашем случае многое уже на русском, но некоторые надписи – английские – можно перевести).
  • Ссылки или меню: добавьте ссылку для администраторов/менеджеров, ведущую на страницу импорта, если это уместно.
  • Ограничьте доступ (см. безопасность ниже).

4.2. Структура базы данных

Администратору необходимо знать, какие таблицы задействованы, и как они связаны, чтобы при необходимости проводить обслуживание (резервное копирование, восстановление, обновление справочников). Ниже описана структура основных таблиц, используемых Battery Import Tool.

Для наглядности приведём схему связей (ER-диаграмму):

erDiagram
    Brand ||--o{ Battery : выпускает
    BatteryType ||--o{ Battery : классифицирует
    Brand {
        int ID
        string Name
    }
    BatteryType {
        int ID
        string Name
        string Alternate
    }
    Battery {
        int ID
        int BrandID
        int BatteryTypeID
        string Manufacturer
        string Model
        int ReferenceValue
        decimal Capacity
        decimal Voltage
        float ChargeCurrent
        float DischargeCurrent
        int LifeTime
        decimal TemperatureCoefficient20
        decimal TemperatureCoefficient25
        decimal C1
        decimal C5
        decimal C20
        decimal Weight
        int Length
        int Width
        int Height
        string Terminals
    }

Диаграмма 4.1. Модель базы данных для Battery Import Tool (основные сущности).

Таблица Brands: хранит список производителей/брендов батарей.

  • ID (INT, PK, авто-инкремент) – уникальный идентификатор бренда.
  • Name (VARCHAR) – название бренда. Должно быть уникальным (желательно, хотя явно это может не проверяться – в интерфейсе дубли бессмысленны).

Таблица BatteryTypes: справочник типов аккумуляторов.

  • ID (INT, PK, авто-инкремент) – уникальный идентификатор типа.
  • Name (VARCHAR) – название типа (например, «Lead-Acid AGM (VRLA)»).
  • Alternate (VARCHAR) – альтернативное название или код. Может быть NULL. Например, для «Lead-Acid AGM (VRLA)» alternate может быть «AGM VRLA» или просто «AGM». Это поле служит для расширения распознавания: инструмент ищет совпадение либо по Name, либо по Alternate. Важно: В справочнике BatteryTypes не должно быть двух записей с одинаковым Name или с одинаковым Alternate в нижнем регистре, иначе при сопоставлении может произойти конфликт (программа возьмёт первый попавшийся ID).
  • Другие поля: могут быть дополнительные колонки (описание, комментарии), но они не участвуют в работе импорта.

Таблица Battery: основная таблица с данными о батареях.

  • ID (INT, PK, авто-инкремент) – уникальный идентификатор записи батареи.
  • BrandID (INT, FK -> Brands.ID) – идентификатор бренда/производителя. Каждая батарея привязана к одному бренду. В таблице могут существовать батареи с одинаковым именем Model, если BrandID разный (то есть разные бренды могут иметь модели с одинаковым названием).
  • BatteryTypeID (INT, FK -> BatteryTypes.ID) – идентификатор типа аккумулятора (технологии).
  • Manufacturer (VARCHAR) – текстовое поле производителя. Как правило, дублирует название бренда, но предусмотрено для возможности указать подбренд или конкретного изготовителя. В импорте, если пользователь явно указал Manufacturer в CSV, сохранится его значение, иначе туда подставляется название бренда из справочника. Это поле скорее справочное, т.к. BrandID уже однозначно говорит о производителе.
  • Model (VARCHAR) – название модели батареи. В сочетании с BrandID должно быть уникально (это желательно обеспечить или по крайней мере отслеживать). Имя модели используется в UI как основной идентификатор батареи.
  • ReferenceValue (SMALLINT или INT) – дополнительное числовое поле (см. описание в разделе 2.3). Может использоваться, например, для обозначения серийного номера, кода или другого классификатора.
  • Capacity (DECIMAL(8,2)) – ёмкость (Ah).
  • Voltage (SMALLINT или DECIMAL) – напряжение (В). Из кода видно, что используется smallint для хранения целого значения напряжения (например, 12 или 48), но при импорте допускается дробное. Если нужно хранить дробное, лучше было бы DECIMAL. Возможно, Voltage задан как SMALLINT, но это ограничивает точность (например, 12.8 В не хранится как 12.8). Если важно хранить десятичную часть, колонку следует сделать DECIMAL(5,2) или FLOAT. Предположим, что slight inconsistency: код проверяет float, но записывает в smallint (потенциально округляя). Администратору стоит выяснить, как в базе определён тип поля Voltage: если smallint, можно рассматривать только целые. Если нужно точнее – стоит пересмотреть тип.
  • ChargeCurrent (FLOAT) – максимальный ток заряда.
  • DischargeCurrent (FLOAT) – максимальный ток разряда.
  • LifeTime (SMALLINT или INT) – срок службы (лет).
  • TemperatureCoefficient20 (DECIMAL(5,4)) – температурный коэффициент при 20°C.
  • TemperatureCoefficient25 (DECIMAL(5,4)) – температурный коэффициент при 25°C.
  • C1, C5, C20 (DECIMAL(8,2)) – ёмкости при соответствующих разрядных временных интервалах.
  • Weight (DECIMAL(8,2)) – вес (кг).
  • Length, Width, Height (SMALLINT) – размеры (мм). Обратите внимание: Width в коде привязан к типу SMALLINT без UNSIGNED (вероятно, опечатка: Width smallint(6) DEFAULT NULL – без UNSIGNED, в отличие от Length и Height, которые UNSIGNED). Это несущественно, но технически ширина может быть отрицательной если не UNSIGNED – нужно следить, чтобы такого не вносили.
  • Terminals (VARCHAR(10)) – тип/описание выводов.

С точки зрения администрирования:

  • Первичные ключи (PK): ID в каждой таблице – PK.
  • Внешние ключи (FK): Желательно настроить на уровне СУБД внешние ключи: Battery.BrandID -> Brands.ID, Battery.BatteryTypeID -> BatteryTypes.ID. Если включить контроль внешних ключей (InnoDB), это обеспечит целостность (нельзя удалить бренд, если есть батареи, и т.д., или нельзя задать батарее несуществующий тип). Если FK не настроены, то контроль обеспечивается приложением (в коде).
  • Индексы: Рекомендуется создать индекс по (BrandID, Model) в таблице Battery, так как часто будут выполняться запросы выборки/обновления по BrandID и Name (Model) в сочетании. В коде было: SELECT Name, ID FROM Battery WHERE BrandID = ? – здесь БД быстро найдёт по индексу все записи бренда. Также UPDATE/INSERT по ID – там PK справляется.
  • Размеры полей: Администратору следует обратить внимание на адекватность типов. Например, VARCHAR(100) для Model – достаточно ли? Если нет, увеличить, но тогда нужно скорректировать код проверки (но bind_param с ‘s’ типом справится). Если ожидаются большие значения токов или ёмкостей, проверьте, что типы (DECIMAL(8,2) = макс 999999.99) подходят. Если нужна бóльшая ёмкость (например, в мАч для небольших батарей, можно перевести к другой единице или увеличить разрядность).
  • Предустановленные справочники: Обычно администратор сам заносит в таблицы Brands и BatteryTypes необходимые данные. Убедитесь, что перед началом работы инструмента в BatteryTypes есть полный перечень типов (как в разделе 2.4). Если нужно дополнить – добавьте записи (ID можно задавать вручную или автонумерацией). ID, кстати, заданы явно (1,2,…22) в списке – можно в базе задать такие же. Если автонумерация выдала иные, следует обновить mapping либо синхронизировать.
  • Например, если компания вводит новый тип батарей, у которого нет в справочнике – инструмент выдаст ошибку при импорте. Значит, сперва администратор должен добавить запись в BatteryTypes, указав Name и, по возможности, Alternate (для удобства).
  • Для брендов: добавлять новые бренды тоже следует в таблицу Brands. Дубли имён избегать. Если бренд удалить (и при этом в Battery есть ссылающиеся записи), либо запрещено FK-связью, либо нужно сперва удалить батареи этого бренда.

4.3. Управление справочниками (бренды и типы)

В ходе эксплуатации может возникнуть необходимость добавить новый бренд или новый тип батареи. Эти действия делаются непосредственно над соответствующими таблицами (если нет отдельного интерфейса для этого):

  • Добавление бренда: вставить новую запись в таблицу Brands с указанием имени. После этого при следующем открытии страницы импорта новый бренд появится в выпадающем списке автоматически (скрипт каждый раз выбирает все бренды). Никаких других действий не требуется.
  • Переименование бренда: если по какой-то причине нужно изменить название бренда (например, опечатка), можно обновить поле Name в таблице Brands. Учтите, что в таблице Battery остается Manufacturer (текст) прежним для уже внесённых батарей. Возможно, это не критично, так как BrandID связь сохраняется, но несоответствие в тексте может путать. Можно по согласованию выполнить SQL: UPDATE Battery SET Manufacturer = 'НовоеИмя' WHERE BrandID = X, чтобы привести названия в данных в соответствие. Но это – по усмотрению администратора.
  • Удаление бренда: крайне нежелательно удалять бренды, если в Battery есть связанные записи. Лучше всего вместо удаления пометить как неиспользуемый (например, добавить флаг, или переименовать в «DEL» и скрыть в UI). Если всё же нужно – сначала удалить все батареи этого бренда, иначе нарушится целостность. Если FK включены, БД не даст удалить.
  • Добавление типа батареи: вставить запись в BatteryTypes (указав Name и, опционально, Alternate). Новые типы мгновенно начнут поддерживаться импортёром (он при запуске всегда перечитывает всю таблицу BatteryTypes). Чтобы пользователь знал новый ID, можно дополнить документацию или раздел 2.4. Но ID можно и не запоминать, если он будет пользоваться именем.
  • Переименование типа: можно изменить Name или Alternate. Если поменять Name, а CSV откуда-то сгенерированы на старое название – могут быть несоответствия. Alternate можно использовать для поддержки старого названия параллельно. Например, если тип «Lead-Acid SLA» решено переименовать в «VRLA SLA», вы можете обновить Name, а старое «Lead-Acid SLA» поместить в Alternate – тогда оба будут признаваться. На существующие записи Battery это не влияет (в Battery хранится ID).
  • Удаление типа: если уверены, что ни одна батарея не использует этот тип, удалите запись. Если есть связанные записи, предварительно либо измените им тип, либо удалите их тоже. Если настроены внешние ключи, БД не позволит удалить тип с связанными батареями.

Резервное копирование данных: Рекомендуется периодически делать бэкап таблиц Battery, Brands, BatteryTypes (например, с помощью mysqldump). Особенно важна таблица Battery – она содержит сами данные батарей, восстановление которых может быть трудоёмким, если потеряны. Справочники добавляются редко, но тоже лучше хранить.

Очистка тестовых данных: Возможно, при тестировании администраторам понадобилось импортировать фейковые или пробные данные. Их можно удалить SQL-запросом (DELETE FROM Battery WHERE ...). Импорт не оставляет мусорных данных, кроме тех, что явно вставлены. Повторный импорт с теми же значениями обновляет записи, не размножает.

4.4. Безопасность и управление доступом

Инструмент импорта способен массово модифицировать содержимое базы, поэтому очень важно ограничить круг лиц, имеющих к нему доступ. Администратору следует учесть следующие аспекты безопасности:

  • Аутентификация и авторизация: Убедитесь, что страница импорта доступна только авторизованным пользователям с достаточными правами. В идеале, должна быть проверка прав перед выполнением импорта. Например, если система имеет механизм логина, можно обернуть вызов импорта условием типа:
  if (!isUserLoggedIn() || !currentUserHasRole('admin')) {
      http_response_code(403);
      exit("Forbidden");
  }

(зависит от реализации). В предоставленном коде такой проверки нет, поэтому, если просто выложить скрипт, любой знающий URL сможет им воспользоваться. Это очень рискованно. Решение: либо перенести этот функционал в админ-панель, которая сама по себе защищена, либо добавить проверку сессии/куки на самом начале скрипта.

  • Валидация входных данных: Серверная часть уже хорошо валидирует формат CSV. Тем самым практически исключаются SQL-инъекции (поскольку используется bind_param) и вставка некорректных данных. Однако, администратору следует поддерживать эту систему: не отключать подготовленные выражения, не заменять их простым конкатенированием строк, иначе безопасность упадёт.
  • Размеры импортируемых файлов: Ограничьте максимально допустимый размер CSV, если опасаетесь DOS-атак (например, чтобы кто-то не вставил 100-мегабайтный текст и не исчерпал память). Это можно делать как на уровне клиента (JS предупреждение), так и на уровне сервера (проверка strlen($csvText) и отказ если слишком велико). Настройки PHP post_max_size защитят от слишком большого POST-а, но лучше иметь представление, сколько строк реально нужно – например, 10k строк вполне, 1e6 строк – уже слишком много для единовременного импорта через веб.
  • Журналирование операций: Для контроля безопасности можно логировать IP, пользователя, и время каждого импорта. Сейчас этого нет, но администратор может добавить: например, перед вызовом $importer->importFromCsv записывать в отдельный лог-файл admin_import.log строку с деталями. Это поможет в случае, если нужно выяснить, кто и когда что импортировал.
  • SQL-права: Настройте пользователя БД (указанного в config.php) так, чтобы у него были только нужные привилегии. Для работы импорта нужны:
  • SELECT на таблицы Brands, BatteryTypes, Battery.
  • INSERT, UPDATE на таблицу Battery.
  • (Опционально DELETE не нужен для работы импорта).
  • Возможно, SELECT на другие таблицы, если связанная логика (но по коду только эти).
    В целях безопасности лучше, чтобы веб-приложение не работало под суперпользователем. Ограничение прав минимизирует последствия, если вдруг какая-то уязвимость позволит выполнить иной SQL.
  • HTTPS: Если инструмент используется через интернет, убедитесь, что соединение защищено (HTTPS), чтобы данные (в т.ч. CSV, содержащие, возможно, коммерчески ценную информацию о продуктах) не передавались в открытом виде.
  • Вывод ошибок: В финальной боевой среде настройте display_errors=Off, чтобы в случае сбоя PHP-код или структура БД не утекли на страницу. В данном скрипте, при ошибке подключения или получения брендов, делается die("...") – что потенциально раскрывает некоторую информацию. Можно заменить эти die на пользовательское сообщение или редирект. Также, в catch exception, сообщение об ошибке выводится пользователю (это хорошо для исправления CSV ошибок, но плохо, если там окажется что-то системное). Однако, exceptions от MySQLi, если они будут, обычно содержат текст запроса или схему – это не критично, но admin может решить скрывать технические детали. Баланс нужно найти: пользователю импортера важно знать, что не так в CSV, поэтому эти ошибки оставляем. А ошибки подключения или подготовленных выражений – это уже для администратора, можно логировать, а пользователю давать общее «Internal error, contact admin.».

4.5. Обслуживание и устранение неполадок

При нормальной работе инструмент не требует постоянного вмешательства. Однако, администратор должен быть готов к следующим ситуациям:

  • Импорт не проходит, хотя CSV корректный: Если пользователь утверждает, что CSV в порядке, а система выдаёт ошибку, администратор должен:
  • Проверить сообщение об ошибке и номер строки. Открыть CSV на этой строке, посмотреть символы-разделители, кавычки и т.п.
  • Возможно, проблема в кодировке файла (например, UTF-8 с BOM). Решение: открыть CSV в Notepad++ или другом редакторе, убедиться, что без BOM. Система могла воспринять BOM как начало данных (неизвестное название колонки «ï»¿Manufacturer», например). В новой версии кода BOM не убирается явно. Администратору стоит рекомендовать использовать UTF-8 without BOM.
  • Если ошибка говорит о нераспознанном типе – свериться со справочником типов, добавить недостающий тип.
  • Если что-то не обновляется – убедиться, что имя модели точно совпадает. Напомним: сравнение имен case-insensitive, но может быть проблема с пробелами (скрипт trim делает, так что пробелы вокруг убираются).
  • Если какие-то строки просто игнорируются без ошибок (такого не должно быть, либо всё, либо ошибка), возможно, пользователь не заметил, что, например, 2 строки с одинаковым именем привели к одной записи (то есть «пропажа» — это на самом деле не пропажа, а объединение). Нужно объяснить, что дубли по имени не создаются.
  • Данные в базе после импорта выглядят искажённо: Такое может случиться, если исходный CSV в неправильной кодировке. Русские буквы могут превратиться в вопросительные знаки или абракадабру. Решение: убедиться, что соединение с БД установлено в UTF-8 (в config обычно делают $mysqlix->set_charset("utf8mb4")). Если забыли – данные вставились в неправильной кодировке. Придётся переимпортировать. Чтобы избежать этого, всегда устанавливайте кодировку соединения.
  • Ошибка «Failed to prepare statements» при импорте: Это очень редкий случай, но, например, может возникнуть, если таблица Battery не существует или у пользователя нет прав на неё, либо синтаксис SQL неверен. Поскольку код прилагался, синтаксис скорее верный. Если администратор вносил изменения (например, добавил новое поле, но забыл обновить запрос) – тогда эта ошибка проявится. Чтобы устранить, проверить SQL (\$insertSql и \$updateSql в коде) – соответствуют ли они реальной структуре таблицы. Например, если добавлено поле Color, а запросы не обновлены, то $insertStmt будет готовиться на старый набор колонок – MySQL выдаст ошибку (которая станет Exception). Решение: поправить запросы в коде.
  • Замечены дубли батарей, которые не должны были появиться: Теоретически, инструмент обновляет существующие по имени, но если два человека параллельно импортировали похожие данные, мог быть race condition (в одном запросе SELECT existing не знал про вставки другого запроса, если они шли одновременно). В таком случае можно получить дубль. Это маловероятно (т.к. обычно один админ импортирует в момент времени). Но если такая ситуация возможна (например, интеграция через API, где несколько импортеров), то нужно задуматься о механизме блокировок или уникальном индексе. Администратор может на уровне БД установить уникальный индекс на (BrandID, Model) – тогда второй параллельный import в случае конфликта просто упадёт с ошибкой дублирования ключа. Что лучше: поймать ошибку, сообщить, что «такая запись уже добавлена в параллельном процессе». Но если уникальный индекс уже есть, а дубли появились – значит, где-то в именах есть различия (регистр или пробелы). Проверьте на это.
  • Производительность: Если база очень большая и импорт стал работать медленно:
  • Убедитесь, что стоят индексы на BrandID (и, возможно, на Name) как говорилось.
  • Посмотрите, не слишком ли большой CSV пытаются загрузить – может, разбить на части.
  • В крайнем случае, если часто требуются массовые загрузки, можно предложить режим оффлайн-импорта (например, загружать CSV через SFTP и запускать отдельным скриптом).
  • Обновление инструмента: Если выходят новые версии PHP или меняется структура базы, администратору нужно обновить и сам инструмент. Пример: переход на PHP 8.2/8.3 – уже сделано. Далее, предположим, MySQL 8 ввёл новые требования (вряд ли что-то сломается, но вдруг). Следует регулярно тестировать после обновления ПО сервера.

В случае критических сбоев (когда инструмент совсем перестал работать), администратор может включить режим отображения ошибок для отладки. Также можно добавить временно var_dump/echo важных переменных (например, печатать разобранный массив \$fields или \$colIndex) для отладки непонятных ситуаций с CSV.

Еще одна административная задача – миграция данных. Если, например, решено изменить справочник типов (ID или названия), нужно учесть:

  • Обновить существующие записи Battery, чтобы ссылались на новые ID (или, проще, добавить новые ID параллельно старым, а старые не трогать, если нет острой необходимости).
  • Обновить документацию для пользователей.

Подытоживая, инструмент достаточно неприхотлив в обслуживании: основное – держать справочники в актуальном состоянии и контролировать доступ. Своевременное резервирование данных и мониторинг ошибок сделают его эксплуатацию безпроблемной.

5. Глоссарий

CSV – (Comma-Separated Values) текстовый формат файлов, где табличные данные представлены строками текста, а отдельные значения колонок в строке отделены запятыми. В русскоязычных CSV вместо запятых часто используются точки с запятой.

Транзакция – в контексте баз данных, группа операций (например, несколько INSERT/UPDATE), которые выполняются как единое целое: либо все успешны (транзакция фиксируется), либо при сбое все изменения отменяются (откат). Гарантирует целостность данных.

Prompt – в данном инструменте, блок текстовой подсказки/шаблона, куда подставляются выбранные параметры (бренд, серия). Позволяет быстро копировать заранее подготовленный текст, например для отчётов.

Prepared Statement (подготовленное выражение) – способ выполнения SQL-запросов, при котором сначала запрос с параметрами компилируется СУБД, а затем значения параметров передаются отдельно. Обеспечивает защиту от SQL-инъекций и может ускорять повторные выполнения с разными параметрами.

MySQLi – расширение PHP для работы с MySQL, предоставляющее улучшенный функционал по сравнению с устаревшим MySQL-расширением. Поддерживает подготовленные запросы, транзакции и пр. (MySQL Improved).

Enum (перечисление) – тип данных в PHP (с версии 8.1), позволяющий определить ограниченный набор именованных значений. В коде используется для ImportOutcome (ADDED, UPDATED).

UTF-8 – популярная кодировка Unicode, охватывающая практически все символы. Используется для хранения многоязычного текста. Рекомендуется для CSV-файлов, чтобы корректно передавать русские буквы.

BOM – (Byte Order Mark) специальный маркер в начале текстового файла, обозначающий эндиянность/кодировку (для UTF-8 не обязателен). Наличие BOM в CSV может приводить к появлению невидимых символов в начале первого поля, что может мешать распознаванию заголовка.

SQL-инъекция – тип атаки на приложения, работающие с базой данных, при котором злоумышленник вставляет в поля ввода специальные символы/конструкции, способные изменить логику SQL-запроса. Использование подготовленных запросов и тщательная валидация данных предотвращает такие атаки.

VRLA – (Valve-Regulated Lead-Acid) тип герметичных свинцово-кислотных аккумуляторов, снабжённых клапаном. К VRLA относятся AGM и гелевые батареи. Особенность – герметичность, рекомбинация газов внутри.

AGM – (Absorbent Glass Mat) технология VRLA-батарей, где электролит впитан в стекловолоконные маты. AGM-аккумуляторы – необслуживаемые, с фиксированным электролитом.

LiFePO4 (LFP) – литий-железо-фосфатный аккумулятор, разновидность литий-ионных батарей. Обладает высокой стабильностью и ресурсом циклов, часто применяется в хранении энергии, электротранспорте.

FTP/SFTP – протоколы передачи файлов. В контексте импорта могут упоминаться, если говорить о загрузке CSV на сервер не через форму, а через отдельный канал (например, админ залил файл по SFTP, а потом импортировал командой).

Внешний ключ (Foreign Key) – ограничение в базе данных, связывающее значение поля одной таблицы со значением PK другой таблицы. Обеспечивает ссылочную целостность (нельзя сослаться на несуществующую запись). Например, BrandID в Battery должен ссылаться на существующий ID в Brands.

ER-диаграмма – (Entity-Relationship diagram) диаграмма «сущность-связь», отображающая структуру базы данных: сущности (таблицы) и связи между ними. Используется для проектирования и документирования БД.

HTTP POST – метод HTTP-протокола, часто используемый для отправки веб-форм на сервер. В данном случае, CSV-данные и выбранный бренд отправляются методом POST, а PHP-скрипт принимает их из $_POST.

GUI – (Graphical User Interface) графический пользовательский интерфейс. В контексте документации для пользователя, GUI – это веб-страница формы, с которой он взаимодействует (в отличие от, например, консольного интерфейса или API).

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх