MongoDB: مشروع شامل
آخر تحديث: 2026-08-26
تُعد المشاريع المتكاملة أفضل وسيلة لتقييم نتائج التعلم — حيث يعمل هذا المقرر على بناء نظام شامل لمراجعة التجارة الإلكترونية من خلال ست وحدات دراسية، يربط بين جميع المفاهيم التي تمت تغطيتها في المراحل من 1 إلى 5.
1. نظرة عامة على المشروع
المشروع: نظام تقييم ShopHub البنية: Node.js + Express + Mongoose + MongoDB + JWT الميزات: مصادقة المستخدمين، إدارة المنتجات، التعليقات المتداخلة، التحليلات المجمعة، التحكم في الأذونات، النشر عبر Atlas
نهج تصميم بنية النظام: يعتمد نظام مراجعة التجارة الإلكترونية على بنية كلاسيكية ثلاثية المستويات — طبقة التوجيه (تعيين عناوين URL + البرمجيات الوسيطة)، وطبقة الأعمال (وحدة التحكم التي تتولى منطق معالجة الطلبات)، وطبقة البيانات (نموذج Mongoose الذي يحدد هياكل البيانات والتحقق من صحتها). ويتيح الفصل بين هذه الطبقات الثلاث اختبار كل طبقة وتعديلها بشكل مستقل: فلا يؤثر تغيير مسارات واجهة برمجة التطبيقات (API) على منطق الأعمال، كما لا يؤثر تغيير طرق الاستعلام على تعريفات التوجيه.
عملية اتخاذ القرار بشأن اختيار التصميم المعماري:
| نقطة اتخاذ القرار | الخيار أ | الخيار ب | الاختيار | السبب |
|---|---|---|---|---|
| أطر عمل الويب | Express | Koa / Fastify | Express | تتمتع بأكثر النظم البيئية نضجًا وبأكبر عدد من موارد الدروس التعليمية |
| ODM | Mongoose | برنامج تشغيل أصلي | Mongoose | التحقق من صحة المخطط + البرامج الوسيطة + التعبئة |
| أنظمة المصادقة | JWT | الجلسة | JWT | لا تعتمد على الحالة، قابلة للتوسع بسهولة، مناسبة لواجهات برمجة التطبيقات |
| بنية التعليقات | المستندات المتداخلة | وضع الاقتباس | وضع الاقتباس (parentId) | عمق مرن، مع تجنب قيود التداخل |
| منصة النشر | Atlas + Render | استضافة ذاتية | Atlas + Render | خدمة مُدارة توفر تكاليف التشغيل والصيانة، ومناسبة للمشاريع الصغيرة والمتوسطة |
قرارات نمذجة البيانات: اختار نظام التعليقات نموذجًا مرجعيًّا (حيث يشير parentId إلى التعليق الأصلي) بدلاً من المستندات المتداخلة — فالوثائق المتداخلة تخضع لحد BSON البالغ 16 ميغابايت، كما أن الردود المتداخلة بعمق يصعب الاستعلام عنها وتقسيمها إلى صفحات. وعلى الرغم من أن النموذج المرجعي يتطلب استعلامات إضافية لتجميع البنية الشجرية، فإنه يدعم مستويات غير محدودة وفرزًا مرنًا.
graph TB
Client[Browser/Mobile App] -->|HTTP| Express[Express Server]
subgraph "Express Routing Layer"
Express --> AuthMW[auth Routing<br/>Register/Log In]
Express --> ProductMW[products Routing<br/>CRUD]
Express --> ReviewMW[reviews Routing<br/>Comments/Likes]
end
subgraph "Controller Layer"
AuthMW --> AuthCtrl[authController]
ProductMW --> ProdCtrl[productController]
ReviewMW --> RevCtrl[reviewController]
end
subgraph "Model Layer"
AuthCtrl --> UserModel[User Model]
ProdCtrl --> ProdModel[Product Model]
RevCtrl --> RevModel[Review Model]
end
subgraph "MongoDB Gathering"
UserModel --> Users[(users)]
ProdModel --> Products[(products)]
RevModel --> Reviews[(reviews)]
end
style Express fill:#d4edda
style Reviews fill:#cce5ff
علاقات نموذج البيانات:
erDiagram
USER ||--o{ REVIEW : "writes"
PRODUCT ||--o{ REVIEW : "has"
REVIEW ||--o{ REVIEW : "parent reply"
USER {
ObjectId _id PK
string email UK
string username UK
string passwordHash
string role
boolean isActive
}
PRODUCT {
ObjectId _id PK
string sku UK
string title
number price
string category
number rating
number reviewCount
}
REVIEW {
ObjectId _id PK
ObjectId productId FK
ObjectId userId FK
string content
number rating
ObjectId parentId FK
number likeCount
boolean isApproved
}
2. الوحدة 1: بدء المشروع
شرح مفصل لعملية اختيار البنية: لا يقتصر اختيار التكنولوجيا المناسبة لنظام المراجعة في التجارة الإلكترونية على «الاعتماد على ما هو شائع»، بل يتعلق بإيجاد الحل الأمثل بناءً على القيود — 1. اختيار Express بدلاً من NestJS: بالنسبة للمشاريع الصغيرة والمتوسطة الحجم، تضيف زخارف NestJS وحقن التبعية (DI) تعقيدًا دون تقديم فوائد إضافية؛ 2. اختر Mongoose بدلاً من برنامج التشغيل الأصلي: تعد آليات التحقق من صحة المخطط والبرمجيات الوسيطة أمراً بالغ الأهمية لنظام التقييم (تجزئة كلمة المرور، وتصفية الحذف المؤقت)؛ 3. اختر JWT بدلاً من الجلسات: تتطلب خدمات واجهة برمجة التطبيقات (API) تشغيلًا بدون حالة؛ بينما تتطلب الجلسات تخزين Redis مشتركًا، مما يزيد من التكاليف التشغيلية؛ 4. اختر النموذج المرجعي بدلاً من المستندات المتداخلة: لا يمكن التنبؤ بعدد التقييمات، كما أن المستندات المتداخلة تنطوي على خطر الوصول إلى الحد الأقصى البالغ 16 ميغابايت.
مبادئ تفاعل الوحدات: تشكل التبعيات بين الوحدات الست تسلسلاً هرميًا واضحًا — حيث توفر الوحدة 1 (التهيئة) البنية التحتية؛ وتحدد الوحدة 2 (النموذج) عقد البيانات؛ وتنفذ الوحدة 3 (CRUD) منطق الأعمال؛ وتضيف الوحدة 4 (التجميع) قدرات تحليلية؛ وتعزز الوحدة 5 (الأمان) الحماية؛ وتكمل الوحدة 6 (النشر) عملية النشر. تعتمد كل وحدة فقط على الوحدة التي تسبقها ولا توجد لها تبعيات إلى الوراء، مما يسمح بمواصلة التطوير بشكل تكراري — حيث يتم أولاً إكمال الوحدات 1–3 للحصول على واجهة برمجة تطبيقات (API) قابلة للاستخدام، ثم إضافة التجميع والأمن والنشر تدريجيًا.
مبادئ التصميم لبدء المشروع: لا يقتصر بدء المشروع على مجرد تشغيل الأمر "npm init" فحسب؛ بل هو أيضًا عملية لتحديد بنية المشروع، واختيار التبعيات، ووضع استراتيجيات لإدارة التكوين. يجب أن يعكس المشروع ذو البنية الجيدة بنية MVC: تعريفات البيانات في models/، والمنطق التجاري في controllers/، وتعيينات المسارات في routes/، والمسائل الشاملة (مثل المصادقة ومعالجة الأخطاء) في middlewares/.
مبادئ تصميم هياكل الدلائل:
| جدول المحتويات | المسؤوليات | توجيهات التبعية | استراتيجية الاختبار |
|---|---|---|---|
| النماذج/ | تعريف البيانات + التحقق من صحتها | عدم وجود تبعيات خارجية | اختبارات الوحدة |
| وحدات التحكم/ | معالجة الطلبات + التنسيق | تعتمد على النماذج | اختبار التكامل |
| المسارات/ | تخطيط عناوين URL → وحدات التحكم | التبعيات: وحدات التحكم + برامج الوساطة | اختبار المسارات |
| البرامج الوسيطة/ | المصادقة/التفويض/معالجة الأخطاء | تكوين التبعيات | الاختبار الوحدوي |
| أدوات التحقق | تعريف مخطط joi | عدم وجود تبعيات خارجية | اختبارات الوحدة |
| utils/ | وظائف مساعدة | لا توجد تبعيات خارجية | اختبارات الوحدة |
أسباب اختيار التبعيات:
| التبعية | الغرض | لماذا تختاره |
|---|---|---|
| Express | إطار عمل ويب | الأكثر نضجًا، مع نظام بيئي غني للبرمجيات الوسيطة |
| Mongoose | ODM | التحقق من صحة المخطط + التعبئة + البرمجيات الوسيطة |
| jsonwebtoken | مصادقة JWT | مصادقة بدون حالة، مناسبة لواجهات برمجة التطبيقات |
| bcrypt | تجزئة كلمة المرور | معيار صناعي، مقاوم لجداول قوس قزح |
| joi | التحقق من صحة المدخلات | على غرار المخطط، قابل لإعادة الاستخدام |
| dotenv | متغيرات البيئة | إرشادات تطبيقات الـ 12 عاملاً |
| cors | عبر الأصول | أساسيات واجهة برمجة التطبيقات |
(1) هيكل المشروع
shophub-reviews/
├── package.json
├── .env
├── .env.example
├── src/
│ ├── app.js # Express Applications
│ ├── config/
│ │ └── db.js # mongoose Connect
│ ├── models/ # Data Model
│ │ ├── User.js
│ │ ├── Product.js
│ │ └── Review.js
│ ├── controllers/ # Business Logic
│ │ ├── authController.js
│ │ ├── productController.js
│ │ └── reviewController.js
│ ├── routes/ # Routing
│ │ ├── auth.js
│ │ ├── products.js
│ │ └── reviews.js
│ ├── middlewares/ # Middleware
│ │ ├── auth.js # JWT Certification
│ │ ├── errorHandler.js
│ │ └── validate.js
│ ├── validators/ # Request for a Trial Certificate
│ │ └── schemas.js
│ └── utils/ # Tools
│ ├── logger.js
│ └── jwt.js
└── README.md
(2) package.json
{
"name": "shophub-reviews",
"version": "1.0.0",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest --watch"
},
"dependencies": {
"express": "^4.19.2",
"mongoose": "^7.6.0",
"jsonwebtoken": "^9.0.0",
"bcrypt": "^5.1.0",
"joi": "^17.13.0",
"dotenv": "^16.4.0",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.1.0",
"jest": "^29.7.0"
}
}
(3) ملف .env
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/shophub
JWT_SECRET=your-super-secret-key-change-in-prod
JWT_EXPIRES_IN=7d
3. الوحدة 2: نماذج المستخدم والمنتج
مبادئ تصميم نموذج البيانات: لا يقتصر تصميم المخطط على مجرد «نسخ حقول الجدول إلى Mongoose»؛ بل يتطلب مراعاة: أنماط الاستعلامات (ما هي الاستعلامات الأكثر شيوعًا؟)، وعلاقات البيانات (ما هي الكيانات التي يجب ربطها ببعضها؟)، ومتطلبات الأداء (هل هناك حاجة إلى الفهارس؟ هل يجب تعيين select:false لحقول معينة؟)، ومتطلبات الأمان (هل يجب إخفاء كلمات المرور؟ هل هناك حاجة إلى الحذف المؤقت؟).
قرارات تصميم المخطط:
| قرارات التصميم | الاختيار | السبب |
|---|---|---|
| تخزين كلمة المرور | passwordHash + select:false | لا يتم إرجاعها في الاستعلامات الافتراضية لمنع الكشف عنها |
| تجزئة كلمة المرور | برمجيات وسيطة قبل الحفظ + bcrypt | تجزئة تلقائية؛ شفافة بالنسبة لمنطق الأعمال |
| تصميم الأدوار | قائمة التعداد + RBAC | ثلاثة أدوار: عميل/مسؤول/مشرف |
| تقييم المنتج | الحقول المكررة: التقييم + عدد التعليقات | تجنب حساب المجموع في كل مرة |
| بنية التعليق | مرجع parentId | يدعم مستويات تداخل غير محدودة |
| الحذف المؤقت | isDeleted + التصفية المسبقة | البيانات قابلة للاستعادة؛ متطلبات الامتثال |
| الطوابع الزمنية | timestamps: true | الإدارة التلقائية لـ createdAt/updatedAt |
علاقات النموذج واتجاهات الإشارة: تشير «المراجعة» إلى «مستخدم» و«منتج» (علاقة «كثير إلى واحد»)، كما تشير «المراجعة» إلى نفسها (تتيح الإشارة الذاتية إمكانية التداخل). واتجاه الإشارة هو «الجانب الذي يمثل علاقة "كثير إلى واحد" يشير إلى الجانب الذي يمثل علاقة "واحد إلى كثير"» — أي أنه بدلاً من تضمين مصفوفة من «المراجعات» داخل «المنتج» (مما قد يؤدي إلى نمو لا نهائي)، تقوم «المراجعة» بتخزين معرّف المنتج (productId).
graph LR
User1[Alice] -->|userId| R1[Review: "Great!"]
User2[Bob] -->|userId| R2[Review: "Good"]
User3[Charlie] -->|userId| R3[Reply: "Thanks!"]
Prod1[Product: Phone] -->|productId| R1
Prod1 -->|productId| R2
R1 -->|parentId| R3
style User1 fill:#cce5ff
style Prod1 fill:#d4edda
style R1 fill:#fff3cd
(1) نموذج المستخدم (بما في ذلك البرمجيات الوسيطة لتشفير كلمات المرور)
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
},
username: {
type: String,
required: true,
unique: true,
minlength: 3,
maxlength: 30,
match: [/^[a-zA-Z0-9_]+$/, 'Alphanumeric + underscore only']
},
passwordHash: {
type: String,
required: true,
minlength: 60, // bcrypt hash Length
select: false
},
role: {
type: String,
enum: ['customer', 'admin', 'moderator'],
default: 'customer'
},
isActive: { type: Boolean, default: true }
}, { timestamps: true });
// Password Hashing Middleware
UserSchema.pre('save', async function(next) {
if (!this.isModified('passwordHash')) return next();
this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
next();
});
// Confirm Password
UserSchema.methods.comparePassword = function(candidate) {
return bcrypt.compare(candidate, this.passwordHash);
};
module.exports = mongoose.model('User', UserSchema);
(2) طراز المنتج
// models/Product.js
const ProductSchema = new mongoose.Schema({
sku: { type: String, required: true, unique: true, index: true },
title: { type: String, required: true, maxlength: 200 },
description: { type: String, maxlength: 5000 },
price: { type: mongoose.Schema.Types.Decimal128, required: true, min: 0 },
category: { type: String, enum: ['Electronics', 'Books', 'Clothing', 'Home'], index: true },
stock: { type: Number, default: 0, min: 0 },
rating: { type: Number, default: 0, min: 0, max: 5 },
reviewCount: { type: Number, default: 0 },
isActive: { type: Boolean, default: true, index: true }
}, { timestamps: true });
ProductSchema.index({ category: 1, price: -1 });
ProductSchema.index({ title: 'text', description: 'text' });
module.exports = mongoose.model('Product', ProductSchema);
(3) نموذج المراجعة (بما في ذلك الردود المتداخلة)
// models/Review.js
const ReviewSchema = new mongoose.Schema({
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true, index: true },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
content: { type: String, required: true, maxlength: 1000 },
rating: { type: Number, required: true, min: 1, max: 5 },
parentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Review', default: null, index: true },
likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
likeCount: { type: Number, default: 0 },
isApproved: { type: Boolean, default: true },
isDeleted: { type: Boolean, default: false }
}, { timestamps: true });
ReviewSchema.index({ productId: 1, createdAt: -1 });
ReviewSchema.pre(/^find/, function(next) {
this.where({ isDeleted: { $ne: true } });
next();
});
module.exports = mongoose.model('Review', ReviewSchema);
4. الوحدة 3: واجهة برمجة تطبيقات CRUD للتعليقات (التعليقات المتداخلة)
ملاحظات إضافية حول مبادئ تصميم التعليقات المتداخلة: تتطلب استراتيجية الاستعلام الخاصة بالنموذج المرجعي (parentId) تحقيق التوازن بين «عدد الاستعلامات» و«سلامة البيانات» — حيث تتطلب طريقة الاستعلامين (التعليقات من المستوى الأعلى + الردود) عمليتي إدخال/إخراج فقط من قاعدة البيانات، لكنها لا تدعم سوى مستويين من التداخل؛ في حين تدعم طريقة الاستعلام التكراري ($graphLookup) عددًا غير محدود من المستويات، لكن أداءها ضعيف. يختار هذا النظام طريقة الاستعلامين مقترنة بالتجميع داخل الذاكرة للأسباب التالية: 1. يكتفي معظم المستخدمين بعرض مستويين فقط من التعليقات (المستوى الأعلى + الردود)؛ 2. يمكن تحميل الردود الأعمق عند الطلب عبر زر «عرض المزيد من الردود»؛ 3. يتفوق أداء طريقة الاستعلامين بشكل كبير على أداء الاستعلام التكراري.
تصميم سياسة أمان التعليقات: تواجه أنظمة التعليقات ثلاثة أنواع من التهديدات الأمنية — 1. أمان المحتوى: التعليقات غير المرغوب فيها، والكلمات المفتاحية الحساسة، والروابط الإعلانية (الحماية: تحديد معدل الاستخدام + تصفية الكلمات المفتاحية الحساسة + المراقبة اليدوية)؛ 2. أمن الأذونات: قيام غير المؤلفين بتحرير أو حذف تعليقات الآخرين (الحماية: المصادقة + مطابقة معرف المستخدم)؛ 3. أمن الحقن: حقن $where، و$regex DoS (الحماية: الاستعلامات المعلمة + الهروب من التعبيرات النمطية). يجب تنفيذ الإجراءات الأمنية بشكل موحد في طبقة البرمجيات الوسيطة، بدلاً من تكرارها في كل وحدة تحكم.
graph TB
subgraph "Nested Comments (Threaded Comments)"
A[Review<br/>_id: ObjectId] --> B[Reply 1<br/>parentId: A._id]
A --> C[Reply 2<br/>parentId: A._id]
B --> D[Reply to Reply<br/>parentId: B._id]
C --> E[Reply to Reply<br/>parentId: C._id]
end
style A fill:#d4edda
style B fill:#fff3cd
style C fill:#fff3cd
style D fill:#f8d7da
style E fill:#f8d7da
يكمن التحدي الأساسي للتعليقات المتداخلة (التعليقات المتسلسلة) في كيفية تمثيل علاقة «الرد». هناك نهجان: (1) المستندات المضمنة (تضمين مصفوفة الردود داخل «Review») — وهي طريقة بسيطة لكنها مقيدة بحد BSON البالغ 16 ميغابايت، كما يصعب ترقيم صفحات الردود المتداخلة؛ (2) نمط الإشارة (يشير «parentId» إلى التعليق الأصلي) — وهو نهج مرن، ويدعم التداخل غير المحدود، ويدعم ترقيم الصفحات، لكنه يتطلب استعلامات إضافية لتجميع البنية الشجرية. يستخدم هذا المشروع نمط الإشارة.
استراتيجية الاستعلام عن التعليقات المتداخلة:
sequenceDiagram
Client Participant
participant API as /products/:id/reviews
participant DB as MongoDB
Client->>API: GET /products/123/reviews
API->>DB: Query top-level comments (parentId=null)
DB-->>API: Returns [R1, R2, R3]
API->>DB: Query all replies (parentId in [R1, R2, R3]._id)
DB-->>API: Returns [R1.1, R1.2, R2.1]
API->>API: Building a tree structure<br/>R1.replies=[R1.1, R1.2]<br/>R2.replies=[R2.1]
API-->>Client: Returns tree-structured data
مراجعة تصميم واجهة برمجة التطبيقات (API) لعمليات CRUD:
| العملية | نقطة النهاية | الطريقة | المصادقة | القواعد التجارية |
|---|---|---|---|---|
| عرض التقييمات | /products/:id/reviews | GET | لا | ترقيم الصفحات + الردود المتداخلة |
| مراجعة المنشور | /products/:id/reviews | منشور | نعم | التحقق من وجود المنتج + التقييم من 1 إلى 5 |
| نشر رد | /products/:id/reviews | نشر | نعم | التحقق من وجود التقييم الأصلي |
| تعديل المراجعة | /reviews/:id | PUT | نعم (المؤلف) | يمكن للمؤلف فقط تعديلها |
| حذف التعليق | /reviews/:id | حذف | نعم (المؤلف/المسؤول) | الحذف المؤقت isDeleted |
| الإعجاب/إلغاء الإعجاب | /reviews/:id/like | POST | نعم | التبديل باستخدام $addToSet/$pull |
تستخدم ميزة «الإعجاب» الدالتين $addToSet (إضافة متكررة) و$pull (إزالة) بدلاً من الدالة push (التي قد تؤدي إلى تكرار الإعجابات). كما تحتفظ بحقل إضافي يُسمى likeCount لتجنب إعادة حساب عدد الإعجابات في كل مرة.
(1) إنشاء تقييم
// controllers/reviewController.js
exports.createReview = async (req, res) => {
const { productId } = req.params;
const { content, rating, parentId } = req.body;
// Verify that the product exists
const product = await Product.findById(productId);
if (!product) return res.status(404).json({ error: 'Product not found' });
// If this is a reply, verify that the parent comment exists
if (parentId) {
const parent = await Review.findById(parentId);
if (!parent) return res.status(404).json({ error: 'Parent review not found' });
}
const review = await Review.create({
productId,
userId: req.user._id,
content,
rating,
parentId: parentId || null
});
// Update product rating statistics
await updateProductRating(productId);
await review.populate('userId', 'username avatar');
res.status(201).json(review);
};
(2) الاستعلام المتداخل
exports.listReviews = async (req, res) => {
const { productId } = req.params;
const { sort = 'createdAt', order = 'desc', limit = 20, page = 1 } = req.query;
// Query top-level comments
const reviews = await Review.find({
productId,
parentId: null
})
.populate('userId', 'username avatar')
.sort({ [sort]: order === 'desc' ? -1 : 1 })
.limit(limit * 1)
.skip((page - 1) * limit)
Continue
// View all replies
const reviewIds = reviews.map(r => r._id);
const replies = await Review.find({
parentId: { $in: reviewIds }
})
.populate('userId', 'username avatar')
.sort({ createdAt: 1 })
Continue
// Build a tree structure
const tree = reviews.map(parent => ({
...parent,
replies: replies.filter(r => r.parentId.toString() === parent._id.toString())
}));
res.json({ data: tree, page, limit });
};
(3) ميزة «الإعجاب»
exports.likeReview = async (req, res) => {
const { reviewId } = req.params;
const userId = req.user._id;
const review = await Review.findById(reviewId);
if (!review) return res.status(404).json({ error: 'Review not found' });
const alreadyLiked = review.likes.some(id => id.toString() === userId.toString());
if (alreadyLiked) {
await Review.updateOne(
{ _id: reviewId },
{ $pull: { likes: userId }, $inc: { likeCount: -1 } }
);
return res.json({ liked: false });
} else {
await Review.updateOne(
{ _id: reviewId },
{ $addToSet: { likes: userId }, $inc: { likeCount: 1 } }
);
return res.json({ liked: true });
}
};
5. الوحدة 4: تحليل التجميع
تفاصيل نمط تصميم مسار التجميع: تنقسم متطلبات التجميع في نظام المراجعات إلى ثلاث فئات: 1. الإحصائيات أحادية البعد (توزيع التقييمات، عدد المراجعات): مسار واحد باستخدام $match + $group/$bucket؛ 2. الإحصائيات المتوازية متعددة الأبعاد (نظرة عامة على صفحة تفاصيل المنتج + التوزيع + المراجعات الحديثة): يجب استخدام $facet لإرجاع النتائج في استعلام واحد؛ 3. الإحصائيات المرتبطة عبر المجموعات (تصنيفات المنتجات الشائعة مع معلومات المنتج): مزيج من $group + $lookup + $unwind. مبدأ التصميم: أولاً $match لتصفية البيانات وتقليل حجمها، ثم $group للتجميع، وأخيراً $sort/$limit للفرز وتحديد العدد.
استراتيجية تصميم الأنابيب الفرعية في $facet: يجب أن تكون كل أنبوب فرعي في $facet بسيطًا قدر الإمكان: 1. لا يحتاج الأنبوب الفرعي الخاص بالعدد الإجمالي سوى إلى $count، مع الحد الأدنى من الأعباء الإضافية؛ 2. تستخدم خط الأنابيب الفرعي الخاص بـ "المتوسط" $group({ _id: null })، مما ينتج مخرجات واحدة؛ 3. يستخدم خط الأنابيب الفرعي الخاص بـ "التوزيع" $group({ _id: '$rating' })، مع عدد مخرجات يساوي حجم مجال القيمة؛ 4. يتطلب خط الأنابيب الفرعي الخاص بـ "القائمة" $sort + $limit + $lookup، مع أعلى تكلفة إضافية، ويجب وضعه في النهاية. لا يؤثر ترتيب خطوط الأنابيب الفرعية على التنفيذ (المتوازي)، ولكنه يؤثر على قابلية قراءة الكود — يُنصح بترتيبها من الأقل تكلفة إلى الأكثر تكلفة.
graph LR
subgraph "Aggregation Pipeline"
A[Raw Data<br/>1M reviews] -->|"$match"| B[Filtered Data<br/>100K reviews]
B -->|"$group"| C[Aggregated<br/>grouped]
C -->|"$sort + $limit"| D[Top Results<br/>top 10]
end
style A fill:#f8d7da
style B fill:#fff3cd
style C fill:#d4edda
style D fill:#d4edda
يحتاج نظام التقييم إلى إحصائيات متعددة الأبعاد — توزيع التقييمات (عدد التقييمات ذات 5 نجوم/4 نجوم)، ومتوسط التقييم، وتصنيفات المنتجات الأكثر شعبية، واتجاهات التقييمات الحديثة. إذا تم حساب هذه الإحصائيات في كود طبقة التطبيق، فسيتعين نقل كميات كبيرة من البيانات إلى Node.js لمعالجتها؛ أما باستخدام مسارات التجميع، فيتم إجراء الحساب في طبقة قاعدة البيانات ولا يتم إرجاع سوى النتائج — ويمكن أن يصل الفرق في الأداء إلى 100 ضعف.
نمط تصميم المجمع:
| المتطلبات الإحصائية | مرحلة التجميع | النتائج |
|---|---|---|
| توزيع النتائج | $match → $bucket | [{_id: 5, count: 120}, ...] |
| إحصائيات المنتج | $match → $facet | {الإجمالي، متوسط التقييم، التوزيع، الأحدث} |
| المنتجات الأكثر رواجًا | $match → $group → $sort → $lookup → $limit | [{productId, avgRating, reviewCount}] |
قيمة $facet: تتيح $facet تشغيل عدة مسارات تجميع بالتوازي على نفس المدخلات — حيث يُرجع استعلام واحد أربعة أبعاد: total، و avgRating، و ratingDistribution، و recentReviews. وبدون $facet، سيكون من الضروري إجراء أربعة استعلامات منفصلة.
graph TB
Input[Comment Data Stream] --> Facet["$facet<br/>Multi-channel parallel processing"]
Facet --> P1["Pipeline1: $count<br/>Total"]
Facet --> P2["Pipeline2: $group<br/>Average Rating"]
Facet --> P3["Pipeline3: $group + $sort<br/>Score Distribution"]
Facet --> P4["Pipeline4: $sort + $limit + $lookup<br/>Recent Comments(Contains user information)"]
P1 --> Output[Combined Output<br/>{total, avg, distribution, recent}]
P2 --> Output
P3 --> Output
P4 --> Output
style Facet fill:#d4edda
style Output fill:#cce5ff
كيفية عمل توزيع الدرجات في $bucket: يقسم $bucket الدرجات إلى مجموعات بناءً على حدود — حيث تمثل الحدود = [1,2,3,4,5,6] خمس مجموعات: [1,2)، [2,3)، [3,4)، [4,5)، [5,6). ويتم حساب العدد والمتوسط لكل مجموعة.
(1) توزيع الدرجات ($bucket)
exports.getRatingDistribution = async (req, res) => {
const { productId } = req.params;
const distribution = await Review.aggregate([
{ $match: { productId: new mongoose.Types.ObjectId(productId), parentId: null } },
{
$bucket: {
groupBy: '$rating',
boundaries: [1, 2, 3, 4, 5, 6],
default: 'Other',
output: {
count: { $sum: 1 },
avgHelpful: { $avg: '$likeCount' }
}
}
}
]);
res.json({ data: distribution });
};
(2) إحصائيات المنتج ($facet)
exports.getProductStats = async (req, res) => {
const { productId } = req.params;
const stats = await Review.aggregate([
{ $match: { productId: new mongoose.Types.ObjectId(productId), parentId: null } },
{
$facet: {
total: [{ $count: 'count' }],
avgRating: [{ $group: { _id: null, avg: { $avg: '$rating' } } }],
ratingDistribution: [
{ $group: { _id: '$rating', count: { $sum: 1 } } },
{ $sort: { _id: 1 } }
],
recentReviews: [
{ $sort: { createdAt: -1 } },
{ $limit: 5 },
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'userInfo'
}
},
{ $unwind: '$userInfo' },
{
$project: {
content: 1,
rating: 1,
createdAt: 1,
username: '$userInfo.username',
avatar: '$userInfo.avatar'
}
}
]
}
}
]);
res.json(stats[0]);
};
(3) أفضل المنتجات
exports.getTopProducts = async (req, res) => {
const { limit = 10 } = req.query;
const topProducts = await Review.aggregate([
{ $match: { parentId: null, isApproved: true } },
{
$group: {
_id: '$productId',
avgRating: { $avg: '$rating' },
reviewCount: { $sum: 1 },
totalLikes: { $sum: '$likeCount' }
}
},
{ $sort: { avgRating: -1, reviewCount: -1 } },
{ $limit: limit * 1 },
{
$lookup: {
from: 'products',
localField: '_id',
foreignField: '_id',
as: 'product'
}
},
{ $unwind: '$product' },
{
$project: {
sku: '$product.sku',
title: '$product.title',
thumbnail: '$product.thumbnail',
avgRating: 1,
reviewCount: 1
}
}
]);
res.json({ data: topProducts });
};
6. الوحدة 5: الأذونات والأمان
شرح مفصل لمبدأ الدفاع المتعدد الطبقات: أمن واجهة برمجة التطبيقات (API) ليس نقطة دفاع واحدة، بل نظام دفاع متعدد الطبقات — حيث تمنع طبقة الشبكة (HTTPS + CORS) التنصت وإساءة استخدام عبر الأصول؛ وتقوم طبقة التطبيق (مصادقة JWT + تفويض RBAC + التحقق من صحة المدخلات) باعتراض الطلبات غير المصرح بها والخبيثة؛ وتمنع طبقة البيانات (الاستعلامات المعلمة + مستخدمو قاعدة البيانات ذوو الامتيازات الدنيا) هجمات الحقن والوصول غير المصرح به. أهمية الدفاع المستقل في كل طبقة: لا يؤدي اختراق أي طبقة بمفردها إلى المساس بالحماية التي توفرها الطبقات الأخرى — فحتى في حالة التكوين الخاطئ لـ CORS، لا يزال بإمكان مصادقة JWT حظر المستخدمين غير المصادق عليهم؛ وحتى في حالة اختراق JWT، لا يزال بإمكان RBAC تقييد نطاق العمليات للمستخدمين ذوي الامتيازات المنخفضة.
أفضل الممارسات الأمنية لرموز JWT: 1. قوة المفتاح: إنشاء مفتاح عشوائي 256 بت باستخدام openssl rand -hex 32؛ 2. مدة الصلاحية: 7 أيام (لتحقيق التوازن بين الأمان وتجربة المستخدم؛ ويمكن تقصير هذه المدة للعمليات الحساسة)؛ 3. تحديث الرمز: مدة صلاحية قصيرة لرموز الوصول + مدة صلاحية طويلة لرموز التحديث (آلية الرمز المزدوج)؛ 4. تخزين الرموز: استخدام ملفات تعريف الارتباط httpOnly (لمنع هجمات XSS) في الواجهة الأمامية بدلاً من localStorage؛ 5. إلغاء الرموز: الاحتفاظ بقائمة سوداء (Redis SET) أو استخدام فترات صلاحية قصيرة لتقليل الحاجة إلى الإلغاء.
graph TB
subgraph "Defense in Depth"
L1[Network Layer<br/>TLS + CORS] --> L2[Application Layer<br/>Auth + Authorization + Input Validation]
L2 --> L3[Data Layer<br/>Parameterized Queries + Least Privilege]
end
style L1 fill:#d4edda
style L2 fill:#fff3cd
style L3 fill:#f8d7da
يتبع أمن واجهة برمجة التطبيقات (API) مبدأ «الدفاع المتعدد المستويات» — أي ليس خط دفاع واحد، بل طبقات متعددة: طبقة الشبكة (TLS/CORS) → طبقة التطبيق (المصادقة + التفويض + التحقق من صحة المدخلات) → طبقة البيانات (استعلامات معلمة + مبدأ أقل الامتيازات). وتقوم كل طبقة بالدفاع بشكل مستقل؛ فإذا تم اختراق إحدى الطبقات، لا تتأثر الطبقات الأخرى.
المصادقة عبر JWT مقابل المصادقة عبر الجلسة:
| البعد | JWT | الجلسة |
|---|---|---|
| موقع التخزين | العميل (الرمز المميز) | الخادم (الذاكرة/Redis) |
| قابلية التوسع | لا تعتمد على الحالة، وتدعم التوزيع بشكل طبيعي | تتطلب تخزينًا مشتركًا للجلسات |
| الأمان | لا يمكن إلغاء تسرب الرمز المميز على الفور | يمكن إنهاء الجلسة على الفور |
| الأداء | لا توجد أعباء إضافية على جانب الخادم | يتم الاستعلام عن تخزين الجلسة في كل مرة |
| إدارة انتهاء الصلاحية | مطالبة بانتهاء الصلاحية، لا يمكن التجديد بشكل استباقي | يمكن تجديدها مع انتهاء صلاحية متدرج |
| السيناريوهات المناسبة | واجهة برمجة التطبيقات (API) / الخدمات الصغيرة | تطبيقات الويب التقليدية |
نموذج أذونات RBAC: يمنح نظام التحكم في الوصول القائم على الأدوار (RBAC) الأذونات بناءً على الأدوار — فالمستخدمون لديهم أدوار، والأدوار لها أذونات. ويضم هذا النظام ثلاثة أدوار:
| الدور | الصلاحيات | النطاق |
|---|---|---|
| العميل | الاطلاع على التقييمات، والإعجاب بها، وتعديل التقييمات الخاصة | /reviews (الخاصة) |
| المشرف | مراجعة/حذف أي تعليقات | /التعليقات (الكل) + لوحة التحكم الخاصة بالإشراف |
| admin | إدارة المنتجات والمستخدمين وجميع التقييمات | /products + /users + /reviews (الكل) |
أساسيات الحماية من هجمات الحقن: تنشأ مخاطر هجمات الحقن في MongoDB بشكل أساسي من $where (التي تُنفِّذ جافا سكريبت تعسفي) و$regex غير المثبتة (هجمات DoS). مبادئ الحماية: (1) استخدم دائمًا الاستعلامات المعلمة بدلاً من تسلسل السلاسل؛ (2) يقوم Mongoose تلقائيًا بتشفير قيم الاستعلام؛ (3) تتطلب $regex تشفير الأحرف الخاصة.
sequenceDiagram
Client Participant
participant Auth as authenticate Middleware
participant Authorize as authorize Middleware
participant Controller
DB participant
Client->>Auth: Request + Bearer Token
Auth->>Auth: jwt.verify(token, secret)
alt Token is invalid
Auth-->>Client: 401 Unauthorized
else Token Valid
Auth->>Authorize: req.user = {id, role}
Authorize->>Authorize: roles.includes(req.user.role)?
alt: No permission
Authorize-->>Client: 403 Forbidden
else has permission
Authorize->>Controller: Execute business logic
Controller >> DB: Parameterized Queries
DB-->>Client: 200 OK
than
than
(1) برمجيات الوسيطة JWT
// middlewares/auth.js
const jwt = require('jsonwebtoken');
exports.authenticate = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'No token' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
exports.authorize = (...roles) => (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
(2) واجهة برمجة تطبيقات المصادقة
// controllers/authController.js
const User = require('../models/User');
const jwt = require('jsonwebtoken');
exports.register = async (req, res) => {
const { email, username, password } = req.body;
const existing = await User.findOne({ $or: [{ email }, { username }] });
if (existing) return res.status(409).json({ error: 'Email or username already exists' });
const user = await User.create({
email,
username,
passwordHash: password // pre-save The middleware performs hashing
});
const token = jwt.sign(
{ id: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({ user: { id: user._id, email: user.email, username: user.username }, token });
};
exports.login = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+passwordHash');
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const valid = await user.comparePassword(password);
if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign(
{ id: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ user: { id: user._id, email: user.email, username: user.username, role: user.role }, token });
};
(3) الحماية من الحقن
// ❌ Danger: $where executes arbitrary JavaScript
db.reviews.find({ $where: 'this.userId == "' + userId + '"' });
// ✅ Security: Use parameterized queries
db.reviews.find({ userId: new ObjectId(userId) });
// ✅ Mongoose auto-escapes
const reviews = await Review.find({ userId: userId });
// ❌ Danger: $regex DoS
db.reviews.find({ content: { $regex: req.query.q } });
// ✅ Security: Escape special characters in regular expressions
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
db.reviews.find({ content: { $regex: escapeRegex(req.query.q), $options: 'i' } });
7. الوحدة 6: النشر
قرار بنية النشر: يعتمد اختيار استراتيجية النشر على حجم المشروع وقدرات الفريق: 1. Atlas + Render (الخيار المختار في هذا المشروع): بدء تشغيل بدون أي عمليات (zero-ops)، مناسب للمشاريع الصغيرة إلى المتوسطة ومراحل النموذج القابل للتطبيق (MVP)، التكلفة الشهرية أقل من 50 دولارًا؛ 2. MongoDB ذاتي الاستضافة + Docker: قابل للتحكم الكامل ولكنه يتطلب قدرات DevOps، ومناسب للمشاريع المتوسطة إلى الكبيرة التي لديها فريق DevOps؛ 3. Kubernetes + Atlas: التوسع المرن + قاعدة بيانات مُدارة، ومناسب لأنظمة الإنتاج ذات حركة المرور المتقلبة. يختار هذا المشروع Atlas + Render للسبب الأساسي التالي: تركيز الطاقة المحدودة على تطوير الأعمال بدلاً من العمليات.
فحص الحالة والاستعادة التلقائية: لا تقتصر فحوصات حالة النشر في بيئة الإنتاج على مجرد إرجاع الرمز 200 عند استدعاء /healthz — بل يجب أن تتحقق من ثلاث طبقات من الحالة: 1. طبقة التطبيق (هل يستجيب Express؟)؛ 2. طبقة قاعدة البيانات (هل يمكن الوصول إلى MongoDB؟)؛ 3. طبقة التبعيات (هل يعمل Redis/ES بشكل طبيعي؟). يتم استدعاء نقطة نهاية فحص الحالة بشكل دوري بواسطة موزع الحمل؛ وبعد N حالات فشل متتالية، تتم إزالة المثيل وإعادة تشغيله تلقائيًا. عند انقطاع الاتصال بقاعدة البيانات، يجب أن تُرجع النتيجة 503 (الخدمة غير متاحة) بدلاً من 200، لتجنب إرسال حركة المرور إلى المثيلات غير السليمة.
graph TB
subgraph "Deployment Architecture"
A[Client] --> B[Render<br/>Node.js App]
B --> C[Atlas<br/>MongoDB Cluster]
B --> D[CDN<br/>Static Assets]
end
style A fill:#d4edda
style B fill:#fff3cd
style C fill:#d4edda
style D fill:#fff3cd
لا يقتصر النشر على مجرد «git push» ثم ينتهي الأمر — بل يتطلب مراعاة عدة أمور: عزل البيئات (التطوير/الاختبار/الإنتاج)، وإدارة الأسرار (عدم تخزين كلمات المرور في الكود)، وفحوصات الحالة (إعادة التشغيل التلقائي للمثيلات التي تعاني من مشاكل)، وأمن البيانات (تشفير TLS + النسخ الاحتياطي والاستعادة). استراتيجية النشر الخاصة بهذا المشروع: قاعدة بيانات مُدارة بواسطة Atlas + تطبيق مُدار بواسطة Render/Railway — شركة ناشئة لا تتطلب أي عمليات تشغيل، ومناسبة للمشاريع الصغيرة والمتوسطة.
بنية النشر:
graph LR
Client[User's browser] -->|HTTPS| CDN[CDN / Static Resources]
Client -->|HTTPS| LB[Load Balancer]
LB -->|HTTP| App1[Render Instance 1<br/>Express App]
LB -->|HTTP| App2[Render Instance 2<br/>Express App]
App1 -->|TLS + SRV| Atlas[MongoDB Atlas<br/>3 Node Replica Set]
App2 -->|TLS + SRV| Atlas
Atlas -->| PITR | Backup[Automatic Backup<br/>7Day Reserved]
style Atlas fill:#d4edda
style App1 fill:#cce5ff
سياسة تكوين البيئة:
| متغير بيئي | القيمة في بيئة التطوير | القيمة في بيئة الإنتاج | طريقة الإدارة |
|---|---|---|---|
| NODE_ENV | التطوير | الإنتاج | .env / إعدادات النظام الأساسي |
| MONGODB_URI | mongodb://localhost:27017 | mongodb+srv://... | إدارة مفاتيح النظام الأساسي |
| JWT_SECRET | test-secret | سلسلة عشوائية من 256 بت | openssl rand -hex 32 |
| CORS_ORIGIN | * | https://shophub.example.com | .env / إعدادات النظام الأساسي |
| الميناء | 3000 | تخصيص الرصيف | الإعداد الافتراضي مناسب |
تصميم فحص الحالة: لا تقتصر وظيفة نقطة النهاية /healthz على التحقق من تشغيل Express فحسب، بل تتأكد أيضًا من إمكانية الوصول إلى MongoDB — ففي حالة انقطاع الاتصال بقاعدة البيانات، يجب أن يُرجع التطبيق رمز الحالة 503 (الخدمة غير متاحة) بدلاً من 200، حتى يتمكن موزع الحمل من إزالة المثيلات غير السليمة تلقائيًا.
(1) تكوين MongoDB Atlas
# 1. Create Atlas Cluster(Recommended Courses #01)
# 2. Layout IP Whitelist:0.0.0.0/0(Development)or application server IP
# 3. Create a Database User:app_user / <password>
# 4. Get the connection string:
mongodb+srv://app_user:<password>@cluster0.mongodb.net/shophub?retryWrites=true&w=majority
(2) متغيرات البيئة (الإنتاج)
# .env.production
NODE_ENV=production
PORT=3000
MONGODB_URI=mongodb+srv://app_user:StrongPass@cluster0.mongodb.net/shophub?retryWrites=true&w=majority
JWT_SECRET=<generated-strong-secret-256-bit>
JWT_EXPIRES_IN=7d
CORS_ORIGIN=https://shophub.example.com
(3) الفحص الطبي
app.get('/healthz', async (req, res) => {
try {
const db = mongoose.connection.db;
await db.admin().command({ ping: 1 });
res.json({
status: 'ok',
uptime: process.uptime(),
mongo: 'connected',
timestamp: new Date().toISOString()
});
} catch (err) {
res.status(503).json({ status: 'error', error: err.message });
}
});
(4) التنفيذ / نشر السكك الحديدية
# === Render Deployment ===
# 1. Connect GitHub Warehouse
# 2. Set Environment Variables (MONGODB_URI, JWT_SECRET, etc.)
# 3. Set Up Build Commands:npm install
# 4. Set the startup command:npm start
# 5. Automatic HTTPS + Deployment
# === Railway Deployment ===
railway login
railway init
railway add mongodb # Add with One Click MongoDB
railway up
(5) قائمة مراجعة لتحسين الأداء
// === src/app.js Optimized Version ===
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGODB_URI, {
maxPoolSize: 50,
minPoolSize: 5,
serverSelectionTimeoutMS: 5000
});
app.use(express.json({ limit: '1mb' }));
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
credentials: true
}));
8. عرض توضيحي شامل للمشروع
عملية بدء المشروع والاختبار: يتبع العرض التوضيحي الكامل للمشروع العملية التالية: «إعداد البيئة → بدء تشغيل الخدمات → اختبار واجهات برمجة التطبيقات (APIs) → التحقق من الأداء الوظيفي → النشر والتشغيل». أولاً، قم بتشغيل مجموعة النسخ المتماثلة لـ MongoDB (تتطلب المعاملات و«تدفقات التغيير» وجود مجموعة نسخ متماثلة)، ثم قم بتشغيل تطبيق Express، وأخيرًا استخدم curl لاختبار نقاط نهاية واجهة برمجة التطبيقات (API) الأساسية.
قائمة مراجعة اختبار واجهة برمجة التطبيقات (API):
| # | الميزة | نقطة النهاية | رمز الحالة المتوقع |
|---|---|---|---|
| 1 | التسجيل | POST /api/auth/register | 201 |
| 2 | تسجيل الدخول | POST /api/auth/login | 200 |
| 3 | إنشاء منتج | POST /api/products | 201 (المسؤول) |
| 4 | قائمة المنتجات | GET /api/products | 200 |
| 5 | نشر تقييم | POST /api/products/:id/reviews | 201 |
| 6 | قائمة التقييمات | GET /api/products/:id/reviews | 200 |
| 7 | إعجاب | POST /api/reviews/:id/like | 200 |
| 8 | إحصائيات المنتج | GET /api/products/:id/stats | 200 |
| 9 | فحص الحالة | GET /healthz | 200 |
# === Launch the Project ===
npm install
npm start
# === API Usage Example ===
# 1. Registered Users
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","username":"alice","password":"Pass123!"}'
# 2. Log In
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"Pass123!"}'
# 3. Create a Review
curl -X POST http://localhost:3000/api/products/<productId>/reviews \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"content":"Great product!","rating":5}'
# 4. View Product Statistics
curl http://localhost:3000/api/products/<productId>/stats
# 5. Health Checkup
curl http://localhost:3000/healthz
▶ المثال 1: عرض توضيحي للتعليقات المتداخلة + ميزة «الإعجاب»
// === Scenario: Alice reviews a product, Bob replies to Alice, Charlie likes it ===
// 1. Alice Published 5 Star Reviews
const aliceReview = await Review.create({
productId: '647f1f77bcf86cd799439011',
userId: aliceId,
content: 'Excellent smartphone! The camera quality is outstanding.',
rating: 5,
parentId: null // Top Comments
});
// Back: { _id: 'review001', content: '...', rating: 5, likeCount: 0 }
// 2. Bob Reply Alice
const bobReply = await Review.create({
productId: '647f1f77bcf86cd799439011',
userId: bobId,
content: 'I agree, the camera is amazing!',
rating: 5,
parentId: aliceReview._id // Quote parent's comment
});
// Back: { _id: 'review002', parentId: 'review001', content: '...' }
// 3. Charlie likes Alice's comment
await Review.updateOne(
{ _id: aliceReview._id },
{ $addToSet: { likes: charlieId }, $inc: { likeCount: 1 } }
);
// Alice Comments on: { likeCount: 1, likes: [charlieId] }
// 4. Like again → Cancel
await Review.updateOne(
{ _id: aliceReview._id },
{ $pull: { likes: charlieId }, $inc: { likeCount: -1 } }
);
// Alice Comments on: { likeCount: 0, likes: [] }
// 5. Querying a Nested Comment Tree
const reviews = await Review.find({ productId: '647f...', parentId: null })
.populate('userId', 'username avatar')
.lean();
const replies = await Review.find({ parentId: { $in: reviews.map(r => r._id) } })
.populate('userId', 'username avatar')
.lean();
const tree = reviews.map(r => ({
...r,
replies: replies.filter(rep => rep.parentId.toString() === r._id.toString())
}));
console.log(JSON.stringify(tree, null, 2));
// [{
// content: 'Excellent smartphone!...',
// userId: { username: 'alice', avatar: '...' },
// replies: [{ content: 'I agree...', userId: { username: 'bob' } }]
// }]
الناتج: بنية شجرية متداخلة للتعليقات: تعليقات أليس → ردود بوب، إعجاب/عدم إعجاب تشارلي.
▶ المثال 2: عرض توضيحي مباشر لنظام «ShopHub Review» الكامل
# === 1. Start MongoDB Dungeon Collection ===
docker run -d --name mongo -p 27017:27017 mongo:7.0 --replSet rs0
docker exec mongo mongosh --eval 'rs.initiate()'
# === 2. Start Node.js Applications ===
npm install
npm start
# Output:
# ✅ MongoDB connected: localhost
# 🚀 Server running on port 3000
# === 3. Test Core API ===
# 3.1 User Registration
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","username":"alice","password":"Pass123!"}'
# Back:
# {
# "success": true,
# "user": { "id": "...", "email": "alice@example.com", "username": "alice" },
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# }
# 3.2 User Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"Pass123!"}'
# 3.3 Create a Product
curl -X POST http://localhost:3000/api/products \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"sku": "PHONE-001",
"title": "Smartphone X",
"price": 599,
"category": "Electronics",
"stock": 50
}'
# 3.4 Product List(With pagination + Projection)
curl 'http://localhost:3000/api/products?page=1&limit=20&category=Electronics'
# Back:
# {
# "success": true,
# "data": [{ "sku": "PHONE-001", "title": "Smartphone X", "price": 599, "thumbnail": "...", "rating": 0 }],
# "meta": { "page": 1, "limit": 20, "total": 1, "pages": 1 }
# }
# 3.5 Add a comment
curl -X POST http://localhost:3000/api/products/507f1f77bcf86cd799439021/reviews \
-H "Authorization: Bearer <user_token>" \
-H "Content-Type: application/json" \
-d '{"content":"Great phone!","rating":5}'
# 3.6 Like and Comment
curl -X POST http://localhost:3000/api/reviews/507f1f77bcf86cd799439031/like \
-H "Authorization: Bearer <user_token>"
# 3.7 View Product Statistics
curl http://localhost:3000/api/products/507f1f77bcf86cd799439021/stats
# Back:
# {
# "total": [{ "count": 1 }],
# "avgRating": [{ "avg": 5 }],
# "ratingDistribution": [{ "_id": 5, "count": 1 }],
# "recentReviews": [{ "content": "Great phone!", "rating": 5, "username": "alice" }]
# }
# 3.8 Health Checkup
curl http://localhost:3000/healthz
# Back:
# {
# "status": "ok",
# "uptime": 1234,
# "mongo": "connected",
# "timestamp": "2026-07-06T10:00:00.000Z"
# }
# === 4. Deploy to Render/Railway ===
git push heroku main
# Automatic Deployment,Environment Variables MONGODB_URI Orientation Atlas
# === 5. Performance Monitoring(Production)===
# Datadog APM Automatic Tracking:
# - mongoose Query Duration
# - API Response Time
# - Number of database connections
# - Slow Query Alerts
الناتج: نظام تقييمات متكامل للتجارة الإلكترونية يوضح سير العمل بأكمله — بدءًا من التسجيل، وتسجيل الدخول، وإدارة المنتجات، وعمليات CRUD الخاصة بالتقييمات، والإعجاب، والإحصاءات، وصولاً إلى فحوصات الحالة — ويمكن نشره مباشرةً في بيئة الإنتاج.
▶ مثال 3: واجهة برمجة تطبيقات إحصاءات التقييمات المتقدمة(الصعوبة ⭐⭐)
// واجهة برمجة تطبيقات إحصاءات التقييمات لـ ShopHub
const express = require('express');
const mongoose = require('mongoose');
const router = express.Router();
const Review = require('../models/Review');
const Product = require('../models/Product');
// إحصاءات التقييمات الشاملة
router.get('/products/:productId/stats', async (req, res) => {
const { productId } = req.params;
const stats = await Review.aggregate([
{
$match: {
productId: new mongoose.Types.ObjectId(productId),
parentId: null,
isDeleted: { $ne: true }
}
},
{
$facet: {
// الإجمالي والمتوسط
overview: [
{
$group: {
_id: null,
totalReviews: { $sum: 1 },
avgRating: { $avg: '$rating' },
totalLikes: { $sum: '$likeCount' }
}
}
],
// توزيع التقييمات (1-5 نجوم)
ratingDistribution: [
{
$group: {
_id: '$rating',
count: { $sum: 1 }
}
},
{ $sort: { _id: 1 } }
],
// أكثر التقييمات فائدة
topHelpful: [
{ $sort: { likeCount: -1 } },
{ $limit: 5 },
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'user'
}
},
{ $unwind: '$user' },
{
$project: {
content: 1,
rating: 1,
likeCount: 1,
createdAt: 1,
username: '$user.username'
}
}
],
// التقييمات الحديثة
recentReviews: [
{ $sort: { createdAt: -1 } },
{ $limit: 5 },
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'user'
}
},
{ $unwind: '$user' },
{
$project: {
content: 1,
rating: 1,
createdAt: 1,
username: '$user.username'
}
}
]
}
}
]);
res.json({
success: true,
data: {
overview: stats[0].overview[0] || { totalReviews: 0, avgRating: 0, totalLikes: 0 },
ratingDistribution: stats[0].ratingDistribution,
topHelpful: stats[0].topHelpful,
recentReviews: stats[0].recentReviews
}
});
});
module.exports = router;
// اختبار API:
// GET /api/products/507f1f77bcf86cd799439021/stats
الإخراج:
JSON{ "success": true, "data": { "overview": { "totalReviews": 128, "avgRating": 4.3, "totalLikes": 892 }, "ratingDistribution": [ { "_id": 1, "count": 5 }, { "_id": 2, "count": 8 }, { "_id": 3, "count": 15 }, { "_id": 4, "count": 35 }, { "_id": 5, "count": 65 } ], "topHelpful": [ { "content": "منتج ممتاز!", "rating": 5, "likeCount": 45, "username": "alice" } ], "recentReviews": [ { "content": "جودة عالية", "rating": 4, "createdAt": "2026-07-20T09:30:00Z", "username": "bob" } ] } }
❓ أسئلة شائعة
📖 ملخص
- بنية نظام مراجعة التجارة الإلكترونية الكاملة (Node.js + Express + Mongoose)
- 6 وحدات: التهيئة → النموذج → CRUD → تحليل التجميعات → الأذونات → النشر
- التعليقات المتداخلة (إشارة إلى parent_id)
- الإحصاءات التجميعية ($bucket + $facet)
- المصادقة باستخدام JWT + أدوار RBAC
- النشر السحابي لـ MongoDB Atlas
📝 تمارين
- السؤال الأساسي (⭐): قم بإعداد الهيكل الأساسي للمشروع (بما في ذلك ملفات package.json و.env وapp.js).
- المشاكل الأساسية (⭐): قم بتنفيذ النماذج الثلاثة: «المستخدم»، و«المنتج»، و«التقييم».
- تمرين متقدم (⭐⭐): تنفيذ عمليات CRUD الخاصة بالتعليقات (إنشاء، عرض قائمة، الإعجاب، الحذف المؤقت).
- تمرين متقدم (⭐⭐): تنفيذ الإحصاءات التجميعية (توزيع التقييمات + إحصاءات المنتج + قوائم الأكثر رواجًا).
- التحدي (⭐⭐⭐): نشر نظام كامل لمراجعة التجارة الإلكترونية على منصة Atlas أو Render، بما في ذلك جميع ميزات الوحدات الست.