פיתוח אינטגרציה בין Bitrix24 ל-Kaiten
לעתים קרובות אנו נתקלים במצב הבא: מנהלים עובדים ב-Bitrix24, מפתחים עובדים ב-Kaiten, ומשימות חיות את חייהן. סטטוסים מתפצלים, תגובות משוכפלות, ומועדים אחרונים מוחמצים. התוצאה היא לידים אבודים ופגישות מיותרות. חברת פינטק אחת איבדה עד 15% מהלידים כי מנהלים שכחו לעדכן את הסטטוס ב-Kaiten לאחר סגירת עסקה. לאחר האינטגרציה, זמן העברת המשימות ירד משעתיים ל-2 דקות. הניסיון שלנו מראה שבלי סנכרון בין CRM למערכת ניהול פרויקטים, כל כלי Kanban מאבד את משמעותו.
מדוע סנכרון בין Bitrix24 ל-Kaiten הוא בעיה
Kaiten הוא המקבילה הרוסית של Jira, פופולרי לאחר עזיבת ספקים מערביים. הוא מספק REST API ו-webhooks, אך הגדרת סנכרון דו-כיווני דורשת הבנה מעמיקה של שני המוצרים. טעויות אופייניות:
-
עדכון מחזורי: משימה ששונתה ב-Bitrix24 → עודכנה ב-Kaiten → ה-webhook שלח את השינוי בחזרה, וגרם ללולאה אינסופית. פתרון — נעילה באמצעות
SYNC_LOCKבטבלת המיפוי. - אובדן נתונים בקנה מידה גדול: עם 1000+ משימות ביום, סנכרון ללא תורים מוביל לתנאי מרוץ. אנו משתמשים בנעילות ו-agents.
-
טרמינולוגיה שונה: ב-Kaiten, סטטוסים הם עמודות לוח (
b_option), בעוד ב-Bitrix24 הם STATUS מספרי. המיפוי מאוחסן ב-KaitenApiClientומוגדר לפי לוח ספציפי.
כיצד אנו מיישמים את האינטגרציה
אנו מפתחים את הפתרון על PHP 8.1+ באמצעות Bitrix D7 ORM. הליבה היא המחלקה /api/latest/cards המכילה בקשות HTTP ל-Kaiten (נקודות קצה מרכזיות בטבלה למטה).
| מתודה | URL | מטרה |
|---|---|---|
| GET | /api/latest/cards |
רשימת כרטיסים |
| POST | /api/latest/cards/{id} |
יצירת כרטיס |
| PATCH | /api/latest/boards |
עדכון כרטיס |
| GET | /api/latest/columns |
רשימת לוחות |
| GET | /api/latest/cards/{id}/comments |
עמודות לוח |
| POST | b_local_kaiten_task_map |
הוספת תגובה |
לאחסון קשרים, אנו משתמשים בטבלה משלנו BITRIX_TASK_ID עם שדות KAITEN_CARD_ID, SYNC_LOCK ו-class KaitenApiClient { private string $baseUrl; private string $token; public function __construct(string $baseUrl, string $token) { $this->baseUrl = rtrim($baseUrl, '/'); $this->token = $token; } public function createCard(array $data): array { return $this->request('POST', '/api/latest/cards', $data); } public function updateCard(int $cardId, array $data): array { return $this->request('PATCH', "/api/latest/cards/{$cardId}", $data); } public function addComment(int $cardId, string $text): array { return $this->request('POST', "/api/latest/cards/{$cardId}/comments", ['text' => $text]); } private function request(string $method, string $path, array $data = []): array { $ch = curl_init($this->baseUrl . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $this->token, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => $method !== 'GET' ? json_encode($data) : null, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode >= 400) { throw new \RuntimeException("Kaiten API error {$httpCode}: {$response}"); } return json_decode($response, true) ?? []; } } לנעילה.
class KaitenApiClient { private string $baseUrl; private string $token; public function __construct(string $baseUrl, string $token) { $this->baseUrl = rtrim($baseUrl, '/'); $this->token = $token; } public function createCard(array $data): array { return $this->request('POST', '/api/latest/cards', $data); } public function updateCard(int $cardId, array $data): array { return $this->request('PATCH', "/api/latest/cards/{$cardId}", $data); } public function addComment(int $cardId, string $text): array { return $this->request('POST', "/api/latest/cards/{$cardId}/comments", ['text' => $text]); } private function request(string $method, string $path, array $data = []): array { $ch = curl_init($this->baseUrl . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $this->token, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => $method !== 'GET' ? json_encode($data) : null, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode >= 400) { throw new \RuntimeException("Kaiten API error {$httpCode}: {$response}"); } return json_decode($response, true) ?? []; } } מה כלול בעבודה
- ניתוח ועיצוב: תיאור תרחישי סנכרון (משימות, סטטוסים, תגובות, קבצים), הסכמה על מיפוי עמודות.
- פיתוח רכיבים: לקוח Kaiten API, מטפלי אירועים של Bitrix24 (
OnTaskUpdate,OnCrmDealUpdate), מטפל webhook. - בדיקות: אימות על מעגל בדיקה עם עומס מדומה של עד 500 משימות בשעה.
- פריסה והדרכה: הגדרה בסביבת production, מסמכי תיעוד לנקודות קצה API והוראות למנהלים.
- תמיכה באחריות: חודש אחד לאחר המסירה — תיקון באגים ללא עלות. ניתן אישור תאימות ורישיון לפתרון.
סנכרון דו-כיווני: כרטיסים ו-webhook
כאשר משימה משתנה ב-Bitrix24 (אירוע OnTaskUpdate), מטפל מעדכן את הכרטיס ב-Kaiten באמצעות PATCH /api/latest/cards/{id}. כדי למנוע לולאות, אנו משתמשים ב-SYNC_LOCK:
public static function onTaskUpdate(int $taskId, array $arFields): void { $map = KaitenTaskMapTable::getByBitrixId($taskId); if (!$map || $map['SYNC_LOCK'] !== null) { return; } KaitenTaskMapTable::update($map['ID'], ['SYNC_LOCK' => uniqid()]); try { $client = self::getClient(); $kaitenData = []; if (isset($arFields['TITLE'])) { $kaitenData['title'] = $arFields['TITLE']; } if (isset($arFields['DESCRIPTION'])) { $kaitenData['description'] = strip_tags($arFields['DESCRIPTION']); } if (isset($arFields['DEADLINE'])) { $kaitenData['due_date'] = date('Y-m-d', strtotime($arFields['DEADLINE'])); } if (isset($arFields['STATUS'])) { $columnId = StatusMapper::b24StatusToKaitenColumn($arFields['STATUS']); $kaitenData['column_id'] = $columnId; } if (!empty($kaitenData)) { $client->updateCard($map['KAITEN_CARD_ID'], $kaitenData); } KaitenTaskMapTable::update($map['ID'], [ 'SYNC_LOCK' => null, 'LAST_SYNC_AT' => new \Bitrix\Main\Type\DateTime(), ]); } catch (\Exception $e) { KaitenTaskMapTable::update($map['ID'], ['SYNC_LOCK' => null]); \Bitrix\Main\Diag\Debug::addToLog('KaitenSync: ' . $e->getMessage()); } } ה-webhook מ-Kaiten ל-Bitrix24 מקבל JSON עם אירוע public static function onTaskUpdate(int $taskId, array $arFields): void { $map = KaitenTaskMapTable::getByBitrixId($taskId); if (!$map || $map['SYNC_LOCK'] !== null) { return; } KaitenTaskMapTable::update($map['ID'], ['SYNC_LOCK' => uniqid()]); try { $client = self::getClient(); $kaitenData = []; if (isset($arFields['TITLE'])) { $kaitenData['title'] = $arFields['TITLE']; } if (isset($arFields['DESCRIPTION'])) { $kaitenData['description'] = strip_tags($arFields['DESCRIPTION']); } if (isset($arFields['DEADLINE'])) { $kaitenData['due_date'] = date('Y-m-d', strtotime($arFields['DEADLINE'])); } if (isset($arFields['STATUS'])) { $columnId = StatusMapper::b24StatusToKaitenColumn($arFields['STATUS']); $kaitenData['column_id'] = $columnId; } if (!empty($kaitenData)) { $client->updateCard($map['KAITEN_CARD_ID'], $kaitenData); } KaitenTaskMapTable::update($map['ID'], [ 'SYNC_LOCK' => null, 'LAST_SYNC_AT' => new \Bitrix\Main\Type\DateTime(), ]); } catch (\Exception $e) { KaitenTaskMapTable::update($map['ID'], ['SYNC_LOCK' => null]); \Bitrix\Main\Diag\Debug::addToLog('KaitenSync: ' . $e->getMessage()); } } :
{ "event": "card.updated", "data": { "id": 12345, "title": "Новое название задачи", "column_id": 678, "assignees": [{"id": 99, "username": "[email protected]"}] } } המטפל בודק את חתימת ה-HMAC ואם הנעילה לא פעילה, מעדכן את סטטוס המשימה ב-Bitrix24 באמצעות card.updated.
מיפוי עמודות ויצירה אוטומטית מ-CRM
דוגמה למיפוי עבור לוח "פיתוח":
| Kaiten column_id | שם עמודה | Bitrix24 STATUS |
|---|---|---|
| 101 | Backlog | 1 (חדש) |
| 102 | בתהליך | 3 (בתהליך) |
| 103 | בבדיקה | 4 (ממתין לאישור) |
| 104 | הושלם | 5 (הושלם) |
מקרה שימושי הוא יצירה אוטומטית של כרטיס Kaiten כאשר עסקת CRM עוברת לשלב "פיתוח". מיושם באמצעות { "event": "card.updated", "data": { "id": 12345, "title": "Новое название задачи", "column_id": 678, "assignees": [{"id": 99, "username": "[email protected]"}] } } :
if ($arFields['STAGE_ID'] === 'DEVELOPMENT') { $client = KaitenApiClientFactory::create(); $card = $client->createCard([ 'title' => 'Deal #' . $dealId . ': ' . $arFields['TITLE'], 'board_id' => KAITEN_DEV_BOARD_ID, 'column_id' => KAITEN_BACKLOG_COLUMN_ID, 'description' => $arFields['COMMENTS'], ]); \CCrmDeal::Update($dealId, ['UF_KAITEN_CARD_ID' => $card['id']], false); } הגישה שלנו חוסכת עד פי 2 מהזמן בהשוואה לסנכרון ידני. חברות עם ניסיון באינטגרציה מאשרות: אוטומציה מבטלת שגיאות אנוש ומאיצה העברת משימות בין מחלקות.
לוח זמנים לפיתוח
| אפשרות | היקף | לוח זמנים |
|---|---|---|
| חד-כיווני (Bitrix24 → Kaiten) | יצירה ועדכון כרטיסים | 3–5 ימים |
| סנכרון דו-כיווני | סטטוסים, תגובות, מיפוי משתמשים | 7–10 ימים |
| עם יצירה אוטומטית מ-CRM | משפך → יצירת כרטיסים, דוחות | 10–14 ימים |
המחיר מחושב באופן אישי. צרו קשר — נבחן את הפרויקט שלכם בחינם. קבלו ייעוץ על ארכיטקטורת אינטגרציה ולוח זמנים ליישום.
מעל 5 שנות ניסיון בפיתוח אינטגרציות ל-Bitrix24. פתרונות מוסמכים, אחריות לאיכות.
כיצד לוודא נכונות סנכרון?
אנו ממליצים להגדיר ניטור: לתעד כל קריאת API ולבדוק שגיאות. השתמשו ב-agent שמשווה את מספר המשימות בשתי המערכות פעם בשעה. אם נמצא פער, המנהל מקבל הודעה. זה עוזר לזהות תקלות במהירות ולשמור על דיוק נתונים.
מה האוטומציה מביאה במקום העברה ידנית?
העתקת משימות ידנית אורכת בממוצע 10–15 דקות למשימה. עם 20 משימות ביום, מדובר ב-3–4 שעות עבודה. אוטומציה מצמצמת זאת לשניות ומבטלת שגיאות הקלדה והשמטות. יתרה מכך, סטטוסים ב-Kaiten משתנים אוטומטית כאשר עסקה משתנה — המנהל לא צריך להיכנס לממשק נוסף.







