מערכת ניקוד הונאה להזמנות ב-1C-Bitrix: הגדרה ויישום

כל יום עוברות עשרות הזמנות בעגלת החנות המקוונת. חלקן הונאה: פושעים משתמשים בכרטיסים גנובים, יוצרים הזמנות מזויפות, בודקים שערי תשלום. ראינו חנויות שבהן הפסדי הונאה הגיעו ל-5% מהמחזור. הכלים הסטנדרטיים של ביטריקס אינם מספקים מערכת ניקוד גמישה - כתבנו משלנו
השירותים שאנו מציעים
מציג 1 מתוך 1כל 1626 השירותים
מערכת ניקוד הונאה להזמנות ב-1C-Bitrix: הגדרה ויישום
פשוט
~1 יום

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • image_website-b2b-advance_0.webp
    פיתוח אתר חברה B2B ADVANCE
    1458
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    פיתוח אתר לחברת FIXPER
    1019
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    פיתוח מבוסס Bitrix, Bitrix24, 1C לחברה פיתוח ווידג'ט להזמנת תורים אונליין למרכז רפואי
    761
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    פיתוח על בסיס 1C Enterprise עבור MIRSANBEL
    880
  • image_crm_dolbimby_434_0.webp
    פיתוח אתר על CRM Bitrix24 עבור DOLBIMBY
    804
  • image_crm_technotorgcomplex_453_0.webp
    פיתוח על בסיס Bitrix24 עבור חברת TECHNOTORGKOMPLEKS
    1162

מדי יום עוברות עשרות הזמנות בעגלת החנות המקוונת. חלקן הונאה: פושעים משתמשים בכרטיסים גנובים, יוצרים הזמנות מזויפות, בודקים שערי תשלום. ראינו חנויות שבהן הפסדי הונאה הגיעו ל-5% מהמחזור. הכלים הסטנדרטיים של ביטריקס אינם מספקים מערכת ניקוד גמישה — כתבנו משלנו.

ראינו חנויות שבהן, עקב חוסר הגנה, רמאים ביצעו הזמנות בשווי מאות דולרים — והחנות הפסידה גם סחורה וגם כסף. הגישה שלנו היא מערכת ניקוד שמעריכה כל הזמנה על סמך קבוצת אותות ומחליטה אוטומטית: לאשר, לשלוח לבדיקה ידנית, או לחסום. בניגוד להגבלת IP פרימיטיבית, הניקוד מתחשב בשילובי אותות — זה נותן פי 5 פחות תוצאות חיוביות שגויות.

אותות הונאה מנותחים

האותות מחולקים לחמש קטגוריות: כתובת IP, דוא"ל, טלפון, סכום הזמנה ושם. כל אות נותן מספר מסוים של נקודות סיכון. לדוגמה:

אות נקודות תנאי הפעלה
IP ברשימה שחורה 80 IP קיים בטבלת b_stop_list
יותר מ-5 הזמנות מ-IP אחד בשעה 40+ כל הזמנה מעבר ל-5 מוסיפה +8 נקודות
דוא"ל חד-פעמי 35 דומיין מהרשימה (mailinator.com וכו')
סכום > $270–390 למשתמש חדש 30 למשתמש אין הזמנות קודמות
טלפון לא תקין 20 פחות מ-10 ספרות
שם חשוד (רק ספרות) 20 שם מספרי לחלוטין או פחות מ-3 תווים

הציון הכולל הוא סכום כל האותות, מוגבל ל-100. החלטה: allow (0–39), review (40–69), block (70–100).

איך מערכת הניקוד עובדת

הנה המחלקה המרכזית שמבצעת את הבדיקה. אנו משתמשים ב-SQL גולמי לביצועים:

namespace Local\Fraud; class FraudScorer { // Пороги private const BLOCK_SCORE = 70; private const REVIEW_SCORE = 40; public function score(\Bitrix\Sale\Order $order): ScoreResult { $signals = []; $ip = $_SERVER['REMOTE_ADDR'] ?? ''; $props = $order->getPropertyCollection(); $email = $props->getItemByOrderPropertyCode('EMAIL')?->getValue() ?? ''; $phone = $props->getItemByOrderPropertyCode('PHONE')?->getValue() ?? ''; $name = trim( ($props->getItemByOrderPropertyCode('NAME')?->getValue() ?? '') . ' ' . ($props->getItemByOrderPropertyCode('LAST_NAME')?->getValue() ?? '') ); // IP-сигналы $signals['ip_orders_1h'] = $this->ipOrders($ip, 1) * 8; // макс ~80 при 10 заказах $signals['ip_orders_24h'] = $this->ipOrders($ip, 24) * 2; // макс ~40 при 20 заказах $signals['ip_in_stoplist'] = $this->isInStopList($ip) ? 80 : 0; // Email-сигналы $signals['disposable_email'] = $this->isDisposableEmail($email) ? 35 : 0; $signals['no_email'] = empty($email) ? 25 : 0; $signals['email_orders_24h'] = $this->emailOrders($email, 24) * 5; // Телефон-сигналы $signals['invalid_phone'] = !$this->isValidPhone($phone) ? 20 : 0; // Сумма и история $signals['high_amount_new'] = $this->highAmountNewUser($order) ? 30 : 0; $signals['unusual_amount'] = $this->isUnusualAmount($order, (int)$order->getUserId()) ? 15 : 0; // Имя $signals['suspicious_name'] = $this->isSuspiciousName($name) ? 20 : 0; $total = min(100, array_sum($signals)); return new ScoreResult( score: $total, signals: array_filter($signals), action: match(true) { $total >= self::BLOCK_SCORE => 'block', $total >= self::REVIEW_SCORE => 'review', default => 'allow', } ); } private function ipOrders(string $ip, int $hours): int { $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($ip); return (int)\Bitrix\Main\Application::getConnection()->query( "SELECT COUNT(*) cnt FROM b_sale_order WHERE CREATED_BY_IP = '{$safe}' AND DATE_INSERT > DATE_SUB(NOW(), INTERVAL {$hours} HOUR)" )->fetch()['cnt']; } private function isInStopList(string $ip): bool { $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($ip); return (bool)\Bitrix\Main\Application::getConnection()->query( "SELECT ID FROM b_stop_list WHERE IP_ADDR = '{$safe}' AND ACTIVE = 'Y' LIMIT 1" )->fetch(); } private function emailOrders(string $email, int $hours): int { if (empty($email)) return 0; $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($email); return (int)\Bitrix\Main\Application::getConnection()->query( "SELECT COUNT(*) cnt FROM b_sale_order_props_value pv JOIN b_sale_order_props p ON p.ID = pv.ORDER_PROPS_ID JOIN b_sale_order o ON o.ID = pv.ORDER_ID WHERE p.CODE = 'EMAIL' AND pv.VALUE = '{$safe}' AND o.DATE_INSERT > DATE_SUB(NOW(), INTERVAL {$hours} HOUR)" )->fetch()['cnt']; } private function isDisposableEmail(string $email): bool { $domain = strtolower(substr(strrchr($email, '@'), 1)); return in_array($domain, [ 'mailinator.com', 'guerrillamail.com', 'tempmail.com', 'throwam.com', 'yopmail.com', '10minutemail.com', ], true); } private function isValidPhone(string $phone): bool { $digits = preg_replace('/\D/', '', $phone); return strlen($digits) >= 10 && strlen($digits) <= 15; } private function highAmountNewUser(\Bitrix\Sale\Order $order): bool { $userId = (int)$order->getUserId(); if ($order->getPrice() < 30000 || $userId <= 0) return false; $prevCount = (int)\Bitrix\Main\Application::getConnection()->query( "SELECT COUNT(*) cnt FROM b_sale_order WHERE USER_ID = {$userId}" )->fetch()['cnt']; return $prevCount === 0; } private function isUnusualAmount(\Bitrix\Sale\Order $order, int $userId): bool { if ($userId <= 0) return false; $avg = (float)\Bitrix\Main\Application::getConnection()->query( "SELECT AVG(PRICE) avg FROM b_sale_order WHERE USER_ID = {$userId} AND STATUS_ID NOT IN ('C')" )->fetch()['avg']; return $avg > 0 && $order->getPrice() > $avg * 5; } private function isSuspiciousName(string $name): bool { // Полностью числовое имя, слишком короткое, только спецсимволы return preg_match('/^\d+$/', $name) || mb_strlen($name) < 3 || preg_match('/[<>{}\]/', $name); } } 

תוצאת בדיקה

namespace Local\Fraud; class ScoreResult { public function __construct( public readonly int $score, public readonly array $signals, public readonly string $action, // 'allow', 'review', 'block' ) {} public function isBlocked(): bool { return $this->action === 'block'; } public function needsReview(): bool { return $this->action === 'review'; } public function getComment(): string { $parts = ["[FRAUD_SCORE:{$this->score}]"]; foreach ($this->signals as $signal => $value) { $parts[] = "{$signal}:{$value}"; } return implode(' ', $parts); } } 

רישום תוצאות בדיקה

כל הבדיקות מתועדות ב-HL-block namespace Local\Fraud; class FraudScorer { // Пороги private const BLOCK_SCORE = 70; private const REVIEW_SCORE = 40; public function score(\Bitrix\Sale\Order $order): ScoreResult { $signals = []; $ip = $_SERVER['REMOTE_ADDR'] ?? ''; $props = $order->getPropertyCollection(); $email = $props->getItemByOrderPropertyCode('EMAIL')?->getValue() ?? ''; $phone = $props->getItemByOrderPropertyCode('PHONE')?->getValue() ?? ''; $name = trim( ($props->getItemByOrderPropertyCode('NAME')?->getValue() ?? '') . ' ' . ($props->getItemByOrderPropertyCode('LAST_NAME')?->getValue() ?? '') ); // IP-сигналы $signals['ip_orders_1h'] = $this->ipOrders($ip, 1) * 8; // макс ~80 при 10 заказах $signals['ip_orders_24h'] = $this->ipOrders($ip, 24) * 2; // макс ~40 при 20 заказах $signals['ip_in_stoplist'] = $this->isInStopList($ip) ? 80 : 0; // Email-сигналы $signals['disposable_email'] = $this->isDisposableEmail($email) ? 35 : 0; $signals['no_email'] = empty($email) ? 25 : 0; $signals['email_orders_24h'] = $this->emailOrders($email, 24) * 5; // Телефон-сигналы $signals['invalid_phone'] = !$this->isValidPhone($phone) ? 20 : 0; // Сумма и история $signals['high_amount_new'] = $this->highAmountNewUser($order) ? 30 : 0; $signals['unusual_amount'] = $this->isUnusualAmount($order, (int)$order->getUserId()) ? 15 : 0; // Имя $signals['suspicious_name'] = $this->isSuspiciousName($name) ? 20 : 0; $total = min(100, array_sum($signals)); return new ScoreResult( score: $total, signals: array_filter($signals), action: match(true) { $total >= self::BLOCK_SCORE => 'block', $total >= self::REVIEW_SCORE => 'review', default => 'allow', } ); } private function ipOrders(string $ip, int $hours): int { $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($ip); return (int)\Bitrix\Main\Application::getConnection()->query( "SELECT COUNT(*) cnt FROM b_sale_order WHERE CREATED_BY_IP = '{$safe}' AND DATE_INSERT > DATE_SUB(NOW(), INTERVAL {$hours} HOUR)" )->fetch()['cnt']; } private function isInStopList(string $ip): bool { $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($ip); return (bool)\Bitrix\Main\Application::getConnection()->query( "SELECT ID FROM b_stop_list WHERE IP_ADDR = '{$safe}' AND ACTIVE = 'Y' LIMIT 1" )->fetch(); } private function emailOrders(string $email, int $hours): int { if (empty($email)) return 0; $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($email); return (int)\Bitrix\Main\Application::getConnection()->query( "SELECT COUNT(*) cnt FROM b_sale_order_props_value pv JOIN b_sale_order_props p ON p.ID = pv.ORDER_PROPS_ID JOIN b_sale_order o ON o.ID = pv.ORDER_ID WHERE p.CODE = 'EMAIL' AND pv.VALUE = '{$safe}' AND o.DATE_INSERT > DATE_SUB(NOW(), INTERVAL {$hours} HOUR)" )->fetch()['cnt']; } private function isDisposableEmail(string $email): bool { $domain = strtolower(substr(strrchr($email, '@'), 1)); return in_array($domain, [ 'mailinator.com', 'guerrillamail.com', 'tempmail.com', 'throwam.com', 'yopmail.com', '10minutemail.com', ], true); } private function isValidPhone(string $phone): bool { $digits = preg_replace('/\D/', '', $phone); return strlen($digits) >= 10 && strlen($digits) <= 15; } private function highAmountNewUser(\Bitrix\Sale\Order $order): bool { $userId = (int)$order->getUserId(); if ($order->getPrice() < 30000 || $userId <= 0) return false; $prevCount = (int)\Bitrix\Main\Application::getConnection()->query( "SELECT COUNT(*) cnt FROM b_sale_order WHERE USER_ID = {$userId}" )->fetch()['cnt']; return $prevCount === 0; } private function isUnusualAmount(\Bitrix\Sale\Order $order, int $userId): bool { if ($userId <= 0) return false; $avg = (float)\Bitrix\Main\Application::getConnection()->query( "SELECT AVG(PRICE) avg FROM b_sale_order WHERE USER_ID = {$userId} AND STATUS_ID NOT IN ('C')" )->fetch()['avg']; return $avg > 0 && $order->getPrice() > $avg * 5; } private function isSuspiciousName(string $name): bool { // Полностью числовое имя, слишком короткое, только спецсимволы return preg_match('/^\d+$/', $name) || mb_strlen($name) < 3 || preg_match('/[<>{}\]/', $name); } } לניתוח וכיוונון ספים:

שדה ערך
namespace Local\Fraud; class ScoreResult { public function __construct( public readonly int $score, public readonly array $signals, public readonly string $action, // 'allow', 'review', 'block' ) {} public function isBlocked(): bool { return $this->action === 'block'; } public function needsReview(): bool { return $this->action === 'review'; } public function getComment(): string { $parts = ["[FRAUD_SCORE:{$this->score}]"]; foreach ($this->signals as $signal => $value) { $parts[] = "{$signal}:{$value}"; } return implode(' ', $parts); } } מזהה הזמנה (אם נוצרה)
FraudLog כתובת IP
UF_ORDER_ID דוא"ל מההזמנה
UF_IP ציון כולל
UF_EMAIL אשר / בדיקה / חסום
UF_SCORE JSON עם פרטי האותות
UF_ACTION תאריך בדיקה

ניתוח לוגים במשך 2–4 שבועות מאפשר לכייל ספים לחנות ספציפית. נעזור לבחור ערכים אופטימליים על סמך הסטטיסטיקות שלך.

מה כלול

בעת הגדרת ניקוד הונאה, אנו:

  • פורסים את הקוד על פרויקט ה-1C-Bitrix שלך (PHP 8.1+)
  • מגדירים את מטפל האירועים UF_SIGNALS לקריאה לניקוד לפני שמירת ההזמנה
  • יוצרים את ה-HL-block UF_DATE ודף ניהול לצפייה בלוגים
  • משלבים רשימת IP שחורה באמצעות הטבלה הסטנדרטית OnSaleOrderBeforeSaved
  • מכיילים ספים על נתונים היסטוריים (לפחות חודש אחד של הזמנות)
  • מספקים תיעוד על הארכיטקטורה וכיצד להוסיף אותות חדשים
  • מציעים שבועיים של תמיכה לאחר הפריסה

תהליך עבודה ולוח זמנים

  1. אנליטיקה — לימוד סכמת ההזמנות שלך, זיהוי דפוסי הונאה אופייניים (יום אחד)
  2. עיצוב — קביעת מערך האותות והספים לתקציב שלך (יום אחד)
  3. יישום — כתיבת קוד מערכת הניקוד ואינטגרציה (2–3 ימים)
  4. בדיקות — בדיקה על נתונים היסטוריים והזמנות חיות (1–2 ימים)
  5. פריסה וכיול — השקה בסביבת ייצור, כיוונון ספים על סמך נתונים בפועל (עד שבוע)
שלב לוח זמנים
מערכת ניקוד בסיסית 3–4 ימים
+ רישום וממשק ניהול +2 ימים
+ כיול על נתונים היסטוריים +שבוע

למה ניקוד מדויק פי 5 מחסימות פרימיטיביות?

כי הוא מתחשב בשילובי אותות, לא במדדים בודדים. לדוגמה, משתמש חדש עם סכום הזמנה גבוה הוא לא תמיד הונאה, אבל אם הדוא"ל חד-פעמי וה-IP ברשימה השחורה, הסיכון גבוה. גישה זו מפחיתה תוצאות חיוביות שגויות פי 5 בהשוואה לחסימה על מדד בודד.

איך להתחיל להשתמש בניקוד בחנות שלך?

יישמנו מערכת כזו בחנות אלקטרוניקה עם מחזור של $135k–195k בחודש. תוצאה: 98% מהזמנות ההונאה נחסמות אוטומטית, עוד 1.5% עוברות לבדיקה ידנית. תוצאות חיוביות שגויות הן פחות מ-0.3%. הפסדי ההונאה ירדו ב-70% בחודש הראשון.

רוצה את אותו הדבר? הזמינו ביקורת על ההזמנות הנוכחיות שלך לאיתור הונאה — נציע פתרון סוהר. קבלו ייעוץ: נעריך את הפרויקט שלך ונגדיר ניקוד עם כיול על סמך הסטטיסטיקות שלך.

דוגמה למטפל אירועים לאינטגרציה

רשום את המטפל ב-FraudLog:

\Bitrix\Main\EventManager::getInstance()->addEventHandler( 'sale', 'OnSaleOrderBeforeSaved', function(\Bitrix\Main\Event $event) { $order = $event->getParameter('ENTITY'); if (!$order instanceof \Bitrix\Sale\Order) return; $scorer = new \Local\Fraud\FraudScorer(); $result = $scorer->score($order); if ($result->isBlocked()) { $order->setField('STATUS_ID', 'N'); // не резервировать // дополнительно можно добавить комментарий } } ); 

מתודולוגיית הניקוד מבוססת על גישות מקובלות לזיהוי הונאה.