Разработка формы заказа обратного звонка 1С-Битрикс
Форма обратного звонка — один из самых высококонверсионных элементов сайта при правильной реализации. Стандартный компонент bitrix:main.feedback технически работает, но в реальных проектах его недостаточно: нет валидации на клиенте, нет защиты от спама, нет интеграции с CRM и телефонией, нет управления рабочим временем операторов. Кастомная разработка решает все эти задачи.
Архитектура компонента
Форма состоит из четырёх уровней:
Клиентский JS
→ Валидация поля телефона (маска + regex)
→ AJAX-запрос на /local/api/callback.php
→ Серверная валидация + антиспам
→ Создание лида в CRM Битрикс
→ (Опционально) Инициация звонка через Asterisk/Манго/Zadarma
→ Email/SMS уведомление менеджеру
Серверный обработчик
// /local/api/callback.php
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit(json_encode(['success' => false, 'error' => 'Method not allowed']));
}
$data = json_decode(file_get_contents('php://input'), true);
// CSRF-проверка
$csrfToken = $data['sessid'] ?? '';
if (!\bitrix_sessid_check($csrfToken)) {
http_response_code(403);
exit(json_encode(['success' => false, 'error' => 'Invalid session']));
}
$phone = preg_replace('/\D/', '', $data['phone'] ?? '');
// Валидация телефона
if (!preg_match('/^[78]\d{10}$/', $phone)) {
exit(json_encode(['success' => false, 'error' => 'Неверный формат телефона']));
}
$phone = '+7' . substr($phone, -10);
// Антиспам: не более 2 заявок с одного IP за час
$limiter = new \Local\Callback\RateLimiter();
if (!$limiter->allow($_SERVER['REMOTE_ADDR'])) {
exit(json_encode(['success' => false, 'error' => 'Слишком много заявок. Попробуйте позже.']));
}
// Создаём лид в CRM
$leadCreator = new \Local\Callback\LeadCreator();
$leadId = $leadCreator->create([
'phone' => $phone,
'name' => htmlspecialchars(mb_substr($data['name'] ?? '', 0, 100)),
'comment' => htmlspecialchars(mb_substr($data['comment'] ?? '', 0, 500)),
'source' => $data['source'] ?? 'callback_form',
'page' => $_SERVER['HTTP_REFERER'] ?? '',
'utm' => $data['utm'] ?? [],
]);
// Инициируем обратный звонок если рабочее время
$scheduler = new \Local\Callback\WorkSchedule();
if ($scheduler->isWorkingNow()) {
(new \Local\Callback\AutoDialer())->initiate($phone, $leadId);
$message = 'Мы перезвоним вам в течение 2 минут';
} else {
$message = 'Мы перезвоним в ближайшее рабочее время: ' . $scheduler->getNextWorkStart();
}
exit(json_encode(['success' => true, 'message' => $message, 'lead_id' => $leadId]));
Создание лида в CRM
namespace Local\Callback;
class LeadCreator
{
public function create(array $data): int
{
$fields = [
'TITLE' => 'Обратный звонок: ' . $data['phone'],
'NAME' => $data['name'] ?: 'Клиент',
'PHONE' => [['VALUE' => $data['phone'], 'VALUE_TYPE' => 'WORK']],
'SOURCE_ID' => 'CALLBACK',
'STATUS_ID' => 'NEW',
'ASSIGNED_BY_ID' => $this->getAvailableManager(),
'COMMENTS' => $this->buildComment($data),
'UF_UTM_SOURCE' => $data['utm']['utm_source'] ?? '',
'UF_UTM_CAMPAIGN'=> $data['utm']['utm_campaign'] ?? '',
'UF_CALLBACK_PAGE' => mb_substr($data['page'] ?? '', 0, 255),
];
$lead = new \CCrmLead(false);
$leadId = $lead->Add($fields, true);
if ($leadId) {
// Добавляем задачу менеджеру: перезвонить
$this->addCallTask($leadId, $data['phone'], $fields['ASSIGNED_BY_ID']);
}
return (int)$leadId;
}
private function addCallTask(int $leadId, string $phone, int $assigneeId): void
{
\CCrmActivity::Add([
'TYPE_ID' => \CCrmActivityType::Call,
'SUBJECT' => 'Перезвонить: ' . $phone,
'OWNER_TYPE_ID' => \CCrmOwnerType::Lead,
'OWNER_ID' => $leadId,
'RESPONSIBLE_ID' => $assigneeId,
'DEADLINE' => (new \Bitrix\Main\Type\DateTime())->add('+1H'),
'COMPLETED' => 'N',
]);
}
private function getAvailableManager(): int
{
// Ротация: выбираем менеджера с наименьшим количеством открытых лидов
$managers = [5, 7, 12, 15]; // ID сотрудников
$counts = [];
foreach ($managers as $id) {
$res = \CCrmLead::GetList(
[], ['ASSIGNED_BY_ID' => $id, 'STATUS_ID' => 'NEW'],
['COUNT' => true]
);
$counts[$id] = (int)$res;
}
asort($counts);
return array_key_first($counts);
}
}
Расписание работы и управление временем
namespace Local\Callback;
class WorkSchedule
{
private array $schedule = [
1 => ['09:00', '19:00'], // Пн
2 => ['09:00', '19:00'], // Вт
3 => ['09:00', '19:00'], // Ср
4 => ['09:00', '19:00'], // Чт
5 => ['09:00', '19:00'], // Пт
6 => ['10:00', '16:00'], // Сб
0 => null, // Вс — выходной
];
public function isWorkingNow(): bool
{
$tz = new \DateTimeZone('Europe/Moscow');
$now = new \DateTime('now', $tz);
$dow = (int)$now->format('w'); // 0=Sun
$hours = $this->schedule[$dow] ?? null;
if (!$hours) return false;
$start = \DateTime::createFromFormat('H:i', $hours[0], $tz);
$end = \DateTime::createFromFormat('H:i', $hours[1], $tz);
return $now >= $start && $now < $end;
}
public function getNextWorkStart(): string
{
$tz = new \DateTimeZone('Europe/Moscow');
$now = new \DateTime('now', $tz);
for ($i = 1; $i <= 7; $i++) {
$next = clone $now;
$next->modify("+{$i} day");
$dow = (int)$next->format('w');
$hours = $this->schedule[$dow] ?? null;
if ($hours) {
$next->setTime(...explode(':', $hours[0]));
return $next->format('d.m в H:i');
}
}
return 'понедельник';
}
}
Rate Limiter через Битрикс-кеш
namespace Local\Callback;
class RateLimiter
{
private const MAX_ATTEMPTS = 2;
private const WINDOW_SECONDS = 3600;
public function allow(string $identifier): bool
{
$key = 'callback_rl_' . md5($identifier);
$cache = \Bitrix\Main\Application::getInstance()->getManagedCache();
$count = (int)$cache->get($key);
if ($count >= self::MAX_ATTEMPTS) {
return false;
}
$cache->set($key, $count + 1, self::WINDOW_SECONDS);
return true;
}
}
Клиентская часть: форма с маской и AJAX
(function () {
const form = document.getElementById('callback-form');
if (!form) return;
const phoneInput = form.querySelector('[name="phone"]');
// Маска ввода телефона
phoneInput.addEventListener('input', function () {
let val = this.value.replace(/\D/g, '');
if (val.startsWith('8') || val.startsWith('7')) val = val.slice(1);
val = val.slice(0, 10);
let formatted = '+7 ';
if (val.length > 0) formatted += '(' + val.slice(0, 3);
if (val.length >= 3) formatted += ') ' + val.slice(3, 6);
if (val.length >= 6) formatted += '-' + val.slice(6, 8);
if (val.length >= 8) formatted += '-' + val.slice(8, 10);
this.value = formatted;
});
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = form.querySelector('[type="submit"]');
submitBtn.disabled = true;
const phone = phoneInput.value.replace(/\D/g, '');
if (phone.length < 11) {
showError('Введите корректный номер телефона');
submitBtn.disabled = false;
return;
}
const payload = {
phone : phone,
name : form.querySelector('[name="name"]')?.value || '',
sessid : BX.bitrix_sessid(),
utm : getUtmParams(),
};
try {
const res = await fetch('/local/api/callback.php', {
method : 'POST',
headers : { 'Content-Type': 'application/json' },
body : JSON.stringify(payload),
});
const data = await res.json();
if (data.success) {
showSuccess(data.message);
form.reset();
} else {
showError(data.error || 'Произошла ошибка');
}
} catch {
showError('Ошибка соединения. Попробуйте ещё раз.');
}
submitBtn.disabled = false;
});
function getUtmParams() {
const params = new URLSearchParams(window.location.search);
return {
utm_source : params.get('utm_source') || getCookie('utm_source') || '',
utm_campaign : params.get('utm_campaign') || getCookie('utm_campaign') || '',
};
}
})();
Состав работ
- Кастомный компонент
local:callback.formс шаблонами (попап, встроенная форма, плавающая кнопка) - Серверный обработчик с CSRF, rate limiting, валидацией
- Создание лида в CRM, задача менеджеру, ротация ответственных
- Расписание рабочего времени с timezone-поддержкой
- JS: маска телефона, AJAX-отправка, передача UTM
- Email/SMS уведомление при поступлении заявки
- (Опционально) Автодозвон через API телефонии
Сроки: базовая форма с CRM-интеграцией — 1–2 недели. Полный функционал с автодозвоном, расписанием и аналитикой — 3–4 недели.







