0%
balqalam Logo
بالقلم
المميزاتالمجتمعالتسعيرالوثائق
تسجيل الدخول
  • مقدمة
  • العرض
  • MVP
  • خارطة الطريق
  • متطلبات المنتج
  • ابدأ الآن
  • البيئة المحلية
  • البنية المعمارية
  • الهيكل
  • الأنماط
  • الحزمة التقنية
  • قاعدة البيانات
  • تعدّد المستأجرين
  • الفهرس (Catalog)
  • العمل دون اتصال
  • المصادقة
  • التهيئة (Onboarding)
  • تدفق التكامل (Integration Flow)
  • التهيئة (Provision)
  • القبول
  • الحضور
  • الامتثال التنظيمي
  • الملف الشخصي
  • الاجتماعات
  • إدارة الرسوم
  • الفاتورة
  • التدويل
  • الترجمة
  • دليل الترجمة
  • أمان قاعدة البيانات
  • المساهمة
  • هَوْغوارتس
  • عرض حي
  • الإلهام
المبيعات والانتشار
  • المبيعات
  • الذهاب إلى السوق
  • التسويق
  • القبول — تسليط الضوء
  • البرنامج التجريبي
  • العملاء المحتملون
  • العرض التجاري
  • قوالب التواصل
  • دراسة الحالة
  • المنافسون
  • نموذج العمل
  • الاقتصاد المشترك
  • الجذب والنمو
التمويل والشراكات
  • الحصول على الدعم
  • عرض المستثمر
  • غرفة البيانات
  • المستثمرون
  • المُسرِّعات
  • الحاضنات
  • المنح
  • الرعاة
  • الشركاء
  • المسابقات والهاكاثونات
  • الجامعات ومراكز التدريب

قاعدة البيانات

السابقالتالي

PostgreSQL متعدّد المستأجرين مع Prisma — 302 نموذجاً، 149 enum، 73 ملف schema. عزل صارم عبر schoolId.

تعمل Hogwarts على قاعدة بيانات PostgreSQL متعدّدة المستأجرين. كل المدارس تشترك في نفس الـschema، والبيانات تُعزَل عبر schoolId على كل صف خاص بالمستأجر. الـORM هو Prisma 6.19+ مع تنظيم schema متعدّد الملفات.

المبادئ

  • عزل على مستوى الصف — كل نموذج مستأجر يحمل schoolId.
  • schema مشترك — schema واحد، مدارس متعدّدة.
  • هوية بالنطاق الفرعي — المدارس فريدة بالنطاق الفرعي (مثل kingfahad.databayt.org)، نطاقات مخصّصة عبر CNAME.
  • أمان نوع end-to-end — Prisma → Zod → TypeScript بلا تسليمات يدوية.
  • schema متعدّد الملفات — 73 ملف .prisma منطقياً في prisma/models/ للصيانة.
  • تخزين بلغة واحدة — المحتوى يُخزَّن بلغة واحدة مع حقل lang، والترجمة عند الطلب عبر Google Translate مع تخزين في Translation.

التخطيط

مخطط قاعدة البيانات

302 نموذجاً عبر 73 ملف schema، مجمَّعة حسب المجال. نماذج الكتالوج تحذف schoolId عمداً — هي محتوى مرجعي على مستوى المنصة يُشارَك بين المدارس عبر نمط الجسر.

المجالالنماذجالملفات
الأساس (school، year، term، period، subscription)12school.prisma
Auth والمستخدمون7auth.prisma
الأشخاص (طلاب، معلمون، أهل، كادر)32students.prisma, staff.prisma, staff-member.prisma, profile.prisma
البنية الأكاديمية13academic.prisma, subjects.prisma, classrooms.prisma, enrollment.prisma
الكتالوج (عام)22+catalog.prisma, bridge.prisma, chapter.prisma, lesson.prisma، إلخ
الحضور33+attendance.prisma, attendance-enhanced.prisma, geo-attendance.prisma
الامتحانات والتقييم40+exam.prisma, school-exam.prisma, school-qbank.prisma, quiz.prisma, quiz-game.prisma, quick-assessments.prisma، إلخ
المالية36finance-core.prisma, finance-fees.prisma, finance-invoices.prisma, finance-payroll.prisma, finance-banking.prisma, finance-budgets.prisma, finance-reports.prisma, subscription.prisma
المراسلة11messages.prisma
الإشعارات7notifications.prisma
WhatsApp5whatsapp.prisma
الإعلانات5announcement.prisma
القبول12admission.prisma, visit.prisma, membership.prisma, promotion.prisma
Lumos / LMS8stream.prisma
الجدول9timetable.prisma, schedule.prisma
النقل7transportation.prisma
المكتبة4book.prisma, school-book.prisma, textbook.prisma
الملفات / الوثائق15files.prisma, file-record.prisma, document.prisma, document-processing.prisma, image.prisma, video.prisma
تدقيق / أمن / متفرق10audit.prisma, legal.prisma, webhooks.prisma, task.prisma, translation.prisma, purge-tokens.prisma

عزل متعدّد المستأجرين

نموذج المدرسة

model School {
  id           String  @id @default(cuid())
  name         String
  domain       String  @unique          // مثلاً "kingfahad"
  preferredLanguage String @default("ar")
  logoUrl      String?
  address      String?
  phoneNumber  String?
  email        String?
  timezone     String  @default("Africa/Khartoum")
  schoolType   String?                  // private, public, international, technical, special
  schoolLevel  String?                  // primary, secondary, both
  curriculum   String?                  // @map column: timetable-structure slug ("us-standard"); curriculum CODE (US/SD/GB/IB-DP) is inferred at runtime, not stored
  planType     String  @default("basic")
  maxStudents  Int     @default(100)
  maxTeachers  Int     @default(10)
  isActive     Boolean @default(true)
}

قواعد الاستعلام

// صحيح — اشمل دائماً schoolId
await db.student.findMany({ where: { schoolId } })
 
// خاطئ — يسرّب البيانات بين المستأجرين
await db.student.findMany()

كل نموذج Prisma خاص بالمستأجر يملك:

  • حقل schoolId String.
  • علاقة school School @relation(...) مع onDelete: Cascade.
  • @@unique([schoolId, ...]) كي تكون التصادمات بين المستأجرين مستحيلة.
  • @@index([schoolId]) لأداء الاستعلام.

نماذج بدون schoolId

نحو 44 نموذجاً يحذف schoolId عمداً:

  • رموز Auth (4): Account, VerificationToken, PasswordResetToken, TwoFactorToken — مرتبطة بالبريد، عابرة.
  • عوام الكتالوج (22+): Subject, Chapter, Lesson, Book, Textbook, CurriculumStandard، إلخ — محتوى مرجعي على مستوى المنصة، نماذج الجسر تُحدّد نطاق الاختيارات لكل مدرسة.
  • مستويات الاشتراك (1): تسعير على مستوى المنصة.

بنية LMS مزدوجة

نظامان متوازيان للمحتوى:

النظامالنطاقالنماذجالمصدر
Lumosبنطاق المدرسة (schoolId)StreamCourse, StreamCategory, StreamEnrollment, StreamLesson, StreamCertificateمساقات تنشئها المدرسة
Catalogعلى مستوى المنصة (بدون schoolId)Subject, Chapter, Lesson, Material, Questionمنتقى

نماذج bridge.prisma (SubjectSelection, BookSelection, ContentOverride, InstructorPreference) تربط محتوى الكتالوج بالبنية الأكاديمية الخاصة بالمدرسة. المدارس تختار المواد التي تستخدمها وتطبّق تجاوزات بدون لمس الأصول المشتركة.

أدوار المستخدمين

ثمانية أدوار في prisma/models/auth.prisma:UserRole. راجع تعدد المستأجرين لمصفوفة RBAC.

الدورنطاق المدرسةالاستخدام
DEVELOPERكل المدارسإدارة المنصة، عمليات عابرة للمستأجرين
ADMINواحدةإدارة المدرسة، الإشراف على المستخدمين
TEACHERواحدةإدارة الفصل، التقييم، الحضور
STUDENTواحدةدرجاته، حضوره، واجباته
GUARDIANواحدةبيانات الطالب المرتبط، المدفوعات، الإشعارات
ACCOUNTANTواحدةفوترة، تقارير مالية (لا أكاديميات)
STAFFواحدةدعم تشغيلي
USERلا شيءافتراضي بعد التسجيل قبل الإعداد

الاشتراك والفوترة

model SubscriptionTier {
  id           String   @id @default(cuid())
  name         String   // basic, premium, enterprise
  monthlyPrice Int      // cents
  annualPrice  Int      // cents
  maxStudents  Int
  features     String[]
  isActive     Boolean  @default(true)
 
  subscriptions Subscription[]
  discounts     Discount[]
}
 
model Discount {
  id          String   @id @default(cuid())
  schoolId    String
  tierId      String
  code        String   @unique
  type        String   // percentage, fixed
  value       Int
  validFrom   DateTime
  validUntil  DateTime
  maxUses     Int?
  currentUses Int      @default(0)
 
  school           School           @relation(fields: [schoolId], references: [id])
  subscriptionTier SubscriptionTier @relation(fields: [tierId], references: [id])
}

إضافة إلى BillingPaymentMethod, BillingHistory, UsageMetrics, CreditNote, BillingPreferences, AppliedDiscount لدورة الفوترة الكاملة.

القانوني والامتثال

model LegalConsent {
  id              String    @id @default(cuid())
  schoolId        String
  userId          String
  documentType    String    // terms, privacy, data-processing
  documentVersion String
  consentType     String    // explicit, implicit, parental
  ipAddress       String?
  userAgent       String?
  consentedAt     DateTime  @default(now())
  revokedAt       DateTime?
 
  @@unique([schoolId, userId, documentType, documentVersion])
}
 
model LegalDocument {
  id               String    @id @default(cuid())
  schoolId         String
  type             String    // terms, privacy, data-processing
  version          String
  content          String    @db.Text
  effectiveFrom    DateTime
  effectiveUntil   DateTime?
  isActive         Boolean   @default(true)
  requiresExplicit Boolean   @default(true)
}
 
model ComplianceLog {
  id        String   @id @default(cuid())
  schoolId  String
  eventType String
  eventData Json
  userId    String?
  timestamp DateTime @default(now())
}

تخزين بلغة واحدة

المحتوى يُخزَّن بلغة واحدة مع حقل lang. الترجمة عند الطلب عبر Google Translate API، مع تخزين في Translation.

model Announcement {
  title String?
  body  String? @db.Text
  lang  String  @default("ar")
}
 
model Translation {
  id             String   @id @default(cuid())
  schoolId       String
  sourceText     String   @db.Text
  sourceLanguage String                       // "ar" أو "en"
  targetLanguage String                       // "ar" أو "en"
  translatedText String   @db.Text
  provider       String   @default("google")
  hitCount       Int      @default(0)
  lastAccessedAt DateTime @default(now())
 
  @@unique([schoolId, sourceText, sourceLanguage, targetLanguage])
  @@index([schoolId, sourceLanguage, targetLanguage])
  @@index([lastAccessedAt])
}
import { getText } from "@/components/translation/display"
 
// مخزَّن بالعربية، يُعرَض بالإنجليزية → يترجم ويخزّن
const title = await getText("مرحبا", "ar", "en", schoolId)
 
// نفس اللغة → يعود مباشرة بدون استدعاء API
const title = await getText("مرحبا", "ar", "ar", schoolId)

نماذج تحمل lang

Announcement, AnnouncementTemplate, NotificationTemplate, Subject, Class, YearLevel, Department, AcademicLevel, AcademicGrade, AcademicStream, AttendanceBadge, AttendanceCompetition, Chapter, Lesson, Book, Textbook, Material, CurriculumStandard, GradingScheme, QuickAssessment, Video, StreamCourse, StreamCategory. الافتراضي lang = "ar" لمحتوى المدرسة، وlang = "en" لـStream/LMS.

راجع التدويل والترجمة.

الأداء

فهارس موصى بها:

CREATE INDEX idx_students_school_id   ON students(school_id);
CREATE INDEX idx_teachers_school_id   ON teachers(school_id);
CREATE INDEX idx_classes_school_id    ON classes(school_id);
CREATE INDEX idx_attendance_school_id ON attendance(school_id);

إرشادات:

  • فلتر دائماً بـ schoolId أولاً في WHERE.
  • استخدم فهارس مركّبة لتركيبات الحقول المُستعلَم عنها كثيراً.
  • connection pooling عبر Neon serverless.
  • read replicas للتقارير والتحليلات.

النشر

البيئة

DATABASE_URL="postgresql://username:password@host:port/database"
DEFAULT_SCHOOL_DOMAIN="demo"
ALLOW_SCHOOL_SIGNUP="true"
BASIC_MAX_STUDENTS=100
PREMIUM_MAX_STUDENTS=500
ENTERPRISE_MAX_STUDENTS=2000

الترحيلات

pnpm prisma generate                       # إعادة توليد client
pnpm prisma migrate dev --name <name>      # محليًا فقط — إنشاء + تطبيق ترحيل
pnpm db:seed:single <name>                 # seed وحدة — لا تستخدم pnpm db:seed

prisma migrate deploy أداة خاطئة على الإنتاج، وهذه الصفحة كانت توصي بها. تم التحقق في 2026-08-18: قاعدة بيانات الإنتاج لا تحتوي جدول _prisma_migrations — فهي مُدارة عبر db push — لذا سيحاول migrate deploy تطبيق كل الترحيلات الـ38 من الصفر فوق مخطط موجود أصلًا. لا تشغّلها على الإنتاج.

لتغيير مخطط الإنتاج، خذ الـ DDL من المقارنة وطبّق ما تقصده فقط:

# المجلد، وليس prisma/schema.prisma. ملف prisma.config.ts يحمّل النماذج من prisma/، لذا
# الإشارة إلى ملف مصدر البيانات وحده تُنتج نموذجًا فارغًا — عندها تدّعي المقارنة حذف 719
# مفتاحًا أجنبيًا و328 جدولًا. هذا أثر جانبي للأمر نفسه وليس انحرافًا حقيقيًا.
pnpm prisma migrate diff --from-url "$PROD_DATABASE_URL" --to-schema-datamodel prisma --script

اقرأ ما يطبعه، وطبّق العبارات التي تقصدها فقط، ثم أعد تشغيل المقارنة للتأكد من أنها فارغة. إضافة عمود يقبل null بدون قيمة افتراضية تغييرٌ في الفهرس فقط في Postgres — فوري، بدون إعادة كتابة الجدول، وبدون المساس بالصفوف الموجودة.

الترتيب مهم: قاعدة البيانات تتغيّر قبل الكود الذي يتوقّعها. بناء Vercel يشغّل prisma generate فقط (انظر postinstall) ولا يشغّل migrate deploy أبدًا، لذا أي حقل يُشحن قبل عموده يجعل كل استعلام على ذلك النموذج يفشل في الإنتاج.

العمليات المدمّرة (db execute, db push --accept-data-loss, migrate reset, DROP TABLE, TRUNCATE) محظورة عبر hooks. استخدم بروتوكول Branch-Before-Touch على Neon للعمليات الخطرة.

مدرسة العرض

مستأجر عرض واحد (demo.databayt.org) مع محاكاة سودانية كاملة لـK-12. راجع Safe seeds.

الكيانالعددملاحظات
المستخدمون3,105كل الأدوار، بريد @databayt.org
المعلمون100أسماء عربية، 6 أقسام
الطلاب970KG1–الصف 12
أولياء الأمور~1,940اثنان لكل طالب
الأقسام6عربي أساسي + حقل lang
المواد19عربي أساسي + حقل lang
المستويات14KG1–12
الفصول28مختبرات، غرف، مرافق خاصة
الفصول الدراسية187تركيبات بمستوى المادة

انظر أيضاً

  • تعدد المستأجرين — تدفق الطلب، RBAC، ضمانات العزل
  • التدويل — عقد i18n
  • الترجمة — تغطية القاموس
  • الكتالوج — محتوى عام + نمط الجسر
  • Seeds — كتالوج الـseeds الكامل
  • Safe seeds — حسابات تطوير محمية
الحزمة التقنيةتعدّد المستأجرين

On This Page

المبادئالتخطيطعزل متعدّد المستأجريننموذج المدرسةقواعد الاستعلامنماذج بدون schoolIdبنية LMS مزدوجةأدوار المستخدمينالاشتراك والفوترةالقانوني والامتثالتخزين بلغة واحدةنماذج تحمل langالأداءالنشرالبيئةالترحيلاتمدرسة العرضانظر أيضاً

من تطوير Databayt ·

مرحباً بك في بالقلم.

رحلة عظيمة على وشك أن تبدأ.