---
title: "مركز الإشعارات"
url: https://docs.growamal.com/ar/guides/notifications/
language: ar
---

# مركز الإشعارات

مركز الإشعارات هو صندوق الوارد الوحيد داخل التطبيق يخدم جميع الأشخاص المعنيين، وقد حلّ محل أسطح التنبيه المتفرقة التي كان يواجهها المعلم وولي الأمر ومدير المدرسة والمدير والمسؤول قبل إطلاق WP-NOTIF-BE. كل إشعار موجَّه إلى مستلم واحد، ولا يستطيع قراءته أو تجاهله إلا ذلك المستلم، وتستند الإشعارات إلى حمولات منظَّمة تجتاز مرشّح سلامة اللغة قبل تخزينها.

يُطلق هذا WP مسار الكتابة (`dispatch`) والمسارات القارئة الخمسة. تعتمد WPs المنتِجة للتنبيهات (RTI وCBM والمعايير وغيرها) المسار بصورة إضافية في تغييراتها الخاصة.

## ما هو المركز (وما لا يتلقاه الطلاب أبدًا)

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

كل صف `Notification` موجَّه إلى `recipientUserId` واحد فقط (مفتاح أجنبي من نوع `Int` يشير إلى `User.id`). لا يوجد صف بثّ ولا صف موجَّه إلى دور؛ التوسيع N صفوف فردية، صف لكل مستلم محدَّد.

| الشخصية | كيفية تحديد المستلمين |
|---|---|
| المعلم | جدول الربط `ClassTeacher(classId, teacherUserId)`؛ يتلقى كل معلم مشترك في الفصل المصدر صفًا |
| ولي الأمر | جدول الربط `ParentStudent(studentUserId, parentUserId)`؛ يتلقى كل ولي أمر مرتبط صفًا |
| مدير المدرسة / المدير / المسؤول | يُحدِّد المنتِج `recipientUserId` واحدًا محددًا من جداول نطاقه الخاصة |

لا يمرّر المنتِج دورًا إلى `dispatch`؛ يمرّر `recipientUserId` محددًا. لا يقرأ توسيع المعلمين `Class.teacherId` المفردة (هذا العمود غير موجود وفق قاعدة `ClassTeacher`، B.4).

## المسارات الخمسة المحدودة بالمستلم

جميع المسارات تتطلب المصادقة (`requireAuth`). لا يقبل أي مسار معامل مسار `:userId`.

| الطريقة | المسار | ملاحظات |
|---|---|---|
| `GET` | `/api/notifications` | عرض قائمة الإشعارات؛ تصفية حسب `state` و`kind` و`severity`؛ ترقيم صفحات بالمؤشر؛ العاجل أولًا ثم الأحدث |
| `GET` | `/api/notifications/unread-count` | يُعيد `{ count }`؛ هدف استطلاع REST (بدون WebSocket؛ قاعدة صارمة 8) |
| `POST` | `/api/notifications/:id/read` | تحديد إشعار واحد مقروءًا؛ للمستلم فقط؛ لا أثر لتكراره؛ الحالة النهائية لا تُغيَّر |
| `POST` | `/api/notifications/:id/dismiss` | تجاهل إشعار نهائيًا؛ للمستلم فقط؛ لا يظهر مجددًا في القوائم |
| `POST` | `/api/notifications/mark-all-read` | يُعيد `{ updated }` بعدد الصفوف التي تغيّرت حالتها |

لا توجد بوابة أذونات تحمي المسارات القارئة سوى المصادقة؛ الإشعارات شخصية، وعبارة WHERE في قاعدة البيانات تُقيّد كل نتيجة بصفوف المستدعي. (لا يزال `requireTenantContext` يعمل للمسؤول الأعلى وتأكيد المستخدم أو المنظمة المعطّلة.)

## النطاق المحدود بالمستلم (بدون IDOR)

> **تنبيه:** كل مسار يستمدّ هوية المستلم من `ctx.userId` (الـ `sub` في JWT)، لا من معامل مسار. التحريرات على إشعار لا يملكه المستدعي تُعيد **404 لا 403**. لا يُكشَف وجود إشعار في صندوق وارد مستخدم آخر (B.6).

جميع استعلامات القراءة والكتابة تحمل عبارة على مستوى الصف: `WHERE recipientUserId = ctx.userId`. في مسارات التحرير (`/read` و`/dismiss`)، يُشغِّل المُعالج أولًا `findFirst` للتحقق من الملكية؛ إذا أعاد `null` كانت الاستجابة 404.

تُغطي اختبارات الإثبات سطح انعدام IDOR صراحةً: يُنشئ المستخدم A إشعارًا؛ يستدعي المستخدم B (في المنظمة ذاتها) `GET /api/notifications` (غائب)، و`POST /:id/read` (404)، و`POST /:id/dismiss` (404)؛ لا يتأثر `unread-count` للمستخدم B.

## مسار الإرسال

`dispatch({ recipientUserId, kind, payload, sourceEntity? })` هو مسار الكتابة الوحيد. يستدعيه المنتِجون داخل العملية بدون طلب HTTP. يُنفّذ المُرسِل ستة خطوات بالترتيب:

1. **البحث في الكتالوج** -- يُحدِّد صف `NotificationKindCatalog` بحسب `kind`. يُعيد 422 إن كان النوع غير معروف أو غير نشط.
2. **التحقق من صحة الحمولة (Zod لكل نوع)** -- يُتحقق من `payload` وفق المخطط المسجَّل للنوع في `schemas/registry.ts`. لا يُسمح بأي حقل نص حر في أي مخطط (Q18). يُعيد 422 عند الفشل ولا يُدرج صفًا.
3. **تقديم القالب العربي** -- يستبدل الرموز `{{var}}` من الحمولة المُتحقَّق منها في `arabicTitleTemplate` و`arabicBodyTemplate` من صف الكتالوج. التقديم حتمي وعديم الحالة (V-6).
4. **تحديد المستلم والمنظمة** -- يُحدِّد `organizationId` و`persona` من سجل `User` الخاص بالمستلم. إذا كان المستلم غير نشط أو محذوفًا، يتوقف الإرسال مبكرًا مع `{ skipped: "recipient_inactive" }` ولا يُدرج أي صف.
5. **مرشّح سلامة اللغة 73** -- يمرّر كلا النصَّين المُقدَّمَين عبر `sanitize()` من `modules/language-safety`. تُحمَّل الطبقة الأكثر صرامة للوالدين عندما يكون `defaultPersona` في الكتالوج هو `PARENT` أو كانت `persona` المستلم المحدَّد هي `PARENT` -- بذلك يخضع النوع الافتراضي للمعلمين الموزَّع إلى مستلم من أولياء الأمور للحزام الأكثر صرامة أيضًا (إصلاح BUG-4، موثَّق في `dispatcher.ts`). إذا كانت أي قاعدة ستُعيد كتابة أي نص، يُلغى الإدراج، يُسجَّل حدث `level=error`، ويُطرح `NotificationLanguageSafetyError`. لا يوجد نص بديل ولا مرور صامت (Q15).
6. **الإدراج (INSERT)** -- يكتب الصف مع النصوص المُقدَّمة والمخزّنة. لا يتدخل أي نموذج لغوي كبير في أي خطوة (V-5)؛ مسار سلامة اللغة حزام قائم على قواعد حتمية.

يُحدِّد المساعدان `dispatchToClass` و`dispatchToStudent` مجموعة المستلمين الكاملة عبر جدولَي `ClassTeacher` و`ParentStudent` (B.4) ثم يستدعيان `dispatch` مرةً لكل مستلم.

يُطلق هذا WP مسار الإرسال واختباراته. تتبنّى WPs المنتِجة للتنبيهات مسار `dispatch` في تغييراتها الخاصة.

## الأنواع التسعة للموجة الأولى

يحمل جدول `NotificationKindCatalog` تسعة أنواع نشطة للموجة الأولى. لكل نوع سلسلة `kind` ثابتة (هدف مفتاح أجنبي في جدول `Notification`)، وقوالب عنوان وجسم عربية، و`defaultSeverity`، و`defaultPersona`، وحدّ `retentionDays`. تُزرع الأنواع التسعة بواسطة `catalog-seed.ts`:

| النوع | الشخصية الافتراضية | الخطورة الافتراضية |
|---|---|---|
| `q5_24h_ack` | TEACHER | action\_required |
| `skill_alert_assigned` | TEACHER | action\_required |
| `student_alert_assigned` | TEACHER | urgent |
| `benchmark_profile_swap` | ADMIN | info |
| `dismissal_expiry` | TEACHER | action\_required |
| `plan_modification_suggestion` | TEACHER | action\_required |
| `cbm_alert_lifecycle` | TEACHER | action\_required |
| `parent_tsp_activation` | PARENT | info |
| `lms_connection_revoked` | ADMIN | urgent |

المصدر: `apps/server/src/modules/notifications/catalog-seed.ts`. النموذج الأولي الذي أنشأه WP-RDM-BE (عنصر نائب لـ `notification_kind_catalog` بحقول نصية مرنة مُعلَّمة `TODO owner-spec 71`) أُلغي وأُسقط بترحيل هذا WP. لا يوجد سوى نموذج واحد متعارف عليه.

## عزل المستأجر وعزل الأشخاص

`organizationId` مُكرَّر ومُفهرَس على كل صف `Notification`. يُحدَّد من `User` الخاص بالمستلم عند وقت الإرسال، لذا فإن منظمة الإشعار هي دائمًا منظمة المستلم.

يتلقى المستدعون من منظمة أخرى نتائج فارغة (صفر صفوف في القائمة، و404 على `/:id/read`، و`{ count: 0 }` على `unread-count`). عبارة WHERE المحدودة بالمستلم والحد الفاصل للمستأجر طبقتان مستقلتان للدفاع المعمّق.

يمكن أن يكون المسؤول الأعلى (`organizationId = null`) `recipientUserId` للأنواع على مستوى المنصة (مثل `benchmark_profile_swap`). تُقيّد عبارة WHERE المحدودة بالمستلم المسؤولَ الأعلى بصفوفه المخصَّصة فقط. لا يُستشار `skipTenantFilter` على مسارات القراءة هذه؛ الإشعارات شخصية ولا يوجد عرض صندوق وارد عبر المستأجرين.

العزل بين الأشخاص داخل المنظمة نفسها يتبع الآلية ذاتها: `parent_tsp_activation` الموجَّه لولي الأمر و`skill_alert_assigned` الموجَّه للمعلم في المنظمة ذاتها لا يظهران في صندوق وارد بعضهما أبدًا لأن `recipientUserId` يختلف.

## الحمولات المنظَّمة (Q18) وسلامة اللغة (Q15)

**Q18 -- لا نص حر:** لكل `kind` مخطط Zod مسجَّل في `schemas/registry.ts`. لا يحتوي أي مخطط على حقل باسم `note` أو `comment` أو `reason` أو `freeText` أو أي سلسلة غير مقيَّدة. يرجع حقل مبرر الرفض إلى رمز من تعداد `OverrideCodeCatalog` التسعة المغلق، لا إلى نص حر (B.3). يحرس lint CI `pnpm schema:lint:notification-payload` ملف السجل. الحمولة التي تفشل في التحقق تجعل `dispatch` يطرح `NotificationPayloadValidationError` قبل إدراج أي صف.

**Q15 -- سلامة اللغة:** يجتاز كلا النصَّين المُقدَّمَين مرشّح `sanitize()` من سلامة اللغة 73 عند وقت الإرسال. تُحمَّل طبقة المرشّح الأكثر صرامة للوالدين (FR-LANG-6) عندما يكون `defaultPersona` في الكتالوج هو `PARENT` أو كانت `persona` المستلم المحدَّد هي `PARENT` -- بذلك يخضع النوع الافتراضي للمعلمين الموزَّع توسيعًا إلى مستلم من أولياء الأمور للحزام الأكثر صرامة أيضًا (إصلاح BUG-4). الانتهاك يُلغي الإدراج بدون بديل. النصوص المُقدَّمة والمُصفَّاة مخزَّنة (`renderedTitleAr` و`renderedBodyAr`) وغير قابلة للتغيير بعد الإدراج. تعديل القالب في الكتالوج لاحقًا ذو اتجاه أمامي فقط؛ لا يُعيد تقديم صف موجود (V-6).

## ما يقبل الكتابة مرة واحدة وما يقبل التغيير

الأعمدة التالية كتابة مرة واحدة فحسب: `createdAt` و`recipientUserId` و`kind` و`payload` و`organizationId` و`renderedTitleAr` و`renderedBodyAr`.

العمود الوحيد القابل للتغيير هو `deliveryState` (بالإضافة إلى الطوابع الزمنية المرتبطة `readAt` و`dismissedAt`). كل كاتب من كتّاب انتقال الحالة الثلاثة (`markRead` و`markDismissed` و`markAllRead`) يحمل العلامة `lint-allow-audit-update`. الانتقالات الصالحة هي:

```
delivered_in_app -> read
delivered_in_app -> dismissed
```

`dismissed` حالة نهائية. الإشعار المُتجاهَل لا يظهر في أي قائمة بعد ذلك. لا يوجد انتقال عكسي.

`Notification` ليست في مجموعة lint الإلحاق فقط؛ هي رأس مُدرَج في قائمة الاستثناءات المتاح تعديلها، وتتبع النمط ذاته الذي يتبعه `bundleAssignment` و`cbmAlert`.

## خارج النطاق (الموجة الثانية)

العناصر التالية مؤجَّلة صراحةً:

- قناة البريد الإلكتروني (لا يوجد عمود `deliveryChannel` في النموذج)
- خيارات الاشتراك وإلغاء الاشتراك لكل نوع
- الإشعارات الفورية وتكامل Service Worker
- توجيه الرسائل النصية وWhatsApp
- تجميع الإشعارات أو تقسيمها
- **وظيفة cron الحذف بالاحتفاظ اليومي** -- حقل `retentionDays` لكل نوع موجود ومُزرَع في صفوف الكتالوج، لكن العامل المجدوَل للحذف لم يُبنَ بعد؛ هذه مهمة تشغيلية لاحقة. تستمر ضمانة الإلحاق فقط خلال هذه المرحلة.
- ربط المنتجين (RTI وCBM والمعايير والـ WPs الأخرى باستدعاء `dispatch`)
- مسارات CTA بالروابط العميقة (تُحسَم مع WP-NOTIF-FE)

> **ملاحظة:** **Q-NTF-2 (مفتوح):** الاحتفاظ لمدة 90 يومًا الافتراضية لصفوف `parent_tsp_activation` ينتظر تأكيد فريق الخصوصية لدى الشريك لمواءمة GDPR وFERPA مع مدارس الطيار في الأردن وفلسطين. عامل الحذف مؤجَّل، لذا لا يُحذف أي صف لولي أمر تلقائيًا بعد. هذا السؤال المفتوح ليس حاسمًا في الوقت الحالي.

## ذو صلة

- [سلامة اللغة](https://docs.growamal.com/ar/guides/language-safety/): مرشّح `sanitize()` وطبقة أولياء الأمور الأكثر صرامة التي تحرس كل نص مُقدَّم
- [توفير المستخدمين](https://docs.growamal.com/ar/guides/provision-users/): إنشاء سجلات المعلم وولي الأمر التي تصبح قيم `User.id` المستخدَمة في `recipientUserId`
- [الفصول وقوائم الطلاب](https://docs.growamal.com/ar/guides/classes-and-rosters/): جدولا `ClassTeacher` و`ParentStudent` المُستخدَمان في مساعدي التوسيع
- [المسرد](https://docs.growamal.com/ar/glossary/): نوع الإشعار، المُرسِل، الإلحاق فقط
