20 мар. 2025 г.·7 мин

Событийно‑ориентированные рабочие процессы против запрос‑ответных API для долгих задач

Сравнение событийно‑ориентированных рабочих процессов и запрос‑ответ API для долгих процессов — согласования, таймеры, повторные попытки и аудит в бизнес‑приложениях.

Событийно‑ориентированные рабочие процессы против запрос‑ответных API для долгих задач

Почему долгие процессы сложны в бизнес‑приложениях

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

Именно здесь простая модель «запрос‑ответ» начинает давать сбои. API‑вызов рассчитан на короткий обмен: отправил запрос, получил ответ, пошёл дальше. Долгие задачи больше похожи на историю с главами. Нужно уметь приостанавливать, точно помнить, на каком вы шаге, и продолжать позже без догадок.

Это видно в повседневных бизнес‑приложениях: согласования покупок, где участвуют менеджер и финансы; адаптация сотрудника, ожидающая проверки документов; возвраты, зависящие от платёжного провайдера; или запросы доступа, которые сначала проверяют, а потом применяют.

Когда команды пытаются трактовать длинный процесс как единый API‑вызов, возникают предсказуемые проблемы:

  • Приложение теряет состояние после рестарта или деплоя и не может надёжно возобновить работу.
  • Повторные попытки создают дубликаты: второй платёж, второе письмо, двойное согласование.
  • Ответственность расплывается: непонятно, кто должен действовать дальше — запросивший, менеджер или системная задача.
  • Служба поддержки лишена видимости и не может ответить «где это застряло?» без копания в логах.
  • Логика ожидания (таймеры, напоминания, дедлайны) превращается в хрупкие фоновые скрипты.

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

Именно поэтому выбор между событийно‑ориентированными рабочими процессами и запрос‑ответ API имеет значение для долгих бизнес‑процессов.

Две ментальные модели: синхронные вызовы против событий во времени

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

Запрос‑ответ — это один обмен: один вызов, один ответ. Он подходит для работы, которая завершается быстро и предсказуемо: создание записи, расчёт цены, проверка наличия. Сервер выполняет работу, возвращает успех или ошибку, и взаимодействие заканчивается.

Событийно‑ориентированный рабочий процесс — это цепочка реакций во времени. Что‑то произошло (заказ создан, менеджер одобрил, таймер сработал), и процесс двигается к следующему шагу. Такая модель подходит для работы с передачами, ожиданиями, повторными попытками и напоминаниями.

Практическая разница — состояние.

В модели запрос‑ответ состояние часто живёт в текущем запросе и в памяти сервера до отправки ответа. В событийных рабочих процессах состояние нужно сохранять (например, в PostgreSQL), чтобы процесс мог возобновиться позже.

И обработка ошибок меняется. В запрос‑ответ обычно при ошибке возвращают ошибку и просят клиента попробовать снова. В рабочих процессах фиксируют неудачу и могут безопасно повторить действие, когда условия улучшатся. Они также могут логировать каждый шаг как событие, что упрощает восстановление хронологии.

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

Согласования: как каждый подход обрабатывает человеческие решения

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

В модели запрос‑ответ согласования часто принимают неудобную форму:

  • Блокировка (непрактично)
  • Опрос (client спрашивает «одобрено?» снова и снова)
  • Коллбэки/вебхуки (сервер звонит вам позже)

Все эти варианты работают, но добавляют инфраструктуру, чтобы связать «человеческое время» с «API‑временем».

В событийной модели согласование читается как последовательность фактов. Приложение записывает «ExpenseSubmitted», а позже приходит «ExpenseApproved» или «ExpenseRejected». Движок рабочего процесса (или ваша собственная машина состояний) переводит запись дальше только после получения следующего события. Это соответствует тому, как люди обычно мыслят о бизнес‑шаге: отправить, проверить, принять решение.

Сложность быстро растёт при множестве согласующих и правилах эскалации. Может потребоваться и менеджер, и финансы, но старший менеджер может переопределить решение. Если такие правила не смоделировать явно, процесс становится трудным для понимания и ещё труднее для аудита.

Простейшая модель согласований, которая масштабируется

Практичный паттерн — держать одну запись‑запрос, а решения хранить отдельно. Так можно поддерживать множество согласующих, не переписывая основную логику.

Зафиксируйте несколько сущностей как первоклассные записи:

  • Запрос на согласование: что согласуется и текущий статус
  • Индивидуальные решения: кто решил, утвердил/отклонил, временная метка, причина
  • Требуемые согласующие: роль или человек и правила порядка
  • Правила исхода: «любой», «большинство», «все обязательные», «доступно переопределение»

Какую бы реализацию вы ни выбрали, всегда храните, кто и когда принял решение и почему, как данные, а не как строку лога.

Таймеры и ожидание: напоминания, дедлайны и эскалации

Ожидание — то место, где долгие задачи начинают выглядеть грязно. Люди уходят на обед, календари заполняются, и «мы сообщим» превращается в «кто теперь отвечает?» Это одно из самых явных различий между событийными рабочими процессами и запрос‑ответ API.

В запрос‑ответах время неудобно. HTTP‑вызовы имеют тайм‑ауты, поэтому нельзя держать запрос открытым два дня. Команды обычно приходят к паттернам типа опроса, отдельной плановой задачи, сканирующей базу, или ручных скриптов для просроченных записей. Это работает, но логика ожидания живёт вне процесса. Лёгко пропустить граничные случаи: что если задача запустится дважды или запись изменилась прямо перед отправкой напоминания?

Рабочие процессы рассматривают время как обычный шаг. Можно задать: подождать 24 часа, отправить напоминание, затем подождать до 48 часов и эскалировать на другого согласующего. Система хранит состояние, поэтому дедлайны не прячутся в отдельном cron‑проекте.

Простое правило согласования можно описать так:

После подачи отчёта о расходах ждать 1 день. Если статус по‑прежнему «Pending», отправить сообщение менеджеру. Через 2 дня, если всё ещё «Pending», переназначить дело на руководителя менеджера и записать эскалацию.

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

  • Загружает последний статус
  • Подтверждает, что он всё ещё «Pending»
  • Подтверждает, что назначенный действующий (assignee) всё ещё валиден (команды меняются)
  • Записывает, что было сделано и почему

Повторные попытки и восстановление без дублирования действий

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

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

В запрос‑ответ модели клиент вызывает endpoint, ждёт, и если не получает явного успеха, пробует снова. Чтобы это было безопасно, сервер должен считать повторные вызовы тем же намерением.

Практичное решение — ключ идемпотентности: клиент отправляет уникальный токен вроде pay:invoice-583:attempt-1. Сервер сохраняет результат для этого ключа и возвращает тот же результат при повторах. Это предотвращает двойные списания, дублирование заявок или повторные согласования.

У событийных рабочих процессов другой тип риска дублирования. События часто доставляются «как минимум один раз», то есть дубликаты могут появляться даже при корректной работе. Потребители должны делать дедупликацию: сохранять event ID (или бизнес‑ключ вроде invoice_id + step) и игнорировать повторы. Это ключевое различие: запрос‑ответ фокусируется на безопасном повторном выполнении вызовов, события — на безопасном повторном проигрывании сообщений.

Несколько правил повторных попыток работают в обеих моделях:

  • Используйте backoff (например: 10 с, 30 с, 2 м).
  • Установите лимит попыток.
  • Разделяйте временные ошибки (повторять) и постоянные ошибки (быстро проваливать).
  • Переводите повторные неудачи в состояние «требуется внимание».
  • Логируйте каждую попытку, чтобы потом объяснить, что случилось.

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

Аудит: как сделать процесс объяснимым

Перестаньте бороться с долгими задачами
Замените опросы и единоразовые cron‑задачи на рабочий процесс, который отслеживает, что делать дальше.
Попробовать no‑code

Аудит — это ваш файл «почему». Когда спросят «почему этот отчёт отклонили?», вы должны уметь ответить без домыслов, даже через месяцы. Это важно и для событийных рабочих процессов, и для запрос‑ответ моделей, но выглядит по‑разному.

Для любого долгого процесса записывайте факты, которые позволят воспроизвести историю:

  • Actor: кто совершил действие (пользователь, сервис или системный таймер)
  • Time: когда это произошло (с часовым поясом)
  • Input: что было известно в тот момент (сумма, поставщик, пороговые политики, согласования)
  • Output: какое решение или действие было выполнено (утверждено, отклонено, оплачено, повторено)
  • Версия правила: какая версия политики/логики использовалась

Событийные рабочие процессы упрощают аудит, потому что каждый шаг естественно порождает событие вроде «ManagerApproved» или «PaymentFailed». Если вы храните эти события с payload и actor, получаете чистую хронологию. Важно делать события описательными и хранить их там, где можно запросить по делу.

В модели запрос‑ответ аудит возможен, но история часто разбросана по сервисам. Один endpoint пишет «approved», другой — «payment requested», третий — «retry succeeded». Если форматы разные, аудит превращается в детективную работу.

Простое исправление — общий case ID (корреляционный ID). Это идентификатор, который вы прикрепляете ко всем запросам, событиям и записям БД для экземпляра процесса, например «EXP-2026-00173». Тогда вы можете проследить весь путь.

Как выбрать подход: сильные стороны и компромиссы

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

Запрос‑ответ хорош, когда работа короткая и правила простые. Пользователь отправляет форму, сервер валидирует, сохраняет данные и возвращает успех или ошибку. Это также подходит для одноступенчатых действий: create, update, check permissions.

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

Событийно‑ориентированные рабочие процессы особенно хороши, когда процесс — это история во времени. Каждый шаг реагирует на новое событие (approved, rejected, timer fired, payment failed) и решает, что делать дальше. Это упрощает паузу, возобновление, повторные попытки и оставляет ясный след причин действий.

Есть реальные компромиссы:

  • Простота против надёжности: запрос‑ответ проще начать, событийные процессы надёжнее при больших задержках.
  • Стиль отладки: запрос‑ответ идёт по прямой линии, рабочие процессы требуют трассировки по шагам.
  • Инструменты и практики: события требуют хорошего логирования, корреляционных ID и ясных моделей состояния.
  • Управление изменениями: рабочие процессы ветвятся и эволюционируют; событийные модели легче поддерживают новые пути, если их правильно смоделировать.

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

Пошагово: как спроектировать долгий процесс, который переживёт задержки

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

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

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

Простой порядок действий

  1. Задайте границы: определите триггер старта, условие завершения и несколько ключевых состояний (Ожидание согласования, Утверждено, Отклонено, Истёк, Завершено).
  2. Дайте имена событиям и решениям: опишите, что может произойти со временем (Submitted, Approved, Rejected, TimerFired, RetryScheduled). Держите имена событий в прошедшем времени.
  3. Выберите точки ожидания: отметьте, где процесс приостанавливается для человека, внешней системы или дедлайна.
  4. Добавьте правила таймеров и повторных попыток для каждого шага: решите, что делать при наступлении времени или ошибке вызова (backoff, max attempts, эскалация, отказ).
  5. Определите, как процесс возобновляется: при каждом событии или коллбэке загружайте сохранённое состояние, проверяйте его валидность и переходите к следующему состоянию.

Чтобы пережить рестарты, сохраняйте минимально необходимое для безопасного продолжения:

  • ID экземпляра процесса и текущее состояние
  • Кто может действовать дальше (исполнитель/роль) и что он решил
  • Дедлайны (due_at, remind_at) и уровень эскалации
  • Метаданные повторных попыток (число попыток, последняя ошибка, next_retry_at)
  • Идемпотентный ключ или флаги «уже выполнено» для побочных эффектов (отправка сообщения, списание карты)

Если вы можете восстановить «где мы» и «что можно делать дальше» из сохранённых данных, задержки перестают быть страшными.

Типичные ошибки и как их избежать

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

Частые ошибки:

  • Держать HTTP‑запрос открытым в ожидании человеческого согласования. Он тайм‑аутит, зануляет ресурсы сервера и даёт пользователю ложное ощущение, что «что‑то происходит».
  • Повторять вызовы без идемпотентности. Сетевой сбой превращается в дублированные счета, письма или повторные переходы в «Approved».
  • Не сохранять состояние процесса. Если состояние в памяти — рестарт его сотрёт. Если только в логах — дальше нельзя корректно продолжить.
  • Делать аудиторский след «размытым». События имеют разные часы и форматы, и временная шкала не будет надёжной при инциденте или проверке соответствия.
  • Смешивать асинхронность и синхронность без единого источника истины. Одна система говорит «Paid», другая — «Pending», и никто не знает, что верно.

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

Большинство исправлений сводится к явности:

  • Сохраняйте переходы состояний (Requested, Approved, Rejected, Paid) в базе с указанием, кто/что их сделал.
  • Используйте идемпотентные ключи для внешних побочных эффектов и сохраняйте результат по ключу.
  • Разделяйте «принять запрос» и «завершить работу»: возвращайте клиенту быстрый ответ, а остальное выполняйте в фоне.
  • Стандартизируйте временные метки (UTC), добавляйте корреляционные ID и записывайте и запрос, и результат.

Короткий чек‑лист перед началом разработки

Сделайте шаги ожидания надежными
Используйте Business Process Editor для ожиданий, ветвлений и безопасного возобновления.
Построить логику

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

Запишите, что для вашего процесса значит «безопасно продолжить». Если приложение рестартует посередине, вы должны уметь продолжить с последнего известного шага без догадок.

Практический чек‑лист:

  • Определите, как процесс возобновится после краша или деплоя. Что сохранено, и что выполнится дальше?
  • Дайте каждой инстанции уникальный ключ процесса (например, ExpenseRequest-10482) и ясную модель статусов (Submitted, Waiting for Manager, Approved, Paid, Failed).
  • Рассматривайте согласования как записи, а не просто как флаги: кто утвердил/отклонил, когда и с каким комментарием.
  • Замапьте правила ожидания: напоминания, дедлайны, эскалации, истечения. Назначьте владельца для каждого таймера (менеджер, финансы, система).
  • Спланируйте обработку ошибок: повторные попытки ограничены и безопасны, и должен быть стоп‑пункт «требует ревью», где человек поправит данные или инициирует повтор.

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

Пример: согласование расходов с дедлайном и повторной попыткой платежа

Постройте полноценное бизнес‑приложение
Выпускайте полный бэкенд, веб- и мобильные приложения для процесса согласования или адаптации.
Создать приложение

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

Проходка в модели запрос‑ответ

В запрос‑ответ приложении часто действует разговорная модель: надо постоянно проверять.

Сотрудник нажимает Submit. Сервер создаёт запись reimbursement со статусом «Pending approval» и возвращает ID. Менеджер получает уведомление, а клиент приложения обычно опрашивает статус через «GET reimbursement status by ID».

Чтобы обеспечить 48‑часовой дедлайн, вы либо запускаете плановую задачу, которая сканирует просроченные заявки, либо сохраняете timestamp дедлайна и проверяете его при опросах. Если задача задержится, пользователи увидят устаревший статус.

Когда менеджер одобряет, сервер меняет статус на «Approved» и вызывает платёжного провайдера. Если Stripe вернул временную ошибку, сервер должен решить: повторить сейчас, позже или провалить. Без аккуратных идемпотентных ключей повтор может привести к двойной выплате.

Проходка в событийной модели

В событийной модели каждое изменение — факт.

Сотрудник отправляет отчёт — порождается событие «ExpenseSubmitted». Запускается workflow и ждёт либо «ManagerApproved», либо таймерное событие «DeadlineReached» через 48 часов. Если таймер срабатывает первым, workflow фиксирует «AutoRejected» и причину.

При одобрении workflow записывает «PayoutRequested» и пытается провести платёж. Если Stripe тайм‑аутит, система записывает «PayoutFailed» с кодом ошибки, планирует повторную попытку (например, через 15 минут) и фиксирует «PayoutSucceeded» только один раз, используя идемпотентный ключ.

Что видит пользователь:

  • Pending approval (осталось 48 часов)
  • Approved, идёт выплата
  • Повторная попытка платежа запланирована
  • Выплачено

Аудит читается как хронология: submitted, approved, deadline checked, payout attempted, failed, retried, paid.

Следующие шаги: превратить модель в рабочее приложение

Выберите один реальный процесс и реализуйте его полностью, прежде чем обобщать. Согласование расходов, адаптация сотрудников и обработка возвратов хороши для старта — они включают человеческие шаги, ожидание и пути ошибок. Цель маленькая: один «happy path» и две наиболее частые исключительные ситуации.

Опишите процесс как набор состояний и событий, а не как экраны. Например: «Submitted» -> «ManagerApproved» -> «PaymentRequested» -> «Paid», с ветвлениями «ApprovalRejected» или «PaymentFailed». Когда точки ожидания и побочные эффекты явны, выбор между событийной моделью и запрос‑ответом становится практическим.

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

Добавляйте поля аудита с самого начала. Храните, кто что сделал, когда это произошло и почему (комментарий или код причины). Когда спросят «почему этот платёж был повторён?», вы хотите ясный ответ без копания в логах.

Если вы строите рабочие процессы в no‑code платформе, AppMaster (appmaster.io) — один из вариантов, где можно моделировать данные в PostgreSQL и визуально строить логику процесса, что облегчает поддержание согласований и аудита в веб‑ и мобильных приложениях.

Вопросы и ответы

Когда стоит использовать запрос‑ответ вместо событийного рабочего процесса?

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

Почему долгие процессы ломаются, если их строить как единый API‑вызов?

Долгие задачи не вписываются в один HTTP‑запрос: соединения тайм-аутят, сервера рестартуют, а работа часто зависит от людей или внешних систем. Если пытаться поместить весь процесс в один вызов, вы теряете состояние, получаете дубликаты при повторных попытках и вынужденно выносите логику ожидания в разбросанные фоновые скрипты.

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

Хорошая практика — сохранять в базе явное состояние процесса и продвигать его только через явные переходы. Храните ID экземпляра процесса, текущий статус, кто может действовать дальше и ключевые временные метки, чтобы после деплоя, краша или задержки можно было безопасно продолжить выполнение.

Как лучше всего обрабатывать человеческие согласования?

Модель — представлять согласование как паузу, которая возобновляется при поступлении решения, а не держать соединение открытым или постоянно опрашивать. Записывайте каждое решение как данные: кто решил, когда, «утвердил/отклонил» и причина — это позволяет предсказуемо двигать процесс и проводить аудит.

Плохо ли опрашивать статус согласования?

Опрос (polling) может работать в простых случаях, но добавляет нагрузку и задержки — клиент постоянно спрашивает «готово ли?». Лучше отправлять уведомление об изменении состояния и позволять клиенту обновляться по требованию, при этом сервер остаётся единственным источником истины.

Как реализовать напоминания, дедлайны и эскалации?

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

Как предотвратить двойные платежи или дублирующие письма при повторных попытках?

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

Как обрабатывать дублирующиеся события в событийной системе?

Предполагаем, что сообщения могут доставляться более одного раза, и делаем потребителей устойчивыми к дубликатам. Практический подход — сохранять event ID (или бизнес‑ключ для шага) и игнорировать повторы, чтобы повторная доставка не вызвала повторного действия.

Что должен включать аудиторский след для долгих рабочих процессов?

Фиксируйте хронологию фактов: кто (actor), временная метка (с часовым поясом), входные данные на момент события, результат (утверждён, отклонён, выплачен, повторная попытка) и версия правил/политики. Привязывайте всё к одному case‑ID/корреляционному ID, чтобы служба поддержки могла легко найти «где застряло» без рытья по несвязным логам.

Какая практичная модель данных для согласований, чтобы правила не превратились в хаос?

Держите одну запись‑запрос как «дело», решения храните отдельно, и делайте переходы состояния через сохранённые транзакции, которые можно воспроизвести. В no‑code инструменте вроде AppMaster вы можете моделировать данные в PostgreSQL и визуально реализовать логику шагов, что помогает сохранить согласования, повторные попытки и поля аудита в едином виде.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster