طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 302 · من الفكرة إلى المنتج (٢): نظام حجز مواعيد
الدرس 4 · الوحدة 2: بناء الخادم والواجهة البرمجية

المحطّة ٤: التحقّق من المدخلات ومنع تعارض الحجز

المتطلّب الحرج ينفّذ: تحقّقٌ صارمٌ ومنعٌ للحجز المزدوج. ⏱ نحو 30 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تتحقّق من المدخلات وترفض الناقص برمز 422.
  • تمنع تعارض الحجز برمز 409.
  • ترتّب بوّابات التحقّق قبل تعديل البيانات.
  • تبني الإلغاء الذي يعيد الموعد متاحًا.

١ثلاث بوّاباتٍ قبل أيّ حجز

الحجز ليس مجرّد «اكتب الاسم في الموعد». قبل أن نلمس البيانات نمرّ على ثلاث بوّابات، وترتيبها مهمّ:

  1. موجود؟ إن لم يوجد الموعد ← 404.
  2. مدخلٌ صالح؟ إن لم يُرسَل اسم المريض ← 422 (تحقّق المدخلات).
  3. متاح؟ إن كان محجوزًا أصلًا ← 409 (منع التعارض).

فقط بعد اجتياز الثلاث نحجز. هذا هو منع التعارض الذي حدّدناه متطلّبًا حرجًا في المحطّة الأولى — ونراه هنا سطرًا واحدًا: if (a.bookedBy !== null) return 409.

٢الحجز يعمل

جرّب السيناريو كاملًا: نحجز الموعد المتاح 1، ثم نحاول حجزه ثانيةً (تعارض)، ثم نحجز بلا اسمٍ (مدخلٌ ناقص)، ثم نحجز موعدًا غير موجود:

const express = require('express');const app = express();app.use(express.json()); let appts = [  { id: 1, doctor: 'د. منى',  time: '09:00', bookedBy: null },  { id: 2, doctor: 'د. منى',  time: '09:30', bookedBy: 'سارة' },  { id: 3, doctor: 'د. أحمد', time: '10:00', bookedBy: null },];const find = (id) => appts.find(a => a.id === Number(id));const notFound = (res) => res.status(404).json({ error: { code: 'NOT_FOUND', message: 'لا يوجد موعد' } }); app.post('/api/appointments/:id/book', (req, res) => {  const a = find(req.params.id);  if (!a) return notFound(res);  if (!req.body.patient || !req.body.patient.trim())    return res.status(422).json({ error: { code: 'VALIDATION', message: 'اسم المريض مطلوب' } });  if (a.bookedBy !== null)    return res.status(409).json({ error: { code: 'CONFLICT', message: 'الموعد محجوز' } });  a.bookedBy = req.body.patient.trim();  res.json({ data: a });}); app.listen(3000);

٣الإلغاء يفتح الموعد من جديد

الإلغاء بسيط: نعيد bookedBy إلى null، فيعود الموعد متاحًا للجميع:

app.post('/api/appointments/:id/cancel', (req, res) => {  const a = find(req.params.id);  if (!a) return notFound(res);  a.bookedBy = null;  res.json({ data: a });});

توقّع وتحقّق

ترتيب البوّابات الصحيح قبل الحجز:

ما رمز حجز موعدٍ محجوزٍ أصلًا؟

ما رمز الحجز بلا اسم مريض؟

مدخلٌ غير صالح.

كيف يعود الموعد متاحًا عند الإلغاء؟

الخلاصة

تحدٍّ برمجي: الحجز والإلغاء ومنع التعارض

برمجي

أكمِل قلب النظام: مسار حجزٍ يمرّ بالبوّابات الثلاث (وجود، تحقّق، تعارض)، ومسار إلغاءٍ يفتح الموعد. هذا تنفيذ متطلّب «لا يُحجَز موعدٌ مرّتين».

المطلوب:
  • POST /:id/book بجسم {"patient": …}: 404 إن لم يوجد، 422 إن لا اسم، 409 إن محجوز، وإلّا 200 مع {"data": …} وbookedBy = الاسم.
  • POST /:id/cancel: يعيد bookedBy إلى null ويرد 200، أو 404 إن لم يوجد.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. POST /api/appointments/1/book {"patient": "سعود"}
  2. POST /api/appointments/1/book {"patient": "خالد"}
  3. POST /api/appointments/3/book {}
  4. POST /api/appointments/99/book {"patient": "نورة"}
  5. POST /api/appointments/1/cancel {}
  6. POST /api/appointments/1/book {"patient": "خالد"}
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • الحجز المتاح ← 200
  • … ويُسجّل اسم الحاجز
  • حجز المحجوز ← 409
  • … برمز CONFLICT
  • الحجز بلا اسم ← 422
  • … برمز VALIDATION
  • حجز غير الموجود ← 404
  • الإلغاء يفتح الموعد (bookedBy=null)
  • بعد الإلغاء يُحجَز من جديد

💡 تلميح 1
الترتيب: تحقّق الوجود (404) ثم المدخل (422) ثم التعارض (409) — قبل أيّ تعديل.
💡 تلميح 2
استخدم .trim() وتحقّق a.bookedBy !== null للتعارض.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

const express = require('express');const app = express();app.use(express.json()); let appts = [  { id: 1, doctor: 'د. منى',  time: '09:00', bookedBy: null },  { id: 2, doctor: 'د. منى',  time: '09:30', bookedBy: 'سارة' },  { id: 3, doctor: 'د. أحمد', time: '10:00', bookedBy: null },];const find = (id) => appts.find(a => a.id === Number(id));const notFound = (res) => res.status(404).json({ error: { code: 'NOT_FOUND', message: 'لا يوجد موعد' } }); app.post('/api/appointments/:id/book', (req, res) => {  const a = find(req.params.id);  if (!a) return notFound(res);  if (!req.body.patient || !req.body.patient.trim())    return res.status(422).json({ error: { code: 'VALIDATION', message: 'اسم المريض مطلوب' } });  if (a.bookedBy !== null)    return res.status(409).json({ error: { code: 'CONFLICT', message: 'الموعد محجوز' } });  a.bookedBy = req.body.patient.trim();  res.json({ data: a });}); app.post('/api/appointments/:id/cancel', (req, res) => {  const a = find(req.params.id);  if (!a) return notFound(res);  a.bookedBy = null;  res.json({ data: a });}); app.listen(3000);

🐞 لاحظت خطأً في هذا الدرس؟ أبلغني بالبريد

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس