Разработка сайта туроператора на 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Разработка сайта туроператора на 1С-Битрикс
Сложная
от 1 недели до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1177
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    747
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Разработка сайта туроператора на 1С-Битрикс

Сайт туроператора отличается от обычного каталога одним свойством: данные в нём живут. Цены меняются каждый день — раннее бронирование, горящие предложения, сезонные коэффициенты. Даты вылетов появляются и исчезают. Наличие мест зависит от внешних систем бронирования, которые отвечают по-разному: одна отдаёт JSON за 200 мс, другая — XML за 8 секунд. Если спроектировать каталог как статический инфоблок с ручным обновлением, через месяц менеджеры перестанут обновлять цены, а клиенты будут бронировать туры по устаревшим данным.

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

Каталог туров: инфоблоки и связи

Каталог строится на двух инфоблоках и одном Highload-блоке.

Инфоблок «Направления» — разделы инфоблока туров. Иерархия: «Европа» → «Италия» → «Тоскана». Свойства разделов через UF_* поля: UF_COUNTRY_CODE (ISO 3166-1), UF_CLIMATE_INFO (текст), UF_VISA_REQUIRED (булево), UF_GALLERY (множественный файл). Разделы используются и для фильтрации, и для SEO-страниц: /tours/italy/, /tours/italy/toscana/.

Инфоблок «Туры» (тип tours) — элементы внутри разделов-направлений. Свойства:

  • DURATION — число, количество дней. Фильтрация по диапазону
  • DEPARTURE_DATES — множественное свойство типа «Дата». Каждый элемент содержит 5-20 дат вылета. Индекс на b_iblock_element_property по IBLOCK_PROPERTY_ID + VALUE_DATE обязателен — без него фильтрация по датам на каталоге из 2000+ туров деградирует до секунд
  • TOUR_TYPE — список: «Экскурсионный», «Пляжный», «Активный», «Круизный», «Комбинированный»
  • DIFFICULTY — список от 1 до 5, для активных и горных маршрутов
  • GROUP_SIZE_MIN, GROUP_SIZE_MAX — числовые
  • INCLUDED_SERVICES — множественная строка: перелёт, трансфер, страховка, экскурсии, питание. Используется в детальной карточке и для фильтра «Что включено»
  • BASE_PRICE — число, базовая цена в основной валюте. Фактическая цена рассчитывается динамически
  • HOTEL_STARS — список: 2-5 звёзд, «Без размещения»
  • BOOKING_SYSTEM_ID — строка, внешний идентификатор тура в системе бронирования (Samo, Sletat)
  • GALLERY — множественный файл, фото направления и отеля
  • VIDEO_URL — строка, YouTube/Vimeo

Привязка к торговому каталогу через CCatalog::Add() нужна только если бронирование проходит полностью через модуль sale. Если оплата уходит на внешнюю систему (Samo.Tourvisor), каталог Битрикс не подключается — инфоблок работает как витрина.

Highload-блок «Ценовые коэффициенты» (PriceCoefficients) — хранит правила динамического ценообразования. Поля: TOUR_ID, DATE_FROM, DATE_TO, COEFFICIENT (float), RULE_TYPE (список: «Раннее бронирование», «Горящий тур», «Сезонный», «Групповая скидка»), PRIORITY (число, порядок применения). Highload выбран потому, что записей будет тысячи — каждый тур × каждый сезон × каждый тип правила. Выборка через \Bitrix\Highloadblock\HighloadBlockTable::getList() с фильтром по TOUR_ID и текущей дате.

Поиск и фасетная фильтрация — ядро проекта

Поиск тура — это не текстовый поиск. Клиент думает категориями: «Италия, июнь, 7-10 дней, до $2000, экскурсионный». Пять фильтров одновременно, и результат должен появляться без перезагрузки страницы.

Стандартный catalog.smart.filter здесь не работает. Он рассчитан на товары с фиксированными свойствами. У тура дата вылета — множественное свойство, цена — вычисляемая, направление — иерархия разделов. Фасетный индекс (\Bitrix\Iblock\PropertyIndex\Manager::buildIndex()) покрывает только часть сценариев.

Решение — кастомный компонент project:tour.filter с собственной логикой:

Фильтрация по направлению. Иерархический выбор: страна → регион → курорт. При выборе страны фильтр подгружает регионы AJAX-запросом на /api/tours/regions/?country=IT. В component.phpCIBlockSection::GetList() с фильтром по SECTION_ID родителя. Кеширование через CPHPCache с тегом iblock_id_N.

Фильтрация по датам вылета. Клиент выбирает диапазон — например, «с 1 по 30 июня». В БД даты хранятся как множественные значения свойства. Запрос:

$filter = [
    '>=PROPERTY_DEPARTURE_DATES' => '2025-06-01',
    '<=PROPERTY_DEPARTURE_DATES' => '2025-06-30',
];

Проблема: стандартный CIBlockElement::GetList() с таким фильтром по множественному свойству работает медленно на больших объёмах. Решение — промежуточная таблица b_tour_departure_index, заполняемая агентом при обновлении тура. Таблица: TOUR_ID, DEPARTURE_DATE (DATE, с индексом). Фильтрация идёт JOIN'ом на эту таблицу, результат — массив TOUR_ID, который подставляется в CIBlockElement::GetList(['ID' => $tourIds]).

Фильтрация по цене. Цена вычисляется в момент запроса: BASE_PRICE × коэффициент сезона × коэффициент раннего бронирования. Прямой фильтр по вычисляемому значению невозможен. Два варианта:

  1. Материализованная цена — агент пересчитывает актуальную цену каждого тура раз в час и записывает в свойство CURRENT_PRICE. Фильтрация по нему стандартная. Минус — задержка до часа между изменением коэффициента и обновлением цены.
  2. Двухэтапная фильтрация — сначала выбираются туры по всем остальным фильтрам (направление, даты, длительность), затем для каждого из них вычисляется цена и отсекаются не попадающие в диапазон. Работает точно, но при каталоге в 5000 туров второй этап может занять 200-500 мс. Решается кешированием вычисленных цен в Redis/Memcached с TTL 15 минут.

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

AJAX-фильтрация. Все фильтры отправляются одним GET-запросом: /api/tours/search/?destination=IT&date_from=2025-06-01&date_to=2025-06-30&duration_min=7&duration_max=10&price_max=2000&type=excursion. Контроллер в local/modules/project.tours/lib/controller/search.php наследует \Bitrix\Main\Engine\Controller, валидирует параметры, собирает фильтр CIBlockElement::GetList(), возвращает JSON с массивом туров и метаданными фасетов (сколько туров по каждому типу при текущих фильтрах).

Фасеты — счётчики рядом с каждым значением фильтра («Экскурсионный (23)», «Пляжный (45)»). Вычисляются отдельными запросами COUNT(*) по каждому свойству с остальными фильтрами. Это 4-6 дополнительных запросов, каждый — лёгкий при наличии индексов. Результат кешируется на 5 минут.

Интеграция с системами бронирования

Туроператор редко продаёт только собственные туры. Сайт агрегирует предложения из нескольких источников: собственные туры (в инфоблоке), пакетные туры из Samo.Tourvisor и предложения из агрегатора Sletat.ru.

Samo.Tourvisor API — RESTful JSON. Основные эндпоинты:

  • GET /api/search — поиск туров по параметрам (страна, курорт, дата, ночи, взрослые/дети). Ответ — массив предложений с ценой, отелем, датой вылета, оператором
  • GET /api/hotel/{id} — детали отеля: фото, описание, координаты
  • POST /api/order — создание заявки на бронирование

Интеграция реализуется через модуль local/modules/project.tourvisor/. Класс \Project\Tourvisor\Client оборачивает HTTP-запросы через \Bitrix\Main\Web\HttpClient. Метод search() принимает параметры фильтра сайта, маппит их в формат API, выполняет запрос, парсит ответ.

Критичный момент — время ответа. Samo.Tourvisor отвечает за 2-8 секунд в зависимости от количества операторов. Пользователь не должен ждать:

  1. Первая загрузка страницы поиска показывает результаты из локального инфоблока (собственные туры) — мгновенно
  2. Параллельно фронтенд отправляет AJAX-запрос на /api/tourvisor/search/
  3. Бэкенд запрашивает Tourvisor API, кеширует результат в Redis с TTL 30 минут
  4. Ответ подгружается на страницу, объединяется с локальными результатами, сортируется по цене

Sletat.ru API — XML/SOAP. Старый протокол, но огромная база туров от сотен операторов. Основной метод — GetTours() с параметрами поиска. Ответ — XML, парсинг через SimpleXMLElement. Время ответа — 5-15 секунд.

Стратегия та же: асинхронная загрузка. Но у Sletat есть особенность — RequestId. Первый запрос возвращает RequestId, по которому нужно опрашивать второй эндпоинт GetSearchResult() каждые 2-3 секунды, пока статус не станет Completed. Это реализуется через polling на фронтенде:

async function pollSletat(requestId) {
    const response = await fetch(`/api/sletat/result/?request_id=${requestId}`);
    const data = await response.json();
    if (data.status === 'completed') {
        renderResults(data.tours);
    } else {
        setTimeout(() => pollSletat(requestId), 3000);
    }
}

Объединение результатов из разных источников. На фронте — единый список с отметкой источника. Каждый результат содержит source (local / tourvisor / sletat), external_id, price, currency. Сортировка по цене требует конвертации валют через курс ЦБ, хранящийся в Highload-блоке CurrencyRates и обновляемый агентом раз в день.

Динамическое ценообразование

Три уровня ценообразования:

  1. Сезонные коэффициенты — высокий сезон ×1.3, низкий ×0.8. Хранятся в Highload-блоке PriceCoefficients с диапазоном дат
  2. Раннее бронирование — скидка 10-20% при бронировании за 60+ дней до вылета. Правило: если DEPARTURE_DATE - TODAY > 60, применить коэффициент 0.85
  3. Горящие туры — скидка 15-40% за 3-7 дней до вылета при незаполненной группе. Коэффициент зависит от процента заполнения: GROUP_FILLED < 50% → 0.6

Расчёт цены в \Project\Tours\PriceCalculator::calculate($tourId, $departureDate):

$basePrice = $tour['BASE_PRICE'];
$coefficients = HighloadBlockTable::getList([
    'filter' => [
        'TOUR_ID' => $tourId,
        '<=DATE_FROM' => $departureDate,
        '>=DATE_TO' => $departureDate,
    ],
    'order' => ['PRIORITY' => 'ASC'],
])->fetchAll();

$finalPrice = $basePrice;
foreach ($coefficients as $c) {
    $finalPrice *= $c['COEFFICIENT'];
}

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

Бронирование и частичная оплата

Поток оформления заказа через модуль sale:

  1. Клиент выбирает тур, дату вылета, количество участников
  2. Формирование заказа: \Bitrix\Sale\Order::create(), корзина с одним товаром (тур), свойства заказа — данные пассажиров (ФИО, паспортные данные, дата рождения)
  3. Частичная оплата — через правило доставки или кастомный обработчик. Первый платёж — 30-50% от стоимости. Второй — за 30 дней до вылета
  4. Реализация частичной оплаты: два платежа в заказе (\Bitrix\Sale\Payment), первый — со статусом «К оплате», второй — «Отложен». Обработчик OnSalePaymentEntitySaved проверяет, оба ли платежа оплачены. Агент за 30 дней до вылета переводит второй платёж в статус «К оплате» и отправляет email с напоминанием

Документооборот: визы и страховки

Раздел «Документы» — отдельный инфоблок или Highload-блок с визовыми требованиями по странам. Поля: COUNTRY_CODE, VISA_TYPE (список: «Не требуется», «По прибытии», «В посольстве», «Электронная»), PROCESSING_DAYS, DOCUMENTS_LIST (текст), NOTES. На детальной странице тура автоматически выводится блок с визовой информацией по стране назначения — подтягивается по UF_COUNTRY_CODE раздела.

Страховка — обязательный допродаж. Реализуется как связанный товар в каталоге или через компонент project:tour.insurance с калькулятором стоимости по длительности и направлению.

Галереи и отзывы

Фотогалерея направления — множественное свойство UF_GALLERY раздела. Lightbox-просмотр через bx.lightbox или кастомный Swiper.js. Видео — свойство VIDEO_URL элемента, встраивается через iframe с lazy loading.

Отзывы — отдельный инфоблок reviews. Свойства: TOUR_ID (привязка), AUTHOR_ID (привязка к пользователю), RATING (число 1-5), TRAVEL_DATE, PHOTOS (множественный файл). Модерация — через workflow: новый отзыв сохраняется со статусом «На модерации», публикуется после проверки в админке. Средний рейтинг тура вычисляется агентом и записывается в свойство AVG_RATING.

B2B-портал для агентов

Отдельная группа пользователей agents с правами доступа через модуль main. Агент видит:

  • Оптовые цены — рассчитываются отдельным типом цены в каталоге (WHOLESALE) или отдельным коэффициентом в Highload-блоке
  • Комиссию по каждому бронированию — свойство заказа AGENT_COMMISSION, вычисляемое процентом от стоимости тура
  • Отчёты по продажам — кастомный компонент с выборкой из b_sale_order по USER_ID агента

Авторизация — стандартная через main.auth, но с редиректом на /agents/ (отдельный раздел с шаблоном agent_cabinet). Регистрация агента — по заявке, подтверждаемой администратором.

SEO: Schema.org и гео-страницы

На детальной странице тура — JSON-LD разметка TouristTrip:

{
  "@context": "https://schema.org",
  "@type": "TouristTrip",
  "name": "Тоскана: винные маршруты",
  "touristType": "Cultural",
  "itinerary": {
    "@type": "ItemList",
    "itemListElement": [...]
  },
  "offers": {
    "@type": "AggregateOffer",
    "lowPrice": "1200",
    "highPrice": "1800",
    "priceCurrency": "USD"
  },
  "subjectOf": {
    "@type": "TravelAction",
    "fromLocation": {"@type": "City", "name": "Москва"},
    "toLocation": {"@type": "City", "name": "Флоренция"}
  }
}

Формируется в result_modifier.php, выводится через $APPLICATION->AddHeadString(). AggregateOffer показывает диапазон цен по всем датам вылета — Google отображает его в сниппете.

Гео-страницы /tours/italy/, /tours/turkey/antalya/ — разделы инфоблока с уникальными UF_SEO_TITLE, UF_SEO_DESCRIPTION, UF_SEO_TEXT. Каждая страница — SEO-оптимизированный каталог с фильтрами, привязанными к данному направлению.

Этапы и сроки

Масштаб проекта Ориентировочные сроки
Витрина собственных туров без онлайн-бронирования 3-5 недель
Каталог с фильтрацией, бронирование, одна интеграция (Tourvisor) 6-10 недель
Полная платформа: несколько API, B2B-портал, динамические цены 10-14 недель

Основное время уходит на поисковый движок и интеграции. Каталог и шаблоны — типовая работа, 2-3 недели. Кастомный фильтр с фасетами — 2 недели. Интеграция с каждым внешним API — 1-2 недели на подключение, плюс неделя на тестирование граничных случаев (таймауты, изменение формата ответа, недоступность сервиса). B2B-портал — 2-3 недели, если нет специфичных требований к отчётности.