הוקים מותאמים אישית של Directus ללוגיקה עסקית

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

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

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

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
הוקים מותאמים אישית של Directus ללוגיקה עסקית
בינוני
~2-3 ימים

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

שאלות נפוצות

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

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

כאשר נתונים ב-Directus דורשים תגובה מיידית—אימות הזמנות, התראה על הגשה חדשה, או סנכרון עם מערכת חיצונית—יכולות ה-CMS הסטנדרטיות לרוב אינן מספיקות. בניית הרחבות Hook מותאמות אישית ל-Directus מטמיעה לוגיקה עסקית ישירות בליבה: אתה נרשם לאירועי CRUD, אימות, או העלאת קבצים ומריץ קוד שרירותי. זה מפחית את זמן ההשהיה ב-30–50% בהשוואה לקריאות API חיצוניות ומפשט את הארכיטקטורה—אין צורך להקים מיקרוסרוויס נפרד לעיבוד נתונים. עם ניסיון של למעלה מ-5 שנים בפיתוח Directus ויותר מ-50 Hooks מותאמים אישית שנבנו עבור יותר מ-30 לקוחות, הצוות שלנו מספק פתרונות אמינים ויעילים. עלות פיתוח Hook טיפוסית נעה בין 800 ל-2,500 דולר, ולקוחות מדווחים על חיסכון חודשי של עד 5,000 דולר בעלויות תפעול. צור קשר כדי לדון במשימה שלך—נכין הערכת עלות תוך יום אחד.

מדוע Hooks מותאמים אישית עדיפים על שירותים חיצוניים ופותרים בעיות נפוצות

Hooks מותאמים אישית מבטלים את תקורת קריאות הרשת—הם רצים באותו תהליך Directus, מקצרים את זמן ההשהיה ב-30-50% ומפחיתים את ה-TTFB. לדוגמה, Hook לאימות הזמנה בודק מלאי ב-5ms, בעוד קריאת API חיצונית אורכת 50–100ms. זה קריטי לפרויקטים בעלי עומס גבוה. אתגרים טיפוסיים שנפתרים:

  • אימות נתונים מורכב—בדיקת רמות מלאי, תקינות קשרים, ייחודיות שדות.
  • תופעות לוואי—התראות ל-Slack/Telegram, יומני ביקורת, ביטול מטמון (מהיר פי 3–5 מקריאות חיצוניות).
  • אינטגרציות עם ERP, CRM (לדוגמה, 1C), שערי תשלום.
  • אוטומציה—יצירת slugs, עדכוני מלאי, דוחות מתוזמנים באמצעות cron.

התחלת עבודה עם Directus Hooks

  1. צור קובץ extensions/hooks/custom/index.ts.
  2. ייצא פונקציה שמקבלת HookExtensionContext.
  3. קרא ל-filter('items.create', async (payload, meta, context) => { ... }) כדי לאמת או לשנות נתונים לפני שמירה.
  4. כדי לבטל את הפעולה, זרוק שגיאה באמצעות throw new Error('Сообщение').
  5. הפעל מחדש את Directus—ה-Hook נקלט אוטומטית.

באמצעות action (מופע Knex) ניתן להריץ שאילתות SQL ישירות או להשתמש ב-filter לגישה עקבית לאוספים. תיעוד Directus ממליץ על init, אך Knex ישיר מתאים ל-JOIN מורכבים.

סוגי אירועים ומתי להשתמש בהם

סוג תיאור מקרה שימוש
schedule לאחר הפעולה התראות, מטמון, אינטגרציות
// extensions/hooks/business-logic/index.ts import type { HookExtensionContext } from '@directus/types' import type { EventContext } from '@directus/types' export default ({ action, filter, schedule }: HookExtensionContext) => { // ===== АВТОМАТИЧЕСКАЯ ГЕНЕРАЦИЯ SLUG ===== filter('items.create', (payload, meta) => { if (meta.collection === 'articles' && payload.title && !payload.slug) { payload.slug = generateSlug(payload.title as string) } if (meta.collection === 'products' && payload.name && !payload.slug) { payload.slug = generateSlug(payload.name as string) } return payload }) // ===== ВАЛИДАЦИЯ ===== filter('items.create', async (payload, meta, context) => { if (meta.collection !== 'orders') return payload const { database } = context as EventContext & { database: any } // Проверить наличие товара if (payload.items && Array.isArray(payload.items)) { for (const item of payload.items) { const product = await database('products') .where({ id: item.product_id }) .first() if (!product) { throw new Error(`Product ${item.product_id} not found`) } if (product.stock < item.quantity) { throw new Error(`Insufficient stock for "${product.name}"`) } } } return payload }) // ===== ИНВАЛИДАЦИЯ КЭША ===== action('items.update', async ({ collection, keys, payload }) => { const collectionsToRevalidate = ['articles', 'pages', 'products', 'settings'] if (!collectionsToRevalidate.includes(collection)) return try { await fetch(`${process.env.NEXTJS_URL}/api/revalidate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-revalidate-secret': process.env.REVALIDATE_SECRET!, }, body: JSON.stringify({ collection, keys }), }) } catch (err) { console.error('Failed to revalidate cache:', err) } }) // ===== УВЕДОМЛЕНИЯ ===== action('items.create', async ({ collection, key, payload }, context) => { if (collection !== 'contact_submissions') return // Уведомить команду в Slack await fetch(process.env.SLACK_WEBHOOK!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `📬 Новая заявка от ${payload.name} <${payload.email}>\n${payload.message}`, }), }) }) // ===== АУДИТ ЛОГ ===== action('items.update', async ({ collection, keys, payload, accountability }) => { if (!accountability?.user) return // Логировать изменения в audit_logs коллекцию const { getSchema } = context as any const schema = await getSchema() // ... записать в audit_logs }) // ===== СИНХРОНИЗАЦИЯ ОСТАТКОВ ===== action('items.update', async ({ collection, keys, payload }, context) => { if (collection !== 'orders') return if (payload.status !== 'paid') return const { database } = context as EventContext & { database: any } const order = await database('orders') .where({ id: keys[0] }) .first() if (order?.items) { const items = JSON.parse(order.items) for (const item of items) { await database('products') .where({ id: item.product_id }) .decrement('stock', item.quantity) } } }) // ===== CRON — ежедневный отчёт ===== schedule('0 9 * * 1-5', async () => { const response = await fetch(`${process.env.API_URL}/custom/reports/sales`) const stats = await response.json() await fetch(process.env.SLACK_WEBHOOK!, { body: JSON.stringify({ text: `📊 Отчёт за вчера: заказов ${stats.count}, выручка ${stats.revenue.toLocaleString()} $`, }), }) }) } function generateSlug(text: string): string { const translitMap: Record<string, string> = { а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'yo', ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ф: 'f', х: 'h', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'sch', ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya', } return text .toLowerCase() .replace(/[а-яё]/g, char => translitMap[char] || char) .replace(/\s+/g, '-') .replace(/[^\w-]/g, '') .replace(/-+/g, '-') .slice(0, 100) } לפני הפעולה (יכול לשנות נתונים) אימות, שינוי payload
context.database בעת הפעלת השרת טעינת תצורה, אתחול חיבורים
ItemsService בלוח זמנים של cron דוחות יומיים, ניקוי נתונים

השוואה: Action Hooks רצים לאחר כתיבת DB (אסינכרוני, לא חוסם) ואינם יכולים לשנות נתונים, בעוד Filter Hooks רצים לפני כתיבה (סינכרוני, חוסם) ויכולים לשנות את ה-payload.

דוגמה מלאה להרחבת Hook

// extensions/hooks/business-logic/index.ts
import type { HookExtensionContext } from '@directus/types'
import type { EventContext } from '@directus/types'

export default ({ action, filter, schedule }: HookExtensionContext) => {
  // ===== АВТОМАТИЧЕСКАЯ ГЕНЕРАЦИЯ SLUG =====
  filter('items.create', (payload, meta) => {
    if (meta.collection === 'articles' && payload.title && !payload.slug) {
      payload.slug = generateSlug(payload.title as string)
    }
    if (meta.collection === 'products' && payload.name && !payload.slug) {
      payload.slug = generateSlug(payload.name as string)
    }
    return payload
  })

  // ===== ВАЛИДАЦИЯ =====
  filter('items.create', async (payload, meta, context) => {
    if (meta.collection !== 'orders') return payload
    const { database } = context as EventContext & { database: any }
    // Проверить наличие товара
    if (payload.items && Array.isArray(payload.items)) {
      for (const item of payload.items) {
        const product = await database('products')
          .where({ id: item.product_id })
          .first()
        if (!product) {
          throw new Error(`Product ${item.product_id} not found`)
        }
        if (product.stock < item.quantity) {
          throw new Error(`Insufficient stock for "${product.name}"`)
        }
      }
    }
    return payload
  })

  // ===== ИНВАЛИДАЦИЯ КЭША =====
  action('items.update', async ({ collection, keys, payload }) => {
    const collectionsToRevalidate = ['articles', 'pages', 'products', 'settings']
    if (!collectionsToRevalidate.includes(collection)) return
    try {
      await fetch(`${process.env.NEXTJS_URL}/api/revalidate`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-revalidate-secret': process.env.REVALIDATE_SECRET!,
        },
        body: JSON.stringify({ collection, keys }),
      })
    } catch (err) {
      console.error('Failed to revalidate cache:', err)
    }
  })

  // ===== УВЕДОМЛЕНИЯ =====
  action('items.create', async ({ collection, key, payload }, context) => {
    if (collection !== 'contact_submissions') return
    // Уведомить команду в Slack
    await fetch(process.env.SLACK_WEBHOOK!, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `📬 Новая заявка от ${payload.name} <${payload.email}>\n${payload.message}`,
      }),
    })
  })

  // ===== АУДИТ ЛОГ =====
  action('items.update', async ({ collection, keys, payload, accountability }) => {
    if (!accountability?.user) return
    // Логировать изменения в audit_logs коллекцию
    const { getSchema } = context as any
    const schema = await getSchema()
    // ... записать в audit_logs
  })

  // ===== СИНХРОНИЗАЦИЯ ОСТАТКОВ =====
  action('items.update', async ({ collection, keys, payload }, context) => {
    if (collection !== 'orders') return
    if (payload.status !== 'paid') return
    const { database } = context as EventContext & { database: any }
    const order = await database('orders')
      .where({ id: keys[0] })
      .first()
    if (order?.items) {
      const items = JSON.parse(order.items)
      for (const item of items) {
        await database('products')
          .where({ id: item.product_id })
          .decrement('stock', item.quantity)
      }
    }
  })

  // ===== CRON — ежедневный отчёт =====
  schedule('0 9 * * 1-5', async () => {
    const response = await fetch(`${process.env.API_URL}/custom/reports/sales`)
    const stats = await response.json()
    await fetch(process.env.SLACK_WEBHOOK!, {
      body: JSON.stringify({
        text: `📊 Отчёт за вчера: заказов ${stats.count}, выручка ${stats.revenue.toLocaleString()} $`,
      }),
    })
  })
}

function generateSlug(text: string): string {
  const translitMap: Record<string, string> = {
    а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'yo', ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ф: 'f', х: 'h', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'sch', ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya',
  }
  return text
    .toLowerCase()
    .replace(/[а-яё]/g, char => translitMap[char] || char)
    .replace(/\s+/g, '-')
    .replace(/[^\w-]/g, '')
    .replace(/-+/g, '-')
    .slice(0, 100)
}

מקרה אמיתי: אינטגרציית 1C

עבור חנות מסחר אלקטרוני גדולה, יישמנו סנכרון אוטומטי של הזמנות ומלאי באמצעות Hooks. לאחר תשלום, Hook שולח נתוני הזמנה ל-1C דרך REST API, וכאשר המלאי משתנה במערכת החיצונית, הוא מעדכן את Directus. זמן הסנכרון ירד מ-15 דקות ל-5 שניות. עומס השרת ירד ב-40% על ידי ביטול קריאות API מיותרות. זמן פיתוח האינטגרציה קוצר בחצי בהשוואה לגישה המסורתית. עלות הפיתוח הייתה 1,200 דולר, והלקוח חסך למעלה מ-3,000 דולר חודשית בהזנת נתונים ידנית. צור קשר כדי לפתח Hooks מותאמים אישית ל-Directus—החל מ-800 דולר ל-Hook.

מה כלול, תמחור והתחייבויות

  • קוד מקור עם הערות TypeScript
  • תיעוד להתקנה והגדרה (README)
  • גישה למאגר Git פרטי
  • הדרכת צוות (שעה אונליין)
  • תמיכה למשך שבועיים לאחר המסירה

פיתוח סט טיפוסי של 3–5 Hooks (אימות, התראות, מטמון) אורך 2–5 ימי עסקים. העלויות נעות בין 800 ל-2,500 דולר בהתאם למורכבות. לוחות זמנים נקבעים לאחר ניתוח הלוגיקה העסקית שלך.

לצוות שלנו יש מומחיות עמוקה בפיתוח Directus, לאחר שהשלים למעלה מ-50 פרויקטי התאמה אישית כולל אינטגרציות עם 1C, CRM ומערכות תשלום. אנו מבטיחים איכות קוד, עמידה בלוחות זמנים ותמיכה מלאה. קבל ייעוץ עם מהנדס—נענה על כל שאלה.