28 يوليو 2025·7 دقيقة قراءة

مخطط قاعدة بيانات للمنظمات والفرق في B2B يحافظ على الاتساق

مخطط قاعدة بيانات للمنظمات والفرق في B2B: نمط علائقي عملي للدعوات، حالات العضوية، وراثة الأدوار، وتتبع التغييرات المهيأ للتدقيق.

مخطط قاعدة بيانات للمنظمات والفرق في B2B يحافظ على الاتساق

المشكلة التي يحلها نمط المخطط هذا

معظم تطبيقات B2B ليست فعلاً تطبيقات "حسابات مستخدمين" بسيطة. هي مساحات عمل مشتركة ينتمي فيها الناس إلى منظمة، مقسمة إلى فرق، ويحصلون على أذونات مختلفة بحسب وظيفتهم. المبيعات، الدعم، الشؤون المالية، والمدراء يحتاجون وصولًا مختلفًا، وهذا الوصول يتغير مع الوقت.

نموذج بسيط جدًا ينهار سريعًا. إذا احتفظت بجدول users واحد مع عمود role واحد، فلن تستطيع أن تعبّر عن "نفس الشخص مسؤول في منظمة، لكنه عارض في أخرى." كما أنك لن تستطيع التعامل مع حالات شائعة مثل المتعاقدين الذين يجب أن يروا فريقًا واحدًا فقط، أو موظف يغادر مشروعًا لكنه ما يزال تابعًا للشركة.

الدعوات مصدر شائع آخر للأخطاء. إذا كانت الدعوة مجرد صف إيميل، يصبح غير واضح ما إذا كان الشخص "داخل" المنظمة أم لا، أي فريق من المفترض أن ينضم إليه، وماذا يحدث إذا سجل بإيميل مختلف. التناقضات الصغيرة هنا تتحول غالبًا إلى مشاكل أمنية.

هذا النمط يستهدف أربعة أهداف:

  • الأمان: تأتي الأذونات من العضوية الصريحة، وليس من افتراضات.
  • الوضوح: تكون المنظمة، الفرق، والأدوار كل منها مصدر حقيقة واحد.
  • الثبات: تتبع الدعوات والعضويات دورة حياة متوقعة.
  • التاريخ: يمكنك شرح من منح الوصول، غيّر الأدوار، أو أزال شخصًا.

الهدف هو نموذج علائقي واحد يبقى مفهوما مع نمو الميزات: من مستخدمين لعدة منظمات، فرق متعددة لكل منظمة، وراثة أدوار متوقعة، وتغييرات قابلة للتدقيق. هي بنية يمكنك تنفيذها اليوم وتوسيعها لاحقًا دون إعادة كتابة كل شيء.

المصطلحات الأساسية: المنظمات، الفرق، المستخدمون، والعضويات

إذا أردت مخططًا لا يزال قابلاً للقراءة بعد ستة أشهر، ابدأ بالاتفاق على بعض الكلمات. معظم الالتباس يأتي من خلط "من هو الشخص" مع "ما الذي يمكنه فعله".

المنظمة (Organization / org) هي حدود المستأجر العليا. تمثل العميل أو حساب الأعمال الذي يملك البيانات. إذا كان مستخدمان في منظمتين مختلفتين، فلا ينبغي أن يرى كل منهما بيانات الآخر افتراضيًا. هذه القاعدة الواحدة تمنع الكثير من الوصول العرضي عبر المستأجرين.

الفريق (Team) هو مجموعة أصغر داخل المنظمة. الفرق تمثل وحدات عمل حقيقية: المبيعات، الدعم، الشؤون المالية، أو "المشروع أ". الفرق لا تحل محل حد المنظمة؛ هي تعيش تحته.

المستخدم (User) هو هوية. هو تسجيل دخول الشخص وملفه الشخصي: البريد الإلكتروني، الاسم، كلمة المرور أو معرف SSO، وربما إعدادات المصادقة متعددة العوامل. يمكن أن يوجد المستخدم بدون أن يكون له وصول بعد.

العضوية (Membership) هي سجل الوصول. تجيب على: "هذا المستخدم ينتمي لهذه المنظمة (واختياريًا لهذا الفريق) بهذه الحالة وبهذه الأدوار." فصل الهوية (User) عن الوصول (Membership) يجعل نمذجة المتعاقدين، إنهاء الخدمة، والوصول عبر عدة منظمات أسهل.

معانٍ بسيطة يمكنك استخدامها في الكود وواجهة المستخدم:

  • عضو (Member): مستخدم له عضوية نشطة في منظمة أو فريق.
  • دور (Role): مجموعة مسماة من الأذونات (مثل Org Admin، Team Manager).
  • إذن (Permission): إجراء واحد مسموح به (مثل "عرض الفواتير").
  • حد المستأجر (Tenant boundary): القاعدة التي تقيد البيانات إلى منظمة.

عامل العضوية كآلة حالات صغيرة، لا كقيمة منطقية. الحالات النموذجية هي invited، active، suspended، و removed. هذا يبقي الدعوات، الموافقات، وإنهاء الخدمة متناسقًا وقابلاً للتدقيق.

النموذج العلاقي الواحد: الجداول الأساسية والعلاقات

مخطط متعدد المستأجرين جيد يبدأ بفكرة واحدة: خزّن "من ينتمي إلى أين" في مكان واحد، واجعل كل شيء آخر جداول داعمة. بهذه الطريقة يمكنك الإجابة عن أسئلة أساسية (من في المنظمة، من في الفريق، ماذا يمكنهم أن يفعلوا) دون التنقل عبر نماذج غير مرتبطة.

الجداول الأساسية التي عادة ما تحتاجها:

  • organizations: صف واحد لكل حساب عميل (مستأجر). يحتوي على الاسم، الحالة، حقول الفوترة، ومعرف ثابت.
  • teams: مجموعات داخل منظمة (Support, Sales, Admin). تنتمي دائمًا إلى منظمة واحدة.
  • users: صف واحد لكل شخص. هذا عالمي، ليس لكل منظمة.
  • memberships: الجسر الذي يقول "هذا المستخدم ينتمي لهذه المنظمة" واختياريًا "أيضًا إلى هذا الفريق".
  • role_grants (أو role_assignments): الأدوار التي لدى العضوية، على مستوى المؤسسة أو الفريق أو كليهما.

احتفظ بالمفاتيح والقيود صارمة. استخدم مفاتيح أولية بديلة (UUIDs أو bigints) لكل جدول. أضف مفاتيح أجنبية مثل teams.organization_id -> organizations.id و memberships.user_id -> users.id. ثم أضف بعض قيود التفرد لإيقاف التكرارات قبل أن تظهر في الإنتاج.

قواعد تلتقط معظم البيانات السيئة مبكرًا:

  • سبيكة/معرف فريد للمنظمة: unique(organizations.slug)
  • أسماء الفرق لكل منظمة: unique(teams.organization_id, teams.name)
  • لا عضوية مكررة على مستوى المنظمة: unique(memberships.organization_id, memberships.user_id)
  • لا عضوية فريق مكررة (إذا نمذجت عضوية الفريق بشكل منفصل): unique(team_memberships.team_id, team_memberships.user_id)

قرّر ما هو قابل للإلحاق فقط مقابل ما يمكن تعديله. المنظمات، الفرق، والمستخدمون قابلون للتحديث. تُعدّل العضويات غالبًا للحالة الحالية (active, suspended)، لكن يجب أن تكتب التغييرات أيضًا إلى سجل وصول إلحاقي بحيث تكون عمليات التدقيق سهلة لاحقًا.

دعوات وحالات عضوية تبقى متسقة

أسهل طريقة للحفاظ على وصول نظيف هي التعامل مع الدعوة كسجل مستقل، لا كعضوية نصف مخلوقة. العضوية تعني "هذا المستخدم يملك الوصول حاليًا." الدعوة تعني "عرضنا الوصول لكنه ليس حقيقيًا بعد." فصلها يجنب الأعضاء الشبح، الصلاحيات نصف الممنوحة، وألغاز "من دعاه هذا الشخص؟".

نموذج حالات بسيط وموثوق

بالنسبة للعضويات، استخدم مجموعة صغيرة من الحالات يمكنك شرحها لأي شخص:

  • active: يمكن للمستخدم الوصول إلى المنظمة (وأي فرق هو عضو فيها)
  • suspended: محظور مؤقتًا، لكن التاريخ يبقى سليمًا
  • removed: لم يعد عضوًا، محفوظ لغرض التدقيق والتقارير

تتجنّب فرق كثيرة حالة عضوية "invited" وتحتفظ بـ "invited" بشكل صارم في جدول الدعوات. هذا أكثر نظافة: صفوف العضوية موجودة فقط للمستخدمين الذين يملكون وصولًا فعليًا (active) أو الذين كانوا يملكونه سابقًا (suspended/removed).

دعوات بالبريد الإلكتروني قبل وجود حساب

غالبًا ما تدعو تطبيقات B2B بالبريد الإلكتروني عندما لا يوجد حساب مستخدم بعد. خزّن البريد الإلكتروني في سجل الدعوة، مع المكان الذي تنطبق عليه الدعوة (منظمة أو فريق)، الدور المقصود، ومن أرسلها. إذا سجّل الشخص لاحقًا بنفس الإيميل، يمكنك مطابقة الدعوات المعلقة ودعوة القبول.

عند قبول الدعوة، تعامل معها في معاملة واحدة: علّم الدعوة كـ accepted، أنشئ العضوية، واكتب إدخال تدقيق (من قبِل، متى، وأي بريد تم استخدامه).

حدّد حالات نهاية واضحة للدعوة:

  • expired: تجاوزت موعدها ولا يمكن قبولها
  • revoked: ألغيت بواسطة مسؤول ولا يمكن قبولها
  • accepted: تحوّلت إلى عضوية

منع الدعوات المكررة بفرض "دعوة معلقة واحدة فقط لكل منظمة أو فريق لكل بريد". إذا دعمت إعادة الدعوة، فإما مدد انتهاء الصلاحية على الدعوة المعلقة القائمة أو ألغِ القديمة وأصدر رمزًا جديدًا.

الأدوار والوراثة دون جعل الوصول مشوشًا

حوّل الدعوات إلى سير عمل
اجعل الدعوات، القبول، وإنهاء الوظائف كعمليات أعمال واضحة يمكن تدقيقها.
ابدأ البناء

تحتاج معظم تطبيقات B2B لمستويين من الوصول: ما يمكن أن يفعله الشخص في المنظمة ككل، وما يمكن أن يفعله داخل فريق محدد. خلطهما في عمود role واحد هو مصدر الإحساس بعدم الاتساق.

تجيب أدوار المؤسسة عن أسئلة مثل: هل يمكن لهذا الشخص إدارة الفوترة، دعوة الناس، أو رؤية كل الفرق؟ وتجيب أدوار الفريق عن: هل يمكنه تعديل عناصر في فريق A، اعتماد طلبات في فريق B، أم فقط العرض؟

وراثة الأدوار تكون أسهل عند اتباع قاعدة واحدة: دور المؤسسة ينطبق في كل مكان ما لم يحدد الفريق خلاف ذلك صراحة. هذا يبقي السلوك متوقعًا ويقلل من تكرار البيانات.

طريقة نظيفة لنمذجة ذلك هي تخزين تعيينات الأدوار مع نطاق:

  • role_assignments: user_id, org_id, اختياري team_id (NULL يعني على مستوى المنظمة), role_id, created_at, created_by

إذا أردت "دور واحد لكل نطاق" أضف قيد تفرد على (user_id, org_id, team_id).

إذن الوصول الفعّال لفريق يصبح:

  1. ابحث عن تعيين خاص بالفريق (team_id = X). إذا وُجد، استخدمه.

  2. وإلا، ارجع إلى التعيين العام للمؤسسة (team_id IS NULL).

لإفتراضات الأقل صلاحية، اختر دور مؤسسة أدنى (غالبًا "Member") ولا تعطه صلاحيات إدارية خفية. لا ينبغي أن يحصل المستخدمون الجدد على وصول فريق ضمني ما لم تكن ميزتك تحتاج ذلك فعلًا. إذا فعلت منحًا تلقائيًا، فافعله بإنشاء عضويات فريق صريحة، لا بتوسيع دور المؤسسة بصمت.

ينبغي أن تكون التجاوزات نادرة وواضحة. مثال: ماريا هي "Manager" في المؤسسة (تستطيع الدعوة، رؤية التقارير)، لكنها في فريق المالية يجب أن تكون "Viewer." تخزن تعيينًا عامًا للمؤسسة لماريا، بالإضافة إلى تجاوز خاص بالفريق للمالية. لا نسخ للأذونات، والاستثناء ظاهر.

أسماء الأدوار تعمل جيدًا للأنماط الشائعة. استخدم الأذونات الصريحة فقط عندما تكون لديك حالات فردية حقيقية، أو عندما تتطلب الامتثال قائمة واضحة بالأفعال المسموح بها. حتى حينها، احتفظ بنفس فكرة النطاق حتى يبقى النموذج الذهني متسقًا.

تغييرات صديقة للتدقيق: تتبع من غيّر الوصول

إذا خزن تطبيقك الحقل الحالي فقط للدور على صف العضوية، فأنت تضيع القصة. عندما يسأل أحدهم "من أعطى ألكس صلاحية المسؤول الثلاثاء الماضي؟" فلن تكون لديك إجابة موثوقة. تحتاج إلى سجل تغييرات، ليس حالة حالية فقط.

أسهل نهج هو جدول سجل تدقيق مخصص يسجل أحداث الوصول. اعتبره دفتر يوميات قابل للإلحاق: لا تعدّل الصفوف القديمة؛ أضف صفوفًا جديدة فقط.

جدول تدقيق عملي عادةً يتضمن:

  • actor_user_id (من أجرى التغيير)
  • subject_type و subject_id (عضوية، فريق، منظمة)
  • action (invite_sent, role_changed, membership_suspended, team_deleted)
  • occurred_at (متى حصل)
  • reason (نص اختياري مثل "offboarding متعاقد")

لالتقاط "قبل" و "بعد"، خزّن لقطة صغيرة من الحقول التي تهمك. اجعلها محدودة لبيانات التحكم بالوصول، لا الملفات الشخصية الكاملة. على سبيل المثال: before_role, after_role, before_state, after_state, before_team_id, after_team_id. إذا فضلت المرونة، استخدم عمودا JSON اثنين (before, after)، لكن اجعل الحمولة صغيرة ومتسقة.

بالنسبة للعضويات والفرق، الحذف الناعم عادة أفضل من الحذف النهائي. بدلًا من إزالة الصف، علّمه مع حقول مثل deleted_at و deleted_by. هذا يحافظ على سلامة المفاتيح الخارجية ويجعل تفسير الوصول الماضي أسهل. الحذف النهائي قد يكون منطقيًا للسجلات المؤقتة حقًا (مثل الدعوات المنتهية)، ولكن فقط إذا كنت متأكدًا من أنك لن تحتاجها لاحقًا.

مع هذا، يمكنك الإجابة عن أسئلة امتثال شائعة بسرعة:

  • من منح أو أزال الوصول، ومتى؟
  • ما الذي تغيّر بالضبط (دور، فريق، حالة)؟
  • هل أزيل الوصول كجزء من سير offboarding الطبيعي؟

خطوة بخطوة: تصميم المخطط في قاعدة علاقية

تجنّب دين متقني من اليوم الأول
سلِّم واجهة خلفية جاهزة للإنتاج مع توليد كود Go وإعادة توليد نظيفة عند تغير المتطلبات.
ولّد الكود

ابدأ بسيطًا: مكان واحد لتقول من ينتمي إلى ماذا، ولماذا. ابنِه على خطوات صغيرة، وأضف قواعد مع تقدمك حتى لا تَنجرف البيانات إلى "تقريبًا صحيحة."

ترتيب عملي يعمل جيدًا في PostgreSQL وقواعد البيانات العلائقية الأخرى:

  1. أنشئ organizations و teams، كل منهما بمفتاح أساسي ثابت (UUID أو bigint). أضف teams.organization_id كمفتاح أجنبي، وقرّر مبكرًا ما إذا كانت أسماء الفرق يجب أن تكون فريدة داخل المنظمة.

  2. احتفظ بـ users منفصلًا عن العضوية. ضع حقول الهوية في users (email, status, created_at). ضع "ينتمي إلى منظمة/فريق" في جدول memberships مع user_id, organization_id, اختياري team_id، وعمود state (active, suspended, removed).

  3. أضف invitations كجدول مستقل، ليس عمودًا على العضوية. خزّن organization_id, اختياري team_id, email, token, expires_at, و accepted_at. فرِض تفردًا لـ "دعوة مفتوحة واحدة لكل org + email + team" حتى لا تنشئ مكررات.

  4. نمذج الأدوار بجداول صريحة. نهج بسيط هو roles (admin, member, إلخ) بالإضافة إلى role_assignments التي تشير إما إلى نطاق المؤسسة (لا team_id) أو نطاق الفريق (team_id معبأ). احتفظ بقواعد الوراثة متسقة وقابلة للاختبار.

  5. أضف أثر تدقيقي من اليوم الأول. استخدم جدول access_events مع actor_user_id, target_user_id (أو البريد للإشعارات), action (invite_sent, role_changed, removed), scope (org/team), و created_at.

بعد وجود هذه الجداول، شغّل بعض استعلامات الإدارة الأساسية للتحقق من الواقع: "من له وصول عام للمؤسسة؟"، "أي فرق بلا مدراء؟"، و "أي دعوات منتهية لكن لا تزال مفتوحة؟" هذه الأسئلة تكشف عادة عن قيود مفقودة مبكرًا.

قواعد وقيود تمنع بيانات فوضوية

مركِز إدارة الوصول
استبدل السكربتات المبعثرة بتطبيق واحد يتعامل مع الدعوات، الأدوار، وتغييرات الوصول بوضوح.
جرّب AppMaster

يبقى المخطط منطقيًا عندما تُجبر قاعدة البيانات، وليس فقط الكود، حدود المستأجر. أبسط قاعدة: كل جدول مقيّم بالمستأجر يحمل org_id، وكل بحث يتضمّنها. حتى لو نسي أحدهم فلترة في التطبيق، يجب أن تقاوم قاعدة البيانات الربط عبر المنظمات.

سِّياج يمنع تلوّث البيانات

ابدأ بالمفاتيح الأجنبية التي تشير دائمًا "ضمن نفس المنظمة." على سبيل المثال، إذا خزّنت عضوية الفريق بشكل منفصل، يجب أن تشير صفوف team_memberships إلى team_id و user_id، ولكن أيضًا تحمل org_id. مع المفاتيح المركبة يمكنك إجبار أن الفريق المشار إليه ينتمي إلى نفس المنظمة.

قيود تمنع أكثر المشاكل شيوعًا:

  • عضوية نشطة واحدة للمؤسسة لكل مستخدم: قيد تفرد على (org_id, user_id) مع شرط جزئي للصفوف النشطة (حيثما كان مدعومًا).
  • دعوة معلقة واحدة لكل إيميل لكل منظمة/فريق: قيد تفرد على (org_id, team_id, email) حيث state = 'pending'.
  • رموز الدعوة فريدة عالميًا ولا تُعاد استخدمها: unique(invite_token).
  • الفريق ينتمي تمامًا إلى منظمة واحدة: teams.org_id NOT NULL مع مفتاح أجنبي إلى orgs(id).
  • أنهِ العضويات بدلًا من حذفها: خزّن ended_at (واختياريًا ended_by) لحماية السجل التدقيقي.

الفهرسة للبحث الذي تنفذه فعليًا

أفهِم استعلامات التطبيق الشائعة وفهرسها:

  • (org_id, user_id) لـ "ما هي المنظمات التي ينتمي إليها هذا المستخدم؟"
  • (org_id, team_id) لـ "قائمة أعضاء هذا الفريق"
  • (invite_token) لـ "قبول الدعوة"
  • (org_id, state) لـ "الدعوات المعلقة" و "الأعضاء النشطون"

حافظ على أسماء المنظمات قابلة للتغيير. استخدم orgs.id الثابت في كل مكان، وتعامل مع orgs.name (وأي slug) كحقول قابلة للتحرير. إعادة التسمية ثم تؤثر على صف واحد.

نقل فريق بين منظمات عادة قرار سياساتي. الخيار الأكثر أمانًا هو منعه (أو استنساخ الفريق) لأن العضويات، الأدوار، وتاريخ التدقيق مرتبطة بالمنظمة. إذا اضطررت للسماح بالنقل، قم به في معاملة واحدة وحدّث كل الصفوف الفرعية التي تحمل org_id.

لتجنب سجلات يتيمة عندما يغادر المستخدمون، تجنّب الحذف النهائي. عطل المستخدم، انهِ عضوياته، وقيّد الحذف على الصفوف الأبوية (ON DELETE RESTRICT) ما لم تكن تريد الحذف المتسلسل فعلاً.

سيناريو مثالي: منظمة واحدة، فريقان، وتغيّر الوصول بأمان

تخيّل شركة اسمها Northwind Co مع منظمة واحدة وفريقين: Sales و Support. يوظفون متعاقدة، ميا، لمساعدة تذاكر Support لشهر واحد. هنا يجب أن يبقى النموذج متوقعًا: شخص واحد، عضوية مؤسسة واحدة، عضويات فرق اختيارية، وحالات واضحة.

مسؤول المؤسسة (Ava) يدعو ميا عبر البريد. ينشئ النظام صف دعوة مربوطًا بالمنظمة، بحالة pending وتاريخ انتهاء. لا يتغير شيء آخر بعد، لذا لا يوجد "مستخدم نصف موجود" مع وصول غير واضح.

عندما تقبل ميا، تُعلم الدعوة كـ accepted، وتُنشأ صف عضوية للمؤسسة بحالة active. تعطي Ava لميا دور مؤسسة member (ليس admin). ثم تضيف ميا إلى فريق Support وتعيّن دور فريق مثل support_agent.

أضف انعطافًا واحدًا: بن موظف بدوام كامل وله دور مؤسسي admin، لكنه لا ينبغي أن يرى بيانات Support. يمكنك معالجة ذلك بتجاوز على مستوى الفريق يخفّض دوره في Support بينما يبقى لديه صلاحيات إدارة على مستوى المؤسسة لإعدادات المنظمة.

بعد أسبوع، تنتهك ميا سياسات وتُعلّق. بدلًا من حذف الصفوف، تضبط Ava حالة عضوية ميا إلى suspended. يمكن أن تبقى عضويات الفريق في مكانها لكنها تصبح غير فعالة لأن عضوية المؤسسة ليست نشطة.

يبقى سجل التدقيق واضحًا لأن كل تغيير حدث كحدث:

  • Ava دعت ميا (من، ماذا، متى)
  • ميا قبلت الدعوة
  • Ava أضافت ميا إلى Support وعَيّنت support_agent
  • Ava ضبطت تجاوز بن في Support
  • Ava علّقت ميا

بهذا النموذج، يمكن لواجهة المستخدم أن تعرض ملخص وصول واضح: حالة المؤسسة (نشطة أو معلّقة)، دور المؤسسة، قائمة الفرق مع الأدوار والتجاوزات، وملف "تغييرات الوصول الأخيرة" الذي يشرح لماذا يمكن لشخص أن يرى أو لا يرى Sales أو Support.

أخطاء شائعة وفخاخ يجب تجنّبها

أطلق بوابة متعددة المستأجرين
ابنِ بوابة عملاء مع تبديل المنظمات، عروض الفرق، وشاشات مبنية على الأدوار.
أنشئ تطبيق

معظم أخطاء الوصول تأتي من نماذج "قريبة من الصحيحة". يبدو المخطط جيدًا في البداية، ثم تتراكم الحالات الطرفية: إعادة الدعوات، تحريك الفرق، تغييرات الأدوار، وإنهاء الخدمة.

فخ شائع هو خلط الدعوات والعضويات في صف واحد. إذا خزّنت "مدعو" و "نشيط" في نفس السجل دون معنى واضح، ستنتهي بك الأسئلة المستحيلة مثل "هل هذا الشخص عضو إذا لم يقبل؟" احتفظ بالدعوات والعضويات منفصلتين، أو اجعل آلة الحالات واضحة ومتسقة.

خطأ آخر متكرر هو وضع عمود دور واحد في جدول المستخدمين واعتباره كافياً. الأدوار عادةً ما تكون ذات نطاق (دور على مستوى المؤسسة، دور على مستوى الفريق، دور على مستوى المشروع). دور عالمي يُجبرك على حلول ملتوية مثل "المستخدم admin لعميل واحد، لكن قراءة فقط لعميل آخر"، وهذا يكسر توقعات تعدد المستأجرين ويخلق متاعب دعم.

فخاخ تؤذي لاحقًا عادة:

  • السماح بعضوية فريق عبر المنظمات بالخطأ (team_id يشير إلى org A، بينما العضوية تشير إلى org B).
  • حذف العضويات حذفًا نهائيًا وفقدان السجل "من كان له وصول الأسبوع الماضي؟".
  • فقدان قواعد التفرد فتجد مستخدمًا يحصل على وصول مكرر عبر صفوف متطابقة.
  • السماح لتراكم الوراثة بصمت (admin للمؤسسة زائد عضو فريق زائد تجاوز) بحيث لا يستطيع أحد تفسير سبب وجود الوصول.
  • التعامل مع "قبول الدعوة" كحدث واجهة، وليس كحقيقة مخزنة في قاعدة البيانات.

مثال سريع: يُدعى متعاقد إلى مؤسسة، ينضم إلى فريق Sales، ثم يُزال ويُعاد دعوته بعد شهر. إذا كتبت فوق الصف القديم، تفقد التاريخ. إذا سمحت بالتكرارات، قد يحصل على عضويتين نشطتين. الحالات الواضحة، الأدوار المقيّدة، والقيود المناسبة تمنع كلا المشكلتين.

فحوصات سريعة وخطوات تالية لبنائها في تطبيقك

قبل أن تبرمج، أعد مرورًا سريعًا على نموذجك وانظر إذا ما زال منطقيًا على الورق. نموذج وصول متعدد المستأجرين جيد يجب أن يشعر بالملل: نفس القواعد تنطبق في كل مكان، و"الحالات الخاصة" نادرة.

قائمة فحص سريعة لالتقاط الفجوات الشائعة:

  • كل عضوية تشير إلى مستخدم ومنظمة واحدة تمامًا، مع قيد تفرد لمنع التكرارات.
  • حالات الدعوة والعضوية والإزالة صريحة (لا تُفهم من قيم NULL)، والانتقالات محدودة (مثلاً لا يمكنك قبول دعوة منتهية).
  • تخزن الأدوار في مكان واحد ويُحتسب الوصول الفعّال باستمرار (بما في ذلك قواعد الوراثة إن استُخدمت).
  • حذف المنظمات/الفرق/المستخدمين لا يمحو التاريخ (استخدم الحذف الناعم أو حقول الأرشفة عند الحاجة).
  • كل تغيير وصول يصدر حدث تدقيق مع الفاعل، الهدف، النطاق، الطابع الزمني، والسبب/المصدر.

ضع التصميم تحت ضغط الأسئلة الحقيقية. إذا لم تستطع الإجابة عنها باستعلام واحد وقاعدة واضحة، فربما تحتاج قيدًا أو حالة إضافية:

  • ماذا يحدث إذا دُعي مستخدم مرتين ثم تغيّر إيميله؟
  • هل يمكن لمسؤول فريق إزالة مالك المؤسسة من ذلك الفريق؟
  • إذا منح دور المؤسسة وصولًا لكل الفرق، هل يمكن لفريق واحد تجاوزه؟
  • إذا قُبلت دعوة بعد تغيير الدور، أي دور يُطبّق؟
  • عندما يسأل الدعم "من أزال الوصول"، هل يمكنك إثبات ذلك بسرعة؟

اكتب ما يجب أن يفهمه المدراء وموظفو الدعم: حالات العضوية (وماذا يُشغّلها)، من يمكنه الدعوة/الإزالة، ماذا تعني وراثة الأدوار بلغة بسيطة، وأين تنظر لسجلات التدقيق عند وقوع حادث.

نفّذ القيود أولًا (التفردات، المفاتيح الأجنبية، الانتقالات المسموح بها)، ثم ابنِ المنطق التجاري حولها حتى تساعدك قاعدة البيانات على الالتزام. احتفظ بقرارات السياسة (تفعيل الوراثة أو إيقافها، الأدوار الافتراضية، انتهاء صلاحية الدعوة) في جداول إعدادات بدلًا من ثوابت في الكود.

إذا أردت بناء هذا دون كتابة كل واجهة خلفية وكل شاشة إدارة يدويًا، AppMaster (appmaster.io) يمكن أن يساعدك على نمذجة هذه الجداول في PostgreSQL وتنفيذ انتقالات الدعوة والعضوية كعمليات أعمال صريحة، مع توليد كود مصدر حقيقي لنشر إنتاجي.

الأسئلة الشائعة

لماذا لا أُخزّن عمود دور واحد في جدول المستخدمين؟

استخدم سجل العضوية منفصلًا بحيث ترتبط الأدوار والوصول بالمنظمة (وأحيانًا بالفريق) وليس بهوية المستخدم العالمية. هذا يتيح لنفس الشخص أن يكون Admin في منظمة و Viewer في أخرى دون حلول ملتوية.

هل يجب أن تنشئ الدعوة صف عضوية فورًا؟

افصل بينهما: الدعوة هي عرض مع بريد إلكتروني، نطاق وانتهاء صلاحية، بينما العضوية تعني أن المستخدم يملك حق الوصول فعلاً. هذا يتجنّب "الأعضاء الشبح" والحالات غير الواضحة ومشاكل الأمان عند تغيير الإيميلات.

ما حالات العضوية التي يجب أن أستخدمها؟

مجموعة صغيرة مثل active، suspended، و removed تكفي لمعظم تطبيقات B2B. إذا احتفظت بـ "invited" فقط في جدول الدعوات، تظل العضويات غير قابلة للالتباس: هي تمثل الوصول الحالي أو السابق، لا الوصول المعلق.

كيف أمثل أدوار المؤسسة مقابل أدوار الفرق دون ارتباك؟

خزن أدوار المؤسسة وأدوار الفريق كتعينات مع نطاق (عام للمؤسسة عندما يكون team_id هو NULL، ومحدّد للفريق عندما يكون مُعبّأ). عند فحص الوصول لفريق، استخدم التعيين الخاص بالفريق إن وُجد وإلا اعتمد على التعيين العام للمؤسسة.

ما أبسط قاعدة لوراثة الأدوار؟

ابدأ بقاعدة متوقعة واحدة: أدوار المؤسسة تنطبق في كل مكان بشكل افتراضي، وأدوار الفريق تتجاوزها فقط عند تعيينها صراحة. اجعل التجاوزات نادرة وواضحة حتى يتمكن الناس من تفسير الوصول بسهولة.

كيف أمنع الدعوات المكررة وأتعامل مع إعادة الدعوة بشكل نظيف؟

فرِض "دعوة معلقة واحدة لكل منظمة/فريق لكل بريد" بواسطة قيد فريد ودورة حياة واضحة pending/accepted/revoked/expired. إذا احتجت لإعادة الدعوة، حدّث الدعوة المعلقة الموجودة أو ألغِها قبل إصدار رمز جديد.

كيف أفرض حدود المستأجر في قاعدة البيانات؟

يجب أن يحتوي كل صف موجَّه للمستأجر على org_id، وأن تمنع المفاتيح الخارجية/القيود خلط المنظمات (مثلاً: يجب أن ينتمي الفريق المشار إليه بواسطة عضوية إلى نفس المنظمة). هذا يقلل من نطاق الخطأ عند نسيان عوامل التصفية في كود التطبيق.

كيف أجعل تغييرات الوصول صديقة للتدقيق؟

احتفظ بسجل أحداث وصول قابل للإلحاق فقط يسجّل من فعل ماذا، لمن، ومتى، وعلى أي نطاق (مؤسسة أو فريق). سجِّل الحقول الأساسية قبل/بعد (الدور، الحالة، الفريق) حتى تستطيع الإجابة عن "من أعطى صلاحية المسؤول يوم الثلاثاء الماضي؟" بثقة.

هل يجب أن أحذف العضويات والدعوات حذفًا نهائيًا؟

تجنّب الحذف الدائم للعضويات والفرق؛ علّمها كمُنتهية/معطّلة حتى تبقى السجلات التاريخية قابلة للاستعلام ولا تنهار المفاتيح الخارجية. بالنسبة للدعوات، يمكنك الاحتفاظ بها أيضًا (حتى لو انتهت) إذا أردت سجل أمني كامل؛ والأهم ألا تُعاد استخدام الرموز.

ما الفهارس الأكثر أهمية لهذا المخطط؟

أدرج فهارس لمسارات الوصول الساخنة: (org_id, user_id) لفحوصات عضوية المؤسسة، (org_id, team_id) لقوائم أعضاء الفريق، (invite_token) لقبول الدعوة، و (org_id, state) لشاشات الإدارة مثل "الأعضاء النشطون" أو "الدعوات المعلقة". صمِّم الفهارس تبعًا لاستعلاماتك الحقيقية.

من السهل أن تبدأ
أنشئ شيئًا رائعًا

تجربة مع AppMaster مع خطة مجانية.
عندما تكون جاهزًا ، يمكنك اختيار الاشتراك المناسب.

البدء