تصميم المشروع — بنية منصة ShopMetrics SaaS
البرمجة بدون تصميم كبناء بيت بدون مخطط—لا تدرك أن الأساس ليس عميقاً بما يكفي حتى تصل إلى الطابق الثالث، وحينها لا خيار أمامك سوى الهدم والبدء من جديد.
1. ما ستتعلمه
- تحليل المتطلبات وقصص المستخدمين: Alice (مدير المستأجر)/Bob (عمليات المنصة)/Charlie (محلل بيانات)
- اختيار بنية التعدد: قاعدة بيانات مشتركة مقابل قاعدة بيانات مخصصة
- تصميم ER لقاعدة البيانات: مستأجرون/مستخدمون/خطط/اشتراكات/متاجر/طلبات/تحليلات
- تصميم توثيق API: مواصفات OpenAPI 3.0 ومجموعات Postman
- اختيار التقنيات: Laravel 11 / MySQL / Redis / S3 / WebSocket
2. قصة حقيقية لبناء مشروع SaaS من الصفر
(1) المشكلة: Bob يريد بناء منصة تحليلات تجارة إلكترونية لكنه لا يعرف من أين يبدأ
يدير Bob شركة استشارات بيانات تجارة إلكترونية تضم أكثر من 50 عميلاً. بيانات كل عميل مبعثرة عبر ثماني منصات تجارة إلكترونية، ويقوم Bob بتجميعها يدوياً في Excel—قضاء أربع ساعات يومياً في إنشاء التقارير، التي غالباً ما تتخللها الأخطاء. Alice، المديرة التشغيلية في أحد العملاء، تقول: "من الرائع أن يكون هناك لوحة معلومات تعرض بيانات جميع المتاجر في الوقت الفعلي." Charlie، محلل البيانات لدى Bob، يقول: "نحتاج منصة SaaS متعددة المستأجرين، لكن كيف يجب أن نصمم البنية؟"
(2) نهج التصميم المنهجي
ابدأ بتحليل المتطلبات → قصص المستخدمين → اختيار البنية → نمذجة البيانات → تصميم API → اختيار التقنيات. كل خطوة لها مخرجات واضحة، وتتبع المخطط عند البرمجة.
تحليل المتطلبات → قصص المستخدمين → اختيار البنية → تصميم ER → تصميم API → اختيار التقنيات → بدء البرمجة
(3) العائد
بعد أن أمضى Bob أسبوعين في إتمام التصميم، تضاعفت كفاءته البرمجية ثلاث مرات—لأن كل متطلب كان لديه واجهة وتصميم قاعدة بيانات واضحين، مما ألغى الحاجة إلى إجراء التغييرات أثناء التنفيذ.
3. تحليل المتطلبات وقصص المستخدمين
(1) ثلاثة أنماط من ملفات المستخدمين
| الدور | الاسم | الحاجة الأساسية | الإجراءات النموذجية |
|---|---|---|---|
| مدير المستأجر | Alice | إدارة متجره وفريقه | إنشاء متاجر، دعوة أعضاء، عرض لوحة المعلومات |
| عمليات المنصة | Bob | إدارة جميع المستأجرين والفواتير | الموافقة على المستأجرين، إدارة الخطط، عرض إحصائيات المنصة |
| محلل بيانات | Charlie | تحليل بيانات التجارة الإلكترونية وإنشاء التقارير | إنشاء تقارير، إعداد التنبيهات، تصدير البيانات |
(2) قصص المستخدمين
بصفتي Alice (مدير المستأجر)، أريد:
- US-01: إضافة متجر تجارة إلكترونية جديد حتى أتمكن من تتبع مقاييسه
- US-02: دعوة أعضاء الفريق حتى يتمكن Charlie من الوصول إلى التحليلات
- US-03: عرض لوحة معلومات تعرض الإيرادات/الطلبات عبر جميع متاجري
- US-04: الاشتراك في خطة تناسب عدد متاجري
- US-05: تصدير التقارير بصيغة CSV/Excel
- US-06: إعداد تنبيهات عندما تنخفض الإيرادات عن حد معين
بصفتي Bob (مشغل المنصة)، أريد:
- US-07: إدارة حسابات المستأجرين (إنشاء/تعليق/حذف)
- US-08: تحديد خطط اشتراك بحدود ميزات مختلفة
- US-09: عرض مقاييس المنصة بالكامل (إجمالي المستأجرين/MRR/التفعيلات)
- US-10: معالجة مدفوعات الاشتراك عبر Stripe
- US-11: إرسال إشعارات للمستأجرين حول الاشتراكات المنتهية
بصفتي Charlie (محلل بيانات)، أريد:
- US-12: بناء تقارير تحليلية مخصصة بنطاق تاريخ وفلاتر
- US-13: مقارنة أداء المتاجر جنبًا إلى جنب
- US-14: جدولة إنشاء تقارير آلية (يومي/أسبوعي/شهري)
- US-15: استقبال إشعارات فورية عند حدوث أحداث مهمة
(1) ▶ مثال: تفكيك قصص المستخدمين إلى وحدات وظيفية
// قصص المستخدمين → تعيين الوحدات الوظيفية
return [
'إدارة المستأجرين' => [
'US-07: إدارة حسابات المستأجرين',
'US-01: إضافة متاجر تجارة إلكترونية',
'US-02: دعوة أعضاء الفريق',
],
'الاشتراك والفواتير' => [
'US-04: الاشتراك في خطة',
'US-08: تحديد خطط الاشتراك',
'US-10: معالجة مدفوعات Stripe',
'US-11: إشعارات انتهاء الصلاحية',
],
'التحليلات والتقارير' => [
'US-03: عرض لوحة معلومات الإيرادات',
'US-05: تصدير التقارير',
'US-12: بناء تقارير مخصصة',
'US-13: مقارنة أداء المتاجر',
'US-14: جدولة إنشاء التقارير',
],
'التنبيهات والإشعارات' => [
'US-06: تنبيهات انخفاض الإيرادات',
'US-15: إشعارات الأحداث الفورية',
],
'إدارة المنصة' => [
'US-09: مقاييس المنصة بالكامل',
],
];
الناتج:
// تم التنفيذ بنجاح
4. اختيار بنية التعدد
(1) ثلاث استراتيجيات للتعدد
| الاستراتيجية | مستوى العزل | التكلفة | التعقيد | حالات الاستخدام |
|---|---|---|---|---|
| قاعدة بيانات مستقلة | الأعلى | عالية | متوسط | متطلبات الامتثال المالي/الرعاية الصحية |
| قاعدة بيانات مشتركة + مخطط مستقل | متوسط | متوسطة | متوسط | متوسط الحجم، مع بعض متطلبات العزل |
| قاعدة بيانات مشتركة + مخطط مشترك | الأدنى | منخفضة | منخفض | معظم SaaS، عزل tenant_id |
(2) قرار اختيار ShopMetrics
flowchart TD
A[استراتيجية التعدد] --> B{متطلب عزل البيانات؟}
B -->|امتثال صارم| C[قاعدة بيانات منفصلة لكل مستأجر]
B -->|SaaS معياري| D{عدد المستأجرين؟}
D -->|أقل من 100| E[قاعدة بيانات مشتركة + مخطط منفصل]
D -->|أكثر من 100| F[قاعدة بيانات مشتركة + مخطط مشترك]
F --> G[tenant_id في كل صف]
G --> H[تصفية تلقائية عبر Global Scope]
H --> I[اختيار ShopMetrics ✅]
اختارت ShopMetrics استراتيجية قاعدة بيانات مشتركة + مخطط مشترك:
- من المتوقع أكثر من 500 مستأجر؛ حساسية للتكلفة
- لا توجد متطلبات امتثال مالي؛ عزل tenant_id كافٍ
- تنفيذ التصفية التلقائية في Global Scope في Laravel
(1) ▶ مثال: تنفيذ Global Scope للتعدد
// app/Models/Traits/BelongsToTenant.php
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope('tenant', function (Builder $builder) {
$tenantId = Tenant::current()?->id;
if ($tenantId) {
$builder->where('tenant_id', $tenantId);
}
});
static::creating(function (Model $model) {
$tenantId = Tenant::current()?->id;
if ($tenantId && ! $model->isDirty('tenant_id')) {
$model->tenant_id = $tenantId;
}
});
}
}
// app/Models/Tenant.php
class Tenant extends Model
{
protected static Tenant $currentTenant;
public static function setCurrent(self $tenant): void
{
static::$currentTenant = $tenant;
}
public static function current(): ?self
{
return static::$currentTenant ?? null;
}
}
// app/Http/Middleware/SetTenantContext.php
class SetTenantContext
{
public function handle(Request $request, Closure $next): Response
{
if ($user = $request->user()) {
Tenant::setCurrent($user->tenant);
}
return $next($request);
}
}
الناتج:
// تم التنفيذ بنجاح
5. تصميم ER لقاعدة البيانات
(1) علاقات الكيانات الأساسية
erDiagram
TENANT ||--o{ USER : "لديه عدة"
TENANT ||--o{ SHOP : "لديه عدة"
TENANT ||--|| SUBSCRIPTION : "لديه واحد"
PLAN ||--o{ SUBSCRIPTION : "مشترك في"
SHOP ||--o{ ORDER : "لديه عدة"
SHOP ||--o{ PRODUCT : "لديه عدة"
ORDER ||--|{ ORDER_ITEM : "يحتوي على"
ORDER_ITEM }o--|| PRODUCT : "يشير إلى"
USER ||--o{ REPORT : "ينشئ"
TENANT ||--o{ ALERT : "يضبط"
TENANT {
bigint id PK
string name
string slug UK
string domain
string status
timestamp created_at
}
USER {
bigint id PK
bigint tenant_id FK
string name
string email UK
string role
timestamp created_at
}
PLAN {
bigint id PK
string name
string slug UK
int shop_limit
int user_limit
int price_cents
string stripe_price_id
}
SUBSCRIPTION {
bigint id PK
bigint tenant_id FK
bigint plan_id FK
string stripe_id
string status
timestamp trial_ends_at
timestamp ends_at
}
SHOP {
bigint id PK
bigint tenant_id FK
string name
string platform
string external_id
string status
}
ORDER {
bigint id PK
bigint tenant_id FK
bigint shop_id FK
string external_id
string customer_email
int total_cents
string status
timestamp ordered_at
}
PRODUCT {
bigint id PK
bigint tenant_id FK
bigint shop_id FK
string name
string sku
int price_cents
}
ORDER_ITEM {
bigint id PK
bigint order_id FK
bigint product_id FK
int quantity
int unit_price_cents
}
REPORT {
bigint id PK
bigint tenant_id FK
bigint user_id FK
string type
string format
string status
string storage_path
timestamp generated_at
}
ALERT {
bigint id PK
bigint tenant_id FK
string type
string condition
string channel
boolean is_active
}
(2) قرارات التصميم الرئيسية
| القرار | الاختيار | السبب |
|---|---|---|
| تخزين المبالغ | int price_cents |
تجنب مشاكل دقة الفاصلة العائمة |
| عزل المستأجر | tenant_id في كل جدول |
سياسة المخطط المشترك |
| حالة الاشتراك | مزامنة Stripe Webhook | مصدر بيانات واحد (Stripe) |
| المعرف الخارجي | external_id UK لكل مستأجر |
تنسيقات معرفات منصات تجارة إلكترونية مختلفة |
| الحذف الناعم | المستخدم/المستأجر فقط | عدم حذف الطلبات/المنتجات؛ تغيير حالتها فقط |
(1) ▶ مثال: تعريف النموذج الأساسي لـ ShopMetrics
// app/Models/Tenant.php
class Tenant extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['name', 'slug', 'domain', 'status'];
protected static function booted(): void
{
static::creating(function (self $tenant) {
$tenant->slug ??= Str::slug($tenant->name);
$tenant->domain ??= "{$tenant->slug}.shopmetrics.io";
});
}
public function users(): HasMany
{
return $this->hasMany(User::class);
}
public function shops(): HasMany
{
return $this->hasMany(Shop::class);
}
public function subscription(): HasOne
{
return $this->hasOne(Subscription::class)->ofMany([], fn ($q) => $q->orderByDesc('created_at'));
}
public function alerts(): HasMany
{
return $this->hasMany(Alert::class);
}
public function isActive(): bool
{
return $this->status === 'active' &&
$this->subscription?->stripe_status === 'active';
}
public function canAddShop(): bool
{
$limit = $this->subscription?->plan->shop_limit ?? 0;
return $this->shops()->count() < $limit;
}
}
// app/Models/Order.php
class Order extends Model
{
use BelongsToTenant, HasFactory;
protected $fillable = [
'tenant_id', 'shop_id', 'external_id',
'customer_email', 'total_cents', 'status', 'ordered_at',
];
protected $casts = [
'total_cents' => 'integer',
'ordered_at' => 'datetime',
];
public function shop(): BelongsTo
{
return $this->belongsTo(Shop::class);
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
public function getTotalDollarsAttribute(): float
{
return $this->total_cents / 100;
}
}
الناتج:
// تم التنفيذ بنجاح
6. تصميم API
(1) إصدارات API وتخطيط الموارد
| المورد | البادئة | الطريقة | الوصف |
|---|---|---|---|
| المصادقة | /api/v1/auth |
POST login/logout/refresh | المصادقة |
| المستأجرون | /api/v1/tenants |
GET/PATCH current | معلومات المستأجر |
| المتاجر | /api/v1/shops |
CRUD | إدارة المتاجر |
| الطلبات | /api/v1/shops/{id}/orders |
GET/POST | استعلام الطلبات |
| المنتجات | /api/v1/shops/{id}/products |
GET/POST | إدارة المنتجات |
| لوحة المعلومات | /api/v1/dashboard |
GET overview/top/revenue | لوحة المعلومات |
| التقارير | /api/v1/reports |
POST generate/GET status | التقارير |
| التنبيهات | /api/v1/alerts |
CRUD | ضبط التنبيهات |
| الخطط | /api/v1/plans |
GET list | قائمة الخطط |
| الاشتراكات | /api/v1/subscriptions |
POST/DELETE | إدارة الاشتراكات |
(2) مواصفات استجابة API
{
"data": {
"id": 1,
"type": "shop",
"attributes": {
"name": "متجر Alice على Amazon",
"platform": "amazon",
"status": "active",
"created_at": "2024-01-15T10:00:00Z"
},
"relationships": {
"tenant": { "data": { "id": 1, "type": "tenant" } }
}
},
"meta": {
"request_id": "req_abc123",
"timestamp": "2024-03-15T14:30:00Z"
}
}
(1) ▶ مثال: تصميم مسارات API لـ ShopMetrics
// routes/api.php
Route::prefix('v1')->group(function () {
// عام: المصادقة
Route::post('auth/login', [AuthController::class, 'login']);
Route::post('auth/register', [AuthController::class, 'register']);
// المسارات المصادق عليها
Route::middleware(['auth:sanctum', 'set-tenant-context'])->group(function () {
// المصادقة
Route::post('auth/logout', [AuthController::class, 'logout']);
Route::get('auth/me', [AuthController::class, 'me']);
// المستأجر (مستأجر المستخدم الحالي)
Route::get('tenant', [TenantController::class, 'show']);
Route::patch('tenant', [TenantController::class, 'update']);
// المتاجر
Route::apiResource('shops', ShopController::class);
// موارد متداخلة تحت المتاجر
Route::prefix('shops/{shop}')->group(function () {
Route::apiResource('orders', OrderController::class)->only(['index', 'show']);
Route::apiResource('products', ProductController::class);
});
// لوحة المعلومات
Route::prefix('dashboard')->group(function () {
Route::get('overview', [DashboardController::class, 'overview']);
Route::get('top-products', [DashboardController::class, 'topProducts']);
Route::get('revenue', [DashboardController::class, 'revenueChart']);
});
// التقارير
Route::apiResource('reports', ReportController::class)->only(['index', 'store', 'show']);
Route::post('reports/{report}/download', [ReportController::class, 'download']);
// التنبيهات
Route::apiResource('alerts', AlertController::class);
// الاشتراك
Route::get('plans', [PlanController::class, 'index']);
Route::post('subscriptions', [SubscriptionController::class, 'store']);
Route::get('subscription', [SubscriptionController::class, 'show']);
Route::delete('subscription', [SubscriptionController::class, 'cancel']);
// إدارة الفريق (tenant_owner فقط)
Route::middleware('role:tenant_owner')->prefix('team')->group(function () {
Route::get('members', [TeamController::class, 'index']);
Route::post('invite', [TeamController::class, 'invite']);
Route::delete('members/{user}', [TeamController::class, 'remove']);
});
});
// Stripe Webhooks (بدون مصادقة)
Route::post('webhooks/stripe', [WebhookController::class, 'handleStripe']);
});
الناتج:
// تم التنفيذ بنجاح
7. قرارات اختيار التقنيات
(1) مقارنة واختيار حزمة التقنيات
| المستوى | الخيار | الاختيار | السبب |
|---|---|---|---|
| إطار العمل | Laravel/Symfony/Lumen | Laravel 11 | كامل الميزات، نظام بيئي غني، تطوير SaaS سريع |
| قاعدة البيانات | MySQL/PostgreSQL | MySQL 8.0 | مألوف للفريق، الافتراضي في Laravel، يدعم JSON |
| التخزين المؤقت | Redis/Memcached | Redis 7 | تخزين مؤقت موحد، جلسات، طوابير، وبث |
| التخزين | S3/MinIO/Local | S3 | قابلية التوسع، تكامل CDN، MinIO للتطوير |
| الطابور | Redis/Database/SQS | Redis | زمن استجابة منخفض، بيئة تطوير وإنتاج موحدة |
| المصادقة | Sanctum/Passport | Sanctum | مصادقة الرمز المميز لـ SPA + Mobile كافية |
| الوقت الفعلي | Pusher/Soketi | Soketi | متوافق مع بروتوكول Pusher؛ مستضاف ذاتياً بدون رسوم |
| الواجهة الأمامية | Blade/Inertia/Livewire | Inertia + Vue | تجربة SPA + توجيه من جانب الخادم |
(2) نظرة عامة على البنية
flowchart TB
subgraph Client["طبقة العميل"]
WEB[Web SPA - Inertia/Vue]
MOBILE[تطبيق الهاتف]
API_CLIENT[مستهلكو API]
end
subgraph LB["موازن التحميل - Nginx"]
direction LR
direction TB
end
subgraph App["طبقة التطبيق"]
API[خادم API - Laravel]
WS[WebSocket - Soketi]
end
subgraph Worker["المعالجة الخلفية"]
QUEUE[عمال الطابور]
SCHED[المجدول]
end
subgraph Data["طبقة البيانات"]
DB[(MySQL 8.0)]
CACHE[(Redis 7)]
S3[S3/MinIO]
end
subgraph External["الخدمات الخارجية"]
STRIPE[Stripe API]
SHOPS[واجهات التجارة الإلكترونية]
MAIL[خدمة البريد]
end
WEB --> LB
MOBILE --> LB
API_CLIENT --> LB
LB --> API
WEB --> WS
API --> DB
API --> CACHE
API --> S3
API --> STRIPE
API --> SHOPS
API --> QUEUE
API --> WS
QUEUE --> DB
QUEUE --> CACHE
QUEUE --> S3
QUEUE --> MAIL
QUEUE --> STRIPE
SCHED --> QUEUE
(1) ▶ مثال: ملف ضبط اختيار تقنيات ShopMetrics
// config/shopmetrics.php
return [
'tenant' => [
'strategy' => env('TENANT_STRATEGY', 'shared_schema'),
'default_plan' => env('DEFAULT_PLAN_SLUG', 'starter'),
'trial_days' => env('TRIAL_DAYS', 14),
],
'limits' => [
'starter' => ['shops' => 3, 'users' => 5, 'reports_per_month' => 10],
'pro' => ['shops' => 25, 'users' => 25, 'reports_per_month' => 100],
'enterprise' => ['shops' => -1, 'users' => -1, 'reports_per_month' => -1],
],
'reports' => [
'max_date_range_days' => 365,
'formats' => ['csv', 'xlsx', 'json'],
'storage_disk' => env('REPORT_DISK', 's3'),
'retention_days' => env('REPORT_RETENTION_DAYS', 90),
],
'sync' => [
'platforms' => ['amazon', 'shopify', 'ebay', 'etsy'],
'sync_interval_minutes' => env('SYNC_INTERVAL', 60),
'batch_size' => env('SYNC_BATCH_SIZE', 500),
],
'alerts' => [
'channels' => ['email', 'slack', 'webhook'],
'check_interval_minutes' => env('ALERT_CHECK_INTERVAL', 15),
],
];
الناتج:
// تم التنفيذ بنجاح
8. مثال شامل: تهيئة مشروع ShopMetrics
// ============================================
// شامل: هيكل المشروع والإعداد الأولي
// ترحيلات قاعدة البيانات، النماذج، والضبط
// ============================================
// database/migrations/2024_01_01_000001_create_tenants_table.php
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('domain')->unique();
$table->string('status')->default('active'); // active/suspended/cancelled
$table->softDeletes();
$table->timestamps();
$table->index('status');
});
// database/migrations/2024_01_01_000002_create_plans_table.php
Schema::create('plans', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->integer('shop_limit')->default(3);
$table->integer('user_limit')->default(5);
$table->integer('price_cents')->default(0);
$table->string('stripe_price_id')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
// database/migrations/2024_01_01_000003_create_subscriptions_table.php
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('plan_id')->constrained();
$table->string('stripe_id')->unique();
$table->string('stripe_status');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'stripe_status']);
});
// database/migrations/2024_01_01_000004_create_shops_table.php
Schema::create('shops', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('platform'); // amazon/shopify/ebay/etsy
$table->string('external_id');
$table->string('status')->default('active');
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['tenant_id', 'platform', 'external_id']);
$table->index(['tenant_id', 'status']);
});
// database/migrations/2024_01_01_000005_create_orders_table.php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('shop_id')->constrained()->cascadeOnDelete();
$table->string('external_id');
$table->string('customer_email')->nullable();
$table->unsignedBigInteger('total_cents')->default(0);
$table->string('status')->default('pending');
$table->timestamp('ordered_at');
$table->timestamps();
$table->unique(['tenant_id', 'external_id']);
$table->index(['tenant_id', 'shop_id', 'status', 'ordered_at']);
$table->index(['tenant_id', 'ordered_at']);
});
❓ أسئلة شائعة
tenant_id تلقائياً؛ طبقة API تتحقق من المستأجر الذي ينتمي إليه المستخدم الحالي؛ ومنع SQL الخام من تجاوز Scope. للعزل الأعلى (مثلاً: المالية أو الرعاية الصحية)، استخدم استراتيجية قاعدة بيانات منفصلة.Alert إلى Shop أو Tenant، لذا يمكن النظر في الارتباطات متعددة الأشكال. لكن إذا كان نوع الارتباط ثابتاً، فإن استخدام مفتاح أجنبي يوفر وضوحاً أكبر./api/v1/). عند تعايش v1 وv2، تُعالج مسارات v2 في ملف منفصل، بينما تُشارك طبقة Model وService. يُعيّن رقم إصدار جديد فقط لترقيات الإصدار الرئيسي؛ التغييرات الطفيفة تُنفذ بطريقة متوافقة مع الإصدارات السابقة.tenant_id وأنشئ سكريبت ترحيل بيانات. عند الانتقال من مستأجر واحد إلى بيئة متعددة المستأجرين، استخدم أمر Artisan لتعيين قيم tenant_id دفعة واحدة؛ بعد اكتمال المعالجة، أضف قيد NOT NULL.📖 ملخص
- تُفكَّك قصص المستخدمين إلى وحدات وظيفية بناءً على ثلاثة أنماط من الشخصيات (Alice، Bob، Charlie)
- قاعدة البيانات المشتركة والمخطط المشترك هما الخيار الأفضل لمعظم تطبيقات SaaS
- تصميم ER الأساسي: عزل tenant_id، المبالغ بالسنتات، قيد فريد على المعرف الخارجي
- تصميم API يتبع بنية RESTful، يستخدم بادئات الإصدارات، ويتبنى تنسيق استجابة موحد
- عند اختيار التقنيات، تُعطى الأولوية لقدرات الفريق ونضج النظام البيئي
- تبدأ تهيئة المشروع بملف الترحيل، laying الأساس لنموذج البيانات
📝 تمارين
-
تمرين أساسي (⭐): بناءً على مخطط ER، اكتب ملفات المخطط للجداول الثلاث المتبقية في ShopMetrics (products، order_items، alerts)، مع الفهارس والمفاتيح الأجنبية اللازمة.
-
تمرين متقدم (⭐⭐): باستخدام تنسيق OpenAPI 3.0 YAML، اكتب توثيق Orders API لـ ShopMetrics (GET list / GET detail / POST sync)، مع معلمات الطلب وتنسيقات الاستجابة ورموز الأخطاء.
-
تحدي (⭐⭐⭐): صمم حل وسيط متعدد المستأجرين كامل لـ ShopMetrics—نفذ وسيط
SetTenantContext، وسمةBelongsToTenant، واستخدم معلمات المسار (النطاق الفرعي) لتحديد المستأجرين. اكتب اختبارات للتحقق من أن المستأجر A لا يمكنه الوصول إلى بيانات المستأجر B.



