> ## Documentation Index
> Fetch the complete documentation index at: https://docs.yume.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Промо-баннеры

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

Полный перечень полей — в [глоссарии промо-баннеров](/ru/logic/settings/promo-glossary).

Промо-баннеры — это часть модуля настроек, которая устроена иначе, чем всё остальное: у баннера вообще нет привязки к компании. Остальные записи настроек ссылаются на компанию (иногда ссылка пуста — тогда запись считается общей платформенной заготовкой), а у баннера такого поля просто нет. Баннер создаётся один раз в административной панели и показывается сотрудникам всех компаний, кроме тех, кому он явно запрещён. Внутри самой записи баннера ведётся журнал: кому и когда его уже показывали.

## Модель данных

Промо-баннер — одна запись с содержимым, правилами доступности и журналом показов.

| Поле                   | Тип           | Назначение                                                                                             |
| ---------------------- | ------------- | ------------------------------------------------------------------------------------------------------ |
| Идентификатор          | число         | Номер баннера; по нему интерфейс потом отмечает баннер прочитанным                                     |
| Заголовок              | текст         | Обязательный, до 255 символов                                                                          |
| Описание               | длинный текст | Обязательное, без ограничения длины                                                                    |
| Промокод               | текст         | Код, который предлагается пользователю; до 255 символов, может быть пустым                             |
| Иконка                 | текст         | Название иконки для оформления; до 255 символов, может быть пустым                                     |
| Ссылка                 | текст         | Адрес перехода по клику; до 255 символов, может быть пустым                                            |
| Изображение            | изображение   | Картинка баннера, загружается в файловое хранилище, в общую папку промо-материалов                     |
| Анимация               | файл          | Анимированный файл баннера, туда же                                                                    |
| Периодичность показа   | число (часы)  | Через сколько часов баннер можно повторно показать тому же человеку в той же компании; по умолчанию 24 |
| Журнал показов         | структура     | Отметки в разрезе «компания → пользователь → время последнего показа»                                  |
| Чёрный список компаний | список чисел  | Компании, которым баннер не показывается вовсе                                                         |
| Активен                | флаг          | Выключенный баннер не участвует в подборе; по умолчанию включён                                        |

Обязательны только заголовок и описание — всё остальное имеет значение по умолчанию или может быть пустым. Порядок сортировки у баннеров не задан.

<Note>
  Журнал показов и чёрный список живут **внутри записи баннера**, а не в данных компании. Это значит, что действия сотрудников разных компаний (отметки о прочтении) пишутся в одну и ту же строку одной общей таблицы.
</Note>

## Что уходит в интерфейс

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

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

## Логика выбора баннера

Запрос за баннером доступен только авторизованному пользователю; кроме того, общий контур доступа пропускает к этому разделу лишь сотрудника выбранной компании (либо сотрудника платформы). Сервер берёт две координаты — текущую компанию и текущего пользователя — и подбирает **один** баннер.

### Шаги подбора

1. Отбираются все включённые баннеры.
2. Из них исключаются те, в чей чёрный список входит текущая компания.
3. Оставшиеся перебираются по очереди. Для каждого сервер смотрит в журнал показов: сначала находит раздел текущей компании, внутри него — отметку по текущему пользователю.
4. **Отметки нет** — баннер этому человеку в этой компании ещё не показывали, он и возвращается, перебор прекращается.
5. **Отметка есть** — сравнивается, сколько времени прошло с последнего показа. Если прошло не меньше, чем указано в периодичности (в часах), баннер возвращается. Если меньше — сервер переходит к следующему баннеру, а не прекращает поиск.
6. **Отметку не удалось разобрать** — если сохранённое время записано строкой в неожиданном формате или без часового пояса, баннер считается непоказанным и возвращается.
7. Если ни один баннер не подошёл, ответ пустой — со статусом «нет содержимого», без тела.

<Info>
  Сравнение идёт по правилу «прошло **не меньше** заданной периодичности». При периодичности 24 часа баннер повторно доступен ровно через сутки после последнего отмеченного показа. Периодичность 0 (или отрицательная) означает, что баннер подходит всегда, при каждом запросе — нижней границы у этого значения нет.
</Info>

### Что означает «первый подходящий»

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

<Warning>
  Подбор не кэшируется и выполняется на каждый запрос: сервер каждый раз выбирает из базы включённые баннеры вместе с их журналами и разбирает журналы в памяти приложения, а не средствами базы. Это допустимо, пока баннеров единицы; при десятках записей с большими журналами запрос станет заметно дороже.
</Warning>

## Отметка о прочтении

Сама выдача баннера **ничего не записывает**. Журнал пополняется только отдельным вызовом, который интерфейс делает по номеру баннера — обычно в момент, когда баннер реально показан пользователю или закрыт им.

Что происходит при этом вызове:

1. Баннер ищется по номеру. Если такого нет — ответ «не найдено».
2. В журнале берётся (или создаётся) раздел текущей компании.
3. В нём по ключу текущего пользователя записывается текущее время.
4. Запись баннера сохраняется целиком.

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

<Warning>
  Пока отметка не поставлена, тот же самый баннер будет возвращаться при каждом обращении. Если интерфейс показывает баннер, но не вызывает отметку, пользователь увидит его снова при следующей загрузке страницы.
</Warning>

<Note>
  Отметка не проверяет ни активность баннера, ни чёрный список — она принимает любой существующий номер. Отметить «прочитанным» можно и выключенный баннер, и тот, который данной компании вообще не показывается.
</Note>

## Разделение по компаниям и пользователям

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

* Один и тот же сотрудник, работающий в двух компаниях, увидит баннер дважды — по разу в каждой компании, и отметки будут независимыми.
* Чёрный список исключает **компанию целиком**. Скрыть баннер от конкретного человека, не скрывая от его коллег, нельзя.
* Записи в журнале не удаляются: ни ручной очистки, ни фоновой уборки не предусмотрено. Со временем журнал накапливает по одной отметке на каждую пару «компания — пользователь», которой баннер когда-либо показывали.
* Чтобы показать баннер заново всем, недостаточно поменять его содержимое: нужно либо очистить журнал вручную, либо завести новый баннер.

## Управление в административной панели

Баннеры создаются и отключаются на отдельной странице платформенной админки.

* Список показывает заголовок и признак активности.
* Поиск работает по заголовку, описанию, промокоду и ссылке.
* Фильтр — по признаку активности.

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

<Tip>
  Самый безопасный способ снять баннер с показа — снять флаг активности, а не удалять запись: журнал показов сохранится, и при повторном включении баннер не «выстрелит» заново всем, кто его уже видел.
</Tip>

## Как это используется

Типовой сценарий работы интерфейса:

1. При входе в рабочее пространство фронтенд запрашивает баннер.
2. Если ответ пустой — ничего не показывается.
3. Если баннер пришёл — он отрисовывается по полученным полям: заголовок, описание, картинка или анимация, иконка, промокод и ссылка перехода.
4. Сразу после показа (или при закрытии) интерфейс вызывает отметку о прочтении с номером этого баннера — иначе баннер вернётся снова.

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

## Подводные камни

<Warning>
  * **Показ сам по себе ничего не записывает.** Если интерфейс не вызовет отдельную отметку о прочтении, один и тот же баннер будет возвращаться при каждом запросе — пользователь увидит его снова и снова.
  * **Порядок перебора баннеров не задан.** Сортировка не указана ни в описании данных, ни в запросе, поэтому при нескольких подходящих баннерах выбор зависит от того, в каком порядке их вернёт база.
  * **Отметки могут затирать друг друга.** Отметка о прочтении читает журнал, дописывает в него запись и сохраняет баннер целиком, поэтому одновременные отметки от разных пользователей — в том числе из разных компаний — могут потерять часть данных.
</Warning>

* Отметка о прочтении принимает любой существующий номер баннера: ни активность, ни чёрный список компании при этом не проверяются, а тело запроса не читается и не валидируется.
* Если сохранённое время последнего показа записано строкой в неразборчивом формате или без часового пояса, баннер считается непоказанным и выдаётся снова — такая ошибка в журнале приводит к бесконечному показу, а не к его прекращению.
* Если журнал испорчен грубее — вместо строки со временем записано число либо раздел компании перестал быть структурой «ключ-значение», — запрос за баннером завершится ошибкой сервера, а не мягким пропуском записи.
* Периодичность показа не ограничена снизу: значение 0 или отрицательное означает, что баннер подходит при каждом запросе.
* Чёрный список исключает компанию целиком; скрыть баннер от отдельного пользователя внутри компании невозможно.
* Изображение и анимация складываются в одну и ту же папку файлового хранилища, причём анимация загружается как произвольный файл без проверки того, что это картинка.

<Info>
  - Когда показывать нечего, ответ приходит со статусом «нет содержимого» и пустым телом, а не пустым объектом — интерфейс должен корректно обрабатывать этот статус.
  - Журнал показов растёт бесконечно: отметки по каждой паре «компания — пользователь» никогда не удаляются и хранятся внутри самой записи баннера; ни фоновой уборки, ни отдельного инструмента очистки нет.
  - Один и тот же человек, работающий в двух компаниях, получает баннер отдельно в каждой из них, потому что ключ журнала составной — сначала компания, затем пользователь.
  - Наружу не отдаются периодичность, журнал показов, чёрный список и флаг активности, поэтому интерфейс не может самостоятельно решить, пора ли показывать баннер.
  - Подбор баннера не кэшируется и на каждый запрос вычитывает включённые баннеры с их журналами, разбирая их в памяти приложения, а не в базе.
  - В административной панели нет отдельных инструментов для чёрного списка и журнала показов: список содержит только заголовок и признак активности, фильтр — только по активности, а обе структуры правятся как обычные поля формы.
  - Страница баннеров, в отличие от соседней страницы настроек таблиц, не ограничена дополнительно правами суперпользователя — редактировать баннеры может любой сотрудник платформы с обычными правами админки на эту запись.
  - У промо-баннера, в отличие от остальных настроек, нет поля компании вовсе; у прочих записей раздела ссылка на компанию есть и может быть пустой — это означает общую платформенную заготовку.
</Info>

## Связанные страницы

<CardGroup cols={2}>
  <Card title="Глоссарий: Промо-баннеры" icon="book" href="/ru/logic/settings/promo-glossary">
    Полный перечень полей баннера, журнала показов и правил подбора.
  </Card>

  <Card title="Настройки компании" icon="sliders" href="/ru/logic/settings">
    Обзорная страница модуля: из чего состоят настройки компании и как они связаны между собой.
  </Card>

  <Card title="Реестр настроек компании" icon="sliders" href="/ru/logic/settings/config">
    Полный перечень параметров компании: что можно включить или выключить, какие значения стоят по умолчанию и как настройки читаются и сохраняются.
  </Card>

  <Card title="Хранение и кэш настроек" icon="database" href="/ru/logic/settings/storage-cache">
    Как значение настройки попадает в базу, как оно кэшируется по компаниям и почему изменение настройки сбрасывает кэш целиком.
  </Card>

  <Card title="Дополнительные поля" icon="list-check" href="/ru/logic/settings/custom-fields">
    Собственные поля компании для клиентов, инвентаря, аренд и других справочников: типы, обязательность, отображение в таблице и фильтрах.
  </Card>

  <Card title="Переименование сущностей" icon="language" href="/ru/logic/settings/labels">
    Как компания меняет названия разделов и статусов под свою нишу, включая формы слова по падежам и числам.
  </Card>

  <Card title="Настройки таблиц" icon="table-columns" href="/ru/logic/settings/tables">
    Сохранённый состав, порядок и ширина колонок для таблиц интерфейса — отдельно по каждой таблице компании.
  </Card>

  <Card title="Сквозная автонумерация" icon="hashtag" href="/ru/logic/settings/counters">
    Общий счётчик компании, из которого берутся номера клиентов, артикулы создаваемых единиц инвентаря и номера документов.
  </Card>

  <Card title="Настройки в админ-панели" icon="screwdriver-wrench" href="/ru/logic/settings/admin">
    Служебный экран платформы для просмотра и правки настроек конкретной компании со сбросом значений к умолчанию.
  </Card>
</CardGroup>
