--- title: "Генератор последовательных идентификаторов с ежегодной сброской — поддержка нескольких префиксов" slug: "yearly-resetting-sequential-id-generator-multi-prefix-support" status: "new" updated: 2026-10-22T16:44:22Z published: 2026-10-22T16:44:22Z canonical: "support.tulip.co/yearly-resetting-sequential-id-generator-multi-prefix-support" --- > ## Documentation Index > Fetch the complete documentation index at: https://support.tulip.co/llms.txt > Use this file to discover all available pages before exploring further. # Генератор последовательных идентификаторов с ежегодной сброской — поддержка нескольких префиксов To download the Yearly Resetting Sequential ID Generator - Multi-Prefix Support, visit: [tulip](https://Library В этой статье один из наших разработчиков библиотеки, Тамаш, подробно рассказывает о решениях, лежащих в основе модуля «Генератор последовательных идентификаторов с ежегодным сбросом — поддержка нескольких префиксов», доступного в библиотеке. ## Почему я реализовал его именно так Я разработал этот логический блок для решения весьма практической задачи: генерации идентификаторов таким образом, чтобы они были надёжными, легко повторно используемыми и быстро развертываемыми во многих приложениях.Моя цель заключалась в том, чтобы сам идентификатор содержал всю информацию, необходимую для вычисления следующего. Каждая часть идентификатора имеет чёткую роль: * префикс указывает, к какой последовательности относится идентификатор; * год разделяет последовательность во времени; * число показывает порядок внутри данного года. Благодаря такой структуре мне не понадобилось вводить какую-либо логику сброса. Последний существующий идентификатор уже содержит всю информацию, необходимую для генерации следующего. Пока последний идентификатор действителен, следующий всегда можно вычислить предсказуемым образом. Я включил год непосредственно в идентификатор, поскольку он естественным образом разделяет записи по времени. Это также позволяет автоматически перезапускать нумерацию каждый год без необходимости запланированного сброса или ручного обновления. Префикс выступает в качестве границы между различными последовательностями. Это позволяет использовать несколько независимых последовательностей нумерации в пределах одной таблицы. Изменение префикса означает просто продолжение другой последовательности. Я добавил проверку, поскольку весь подход зависит от последовательной структуры идентификаторов. Если существующий идентификатор не соответствует ожидаемому формату, продолжение последовательности может привести к запутанным или несогласованным результатам. Я предпочитаю остановиться на раннем этапе, выявить проблему и избежать генерации некорректных идентификаторов. Я выбрал поиск на основе коннектора, чтобы логический блок мог работать с разными таблицами без изменения основной логики. После настройки коннектора достаточно указать tableId и префикс, и один и тот же логический блок можно будет повторно использовать в нескольких местах. Для меня цель заключалась не просто в генерации идентификаторов, а в создании повторно используемого шаблона, в котором сам формат идентификатора определяет, как работает последовательность. Именно это позволяет сохранить логику простой, предсказуемой и легко применимой в разных приложениях. ## Что делает этот логический блок Этот логический блок генерирует последовательные идентификаторы, которые сбрасываются ежегодно, со следующей структурой:**префикс + год + порядковый номер** Пример: TEST20240001 TEST20240002 При смене года нумерация автоматически начинается с 1: TEST20250001 В одной таблице может существовать несколько префиксов. Каждый префикс имеет свою собственную независимую последовательность: ORD20240001 ORD20240002 INV20240001 INV20240002 Логический блок обеспечивает: * единообразный формат идентификаторов * автоматический ежегодный сброс * независимую нумерацию для каждого префикса * раннее обнаружение некорректных исторических идентификаторов * раннее обнаружение переполнения числового диапазона ## Почему коннектор является частью проекта Перед генерацией следующего идентификатора логическому блоку требуется последний существующий идентификатор для данного префикса. Коннектор выполняет фильтрованный поиск: * отфильтровывает записи, в которых идентификатор начинается с данного префикса; * сортирует их в порядке убывания по _sequenceNumber; * возвращает самую последнюю запись Поскольку префикс является частью фильтра, каждый префикс имеет свой собственный счетчик, даже внутри одной и той же таблицы. Это позволяет сохранять изолированность и предсказуемость последовательностей. ## Как устроена сборка В фактической реализации используются три **триггера**. Такая структура выбрана намеренно. Я хотел, чтобы логический блок было легко проследить сверху вниз, при этом сохранив основную логику отдельной и пригодной для повторного использования. Поэтому вместо того, чтобы распределять логику по множеству редактируемых триггеров, я организовал её в три этапа:**1. НАСТРОЙКИ ПОЛЬЗОВАТЕЛЯ: ЭТАП ВВОДА 2. MAGIC: проверка + генерация ID 3. НАСТРОЙКИ ПОЛЬЗОВАТЕЛЯ: ЭТАП ВЫВОДА** Это обеспечивает хороший баланс между удобством использования и контролем.* В первом триггере вы определяете входные данные.* Второй триггер содержит внутреннюю логику, которую обычно не нужно редактировать для работы логического блока.* В третьем триггере вы решаете, что делать с полученным результатом. Это означает, что обычно вам не нужно работать внутри триггера основной логики. Достаточно настроить только входную и выходную стороны. Это было одним из ключевых проектных решений: сделать логический блок многоразовым, открыв доступ только к тем частям, которые обычно требуют изменения. ## Триггер 1 — USER CONFIG: INPUT STAGE Это первый триггер, с которым вы взаимодействуете. Его цель — определить входные значения, необходимые логическому блоку перед генерацией любого идентификатора. На практике этот триггер используется для определения:* **Префикса: _configPrefix*** **Длины идентификатора: _configIdNumberFormat*** Идентификатора таблицы, передаваемого в коннектор: **tableId** ### Значение каждого параметра **_configPrefix**— это текст, который ставится в начало каждого сгенерированного идентификатора. Его используют для определения последовательности, которую необходимо сгенерировать. Примеры:* ORD* INV* TEST Если **_configPrefix = ORD**, логический блок учитывает только существующие идентификаторы, начинающиеся с ORD, и продолжает только эту последовательность. Таким образом, это значение выполняет две функции одновременно:* оно определяет видимый префикс в окончательном идентификаторе* оно определяет, какие исторические идентификаторы коннектор должен использовать в качестве источника для следующего номера Это означает, что разные префиксы в одной и той же таблице генерируют разные последовательности**._configIdNumberFormat** Этот параметр определяет длину числовой части идентификатора. Его следует задавать в виде строки, заполненной нулями. Примеры:**000000000000000000** Количество нулей определяет ширину числовой части. Примеры:**0000 → 0001, 0002, 0003 … 9999 000000 → 000001, 000002 … 999999** Это значение не задаёт само число. Оно определяет формат, в который должно вписываться число. Я использую его для двух целей: * чтобы добавить ведущие нули к сгенерированному числу; * чтобы проверить, соответствует ли следующее число допустимой длине **tableId** Это определяет, к какой таблице должен обращаться коннектор. На практике именно это делает логический блок многократно используемым для разных таблиц. Вы можете скопировать один и тот же логический блок и настроить его на другую таблицу, изменив только это значение в конфигурационном триггере. Логический блок не выполняет поиск по таблицам самостоятельно. Он передаёт **tableId** и **_configPrefix** коннектору, а коннектор возвращает последний соответствующий идентификатор из этой таблицы. ### Как использовать их вместе Эти три значения совместно определяют поведение генератора:* **tableId** указывает логическому блоку, где искать* **_configPrefix** указывает, с какой последовательности следует продолжить* **_configIdNumberFormat** указывает, как должна выглядеть числовая часть ### Практические рекомендации по настройке При настройке логического блока убедитесь, что:* **tableId** указывает на правильную таблицу* **_configPrefix** соответствует префиксу, который вы хотите продолжить* **_configIdNumberFormat** соответствует желаемой длине числа Если вы измените префикс, вы больше не будете продолжать старую последовательность. Вы начнёте или продолжите другую последовательность, специфичную для данного префикса. Если позже вы увеличите длину формата номера, логический блок будет ожидать этот более широкий формат при проверке существующих ID в данной таблице и в рамках данного префикса.Коннектор извлекает последний идентификатор, соответствующий префиксу, и сохраняет его в **_stateLastId**. Если идентификатора ещё не существует, **LINK()** гарантирует, что генератор всё равно сможет сгенерировать первый допустимый идентификатор. ### Почему коннектор является частью архитектуры Коннектор — это не просто техническая деталь. Он является частью стратегии повторного использования. Я использую коннектор для чтения последнего существующего идентификатора из целевой таблицы. Коннектор получает идентификатор таблицы в качестве входных данных, применяет фильтр префикса, сортирует записи по **_sequenceNumber** в порядке убывания и возвращает последнюю соответствующую запись. Это важно по двум причинам. Во-первых, это позволяет логическому блоку продолжить нумерацию, исходя из реального состояния таблицы. Во-вторых, это значительно упрощает повторное использование логического блока. Если скопировать кнопку в другое приложение, не нужно переписывать логику. Достаточно лишь направить коннектор на нужную таблицу и указать необходимые настройки. Вот практический шаблон, лежащий в основе этой реализации:* создать кнопку один раз* настроить вход таблицы в коннекторе* определить префикс и формат номера* повторно использовать её везде, где это необходимо Это был осознанный выбор, направленный на ускорение разработки приложений. ### Что делает коннектор в логическом блоке? Что происходит здесь:* коннектор получает ID таблицы* коннектор получает префикс* коннектор фильтрует записи по префиксу* коннектор сортирует записи по _sequenceNumber в порядке убывания* коннектор возвращает последний совпадающий ID Коннектор также поддерживает очень важный сценарий использования: наличие нескольких сущностей в одной таблице. Если в одной и той же таблице содержатся такие записи: ORD20240001 MAT20240011 то логический блок не должен рассматривать их как одну общую последовательность. Каждый префикс должен продолжаться только от своего собственного последнего значения. Поэтому коннектор сначала фильтрует по префиксу. Это означает, что следующими значениями станут: ORD → ORD20240002, MAT → MAT20240012 Я выбрал эту модель, потому что в одной таблице могут находиться разные сущности, но это не означает, что они должны использовать один и тот же порядковый номер. Стоит также чётко указать одно требование к настройке: коннектор требует аутентификации, поэтому необходимо сгенерировать токен и добавить его в настройки аутентификации коннектора. Я рассматриваю это как работу по настройке, а не как часть самой логики блока, но это всё же важная составляющая, обеспечивающая работоспособность блока. ## Триггер 2 — MAGIC: проверка + генерация ID Этот триггер представляет собой основную логику логического блока. Он выполняет три действия в указанном порядке: 1. проверяет, имеет ли последний соответствующий идентификатор ожидаемую структуру; 2. проверяет, укладывается ли следующий номер в настроенную длину; 3. формирует следующий идентификатор Я объединил эти шаги в один триггер, поскольку они относятся к одному и тому же потоку принятия решений. Прежде чем создать новый ID, я хочу ответить на три вопроса: * безопасно ли с точки зрения структуры использовать предыдущий ID? * будет ли следующее число по-прежнему соответствовать формату? * если да, каким должен быть следующий ID? ### Проверка формата #### Expression `REGEX_REPLACE(LINK(Variable._stateLastId.id, Variable._configPrefix + DATETIMETOTEXT(App info.Current Date and Time,'YYYY') + Variable._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}' + '[0-9]{' + LEN(Variable._configIdNumberFormat) + '}$','ok')` #### Что делает этот блок « expression » Этот блок проверки ( expression ) проверяет, соответствует ли последний идентификатор (ID) точно той структуре, которую ожидает логический блок. Он работает в следующем порядке: 1. **LINK(...)** принимает **значение Variable._stateLastId.id**, когда коннектор возвращает значение. 2. Если идентификатор (ID) ещё не существует, **LINK(...)** использует резервное значение, построенное из: * **Variable._configPrefix** * **DATETIMETOTEXT(...,'YYYY')** * **Variable._configIdNumberFormat3**. Это резервное значение включено для того, чтобы проверка могла оценить действительную структуру даже в том случае, если это первый сгенерированный ID.4. **Variable._configPrefix** определяет точный префикс, с которого должен начинаться идентификатор. 5. **DATETIMETOTEXT(...,'YYYY')** не используется в качестве значения для сопоставления внутри самого регулярного выражения, но является частью структуры резервного значения, благодаря чему оно выглядит как настоящий идентификатор. 6. **LEN(Variable._configIdNumberFormat)** определяет, сколько цифр должна содержать числовая часть.7. Затем **REGEX_REPLACE(...)** проверяет всю строку на соответствие следующему шаблону: * префикс * ровно 4 цифры для года * ровно N цифр для числовой части8. Если полная строка соответствует шаблону, всё значение заменяется на «ok».9. Если не соответствует, исходное значение остаётся без изменений.10. Затем триггер проверяет, равно ли результат «ok». Если нет, выполнение останавливается. Таким образом, этот триггер expression не извлекает значения для вычислений. Его роль заключается в проверке того, что ID имеет ожидаемую структуру, прежде чем к нему будет применена какая-либо числовая логика. #### Почему я выбрал именно такой подход Я решил проверять структуру ID целиком перед применением любой числовой логики. Я построил его таким образом, потому что остальная часть триггера предполагает фиксированные позиции:* префикс в начале* год сразу после префикса* число сразу после года Если предыдущий ID не соответствует этому соглашению, инкрементирование от него будет ненадежным. Я также решил включить здесь резервный вариант **LINK(...)**, чтобы первому сгенерированному ID не требовалась отдельная логика проверки. Один и тот же путь проверки работает независимо от того, существует ли более старый идентификатор или нет. Я намеренно останавливаю триггер, вместо того чтобы пытаться исправить некорректные идентификаторы. Автоматическое исправление скрыло бы проблемы с данными и могло бы создать последовательность, которая выглядит действительной, но построена на неверных входных данных. ### Защита от переполнения #### Expression `LEN(IF(MID(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat), LEN(Variable._configPrefix), 4) = DATETIMETOTEXT(App info.Текущая дата и время,'YYYY'),RIGHT(Variable._configIdNumberFormat + TOTEXT(TEXTTOINTEGER(REGEX_REPLACE(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}','')) + 1), LEN(Variable._configIdNumberFormat)), RIGHT(Variable._configIdNumberFormat + '1', LEN(Variable._configIdNumberFormat))))` #### Что делает эта функция expression Эта функция expression вычисляет, какой будет следующая числовая часть, а затем определяет её длину. Она работает в следующем порядке: 1. **LINK(...)** использует последний ID, если он существует. Если нет, то используется значение **Variable._configPrefix + Variable._configIdNumberFormat**. 2. **MID(..., LEN(Variable._configPrefix), 4)** извлекает 4 символа, следующих сразу за префиксом. Они рассматриваются как часть предыдущего идентификатора, обозначающая год. 3. **DATETIMETOTEXT(...,'YYYY')** возвращает текущий год для сравнения. 4. **IF(...)** определяет, следует ли продолжить или сбросить числовую часть: * тот же год → инкремент * другой год → начать с 15. В ветке инкремента **REGEX_REPLACE(...)** удаляет префикс и 4-значный год из начала предыдущего идентификатора, так что остается только числовая часть.6. **TEXTTOINTEGER(...) + 1** преобразует этот числовой текст в число и увеличивает его на единицу.7. **TOTEXT(...)** преобразует новое число обратно в текст. 8. **Variable._configIdNumberFormat + ...** добавляет настроенный формат с нулями в начало значения. 9. **RIGHT(..., LEN(Variable._configIdNumberFormat))** сохраняет только необходимое количество цифр. 10. В ветке сброса **RIGHT(Variable._configIdNumberFormat + '1', LEN(...))** формирует первое допустимое число для нового года.11. **LEN(...)** измеряет длину полученной числовой части. Триггер сравнивает эту длину с допустимой длиной, заданной в Variable._configIdNumberFormat. Если следующая числовая часть окажется слишком длинной, выполнение останавливается. #### Почему я выбрал именно это Я решил вычислять следующее число так же, как на самом деле генерируется ID, вместо того чтобы использовать отдельную формулу для определения момента, когда число превысит допустимую длину. Я реализовал это именно так, потому что хотел, чтобы проверка на переполнение следовала точно тем же правилам, что и при реальной генерации ID. Это снижает вероятность того, что логика валидации и логика генерации впоследствии разойдутся. Остановиться на этом этапе безопаснее, чем продолжать. Если следующее числовое значение больше не укладывается в настроенную длину, результирующий ID нарушит настроенную вами структуру. Например, если числовой формат — 0000, то 9999 является допустимым, а 10000 — нет. Я хочу, чтобы логический блок выявлял это до создания записи, а не после. ### Логика генерации идентификаторов #### Expression `Variable._configPrefix + DATETIMETOTEXT(App info.Текущая дата и время,'YYYY')+ IF(MID(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),LEN(Variable._configPrefix),4) = DATETIMETOTEXT(Информация о приложении.Текущая дата и время, 'YYYY'), RIGHT(Variable._configIdNumberFormat + TOTEXT(TEXTTOINTEGER(REGEX_REPLACE(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}','')) + 1), LEN(Variable._configIdNumberFormat)), RIGHT(Variable._configIdNumberFormat + '1', LEN(Variable._configIdNumberFormat)))` #### Что делает эта функция expression Это функция expression, которая формирует окончательный идентификатор. Она работает в следующем порядке: 1. **Variable._configPrefix** добавляет настроенный префикс в начало идентификатора. 2. **DATETIMETOTEXT(App info.Current Date and Time, 'YYYY')** добавляет текущий 4-значный год. 3. **LINK(...)** использует последний идентификатор, возвращённый коннектором, если он существует. Если коннектор не возвращает идентификатор, в качестве резервной структуры используется **Variable._configPrefix + Variable._configIdNumberFormat**. 4. **MID(..., LEN(Variable._configPrefix), 4)** считывает 4 символа, следующих сразу за префиксом. Они рассматриваются как часть предыдущего идентификатора, обозначающая год. 5. Здесь снова используется **DATETIMETOTEXT(...,'YYYY')**, чтобы функция « expression » могла определить, относится ли предыдущий идентификатор к текущему году. 6. **IF(...)** управляет двумя возможными вариантами: * тот же год → продолжить нумерацию * другой год → сбросить до 17. В ветви «тот же год» функция **REGEX_REPLACE(...)** удаляет префикс и четырёхзначный год из начала предыдущего идентификатора, так что остаётся только числовая часть.8. **TEXTTOINTEGER(...) + 1 ** увеличивает это числовое значение.9. TOTEXT** (...) **преобразует увеличенное значение обратно в текст.10.** Variable._configIdNumberFormat + ... **добавляет настроенный формат с нулями в начало нового значения.11. RIGHT** (..., LEN(Variable._configIdNumberFormat)) **сохраняет только необходимое количество цифр, благодаря чему числовая часть всегда имеет фиксированную длину.12. В ветке сброса RIGHT** (Variable._configIdNumberFormat + '1', LEN(...))** формирует первое число новой годовой последовательности.13. Префикс, текущий год и отформатированное число объединяются в окончательный идентификатор. Таким образом, окончательный результат всегда имеет следующую структуру:**префикс + год + число фиксированной длины** #### Почему я выбрал именно это Я решил выводить следующий идентификатор исключительно на основе последнего реального идентификатора в таблице. Я построил алгоритм таким образом, поскольку предыдущий идентификатор уже содержит всю информацию, необходимую для логики:* к какому префиксу относится последовательность* к какому году она относится* каким было последнее числовое значение Это означает, что мне не нужна отдельная таблица счётчиков, сохранённое поле года или запланированный ежегодный сброс. Я также выбрал формат фиксированной длины, поскольку он позволяет сохранить предсказуемость структуры и упрощает проверку. Компромисс заключается в том, что функция « expression » зависит от строгой структуры идентификаторов. Именно поэтому этап проверки происходит в первую очередь. Я согласился на этот компромисс, поскольку строгий и понятный контракт в данном случае более полезен, чем попытка поддерживать множество неструктурированных форматов ввода. ## Триггер 3 — USER CONFIG: ЭТАП ВЫВОДА Это последний триггер в логическом блоке. Его цель — определить, что должно произойти с сгенерированным ID, как только **_stateOutput** будет содержать допустимое значение. Важно отметить, что этот этап намеренно оставлен открытым для вас. Если **_stateOutput** не пуст, вы можете решить, что должен делать логический блок дальше. В текущей настройке триггер отображает подсказку, объясняющую следующие шаги: * добавить настроенную таблицу в приложение * заменить этап «Сообщение» на триггер «Создать запись» * использовать **_stateOutput** в качестве идентификатора новой записи Я реализовал это именно так, поскольку поведение на выходе может варьироваться в зависимости от конкретного случая использования. Вы можете захотеть: * сразу создать запись; * сначала сохранить ID в другой переменной; * использовать ID в более длинном рабочем процессе; * проверить дополнительные условия перед записью в таблицу Вместо того чтобы жестко закрепить одно фиксированное поведение вывода, я оставил этап вывода настраиваемым. Именно поэтому настройки, доступные пользователю, присутствуют на обоих концах логического блока:* структура входа настраивается* поведение вывода настраивается* основная логика остается в середине ## Подход к именованию переменных Внутренние переменные используют префикс в виде подчеркивания:* _configPrefix* _configIdNumberFormat* _stateLastId* _stateOutput Это помогает быстро различать значения конфигурации и внутренние переменные обработки. ## Границы проектирования Логический блок предполагает следующую структуру:**префикс + год + номер**. Каждый префикс имеет свою собственную независимую последовательность. Логический блок не поддерживает использование одного общего счетчика для нескольких префиксов. Если год или числовая часть перемещаются в пределах идентификатора, необходимо соответствующим образом скорректировать « expressions ». ## Краткое изложение решений по построению Если свести весь логический блок к лежащим в его основе ключевым решениям, то это следующие:* Мне требовалось быстрое повторное использование, поэтому логический блок управляется конфигурацией.* Мне требовалось, чтобы таблица была «источником истины», поэтому следующий ID всегда основан на самой последней реальной записи.* Я хотел, чтобы логика оставалась удобной для обслуживания, поэтому вычисления централизованы в одном месте.* Я хотел, чтобы несколько сущностей в одной таблице работали корректно, поэтому каждый префикс имеет свою собственную независимую последовательность.* Я хотел, чтобы структура идентификатора оставалась неизменной со временем, поэтому год является частью идентификатора, а последовательность сбрасывается автоматически при смене года.* Я хотел защитить таблицу от некорректных шаблонов, поэтому проверка формата и переполнения предотвращают появление некорректных идентификаторов до создания записи.* Я хотел, чтобы поток триггера оставался понятным, поэтому функции разделены на четкие этапы: настройка, проверка, генерация, вывод. Именно поэтому блок логики выглядит именно так. Это не просто генератор идентификаторов. Это многократно используемый шаблон для контролируемого создания идентификаторов, который стремится сбалансировать простоту, возможность повторного использования и защиту данных, сохраняя при этом годовую сегментацию, встроенную непосредственно в структуру идентификатора. ## **Присоединяйтесь** [Присоединяйтесь к сообществу](https://community.tulip.co/), чтобы делиться улучшениями, предлагать дополнительные единицы измерения, сообщать о проблемах или обсуждать архитектурные решения.