404 Not Found

404 Not Found


nginx

تصميم المشروع — بنية منصة ShopMetrics SaaS

البرمجة بدون تصميم كبناء بيت بدون مخطط—لا تدرك أن الأساس ليس عميقاً بما يكفي حتى تصل إلى الطابق الثالث، وحينها لا خيار أمامك سوى الهدم والبدء من جديد.

1. ما ستتعلمه


2. قصة حقيقية لبناء مشروع SaaS من الصفر

(1) المشكلة: Bob يريد بناء منصة تحليلات تجارة إلكترونية لكنه لا يعرف من أين يبدأ

يدير Bob شركة استشارات بيانات تجارة إلكترونية تضم أكثر من 50 عميلاً. بيانات كل عميل مبعثرة عبر ثماني منصات تجارة إلكترونية، ويقوم Bob بتجميعها يدوياً في Excel—قضاء أربع ساعات يومياً في إنشاء التقارير، التي غالباً ما تتخللها الأخطاء. Alice، المديرة التشغيلية في أحد العملاء، تقول: "من الرائع أن يكون هناك لوحة معلومات تعرض بيانات جميع المتاجر في الوقت الفعلي." Charlie، محلل البيانات لدى Bob، يقول: "نحتاج منصة SaaS متعددة المستأجرين، لكن كيف يجب أن نصمم البنية؟"

(2) نهج التصميم المنهجي

ابدأ بتحليل المتطلبات → قصص المستخدمين → اختيار البنية → نمذجة البيانات → تصميم API → اختيار التقنيات. كل خطوة لها مخرجات واضحة، وتتبع المخطط عند البرمجة.

TEXT
تحليل المتطلبات → قصص المستخدمين → اختيار البنية → تصميم ER → تصميم API → اختيار التقنيات → بدء البرمجة

(3) العائد

بعد أن أمضى Bob أسبوعين في إتمام التصميم، تضاعفت كفاءته البرمجية ثلاث مرات—لأن كل متطلب كان لديه واجهة وتصميم قاعدة بيانات واضحين، مما ألغى الحاجة إلى إجراء التغييرات أثناء التنفيذ.


3. تحليل المتطلبات وقصص المستخدمين

(1) ثلاثة أنماط من ملفات المستخدمين

الدور الاسم الحاجة الأساسية الإجراءات النموذجية
مدير المستأجر Alice إدارة متجره وفريقه إنشاء متاجر، دعوة أعضاء، عرض لوحة المعلومات
عمليات المنصة Bob إدارة جميع المستأجرين والفواتير الموافقة على المستأجرين، إدارة الخطط، عرض إحصائيات المنصة
محلل بيانات Charlie تحليل بيانات التجارة الإلكترونية وإنشاء التقارير إنشاء تقارير، إعداد التنبيهات، تصدير البيانات

(2) قصص المستخدمين

TEXT
بصفتي 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) ▶ مثال: تفكيك قصص المستخدمين إلى وحدات وظيفية

PHP
// قصص المستخدمين → تعيين الوحدات الوظيفية
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: مقاييس المنصة بالكامل',
    ],
];

الناتج:

TEXT
// تم التنفيذ بنجاح

4. اختيار بنية التعدد

(1) ثلاث استراتيجيات للتعدد

الاستراتيجية مستوى العزل التكلفة التعقيد حالات الاستخدام
قاعدة بيانات مستقلة الأعلى عالية متوسط متطلبات الامتثال المالي/الرعاية الصحية
قاعدة بيانات مشتركة + مخطط مستقل متوسط متوسطة متوسط متوسط الحجم، مع بعض متطلبات العزل
قاعدة بيانات مشتركة + مخطط مشترك الأدنى منخفضة منخفض معظم SaaS، عزل tenant_id

(2) قرار اختيار ShopMetrics

100%
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 استراتيجية قاعدة بيانات مشتركة + مخطط مشترك:

(1) ▶ مثال: تنفيذ Global Scope للتعدد

PHP
// 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);
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

5. تصميم ER لقاعدة البيانات

(1) علاقات الكيانات الأساسية

100%
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

PHP
// 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;
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

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

JSON
{
    "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

PHP
// 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']);
});

الناتج:

TEXT
// تم التنفيذ بنجاح

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) نظرة عامة على البنية

100%
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

PHP
// 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),
    ],
];

الناتج:

TEXT
// تم التنفيذ بنجاح

8. مثال شامل: تهيئة مشروع ShopMetrics

PHP
// ============================================
// شامل: هيكل المشروع والإعداد الأولي
// ترحيلات قاعدة البيانات، النماذج، والضبط
// ============================================

// 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']);
});

❓ أسئلة شائعة

س هل عزل المستأجرين بالمخطط المشترك آمن بما يكفي؟
ج كافٍ لمعظم تطبيقات SaaS. التدابير الرئيسية: Global Scope يصفّي tenant_id تلقائياً؛ طبقة API تتحقق من المستأجر الذي ينتمي إليه المستخدم الحالي؛ ومنع SQL الخام من تجاوز Scope. للعزل الأعلى (مثلاً: المالية أو الرعاية الصحية)، استخدم استراتيجية قاعدة بيانات منفصلة.
س متى يجب استخدام الارتباطات متعددة الأشكال في تصميم ER؟
ج الارتباطات متعددة الأشكال مناسبة لسيناريوهات "كيان واحد يمكن أن ينتمي إلى عدة كيانات أبوية" (مثلاً: تعليق يمكن أن ينتمي إلى منشور أو فيديو). في ShopMetrics، قد ينتمي Alert إلى Shop أو Tenant، لذا يمكن النظر في الارتباطات متعددة الأشكال. لكن إذا كان نوع الارتباط ثابتاً، فإن استخدام مفتاح أجنبي يوفر وضوحاً أكبر.
س كيف يجب إدارة إصدارات API؟
ج أبسط نهج هو استخدام بادئة URL للإصدارات (/api/v1/). عند تعايش v1 وv2، تُعالج مسارات v2 في ملف منفصل، بينما تُشارك طبقة Model وService. يُعيّن رقم إصدار جديد فقط لترقيات الإصدار الرئيسي؛ التغييرات الطفيفة تُنفذ بطريقة متوافقة مع الإصدارات السابقة.
س هل يجب مراعاة قدرات الفريق عند اختيار التقنيات؟
ج بالتأكيد. حتى أحدث حزمة تقنيات يمكن أن تبطئ الأمور فعلياً إذا لم يكن الفريق مألوفاً بها. أعِل أولوية التقنيات الأكثر ألفة للفريق، ثم نضج النظام البيئي، وأخيراً التقدم التقني.
س كم يجب أن يكون التصميم مفصلاً؟
ج يجب إنهاء الكيانات والعلاقات الأساسية بنسبة 100% (مخطط ER، نقاط نهاية API)؛ يمكن تعديل تفاصيل التنفيذ أثناء البرمجة. هدف التصميم: ألا تكون هناك حاجة لـ "اختراع" جداول أو واجهات جديدة أثناء البرمجة.
س كيف أتعامل مع ترحيل البيانات في بيئة متعددة المستأجرين؟
ج بسيط للمخططات المشتركة—فقط أضف عمود tenant_id وأنشئ سكريبت ترحيل بيانات. عند الانتقال من مستأجر واحد إلى بيئة متعددة المستأجرين، استخدم أمر Artisan لتعيين قيم tenant_id دفعة واحدة؛ بعد اكتمال المعالجة، أضف قيد NOT NULL.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): بناءً على مخطط ER، اكتب ملفات المخطط للجداول الثلاث المتبقية في ShopMetrics (products، order_items، alerts)، مع الفهارس والمفاتيح الأجنبية اللازمة.

  2. تمرين متقدم (⭐⭐): باستخدام تنسيق OpenAPI 3.0 YAML، اكتب توثيق Orders API لـ ShopMetrics (GET list / GET detail / POST sync)، مع معلمات الطلب وتنسيقات الاستجابة ورموز الأخطاء.

  3. تحدي (⭐⭐⭐): صمم حل وسيط متعدد المستأجرين كامل لـ ShopMetrics—نفذ وسيط SetTenantContext، وسمة BelongsToTenant، واستخدم معلمات المسار (النطاق الفرعي) لتحديد المستأجرين. اكتب اختبارات للتحقق من أن المستأجر A لا يمكنه الوصول إلى بيانات المستأجر B.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%