---
title: "مزامنة قوائم نظام إدارة التعلم"
url: https://docs.growamal.com/ar/guides/lms-sync/
language: ar
---

# مزامنة قوائم نظام إدارة التعلم

يستطيع أمل استيراد قوائم الطلاب والمعلمين من نظام إدارة التعلم في مدرستك والإبقاء عليها متزامنة.
يدعم الإصدار الأول موفِّرَين: **Google Classroom** (مستند إلى OAuth بتوكن حي) و**OneRoster** (رفع مجموعة ملفات CSV، دون بيانات اعتماد حية). يتشارك الموفِّران سير عمل المراجعة نفسه القائم على مبدأ المعاينة ثم التأكيد.

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

---

## نوعا الاتصال

### Google Classroom (OAuth)

يُنشَأ اتصال Google Classroom عبر تدفق موافقة OAuth 2.0 القياسي:

1. استدعِ `POST /api/lms/connections/google_classroom/oauth-start`. يحمل الرد حقل `authUrl`
   يشير إلى شاشة موافقة Google.
2. أعِد توجيه مستخدم المسؤول إلى ذلك العنوان. بعد منحه التفويض، يعيد Google التوجيه إلى
   `GET /api/lms/connections/google_classroom/oauth-callback?code=...&state=...`.
3. يتبادل مسار الاستدعاء العكسي رمز التفويض، ويخزِّن رمزَي الوصول والتحديث، ويُعيد
   جسم JSON من نوع `200 LmsConnection`. لا يوجد إعادة توجيه HTTP من مسار الاستدعاء العكسي؛ يتولّى العميل التنقل بعد قراءة جسم الرد.

يُطبَّق الاتصال بأسلوب "أدرج أو حدِّث" على المفتاح الطبيعي `(organizationId, provider, scope, createdByUserId)`، وبذلك تُحدِّث إعادة التفويض لبيانات الاعتماد المنتهية صلاحيتها الرموزَ في موضعها دون إنشاء اتصال مكرَّر.

### OneRoster (مجموعة ملفات CSV)

لا يتطلب اتصال OneRoster أي OAuth. ارفع مجموعة ملفات CSV الخاصة بـ OneRoster 1.1 إلى
`POST /api/lms/connections/oneroster/upload` بجسم JSON يحتوي على خريطة `files`:

```json
{
  "files": {
    "classes.csv": "<csv text>",
    "users.csv": "<csv text>",
    "enrollments.csv": "<csv text>"
  }
}
```

يخزِّن الخادم محتوى CSV في سجل الاتصال. تُعيد المعاينة والتأكيد تحليله محليًا دون الحاجة إلى رفع جديد. يُعيد رفع ناجح `201 LmsConnection`. يؤدي رفع مجموعة ملفات جديدة إلى استبدال المحتوى المخزَّن ومسح ذاكرة التخزين المؤقت للمقررات في ذلك الاتصال.

---

## توزيع الصلاحيات

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

| الصلاحية | تتحكّم في |
|---|---|
| `MANAGE_LMS_CONNECTIONS` | سرد الاتصالات؛ بدء OAuth؛ استدعاء OAuth العكسي؛ رفع OneRoster؛ حذف اتصال؛ سرد المقررات؛ حفظ التعيينات؛ سرد تشغيلات المزامنة |
| `MANAGE_ROSTER` | إنشاء معاينة؛ تعديل صفوف المعاينة؛ التأكيد؛ بدء مزامنة فورية |

> **تنبيه:** المسؤول الذي يملك `MANAGE_LMS_CONNECTIONS` دون `MANAGE_ROSTER` يستطيع تكوين الاتصال وتعيينه، غير أنه لا يستطيع بدء أي عملية تنشئ حسابات مستخدمين أو تحدِّثها. امنح `MANAGE_ROSTER` للمسؤولين المخوَّلين فحسب بإنشاء الحسابات.

---

## اكتشاف المقررات وتعيينها

بعد تفعيل الاتصال، استرجع المقررات المتاحة عبر
`GET /api/lms/{provider}/courses`. تُقدَّم النتائج من ذاكرة تخزين مؤقت داخل العملية لمدة 5 دقائق لكل اتصال.
مرِّر `?cache=bypass` لإجبار الخادم على جلب بيانات جديدة من الموفِّر (مفيد مباشرةً بعد تغيير القائمة).
يتضمّن الرد حقل `cached: boolean` حتى يعرف المستدعي أيّ المسارين جرى اتخاذه.

عيِّن مقررًا أو أكثر من مقررات الموفِّر على فصول أمل عبر `POST /api/lms/{provider}/mappings`. أرسِل `connectionId` ومصفوفة من أزواج `{ externalCourseId, amalClassId }`. يجب أن ينتمي كل `amalClassId` إلى مؤسسة المستدعي؛ يُنتج معرّف فصل من مؤسسة مختلفة خطأ `422`. تُطبَّق التعيينات بأسلوب "أدرج أو حدِّث" على المفتاح الفريد `(organizationId, providerCourseId)`، وبذلك يستبدل تعيين مقرر لفصل مختلف التعيينَ السابق.

---

## المعاينة والتأكيد

إنشاء الحسابات عملية من خطوتين. لا يُكتَب شيء في حسابات المستخدمين حتى تُؤكِّد صراحةً.

### الخطوة الأولى: إنشاء معاينة

يقرأ `POST /api/lms/preview` القائمة الخارجية للمقررات المُعيَّنة في الاتصال ويُنتج مقارنة تفاضلية للقراءة فحسب. **لا يكتب شيئًا** في `User`، أو `StudentProfile`، أو `TeacherProfile`، أو `ClassStudent`،
أو `ClassTeacher`، أو `ParentStudent`. الرد هو `200 LmsPreviewResponse`:

```json
{
  "previewId": "<cuid>",
  "expiresAt": "<ISO-8601>",
  "summary": { "create": 4, "update": 12, "conflict": 2, "skip": 0 },
  "rows": [ ... ]
}
```

للمعاينة **صلاحية 24 ساعة**. بعد انتهاء الصلاحية، تُعيد استدعاءات التأكيد وتعديل الصفوف `410 Preview expired`.
أنشئ معاينة جديدة للبدء من جديد.

يحمل كل صف حقل `action` بأحد أربعة قيم:

| الإجراء | المعنى |
|---|---|
| `create` | لم يُعثَر على مستخدم مطابق في أمل؛ سينشئ التأكيد حسابًا جديدًا |
| `update` | طابق مستخدمًا موجودًا في أمل دون حقول متعارضة؛ سيحدِّث التأكيد عضوية الفصل |
| `conflict` | طابق مستخدمًا موجودًا لكن بيانات نظام إدارة التعلم تختلف عن بيانات أمل (الاسم أو المرحلة الدراسية)؛ يجب حل التعارض قبل التأكيد |
| `skip` | الصف مستثنى عمدًا من المقارنة التفاضلية (مثال: هوية فرعية مكرَّرة) |

يتبع مطابقة الهوية سلسلة أولويات: أولًا عبر رابط الهوية الخارجي للموفِّر `(provider, externalSub)`،
ثم عبر مطابقة البريد الإلكتروني التامة داخل المؤسسة ذاتها. لا توجد مطابقة تلقائية تقريبية.

### الخطوة الثانية: حل التعارضات

يجب تحديد قرار لصفوف ذات `action = conflict` قبل التأكيد. استخدم
`PATCH /api/lms/preview/{previewId}/rows/{rowId}` بجسم `{ "resolution": "<value>" }`.

يُقبَل قيمتان للقرار:

| القيمة | الأثر عند التأكيد |
|---|---|
| `accept_lms` | يستبدل حقل أمل بقيمة نظام إدارة التعلم |
| `keep_amal` | يحتفظ بحقل أمل؛ يحدِّث عضوية الفصل فحسب |

أي قيمة أخرى تُنتج خطأ تحقق من Zod. اترك الصفوف التي لا تريد استيرادها دون قرار؛ سيرفض التأكيد بـ `422 Unresolved conflict rows` حتى يُحدَّد قرار لكل صف تعارض.

### الخطوة الثالثة: التأكيد

يُنشئ `POST /api/lms/commit/{previewId}` القائمة المُراجَعة بأكملها في **معاملة Postgres واحدة**:
تُطبَّق جميع عمليات الإنشاء والتحديث وصفوف التعارض المحلولة ذريًا أو لا تُطبَّق أيٌّ منها. يُعيد تأكيد ناجح `200 LmsCommitResponse` مع أعداد الصفوف التي جرى إنشاؤها وتحديثها وتخطّيها.

التأكيد متكافئ: إذا سبق تأكيد المعاينة ذاتها، يُعيد الرد `{ alreadyCommitted: true }` دون أي كتابة إضافية.

---

## المزامنة التلقائية (autoApplySimpleChanges)

يحمل كل اتصال علامة `autoApplySimpleChanges`. عند تشغيل جدولة cron أو مزامنة يدوية فورية، تتحكّم العلامة فيما يجري بعد إنشاء المقارنة التفاضلية:

- **العلامة مُغلَقة (الافتراضي):** تُقيَّد المعاينة دائمًا لمراجعة المسؤول، بصرف النظر عن محتوى المقارنة. يُعيَّن `LmsSyncRun.status` إلى `pending_admin_review`. لا تُكتَب أي صفوف أشخاص تلقائيًا.
- **العلامة مفتوحة، دون صفوف تعارض:** تُؤكَّد المقارنة التفاضلية تلقائيًا في معاملة واحدة. يُعيَّن `LmsSyncRun.status` إلى `success`.
- **العلامة مفتوحة، مع وجود صفوف تعارض:** لا يستطيع النظام حل التعارض تلقائيًا، فتُقيَّد المعاينة لمراجعة المسؤول تمامًا كما لو كانت العلامة مُغلَقة. يُعيَّن `LmsSyncRun.status` إلى `pending_admin_review`.

في جميع الحالات يُحدَّث `lastSyncAt` على الاتصال بعد تشغيل المزامنة. يُحدَّث `lastSuccessfulSyncAt` فقط عند نجاح التأكيد التلقائي.

---

## المزامنة الفورية اليدوية

لبدء مزامنة فورية خارج جدول cron، استدعِ
`POST /api/lms/connections/{id}/sync-now`. يُعيد المسار `202` بجسم
`{ syncRunId, status }` حيث `status` إحدى القيم: `completed`، أو `pending_admin_review`، أو `error`. يجب أن يكون الاتصال في حالة `active`؛ يُعيد اتصال في أي حالة أخرى `404`.

---

## إلغاء الرمز

عند إعادة الموفِّر `HTTP 401` أو `403` أثناء أي استدعاء لمهيّئ البيانات (جلب المقررات أو جلب القائمة)،
تتعامل خدمة المزامنة مع ذلك باعتباره إلغاءً للرمز. يحدث الآتي ذريًا:

1. يُعيَّن `LmsConnection.connectionStatus` إلى `revoked`.
2. يُعيَّن `LmsConnection.syncCronEnabled` إلى `false` (يُعطَّل cron).
3. يُرسَل إشعار `lms_connection_revoked` إلى **كل مسؤول نشط** في المؤسسة عبر نظام الإشعارات.
4. يُضاف صف `LmsSyncRun` بـ `status = error` و`errorMessage = token_revoked`.

تُلزِم إعادة الاتصال المسؤولَ بتشغيل تدفق OAuth من جديد (أو إعادة رفع مجموعة ملفات CSV لـ OneRoster) لاستعادة الاتصال إلى حالة `active` وإعادة تفعيل cron.

---

## سجل المزامنة

يُعيد `GET /api/lms/sync-runs` سجلات `LmsSyncRun` مُصفَّحة لمؤسسة المستدعي، الأحدث أولًا.
استخدم `?page=` و`?limit=` (الحد الأقصى 100) للتصفح. يسجِّل كل تشغيل ما إذا كان مُشغَّلًا بجدولة cron أو يدويًا، وما إذا جرى تطبيقه تلقائيًا، وأوقات البدء والانتهاء، وأعداد الصفوف، ورسالة الخطأ عند الاقتضاء.

---

## مرجع واجهة برمجة التطبيقات

تتطلب جميع المسارات جلسة موثَّقة. يُستَخلَص `organizationId` دائمًا من سياق الجلسة ولا يُقبَل في أجسام الطلبات أو معاملات المسار.

| الطريقة | المسار | الصلاحية | التجريبي | تُعيد |
|---|---|---|---|---|
| `GET` | `/api/lms/connections` | `MANAGE_LMS_CONNECTIONS` | مسموح | `200 LmsConnectionListResponse` |
| `POST` | `/api/lms/connections/{provider}/oauth-start` | `MANAGE_LMS_CONNECTIONS` | محظور | `200 LmsOAuthStartResponse` |
| `GET` | `/api/lms/connections/{provider}/oauth-callback` | `MANAGE_LMS_CONNECTIONS` | محظور | `200 LmsConnection` |
| `POST` | `/api/lms/connections/oneroster/upload` | `MANAGE_LMS_CONNECTIONS` | محظور | `201 LmsConnection` |
| `DELETE` | `/api/lms/connections/{id}` | `MANAGE_LMS_CONNECTIONS` | محظور | `200 { ok: true }` |
| `GET` | `/api/lms/{provider}/courses` | `MANAGE_LMS_CONNECTIONS` | مسموح | `200 LmsCoursesResponse` |
| `POST` | `/api/lms/{provider}/mappings` | `MANAGE_LMS_CONNECTIONS` | محظور | `200 LmsMappingResponse` |
| `POST` | `/api/lms/preview` | `MANAGE_ROSTER` | محظور | `200 LmsPreviewResponse` |
| `PATCH` | `/api/lms/preview/{previewId}/rows/{rowId}` | `MANAGE_ROSTER` | محظور | `200 LmsPreviewResponse` |
| `POST` | `/api/lms/commit/{previewId}` | `MANAGE_ROSTER` | محظور | `200 LmsCommitResponse` |
| `POST` | `/api/lms/connections/{id}/sync-now` | `MANAGE_ROSTER` | محظور | `202 LmsSyncNowResponse` |
| `GET` | `/api/lms/sync-runs` | `MANAGE_LMS_CONNECTIONS` | مسموح | `200 LmsSyncRunsResponse` |

للاطلاع على تعريفات المخططات الكاملة راجع [مرجع واجهة برمجة التطبيقات](https://docs.growamal.com/ar/api-reference/).

---

## أدلة ذات صلة

- [إنشاء المستخدمين](https://docs.growamal.com/ar/guides/provision-users/): إنشاء المستخدمين يدويًا وتسجيلهم في الفصول
- [الفصول والقوائم](https://docs.growamal.com/ar/guides/classes-and-rosters/): كيف تُحدِّد عضوية الفصل تفويض المعلم
- [مركز الإشعارات](https://docs.growamal.com/ar/guides/notifications/): المكان الذي تظهر فيه تنبيهات `lms_connection_revoked`
