Перейти к содержимому

Свои номера документов и заявок - шаблон, счётчики, уникальность

Выдаём номера заявкам, счетам и своим документам штатным нумератором ядра: шаблон, независимые счётчики, правка конфигурации и свой генератор.

Механика

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

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

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

Конфигурация нумератора - это ассоциативный массив «тип генератора - его параметры». Начальное значение счётчика, шаг, длина случайной части и текст префикса задаются здесь же, рядом с самим шаблоном.

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

Один нумератор умеет держать несколько независимых последовательностей по хешу. Хеш - произвольная строка вроде идентификатора менеджера или года, и каждая такая строка получает собственный счётчик с первого номера.

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

Свой генератор регистрируют событием сбора генераторов при инициализации ядра. Так в шаблон добавляют собственное служебное слово - например, код города или букву склада, откуда уходит документ.

Шаги

  1. Решить, из чего состоит номер: префикс, год, счётчик и случайная часть.
  2. Создать нумератор с шаблоном и параметрами генераторов один раз при установке.
  3. Загружать нумератор по идентификатору или по типу перед выдачей номера.
  4. Брать следующий номер и сохранять его вместе с самим документом.
  5. Развести независимые последовательности по хешу, если счётчиков нужно несколько.
  6. Править конфигурацию методом обновления, а не повторным сохранением нумератора.

Код

Создаём нумератор с шаблоном:

use Bitrix\Main\Numerator\Numerator;
use Bitrix\Main\Numerator\Generator;
$numerator = Numerator::create();
$numerator->setConfig([
Numerator::getType() => ['name' => 'Заявки с сайта', 'template' => '{PREFIX}-{YEAR}/{NUMBER}'],
Generator\SequentNumberGenerator::getType() => ['start' => 1, 'step' => 1],
Generator\PrefixNumberGenerator::getType() => ['prefix' => 'REQ'],
]);
$result = $numerator->save(); // создание выполняют один раз, при установке

Имя и шаблон - обязательные параметры самого нумератора. Остальные ключи массива описывают генераторы: каждый отвечает за своё служебное слово и получает собственные параметры вроде начального значения или длины.

Берём следующий номер:

$numerator = Numerator::load($numeratorId);
$number = $numerator->getNext(); // например REQ-2026/000042
$request->setNumber($number); // номер сохраняют вместе с документом

Номер выдаётся один раз и сохраняется вместе с записью. Повторный вызов вернёт следующее значение, поэтому номер получают в момент создания документа, а не при каждом его показе на странице.

Разводим независимые счётчики по хешу:

echo $numerator->getNext('MANAGER_42'); // своя последовательность менеджера
echo $numerator->getNext('MANAGER_17'); // независимый счётчик, снова с единицы
// один нумератор держит сколько угодно параллельных последовательностей

Хеш решает частую задачу «свой счётчик на подразделение». Вместо десятка нумераторов с одинаковыми шаблонами держат один, а разделение обеспечивает строка хеша, собранная из идентификатора менеджера или города.

Правим конфигурацию существующего нумератора:

Numerator::update($numeratorId, [
Numerator::getType() => ['name' => 'Заявки с сайта', 'template' => '{PREFIX}-{YEAR}/{NUMBER}'],
Generator\PrefixNumberGenerator::getType() => ['prefix' => 'ORD'],
]);
// повторный save() существующего нумератора ломает последовательность номеров

Правка через метод обновления защищена от одновременных запросов. Это единственный поддерживаемый способ поменять шаблон или префикс у нумератора, который уже выдаёт номера в бою.

Показываем редактору доступные слова шаблона:

$words = Numerator::getTemplateWordsForType('DOCUMENT'); // список слов для типа
print_r($words);
// набор слов зависит от типа нумератора и не совпадает у заказов и документов

Список слов запрашивают у ядра, а не переписывают в свой интерфейс руками. Тогда форма настройки шаблона не устаревает после обновления платформы и не предлагает редактору слово, которого в его типе нет.

Находим нумератор по типу при сохранении документа:

$numerator = Numerator::getOneByType('DOCUMENT');
if ($numerator === null) {
throw new \RuntimeException('нумератор документов не создан'); // защита от пустой установки
}
$document['NUMBER'] = $numerator->getNext();

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

Сохраняем номер вместе с самим документом:

$connection = \Bitrix\Main\Application::getConnection();
$connection->startTransaction();
try {
$number = $numerator->getNext(); // номер выдаётся ровно один раз
RequestTable::add(['NUMBER' => $number, 'TITLE' => $title]);
$connection->commitTransaction();
} catch (\Throwable $e) {
$connection->rollbackTransaction(); // документа нет, а номер уже потрачен
}

Откат транзакции возвращает данные, но не возвращает выданный номер. Пропуск в ряду номеров - нормальная плата за уникальность, и объяснить его бухгалтерии проще, чем разбирать два документа с одним номером.

Регистрируем свой генератор служебного слова:

$em = \Bitrix\Main\EventManager::getInstance();
$em->addEventHandler('main', 'onNumberGeneratorsClassesCollect', static function () {
return new \Bitrix\Main\EventResult(
\Bitrix\Main\EventResult::SUCCESS, ['class' => \Vendor\Module\CityNumberGenerator::class]);
});

Свой генератор наследуют от базового класса генераторов номера. Он объявляет собственное служебное слово и подставляет значение, которого в поставке нет: код города, букву склада или обозначение канала продаж.

Ограничения

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

Смена шаблона не переписывает выданные раньше номера. В базе останутся записи старого вида, и отчёты по номерам придётся строить с учётом обоих форматов сразу.

Номер заказа магазина живёт по своим правилам модуля продаж. Подменять его нумератором ядра не стоит: обмен с учётной системой и печатные формы рассчитывают на штатное поле номера.

Случайная часть в шаблоне не заменяет уникальности счётчика. Она затрудняет подбор чужого номера в публичной ссылке, но сама по себе не гарантирует отсутствия совпадений.

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

Типичные проблемы

Два документа получили одинаковый номер.

Номер собирается своим кодом по количеству записей в таблице. Такой подход ломается при одновременных запросах, а нумератор ядра защищён от этого.

После правки шаблона счётчик начался заново.

Существующий нумератор сохранили повторно вместо вызова обновления. Конфигурацию правят методом обновления, который не создаёт нумератор заново.

В шаблоне не работает нужное служебное слово.

Слово недоступно для этого типа нумератора или не подключён его генератор. Список слов запрашивают у ядра по типу нумератора.

Номера у разных менеджеров идут вперемешку.

Все документы берут номер из общей последовательности без хеша. Отдельные счётчики получают передачей строки хеша при выдаче номера.

Документ сохранился с пустым номером.

Нумератор не создан на этом стенде, а результат поиска по типу не проверен. Отсутствие нумератора обрабатывают явной ошибкой при сохранении.

Частые вопросы

Можно ли поменять формат номера заказа магазина?

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

Как сделать сквозную нумерацию по годам?

Добавить слово года в шаблон и завести отдельный счётчик по хешу для каждого года. Тогда с началом года последовательность стартует заново.

Гарантируется ли уникальность при большой нагрузке?

Да, последовательные номера уникальны и при одновременных запросах. Самодельный счётчик по числу записей такой гарантии не даёт вовсе.

Где хранится состояние счётчика?

В таблицах ядра вместе с конфигурацией нумератора. Отдельного файла или своей таблицы для этого заводить не нужно.

Можно ли добавить своё слово в шаблон?

Да, своим генератором, зарегистрированным на событии сбора генераторов. Он объявляет слово и подставляет нужное значение при выдаче номера.

Смежное

Первоисточник