Источник импорта ровно один. Таблица ограничивает его условием
CHECK (source = 'remnawave'), а читателем выступает эндпоинт списка Remnawave. Нет ни импорта из
CSV, ни загрузки файлов, ни второго поставщика. CSV, описанный в конце этой страницы, — это только
выгрузка.Что производит импорт
Для каждой применённой записи — ровно две строки: строкаusers со статусом active и сопоставление
remnawave_users, несущее идентификатор пользователя Remnawave, Telegram ID, если он был, наблюдаемый
статус и отметку времени сверки.
Он не производит ни подписку, ни право на услугу, ни заказ, ни подтверждённую личность. Omniflow
не знает, что эти клиенты платили, когда и за что — Remnawave этого не фиксирует, — а придумывание
права на услугу внесло бы в книгу проводок период и цену, о которых никто не договаривался. См.
Владение данными — границу, которую это уважает.
Что даёт сопоставление, так это узнавание. Когда импортированный клиент в следующий раз напишет боту,
путь разрешения клиента сначала ищет личность Telegram, а не найдя её, откатывается к сопоставлению
Remnawave по его Telegram ID. Он принимает этого клиента и привязывает к нему личность Telegram,
вместо того чтобы создавать второго клиента. В этом и вся причина, по которой предпросмотр считает уже
привязанный Telegram ID конфликтом: два клиента, держащие одну личность Telegram, сделали бы этот
откат неоднозначным, а неоднозначность проявилась бы как чужая подписка, появившаяся на аккаунте
другого человека.
Фаза первая: предпросмотр
Предпросмотр читает одну страницу пользователей Remnawave и размещает их во временной области. Его безопасно запускать повторно, и он не пишет ничего за пределами таблиц импорта. Каждый кандидат размещается только с четырьмя полями —remnawaveId, username, status,
telegramId. Размещённая полезная нагрузка — это то, что читают проверки конфликтов и из чего
вычисляются счётчики оператора; ничего больше из записи Remnawave в Omniflow не копируется.
Как работает сопоставление
Перед размещением страницы сервис строит множество уже существующих личностей из трёх источников:- Все
remnawave_idиtelegram_idвremnawave_users— клиенты, которых Omniflow уже знает. - Все активные строки
identitiesсprovider = 'telegram'— клиенты, вошедшие через бот или веб-панель и пока не имеющие сопоставления с Remnawave. - Все Telegram ID, уже размещённые в этом прогоне, чей идентификатор источника отсутствует на текущей странице.
Коды валидации
Статус элемента вытекает из кодов: ни одного кода —
valid; любой код — conflict; код, начинающийся
с missing_ или invalid_, делает его invalid вместо этого. Различие — это и есть смысл счётчиков:
конфликт — это запись, принадлежащая кому-то, а невалидная запись — та, которую вообще нельзя
прочитать, и они требуют от оператора разных решений.
Каждый элемент также хранит отпечаток SHA-256 своей размещённой полезной нагрузки, поэтому повторный
предпросмотр может отличить изменившуюся запись от неизменившейся. Повторный предпросмотр никогда не
перезаписывает элемент, который уже применён или пропущен: upsert несёт
WHERE customer_import_items.status NOT IN ('applied', 'skipped'), и именно это делает повторный
запуск предпросмотра на частично применённом прогоне безвредным.
Курсор и статус прогона
Курсор — это числовое смещение в списке Remnawave, хранимое как текст и начинающееся с нуля. Каждый проход увеличивает его на число пользователей, которое вернула страница, и переводит прогон вready,
как только курсор достигает заявленного итога; до этого он остаётся в previewing.
pageSize зажимается: всё, что не больше нуля или больше 500, становится 100.
Если Remnawave недоступен, прогон не остаётся наполовину завершённым и ни одна ошибка не выбрасывается
впустую. Он записывается в статус failed с error_summary, равным
{"code":"remnawave_unavailable"}, и возвращается как есть. Поскольку путь возобновления
останавливается только на completed и cancelled, прогон в статусе failed можно возобновить с
сохранённого курсора, как только панель снова будет доступна.
Фаза вторая: применение
Применение выбирает доlimit элементов со статусом valid, упорядоченных по идентификатору
источника, и фиксирует каждый в собственной транзакции уровня Serializable. limit зажимается так
же, как pageSize.
Каждый элемент — это одна цепочка CTE в одном выражении: заблокировать элемент через FOR UPDATE по
status = 'valid', вставить клиента, вставить сопоставление Remnawave, пометить элемент как applied
с его новым идентификатором клиента и отметкой времени. Поскольку это одно выражение в одной
транзакции, сбой не создаёт ни частичного клиента, ни осиротевшего сопоставления. Параллельный второй
прогон не может применить тот же элемент дважды, потому что блокировка совпадает только с элементом,
всё ещё находящимся в valid.
Конфликтующие и невалидные элементы никогда не выбираются, а значит, никогда не применяются. После
пакета, если не осталось ни одного элемента valid, прогон переходит в completed и проставляется
completed_at; иначе он остаётся в applying, и следующий вызов продолжает работу.
Ещё две вещи, которых стоит ожидать:
- Импортированные клиенты всегда создаются с локалью
ru. Локаль жёстко задана в пути применения независимо от чего бы то ни было в записи Remnawave. Клиент может сменить её сам в боте или в веб-панели впоследствии. - Импорт не пишет событий аудита. Таблицы импорта и есть запись о том, что произошло; журнал аудита, описанный в Административных операциях, их не покрывает.
Как выполнить импорт
Маршруты предпросмотра, возобновления и применения находятся на поверхности операторского токена. Элемента управления в панели, который запускает импорт, нет —/admin/customers перечисляет прогоны и
предлагает выгрузку, и это всё, что он делает.
1
Проверьте учётные данные панели
Воркер и API читают
APP_REMNAWAVE_URL и APP_REMNAWAVE_TOKEN; импорт обращается к той же панели
через тот же адаптер. APP_OPERATOR_TOKEN авторизует /v1/admin.2
Откройте прогон
{id, status, cursor, totalCount, validCount, conflictCount, invalidCount, startedAt, updatedAt, completedAt}.3
Листайте страницы, пока прогон не будет готов
status не станет ready. Каждый вызов читает одну страницу и продвигает курсор.4
Прочитайте счётчики, прежде чем что-либо записывать
validCount, conflictCount и invalidCount — это и есть вся причина, по которой существует фаза
предпросмотра. Большое число конфликтов обычно означает, что установка уже знает этих клиентов, и
это повод остановиться, а не повод продолжать.5
Примените
status не станет completed.
Отказы, которых стоит ожидать:
- Применение прогона в статусе
previewing,completed,failedилиcancelledвозвращает422 import_failedс деталью «import is not ready». - Некорректный идентификатор импорта возвращает
422 import_failedс «invalid import ID». - В отличие от маршрутов панели, которые отображают любой сбой на фиксированное сообщение, эти три
маршрута помещают сырой текст ошибки в поле
detailдокумента проблемы. Считайте ответ предназначенным для оператора.
Наблюдение за прогоном в панели
Откройте/admin/customers и прочитайте карточку Imports. Она опрашивает GET /v1/panel/imports
каждые пять секунд, потому что идущий прогон меняется, пока оператор за ним наблюдает, и показывает по
каждому прогону:
- первые восемь символов идентификатора прогона и значок статуса;
- значок resumable всякий раз, когда сохранён курсор — прогон, прерванный на сорокатысячной записи, не начинается заново с первой;
- четыре счётчика: всего, валидных, конфликтов, невалидных;
- время начала и, после завершения, время окончания;
- сводку ошибок, отрисованную парами «класс и количество». Оператору нужно знать, что у 812 записей не было идентификатора Telegram, а не пролистывать 812 одинаковых строк.
Сводка ошибок бывает либо пустой, либо содержит единственную запись
{"code":"remnawave_unavailable"}. Карточка построена так, чтобы отрисовывать количества по кодам,
но сервис импорта их никогда не заполняет, поэтому сбойный прогон показывает одну пару с названием
сбоя панели, а не разбивку по кодам валидации. Разбивка, на которую можно опираться, — это три
счётчика.Выгрузка клиентов в CSV
Рядом с историей импорта есть кнопка Export — обычная ссылка наGET /v1/panel/customers/export, которой нужно право customers.read. Она отдаёт потоком CSV со
списком клиентов, при желании отфильтрованным через ?status=.
Набор колонок — это и есть суть. Выгрузка — это файл, покидающий установку, поэтому он несёт
идентификаторы, которые оператору можно безопасно отдать, и факты, необходимые ему для сверки с другой
системой, — и намеренно ни ссылки на подписку, ни платёжного токена, ни идентификатора устройства.
Контактные значения тоже не появляются; они зашифрованы при хранении и не входят в этот файл.
Обработчик проходит курсор на стороне сервера по 200 строк на страницу и сбрасывает данные по ходу,
поэтому выгрузка по большой установке никогда не удерживается в памяти. Он останавливается после 200
страниц — по той же причине, что и выгрузки аудита и финансов: один запрос не должен иметь возможности
бесконечно обходить неограниченную таблицу. Установке с более чем сорока тысячами клиентов следует
ожидать, что файл на этом и закончится.
Объединение двух аккаунтов клиента
Привязка личности к аккаунту, в который вы уже вошли, работает. Соединение двух существующих аккаунтов — другая задача, и именно она возникает на практике: клиент покупает в Telegram, позже входит в вебе через провайдера, которого не было у первого аккаунта, и держит пустой второй аккаунт, тогда как подписка лежит на первом. Это есть на/admin/customers/{id}, под вкладками. Аккаунт, который вы
смотрите, — поглощаемый: направление зафиксировано и названо, потому что
«объединить A и B» двусмысленно ровно там, где это важно.
Сначала предпросмотр
Объединение необратимо, поэтому всё, что переедет, пересчитывается до того, как что-либо переедет: подписки, заказы, способы входа, обращения, рефералы и баланс кошелька по валютам — с обеих сторон. Блокировки пересчитываются в момент применения, а не берутся из предпросмотра. Это два разных запроса, и объединение, разрешённое устаревшим экраном, — из тех вещей, что ломаются ровно один раз.Что не является простой переприпиской
Наusers ссылаются сорок шесть таблиц. Для большинства это смена владельца.
Пять — это решение, и поэтому здесь нет цикла по каталогу внешних ключей:
- Слоты подписок уникальны в пределах клиента, поэтому переехавшие подписки перенумеровываются на первые свободные слоты цели.
- Кошелёк переезжает компенсирующей парой записей реестра — списание у
источника до нуля, зачисление той же суммы у цели, — потому что
ledger_entriesappend-only. Переписыватьuser_idв финансовой записи клиента недоступно и не должно быть: эти записи — доказательство каждого баланса, который установка когда-либо показывала. - Сохранённая корзина отменяется, а не переезжает. У клиента может быть открыта только одна, а корзина — это выбор, сделанный в заканчивающейся сессии.
- Отметки о прочтении переезжают только там, где цель не читала тот же пост; пара — первичный ключ, и всё объединение не должно падать из-за отметки.
- Сохранённые способы оплаты переезжают, но приезжают не по умолчанию. Один способ по умолчанию на клиента — уникальный индекс, и у выжившего это тот, который он выбрал последним.
После
Источник сохраняет свои строки и историю и помечается какmerged со ссылкой на
цель. Он не удаляется: запись о том, что произошло, ценнее, чем освобождённая
строка, а поддержке, отвечающей на «куда делась моя подписка», нужно куда-то
приземлиться.
Оба аккаунта получают событие жизненного цикла — merged_away и merged_in, —
поэтому объединение читается из собственной истории любого из клиентов, а не
только из операторского журнала. Событие аудита — customer.merged, оно несёт
счётчики.
Причина обязательна. Она попадает и в журнал аудита, и в обе истории, потому что
«почему эти два аккаунта соединили» — первый вопрос, который потом задают.
Связанные страницы
Интеграция с Remnawave
Адаптер, через который читает импорт, и каждый вызов, который он делает.
Приватность клиента
Что хранится о клиенте, что он может выгрузить и как происходит удаление.
Административные операции
Массовые действия, журнал аудита и остальная клиентская поверхность.
Выдача доступа
Как первая покупка импортированного клиента становится правом на услугу.