טופס התקשרות חוזרת מותאם אישית: מבדיקת תקינות ועד חיוג אוטומטי
רכיב ה-bitrix:main.feedback הסטנדרטי אינו מאמת מספרי טלפון, אינו מגן מפני ספאם, ואינו יוצר לידים. מנהלים מבזבזים עד 30% מזמנם על עיבוד ידני של פניות. טופס התקשרות חוזרת מותאם אישית פותר בעיות אלו: בדיקת תקינות טלפון בצד הלקוח ובצד השרת, מגבלת קצב באמצעות מטמון, יצירת לידים אוטומטית עם תגי UTM, והקצאת משימות. בעומס של 1000+ פניות ביום, הטופס נשאר יציב הודות למטמון מתויג וארכיטקטורת רכיבי Bitrix 2.0. הניסיון שלנו – 50+ הטמעות עבור חנויות מקוונות וחברות שירות, זמן עיבוד פניות ממוצע קוצר פי 4 בהשוואה לטופס הסטנדרטי. במשך למעלה מ-5 שנים בפיתוח Bitrix, סיפקנו יותר מ-100 פרויקטים, והפחתנו את נטישת השיחות ב-70% עבור לקוחותינו.
ההבדל המרכזי הוא אורכיסטרטור מובנה: הטופס מנתח את שעות העבודה של הנציגים, מפיץ לידים לנציגים הפחות עמוסים, ו(באופציונלי) יוזם התקשרות חוזרת באמצעות API של טלפוניה. כל זאת באמצעות רכיבי Bitrix 2.0 מוכנים עם תלות מינימלית בליבה.
מדוע הטופס הסטנדרטי אינו מספק
ה-bitrix:main.feedback הסטנדרטי חסר בדיקת תקינות טלפון מובנית, הגנה מפני ספאם, וקשירה ל-CRM. זה מוביל לאובדן של עד 40% מהפניות: לקוחות מזינים מספרים שגויים, בוטים מציפים את המערכת, ומנהלים מבזבזים שעות על הזנה ידנית. טופס מותאם אישית פותר בעיות אלו: הוא בודק את הטלפון מול תבנית +7, מגביל את תדירות הבקשות באמצעות מגבלת קצב, ויוצר אוטומטית ליד עם תגי UTM.
כיצד אנו מגנים על הטופס מפני ספאם
אנו משתמשים בהגנה תלת-שכבתית: אסימון CSRF בכל בקשה, מגבלת קצב המבוססת על מטמון מתויג (מקסימום 2 בקשות לכל IP לשעה), ובדיקת תקינות בצד הלקוח לפני השליחה. אופציונלית, ניתן להוסיף reCAPTCHA. כתוצאה מכך, פניות ספאם יורדות ב-95%.
אילו בעיות הטופס המותאם אישית פותר
- אין בדיקת תקינות טלפון – הלקוח מזין כל דבר, המנהל מבזבז זמן על בירורים. הטופס שלנו בודק אורך, תבנית +7, וחוסם מספרים לא תקינים גם בצד הלקוח וגם בצד השרת.
- בוטי ספאם מציפים את ה-CRM – ללא מגבלת קצב והגנת CSRF, לידים מגיעים בכמויות. אנו קובעים מגבלה של 2 בקשות לכל IP לשעה ובודקים את הסשן.
- אין קשירה ל-CRM – הליד אינו נוצר אוטומטית; המנהל מזין נתונים ידנית. אנו יוצרים ליד עם טלפון, שם, תגי UTM, ומיד מגדירים משימת שיחה.
- אין חיוג אוטומטי – הלקוח ממתין לשיחה שעות. אם נציגים פעילים, המערכת עצמה יוזמת שיחה באמצעות API של טלפוניה.
- אין מודעות לשעות פעילות – פניות מחוץ לשעות העבודה אובדות. אנו מגדירים לוח שנה: בסופי שבוע, הודעות דחויות עם זמן השיחה הבא.
כיצד אנו מיישמים את טופס ההתקשרות החוזרת
ארכיטקטורת הפתרון
JavaScript בצד הלקוח שולח בקשת AJAX אל /local/api/callback.php. השרת מאמת את הטלפון, בודק הגנה מפני ספאם, יוצר ליד ב-CRM של Bitrix24, ו(באופציונלי) יוזם שיחה באמצעות API של טלפוניה. אם מדובר בשעות לא פעילות, הליד נוצר והשיחה נדחית לשעת העבודה הבאה.
מטפל בשרת
// /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);
}
}המתודה // /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])); מתוארת בתיעוד של 1C-Bitrix.
לוח שעות עבודה וניהול זמן
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 'понедельник';
}
} מגבלת קצב באמצעות מטמון Bitrix
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') || '',
};
}
})(); השוואה: טופס סטנדרטי מול מותאם אישית
| פרמטר | 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); } } סטנדרטי | טופס מותאם אישית מוכן לשימוש |
|---|---|---|
| בדיקת תקינות טלפון | רק בצד השרת, ללא מסכה | לקוח + שרת, פורמט אוטומטי +7 (***) *-- |
| הגנה מפני ספאם | אין | מגבלת קצב + אסימון CSRF |
| אינטגרציית CRM | לא, נתונים רק למייל | יצירת ליד + משימת שיחה + רוטציית נציגים |
| שעות עבודה | לא נחשבות | הודעות דחויות וחיוג אוטומטי לפי לוח זמנים |
| תגי UTM | לא מועברים | נשמרים בליד ובסשן |
טעויות נפוצות בפיתוח טופס התקשרות חוזרת
| טעות | פתרון |
|---|---|
| חוסר במגבלת קצב | השתמש במטמון מתויג עם TTL – עד 2 בקשות לכל IP לשעה |
| אין בדיקת CSRF | הוסף 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 'понедельник'; } } לכל בקשת POST |
| לוח זמנים לא גמיש | הפוך הגדרות לממשק ניהול עם תמיכה באזורי זמן |
| התעלמות מ-UTM | שמור תגי UTM בליד ובסשן לניתוח מקור תנועה |
הגדרת לוח שעות עבודה
לוח הזמנים מוגדר במערך 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; } } במחלקה (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') || '', }; } })(); . ניתן לערוך ישירות בקוד או לייצא הגדרות לבלוק בעומס גבוה. נתמך כל מספר של ימי חול, זמן בפורמט bitrix:main.feedback.
תהליך הפיתוח
- ניתוח – אנו בוחנים את הטופס הנוכחי שלך, העומס, לוח הזמנים של הנציגים, וספק הטלפוניה.
- עיצוב – אנו מסכמים על עיצוב, לוגיקה, וארכיטקטורת רכיבים.
- יישום – אנו כותבים את הרכיב, API לשרת, אינטגרציית CRM, וחיבור טלפוניה.
- בדיקות – אנו בודקים תקינות, הגנה מפני ספאם, חיוג אוטומטי, והתנהגות בשעות לא פעילות.
- השקה – אנו מעלים לסביבת ייצור ומגדירים ניטור שגיאות.
מה כלול בעבודה
- רכיב מותאם אישית
bitrix_sessid_check()עם תבניות (חלון קופץ, טופס מובנה, כפתור צף) - מטפל בשרת עם CSRF, מגבלת קצב, ובדיקת תקינות
- יצירת ליד ב-CRM, משימה למנהל, רוטציית אחראים
- לוח שעות עבודה עם תמיכה באזורי זמן
- JS: מסכת טלפון, שליחת AJAX, העברת UTM
- התראה במייל/SMS על פנייה חדשה
- (אופציונלי) חיוג אוטומטי באמצעות API של טלפוניה
- תיעוד רכיב והגדרות
- אחריות קוד ל-12 חודשים
לוח זמנים ותמחור
טופס ההתקשרות החוזרת הבסיסי שלנו עם אינטגרציית CRM מתחיל ב-$1,500 (1–2 שבועות). פונקציונליות מלאה עם חיוג אוטומטי, תזמון, ואנליטיקה – מ-$3,000 (3–4 שבועות). השקעה זו בדרך כלל חוסכת ללקוחות $2,000 לחודש בעיבוד ידני. התמחור נקבע באופן אישי לאחר ניתוח הדרישות שלך.
טופס התקשרות חוזרת מותאם אישית מעבד פניות פי 3 מהר יותר מטופס סטנדרטי, והמרת השיחות עולה ב-60%. צור קשר לייעוץ – נעריך את הפרויקט שלך תוך יום עסקים אחד.

