سند طراحی داده — Doc 03

اسکیمای کامل دیتابیس و ERD
Database Schema & Entity Relationships

طراحی کامل داده‌ای پلتفرم اتوماسیون: اصول نام‌گذاری، استراتژی کلیدها، اسکیمای هسته و پلتفرم و ماژول CRM، ایندکس‌گذاری، پارتیشن‌بندی، مهاجرت بدون‌وقفه، حجم داده در مقیاس ۱۰۰ مشتری و استراتژی پشتیبان‌گیری.

جداول: ۴۱ عدد DB پیشنهادی: PostgreSQL 16 جایگزین: MySQL 8 (InnoDB) Charset: utf8mb4 مکمل: Doc 01 + Doc 02

۱ اصول و قواعد طراحی اسکیما

پیش از طراحی هر جدول، این ۱۲ قاعده به‌عنوان قانون غیرقابل تغییر پروژه تعیین می‌شوند. هر جدول جدید باید از این قواعد تبعیت کند؛ در غیر این صورت در بازبینی رد می‌شود.

#قاعدهتعیین‌شدهدلیل
۱نام جدول snake_case · جمع · پیشوند ماژول هسته بدون پیشوند، ماژول‌ها با پیشوند (crm_) تا مرزها شفاف باشد
۲نام ستون snake_case · مفرد قابل پیش‌بینی بودن در کوئری و کد
۳کلید اصلی (داخلی) bigint unsigned (هسته) / id() سرعت join و اندازه ایندکس بهینه
۴شناسه عمومی (Public ID) ulid در ستون public_id با ایندکس یکتا عدم افشای تعداد رکورد و امکان ادغام آینده بین محیط‌ها
۵مهرهای زمانی created_at · updated_at · deleted_at همیشه UTC ذخیره، تبدیل در لایه نمایش
۶مقادیر پولی bigint به کوچک‌ترین یکای ارز (ریال) + ستون currency حذف کامل خطای اعشاری؛ استاندارد سیستم‌های مالی
۷گراف‌های عددی unsignedInteger — هرگز float برای پول/شمارش دقت و پایداری محاسبات
۸مقادیر شمارشی varchar(24-32) + چک‌کننده + PHP Enum افزودن مقدار جدید بدون ALTER TYPE سنگین
۹داده انعطافی json/jsonb فقط برای پیکربندی، نه برای فیلتر اصلی هر چه باید فیلتر شود، ستون مستقل دارد
۱۰ایندکس‌گذاری هر ایندکس با الگوی کوئری مشخص توجیه شود جلوگیری از ایندکس‌های بی‌استفاده که نوشتن را کند می‌کنند
۱۱کلید خارجی constrained() + رفتار صریح (cascade / nullOnDelete / restrict) یکپارچگی داده در سطح دیتابیس، نه فقط کد
۱۲جداسازی tenant ستون tenant_id در همه جداول داده‌ای + ایندکس ترکیبی به‌عنوان اولین ستون معماری Single-DB؛ بدون این، کارایی اسکوپ فاجعه می‌شود
سه قاعده‌ای که نقض کردنشان گران تمام می‌شود:
۱) ذخیره پول به‌صورت اعشاری — منبع بی‌پایان اختلاف حساب در بیلینگ.
۲) فیلتر روی ستون JSON — روی PostgreSQL کار می‌کند اما روی MySQL کند است و مسیر مهاجرت را می‌بندد.
۳) فراموشی tenant_id در ایندکس ترکیبی — کوئری‌های پرتکرار بعد از ۱۰٬۰۰۰ رکورد کند می‌شوند.

۲ نقشه کلان داده (Domain Map)

۳۸ جدول در سه گروه مستقل سازمان می‌یابند. مرز گروه‌ها دقیقاً با مرز کد (Core / Platform / Modules) منطبق است.

┌──────────────────────────────────────────────────────────────────────────────┐
│                       GROUP A — CORE  (موتور اتوماسیون)                       │
│                          جداول بدون پیشوند · ۱۳ جدول                          │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   tenants ──┬─► workspaces ──┬─► workflows ──┬─► workflow_nodes             │
│       │     │                │       │       └─► workflow_edges             │
│       │     │                │       ├─► executions ──► execution_logs       │
│       │     │                │       └─► workflow_versions                   │
│       │     │                └─► secrets                                     │
│       │     ├─► webhooks                                                    │
│       │     └─► schedules                                                   │
│       │                                                                     │
│   node_types (رجیستری گره‌ها — سراسری و مشترک، بدون tenant_id)              │
│   idempotency_keys (کش درخواست‌های تکراری)                                   │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│                     GROUP B — PLATFORM (لایه تجاری)                          │
│                        فقط در حالت SaaS بارگذاری می‌شود · ۱۱ جدول             │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   plans ──┬─► plan_modules ──► modules                                       │
│           └─► subscriptions ──► tenants                                      │
│                                                                              │
│   module_access  (دسترسی tenant به ماژول: از پلن / add-on / trial)            │
│   usage_counters (شمارنده مصرف ماهانه per tenant per metric)                  │
│   usage_periods  (خلاصه مصرف دوره گذشته برای صورتحساب)                        │
│   invoices       (فاکتورها)                                                  │
│   api_keys       (کلیدهای API با hash)                                       │
│   licenses       (لایسنس نسخه Self-Hosted)                                   │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│                    GROUP C — MODULE CRM (پیشوند crm_)                        │
│                             قابل نصب/حذف کامل · ۱۷ جدول                        │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   crm_companies ──► crm_contacts ──┬─► crm_deals ──► crm_deal_stage_history  │
│                            │       ├─► crm_activities                        │
│                            │       └─► crm_tasks                             │
│                            └─► crm_taggables ──► crm_tags                    │
│                                                                              │
│   crm_pipelines ──► crm_stages ──► crm_deals                                 │
│   crm_segments · crm_custom_fields · crm_field_values                        │
│   crm_assignment_rules · crm_scoring_rules · crm_lead_forms                  │
│   crm_merge_logs · crm_scoring_events                                        │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

۲.۱ اصل وابستگی یک‌طرفه

        GROUP A (CORE)        ◄──── خوانده می‌شود توسط همه
             ▲  ▲  ▲
             │  │  └──────────────────────┐
             │  └──────────────┐          │
             │                 │          │
     GROUP B (PLATFORM)   GROUP C (CRM)   ماژول‌های آینده
        ─ هیچ FK‌ای به CRM ندارد
        ─ CRM هیچ FK‌ای به PLATFORM ندارد
        ─ ارتباط فقط از طریق tenant_id مشترک

  نتیجه: حذف ماژول CRM ⇒ فقط DROP ۱۷ جدول crm_*
          هیچ جدول هسته‌ای آسیب نمی‌بیند و نیازی به تغییر نیست.
چرا CRM به جداول Platform وابسته نیست؟ چون در نسخه Self-Hosted لایه Platform وجود ندارد. اگر CRM به subscriptions وابسته بود، نسخه نصب‌شده روی سرور مشتری از کار می‌افتاد. بررسی مجوزها همیشه از طریق سرویس PlanGate انجام می‌شود که در حالت Self-Hosted یک پیاده‌سازی ساده «همه‌چیز مجاز» دارد.

۳ راهنمای نمادها (Legend)

در تمام جداول این سند از نمادهای زیر استفاده می‌شود:

PK کلید اصلی FK کلید خارجی IX ایندکس UQ یکتا table نام جدول
نوع دادهمعادل Laravelکاربرد
bigint unsigned$t->id()کلید اصلی و کلید خارجی
char(26)$t->ulid('public_id')شناسه عمومی در API
varchar(n)$t->string('x', n)متن کوتاه، شمارشی، اسلاگ
text$t->text('x')متن بلند، بدنه پیام، خطا
bigint$t->bigInteger('x')مبلغ پولی (کوچک‌ترین یکا)، شمارنده
unsignedInteger$t->unsignedInteger('x')امتیاز، ترتیب، مدت (ms)
boolean$t->boolean('x')پرچم‌های وضعیت دودویی
json / jsonb$t->json('x')پیکربندی، تنظیمات، متادیتا
timestamp$t->timestamp('x')زمان‌های کسب‌وکاری (nullable)
timestamps$t->timestamps()created_at + updated_at
softDeletes$t->softDeletes()deleted_at — حذف منطقی
مواردی که در اسکیما نخواهیم داشت: enum بومی دیتابیس · float/double برای پول · جدول بدون کلید اصلی · نام‌گذاری CamelCase · ستون‌های NULL-پذیر با معنی مبهم.

۴ اسکیمای هسته اتوماسیون

۱۳ جدول که موتور اتوماسیون را می‌سازند. این جداول هرگز توسط ماژول‌ها تغییر نمی‌کنند.

هسته — موجودیت سازمانی و تعریف جریان ۵ جدول
جدولهدفستون‌های کلیدیحجم تخمینی
tenants هر مشتری = یک tenant؛ ریشه همه داده‌ها id PK · public_id UQ · name · slug UQ · custom_domain UQ · status · mode · timezone · locale · branding JSON · settings JSON · trial_ends_at IX ~۱۰۰
workspaces تفکیک دپارتمان/تیم داخل یک tenant id PK · tenant_id FK IX · name · is_default · settings JSONUQ(tenant_id,slug) ~۳۰۰
workflows تعریف یک جریان اتوماسیون (بدون نسخه‌بندی تاریخی) id PK · tenant_id FK IX · workspace_id FK · public_id UQ · name · slug · status IX · trigger_type IX · trigger_config JSON · settings JSON · version · last_run_at · run_count ~۱۰٬۰۰۰
workflow_nodes گره‌های جریان (محرک، شرط، اقدام) id PK · workflow_id FK IX · node_key (مثل n1)UQ(workflow_id,node_key) · type IX · config JSON · position_x/y · label · is_disabled · error_policy ~۱۰۰٬۰۰۰
workflow_edges یال‌های اتصال بین گره‌ها id PK · workflow_id FK IX · from_node_key · to_node_key · branch (default/true/false/error)UQ(workflow_id,from_node_key,branch) ~۱۲۰٬۰۰۰
هسته — اجرا، رجیستری و محرک‌ها ۸ جدول
جدولهدفستون‌های کلیدیحجم تخمینی
executions هر بار اجرای یک جریان — منبع حقیقت وضعیت اجرا id PK · public_id UQ · tenant_id FK · workflow_id FK · workflow_version · status IX · trigger_type · trigger_context JSON · current_node_key · nodes_executed · duration_ms · error_code · error_message · started_at IX · finished_at · expires_at IX ~۵٬۰۰۰٬۰۰۰
execution_logs لاگ سطح گره (ورودی/خروجی/خطا) id PK · execution_id FK IX · node_key · node_type · status IX · attempt · input JSON · output JSON · error JSON · duration_ms · started_at ~۲۵٬۰۰۰٬۰۰۰
workflow_versions نسخه‌های قبلی تعریف جریان برای بازگشت (Rollback) id PK · workflow_id FK IX · versionUQ(workflow_id,version) · snapshot JSON (کل گراف) · published_by · published_at ~۳۰٬۰۰۰
webhooks endpointهای دریافتی (Inbound) برای هر جریان id PK · tenant_id FK IX · workflow_id FK · token (ULID) UQ · secret_hash · method · is_active · allowed_ips JSON · last_received_at · receive_count ~۵٬۰۰۰
schedules زمان‌بندی‌های Cron برای جریان‌ها id PK · tenant_id FK IX · workflow_id FK · cron_expression · timezone · is_active IX · last_run_at IX · next_run_at IX · run_count ~۳٬۰۰۰
secrets اعتبارنامه‌های رمزنگاری‌شده (توکن سرویس‌های بیرونی) — بدون tenant_id قابل خواندن از JSON id PK · tenant_id FK IX · keyUQ(tenant_id,key) · value_encrypted text · type · last_used_at · rotated_at ~۵٬۰۰۰
node_types رجیستری انواع گره؛ از ماژول‌ها پر می‌شود و مرجع پنل است. بدون tenant_id id PK · slug UQ · module_slug IX · kind IX · label · category · schema JSON · output_schema JSON · icon · is_deprecated · module_version ~۱۵۰
idempotency_keys جلوگیری از اجرای تکراری درخواست‌های وب‌هوک/API id PK · tenant_id FK · keyUQ(tenant_id,key) · request_hash · response_code · response_body JSON · expires_at IX ~۱٬۰۰۰٬۰۰۰ (کوتاه‌عمر)
تنها استثناها به قاعده tenant_id: node_types (رجیستری سراسری) و crm_stages (چون از طریق pipeline_id به tenant می‌رسد). برای هر استثنا باید در سند بازبینی دلیل نوشته شود.

۵ اسکیمای لایه پلتفرم

۱۱ جدول که فقط در حالت SaaS وجود دارند. در نسخه Self-Hosted این Migrationها اجرا نمی‌شوند و سرویس PlanGate با پیاده‌سازی AllowAll جایگزین می‌شود.

پلتفرم — بیلینگ و دسترسی ۷ جدول
جدولهدفستون‌های کلیدیحجم تخمینی
plans تعریف پلن‌های قابل فروش id PK · slug UQ · name · price_monthly · price_yearly · currency · limits JSON · is_public · sort_order · trial_days ~۶
modules کاتالوگ ماژول‌های قابل نصب و فروش id PK · slug UQ · name · version · is_core · price_monthly · node_count · dependencies JSON · manifest JSON · is_published ~۲۰
plan_modules کدام ماژول در کدام پلن گنجانده شده است plan_id FK · module_id FK · is_included · max_instances UQ(plan_id,module_id) ~۶۰
subscriptions اشتراک فعال هر tenant (تاریخچه‌دار، نه بازنویسی‌شده) id PK · tenant_id FK IX · plan_id FK · status IX · interval · current_period_start · current_period_end IX · trial_ends_at · canceled_at · cancel_at_period_end · payment_gateway · gateway_ref ~۵۰۰
module_access منبع واحد حقیقت برای «آیا این tenant به این ماژول دسترسی دارد؟» id PK · tenant_id FK IX · module_id FK · source (plan/addon/trial/manual) · is_active IX · activated_at · expires_at IX · settings JSON UQ(tenant_id,module_id) ~۱٬۰۰۰
usage_counters شمارنده جمع‌شده مصرف در دوره جاری id PK · tenant_id FK · metric · period_key (مثل 2026-09) · value bigint · updated_at UQ(tenant_id,metric,period_key) ~۵٬۰۰۰
usage_periods عکس فوری پایان هر دوره برای صورتحساب و گزارش id PK · tenant_id FK IX · period_key IX · breakdown JSON (مصرف per metric) · overage_amount · closed_at UQ(tenant_id,period_key) ~۲٬۵۰۰
تفکیک مهم: usage_counters فقط مقدار جمع‌شده را نگه می‌دارد (یک ردیف به‌ازای هر tenant × metric × ماه). ثبت مصرف رکورد به رکورد در جدول، در مقیاس ۵ میلیون اجرا فاجعه است. داده ریز اجرا در executions هست؛ شمارنده فقط برای بررسی سریع سقف در مسیر گرم (Hot Path) سرو می‌شود.
پلتفرم — فاکتور، کلید API، لایسنس و ردیابی ۴ جدول
جدولهدفستون‌های کلیدی
invoices فاکتور صادره برای هر دوره صورتحساب id PK · public_id UQ · tenant_id FK IX · number UQ · period_key · subtotal · discount · tax · total · currency · status IX · issued_at · due_at · paid_at · gateway_ref · lines JSON
api_keys کلیدهای API — فقط hash ذخیره می‌شود، کلید خام هرگز id PK · tenant_id FK IX · name · prefix UQ · key_hash · scopes JSON · environment (live/test) IX · rate_limit_per_minute · last_used_at · expires_at IX · revoked_at
licenses لایسنس نسخه Self-Hosted — قفل روی دامنه + اثر انگشت سرور id PK · key UQ · tenant_id FK · plan_id FK · domain IX · fingerprint · status IX · seats · modules JSON · issued_at · expires_at IX · last_heartbeat_at IX · heartbeat_ip · revoked_at
audit_logs ردیابی عملیات حساس: تغییر پلن، حذف داده، ورود Super Admin به tenant مشتری id PK · tenant_id FK IX · actor_type · actor_id · event IX · subject_type · subject_id · changes JSON · ip · user_agent · created_at IX
نکته امنیتی api_keys: ستون prefix (مثل ak_live_8f2a) برای شناسایی سریع کلید در فرآیند احراز هویت استفاده می‌شود؛ سپس key_hash با hash_equals مقایسه می‌شود. امکان بازیابی کلید خام پس از ساخت وجود ندارد — این ویژگی عمدی است و باید در UI هم به مشتری گفته شود.

۶ اسکیمای ماژول CRM

۱۷ جدول با پیشوند crm_. همه دارای tenant_id با ایندکس ترکیبی هستند (به‌جز crm_stages که از طریق pipeline_id به tenant می‌رسد). گروه‌بندی در سه کارت زیر آمده است: ارتباطی (۴)، قیف و فرصت (۴)، عملیاتی و پیکربندی (۹).

CRM — مخاطب، شرکت و برچسب ۴ جدول
جدولهدفستون‌های کلیدیحجم تخمینی
crm_contacts موجودیت مرکزی ماژول — مخاطب/لید id PK · public_id UQ · tenant_id FK · company_id FK · owner_user_id FK · first_name · last_name · email · email_normalized · mobile · mobile_normalized · status IX · score IX · source · last_activity_at IX · meta JSON · external_id UQ(tenant_id,external_id) ~۲٬۵۰۰٬۰۰۰
crm_companies سازمان/شرکت مرتبط با مخاطبان id PK · tenant_id FK IX · name · domain_normalized UQ(tenant_id,domain_normalized) · industry · size · owner_user_id FK · meta JSON ~۴۰۰٬۰۰۰
crm_tags برچسب‌های قابل تخصیص id PK · tenant_id FK · name · slug · color · usage_count UQ(tenant_id,slug) ~۵٬۰۰۰
crm_taggables رابطه چندبه‌چند برچسب با هر موجودیت (Polymorphic) id PK · tag_id FK IX · taggable_type · taggable_id UQ(tag_id,taggable_type,taggable_id) IX(taggable_type,taggable_id) ~۸٬۰۰۰٬۰۰۰
ستون‌های *_normalized: ایمیل و موبایل در دو ستون نگه داشته می‌شوند: یکی به‌شکلی که کاربر وارد کرده (برای نمایش) و یکی نرمال‌شده (برای تشخیص تکراری و ایندکس یکتا). این جداسازی، پایه کل مکانیزم جلوگیری از رکورد تکراری است.
CRM — قیف فروش و فرصت ۴ جدول
جدولهدفستون‌های کلیدیحجم تخمینی
crm_pipelines قیف فروش قابل تعریف برای هر tenant id PK · tenant_id FK IX · name · slug · is_default · sort_order UQ(tenant_id,slug) ~۵۰۰
crm_stages مراحل داخل هر قیف با ترتیب و احتمال موفقیت id PK · pipeline_id FK IX · name · sort_order IX · probability (۰-۱۰۰) · is_won · is_lost · rotting_days · color UQ(pipeline_id,sort_order) ~۲٬۵۰۰
crm_deals فرصت فروش — قلب ارزش مالی ماژول id PK · public_id UQ · tenant_id FK IX · pipeline_id FK · stage_id FK IX · contact_id FK · company_id FK · owner_user_id FK · title · amount bigint · currency · expected_close_date IX · status (open/won/lost) IX · lost_reason · stage_changed_at IX · rotting_notified_at · meta JSON ~۱٬۵۰۰٬۰۰۰
crm_deal_stage_history تاریخچه کامل حرکت بین مراحل (پایه گزارش قیف) id PK · deal_id FK IX · from_stage_id FK · to_stage_id FK · changed_by_user_id · changed_by_source (manual/automation/api) · duration_seconds · changed_at IX ~۶٬۰۰۰٬۰۰۰
گلوگاه کارایی: crm_deals پرکوئری‌ترین جدول ماژول است. الگوی اصلی «نمایش برد قیف برای یک قیف مشخص» است، پس ایندکس (tenant_id, pipeline_id, stage_id, sort_key) ضروری است. بدون آن، برد قیف با ۵۰۰ فرصت باز کند می‌شود.
CRM — فعالیت‌ها، پیکربندی و قوانین ۹ جدول
جدولهدفستون‌های کلیدیحجم تخمینی
crm_activities همه تعاملات: تماس، ایمیل، جلسه، یادداشت، اجرای اتوماسیون id PK · tenant_id FK IX · contact_id FK IX · deal_id FK · type IX · subject · body · occurred_at IX · user_id · source (manual/automation/api) · meta JSON ~۱۰٬۰۰۰٬۰۰۰
crm_tasks کار زمان‌دار پیگیری — پایه گره task.due id PK · tenant_id FK IX · contact_id FK · deal_id FK · title · description · assignee_user_id FK IX · status IX · priority · due_at IX · completed_at · reminded_at · source_workflow_id ~۵٬۰۰۰٬۰۰۰
crm_segments گروه داینامیک بر اساس فیلتر JSON ذخیره‌شده id PK · tenant_id FK IX · name · slug · entity (contact/deal) · filters JSON · is_dynamic · last_count · last_counted_at UQ(tenant_id,slug) ~۱۰٬۰۰۰
crm_custom_fields تعریف فیلدهای سفارشی هر tenant id PK · tenant_id FK IX · entity · key · label · type · options JSON · is_required · is_filterable · sort_order UQ(tenant_id,entity,key) ~۲۵٬۰۰۰
crm_field_values مقادیر فیلدهای سفارشی — ستون‌های typed برای فیلترپذیری id PK · tenant_id FK · custom_field_id FK IX · entity_type · entity_id · value_text · value_number · value_date · value_json UQ(custom_field_id,entity_type,entity_id) ~۱۵٬۰۰۰٬۰۰۰
crm_assignment_rules قوانین تخصیص خودکار مخاطب/فرصت id PK · tenant_id FK IX · name · entity · strategy (round_robin/load_balanced/fixed/rule_based) · conditions JSON · user_pool JSON · priority · is_active · last_assigned_index ~۲٬۰۰۰
crm_scoring_rules قوانین امتیازدهی سرنخ id PK · tenant_id FK IX · name · entity · expression JSON · points · direction (bonus/penalty) · is_active · priority ~۲۰۰
crm_lead_forms فرم جذب لید با endpoint عمومی id PK · tenant_id FK IX · name · public_token UQ · fields JSON · workflow_id FK · redirect_url · honeypot_field · is_active · submit_count · last_submit_at ~۲٬۰۰۰
crm_merge_logs ردیابی ادغام مخاطبان تکراری برای بازگشت (Undo) id PK · tenant_id FK · primary_id FK IX · merged_ids JSON · merged_fields JSON · merged_by_user_id · reverted_at · created_at IX ~۵٬۰۰۰

۷ جداول بحرانی — جزئیات ستونی

سه جدول پرکاربرد به‌صورت کامل ستون‌به‌ستون. بقیه جداول از همین الگو پیروی می‌کنند: کلیدها + tenant_id + timestamps + ایندکس‌های توجیه‌شده.

جریان اجرا — executions ۱۹ ستون
ستوننوعNULLنشانتوضیح
idbigintخیرPKکلید داخلی
public_idchar(26)خیرUQULID برای API (ex_01J...)
tenant_idbigintخیرFKحذف با CASCADE از tenants
workflow_idbigintخیرFK IXحذف با CASCADE — با حذف جریان، اجراهایش هم می‌روند
workflow_versionunsignedIntegerخیر—کدام نسخه اجرا شد (برای دیباگ با نسخه قدیمی)
statusvarchar(20)خیرIXqueued/running/success/failed/canceled/timeout
trigger_typevarchar(24)خیر—webhook/schedule/event/manual/api
trigger_contextjsonآری—ورودی محرک (payload وب‌هوک، خروجی cron)
current_node_keyvarchar(16)آری—گره در حال اجرا — برای نمایش زنده در پنل
nodes_executedunsignedIntegerخیر—تعداد گره‌های تکمیل‌شده
nodes_totalunsignedIntegerخیر—کل گره‌های نسخه اجرا (برای نوار پیشرفت)
duration_msunsignedIntegerآری—زمان کل اجرا — NULL یعنی هنوز در حال اجراست
error_codevarchar(40)آریIXکد ماشین‌خوان: node_failed/timeout/quota/invalid_graph
error_messagetextآری—پیام انسانی — باید ماسک شود و بدون داده حساس باشد
failed_node_keyvarchar(16)آری—گره شکست‌خورده برای جهت‌دهی مستقیم کاربر
retry_of_idbigintآریFKاگر «اجرای مجدد» بود، رکورد قبلی — تاریخچه قابل ردیابی
started_attimestampآریIXشروع اجرا
finished_attimestampآری—پایان اجرا
expires_attimestampخیرIXمبنای پاک‌سازی دوره‌ای (مطابق retention پلن)
چرا expires_at از روز اول؟ چون مدت نگهداری لاگ یک محدودیت پلن است (Free = ۳۰ روز، Business = ۹۰ روز). اگر این ستون بعداً اضافه شود، پرکردنش روی میلیون‌ها ردیف مستقیم به‌معنای UPDATE سنگین خواهد بود. با یک ایندکس، پاک‌سازی به DELETE ... WHERE expires_at < now() ساده تبدیل می‌شود.
مخاطب — crm_contacts ۲۱ ستون
ستوننوعNULLنشانتوضیح
idbigintخیرPK—
public_idchar(26)خیرUQctc_01J... در پاسخ API
tenant_idbigintخیرFKبدون آن جدول معنا ندارد
company_idbigintآریFK IXnullOnDelete — حذف شرکت، مخاطب را نمی‌برد
owner_user_idbigintآریFK IXمالک داخلی؛ NULL یعنی «بدون مالک» (صف انتظار)
first_namevarchar(80)آری——
last_namevarchar(80)آری——
emailvarchar(190)آری—به شکل اصلی برای نمایش
email_normalizedvarchar(190)آریUQ(tenant_id,email_normalized)lowercase + trim — کلید تشخیص تکراری
mobilevarchar(32)آری—به‌شکل واردشده
mobile_normalizedvarchar(32)آریIX(tenant_id,mobile_normalized)بدون پیشوند کشوری و بدون صفر اول
statusvarchar(24)خیرIX(tenant_id,status,score)ماشین وضعیت — بخش ۱۱
scoreintegerخیرIXمقدار فعلی امتیاز (۰ تا ۱۰۰۰)
sourcevarchar(32)آریIXform/webhook/api/manual/import/automation
source_detailvarchar(120)آری—مثل utm_campaign=webinar-01
last_activity_attimestampآریIXبه‌روزرسانی خودکار با هر crm.activity.log
next_activity_attimestampآریIXسررسید اولین تسک باز — مبنای نمای «کارهای امروز»
scored_attimestampآری—آخرین بازمحاسبه امتیاز (برای decay شبانه)
external_idvarchar(64)آریUQ(tenant_id,external_id)شناسه در سیستم بیرونی (ERP/حسابداری)
metajsonآری—فیلدهای فرار — هرگز ستون فیلتر اصلی نباشد
deleted_attimestampآریIXحذف منطقی + شرط WHERE deleted_at IS NULL
الگوی یکتایی دو‌لایه: یکتا روی email_normalized و ایندکس معمولی روی mobile_normalized. یعنی تکراری بر اساس ایمیل امکان‌ناپذیر است (جبران دیتابیس) و تکراری بر اساس موبایل توسط کد تشخیص داده می‌شود (سیاست‌مند). اگر یکتا را هم روی موبایل می‌گذاشتیم، نمی‌توانستیم در حالت «فقط ایمیل» به‌صورت خودکار رکورد بسازیم.

۸ DDL نمونه (Migration)

چهار Migration کلیدی که الگوی کل پروژه را تعریف می‌کنند. بقیه Migrationها از همین الگو تبعیت می‌کنند.

۸.۱ جریان اتوماسیون

// database/migrations/2026_01_01_000010_create_workflows_table.php
Schema::create('workflows', function (Blueprint $t) {
    $t->id();
    $t->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $t->foreignId('workspace_id')->constrained()->cascadeOnDelete();
    $t->ulid('public_id')->unique();
    $t->string('name', 140);
    $t->string('slug', 140);
    $t->text('description')->nullable();

    // وضعیت جریان: draft | active | paused | archived
    $t->string('status', 20)->default('draft');

    // محرک: webhook | schedule | event | manual
    $t->string('trigger_type', 24);
    $t->json('trigger_config')->nullable();

    // تنظیمات اجرایی: timeout, retry, concurrency, error_branch
    $t->json('settings')->nullable();
    $t->unsignedInteger('version')->default(1);

    // آمار (denormalized برای لیست بدون count روی executions)
    $t->unsignedBigInteger('run_count')->default(0);
    $t->unsignedBigInteger('failed_count')->default(0);
    $t->timestamp('last_run_at')->nullable();

    $t->timestamps();
    $t->softDeletes();

    $t->unique(['tenant_id', 'slug']);
    $t->index(['tenant_id', 'status']);
    $t->index(['tenant_id', 'trigger_type', 'status']);
    $t->index(['tenant_id', 'last_run_at']);
});

۸.۲ لاگ اجرای هر گره

// database/migrations/2026_01_01_000040_create_execution_logs_table.php
Schema::create('execution_logs', function (Blueprint $t) {
    $t->id();
    $t->foreignId('execution_id')->constrained()->cascadeOnDelete();
    $t->string('node_key', 16);
    $t->string('node_type', 80)->index();   // برای فیلتر «گره‌های شکست‌خورده»
    $t->string('status', 20)->index();        // success | failed | skipped
    $t->unsignedTinyInteger('attempt')->default(1);

    $t->json('input')->nullable();   // ← ماسک‌شده هنگام ثبت
    $t->json('output')->nullable();  // ← ماسک‌شده هنگام ثبت
    $t->json('error')->nullable();   // {code, message, retryable}

    $t->unsignedInteger('duration_ms')->nullable();
    $t->timestamp('started_at')->nullable();
    $t->timestamps();   // بدون softDeletes — داده بایگانی‌شده است، نه قابل حذف

    $t->index(['execution_id', 'node_key']);
    $t->index(['status', 'node_type']);
});

۸.۳ مخاطب (ماژول CRM)

// app/Modules/Crm/Database/Migrations/..._create_crm_contacts_table.php
Schema::create('crm_contacts', function (Blueprint $t) {
    $t->id();
    $t->ulid('public_id')->unique();
    $t->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $t->foreignId('company_id')->nullable()->constrained('crm_companies')->nullOnDelete();
    $t->foreignId('owner_user_id')->nullable()->constrained('users')->nullOnDelete();

    $t->string('first_name', 80)->nullable();
    $t->string('last_name',  80)->nullable();

    // لایه نمایش + لایه فهرست‌سازی
    $t->string('email',  190)->nullable();
    $t->string('email_normalized', 190)->nullable();
    $t->string('mobile', 32)->nullable();
    $t->string('mobile_normalized', 32)->nullable();

    $t->string('status', 24)->default('new');
    $t->integer('score')->default(0);
    $t->string('source', 32)->nullable();
    $t->string('source_detail', 120)->nullable();
    $t->string('external_id', 64)->nullable();

    $t->timestamp('last_activity_at')->nullable();
    $t->timestamp('next_activity_at')->nullable();
    $t->timestamp('scored_at')->nullable();

    $t->json('meta')->nullable();
    $t->timestamps();
    $t->softDeletes();

    // یکتا: جلوگیری مطلق از تکراری ایمیلی
    $t->unique(['tenant_id', 'email_normalized']);

    // مسیرهای پرکوئری — ترتیب ستون‌ها مهم است (tenant اول)
    $t->index(['tenant_id', 'status', 'score']);
    $t->index(['tenant_id', 'owner_user_id', 'status']);
    $t->index(['tenant_id', 'mobile_normalized']);
    $t->index(['tenant_id', 'last_activity_at']);
    $t->index(['tenant_id', 'next_activity_at']);
    $t->index(['tenant_id', 'source']);
    $t->unique(['tenant_id', 'external_id'], 'crm_contacts_tenant_external_unique');
});
نام ایندکس یکتا: چون دو یکتای tenant_id-دار داریم، نام صریح بدهید تا خطای مهاجرت گیج‌کننده نشود. در MySQL نام ایندکس‌ها در کل schema یکتا هستند نه در هر جدول.
بارگذاری Migration ماژول‌ها: ModuleInstaller هنگام نصب ماژول، فایل‌های Database/Migrations همان ماژول را از مسیر ثبت‌شده بارگذاری می‌کند؛ در حالت Self-Hosted اگر ماژول نصب نباشد، هیچ کوئری DDL اجرا نمی‌شود. این کلید سازگاری دو حالت است.

۹ استراتژی ایندکس‌گذاری

ایندکس‌ها چون خواندن را تند و نوشتن را کند می‌کنند، باید توجیه‌پذیر باشند. این سه قاعده کل را تعیین می‌کنند:

قاعده ۱ — ترتیب ستون‌ها

همیشه tenant_id اولین ستون ایندکس ترکیبی باشد. دلیل: محدودترین فیلتر در همه کوئری‌ها «کدام مشتری؟» است. ترتیب برعکس، ایندکس را در مسیر گرم بی‌اثر می‌کند.

قاعده ۲ — اصل ترکیب

ایندکس (A, B) هم می‌تواند فیلتر «فقط A» را سرو کند و هم «A + B». پس (tenant_id, status) بدون نیاز به ایندکس tenant_id جدا هم کار می‌کند.

قاعده ۳ — هر ایندکس = یک کوئری

قبل از افزودن هر ایندکس، کوئری سمت کد و الگوی آن را بنویسید. اگر نتوانستید کوئری را بنویسید، ایندکس نخواهید خواست. سند EXPLAIN پیوست کنید.

۹.۱ نقشه ایندکس‌های پرکاربرد

الگوی کوئری (پرکاربردترین)ایندکس مورد نیازکاربرد
WHERE tenant_id=? AND status='active' ORDER BY id (tenant_id, status) لیست جریان‌های فعال در داشبورد
WHERE tenant_id=? AND pipeline_id=? AND stage_id=? (tenant_id, pipeline_id, stage_id) ستون‌های برد قیف (Kanban)
WHERE tenant_id=? AND email_normalized=? UQ (tenant_id, email_normalized) تشخیص تکراری + ورود API
WHERE tenant_id=? AND owner_user_id=? AND status (tenant_id, owner_user_id, status) «کارهای من» برای کارشناس
WHERE tenant_id=? AND due_at < now() AND status='open' (tenant_id, due_at) Job شناسایی تسک‌های سررسیدشده
WHERE tenant_id=? AND expires_at < now() expires_at (تکی) پاک‌سازی لاگ‌های منقضی
WHERE execution_id=? (execution_id, node_key) نمایش timeline یک اجرا
WHERE schedule.date > ? ORDER BY ... next_run_at + فیلتر is_active Job هر دقیقه: «چه چیزی باید اجرا شود»
WHERE custom_field_id=? AND value_number > ? (custom_field_id, value_number) فیلتر روی فیلد سفارشی عددی

۹.۲ ایندکس‌هایی که عمداً نداریم

موردچرا نداریمجایگزین
ایندکس روی ستون‌های JSON (معمولاً)کارایی ناچیز در MySQL و نگهداری پرهزینهفیلد پرکاربرد را ستون مستقل کنید
ایندکس تکی tenant_idهمه ایندکس‌های ترکیبی آن را پوشش می‌دهندترکیبی با حداقل یک ستون دیگر
ایندکس روی created_at در جداول کوچکتعداد ردیف کم، اسکن سریع استدر جداول بالای ۱ میلیون ردیف فعال می‌شود
ایندکس روی ستون‌های text/jsonحجم ایندکس انفجاریsearch بهینه‌شده (PostgreSQL) یا موتور جست‌وجو در فاز ۴
ایندکس روی error_messageجست‌وجو روی پیام، ارزش عملیاتی نداردروی error_code فیلتر کنید
هشدار عملیاتی: هر ایندکس جدید، زمان INSERT/UPDATE را بالا می‌برد. جدول‌های پرنویس (مثل executions با میلیون‌ها ردیف) باید کم‌ایندکس بمانند. ایندکس‌های غیرضروری executions را فقط با EXPLAIN ANALYZE و یک تست کارایی واقعی اضافه کنید — نه از روی حدس.
ابزار نظارت: فهرست ایندکس‌های بلااستفاده را با دستور زیر پیدا کنید (شبانه در CI اجرا نکنید؛ فقط ماهی یک بار). خروجی را به‌صورت گزارش مرور کنید: SELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0;

۱۰ سیاست استفاده از JSON

JSON قدرتمند است و خطرناک. بدون سیاست روشن، به مرور «جای فیلد شکسته» تبدیل می‌شود.

✅ JSON مجاز است برای:

  • پیکربندی: workflows.trigger_config، settings
  • قالب: node_types.schema، secrets نه، ولی manifest بله
  • ورودی/خروجی اجرا: execution_logs.input/output
  • تعریف کاربری: crm_segments.filters، crm_lead_forms.fields
  • دنباله رویداد: crm_merge_logs.merged_ids، invoices.lines
  • ماتریک جمعی: usage_periods.breakdown

⛔ JSON ممنوع است برای:

  • هویت: هرگز tenant_id داخل JSON
  • فیلتر اصلی: وضعیت، مالک، تاریخ سررسید، مبلغ
  • کلید خارجی: هیچ FK از درون JSON
  • داده حساس: رمز و توکن (سرنستون‌ها جدا هستند)
  • داده تکرارشونده: آدرسی که باید سرچ و ایندکس شود
  • داده مالی: مبالغ و آمار صورتحساب

۱۰.۱ قاعده «سه آزمون» پیش از افزودن ستون JSON

  سؤال ۱: آیا باید روی این داده WHERE یا ORDER BY زده شود؟
        ├─ بله  ⇒ ستون مستقل بساز
        └─ خیر ↓

  سؤال ۲: آیا ساختار هر رکورد متفاوت است (Schema های ناهمگون)؟
        ├─ خیر  ⇒ آیا نیاز به فیلتر در آینده دارم؟ اگر بله ⇒ ستون مستقل
        └─ بله ↓

  سؤال ۳: آیا فقط برای خواندن و نمایش استفاده می‌شود؟
        ├─ بله  ⇒ JSON مناسب است ✅
        └─ خیر  ⇒ دوباره برگرد به سؤال ۱

۱۰.۲ قرارداد ساختار JSON هر ستون

// برای هر ستون JSON یک cast در Model تعریف کنید + یک FormRequest اعتبارسنج.

// app/Core/Workflow/Models/Workflow.php
class Workflow extends Model
{
    protected $casts = [
        'trigger_config' => 'array',
        'settings'       => 'array',
    ];

    // نرمال‌سازی هنگام ذخیره — کلیدها همیشه یکسان باشند
    protected static function saving($model): void
    {
        $model->settings = static::normalizeSettings($model->settings);
    }
}
خطر پنهان JSON: اگر ستون settings فاقد اعتبارسنجی سمت سرور باشد، یک API-Client می‌تواند ساختار را بشکند و جریان در لحظه اجرا از کار بیفتد. قاعده: هر ستون JSON حتماً یک Schema validator دارد که هنگام ذخیره و هنگام اجرا هر دو بررسی می‌شوند.
مسیر مهاجرت: اگر روزی مجبور شدید فیلدی را از JSON به ستون مستقل ارتقا دهید، این عملیات را با ADD COLUMN + UPDATE ... (data ->>'k')::type در یک مهاجرت و بدون داون‌تایم (PostgreSQL) انجام دهید. سپس یک ماه هر دو را بنویسید (Dual-Write) و در پایان JSON را حذف کنید.

۱۱ ماشین‌های وضعیت (Enums)

هسته‌ای‌ترین تصمیم مدل داده: هر وضعیت، یک ماشین با گذارهای مجاز است. این نه‌تنها اعتبارسنجی، بلکه گزارش‌گیری و حتی طراحی API را شکل می‌دهد.

۱۱.۱ گذار وضعیت مخاطب (Contact Status)

                    ┌────────────────────────────────────────┐
                    │            crm_contacts.status          │
                    └────────────────────────────────────────┘

   new ──► qualified ──► contacted ──► nurturing ──► customer
    │          │             │            │             │
    │          │             │            │             ▼
    │          │             │            └──────► churned / inactive
    │          │             │                              │
    │          └──────► lost │                              │
    │                       │                              │
    └──(بدون نیاز)──────────┘◄───────────────(بازگشت ممکن)──┘

   ممنوع‌ها (throws InvalidStatusTransition):
     new ⇄ customer   |   lost → contacted   |   churned → qualified
وضعیتپیش‌فرضگذارهای مجازمعنی عملیاتی
new✔→ qualified · → lostتازه واردشده، بدون تعامل
qualified—→ contacted · → lostاز آستانه امتیاز گذشته (MQL)
contacted—→ nurturing · → customer · → lostحداقل یک فعالیت ثبت شده
nurturing—→ customer · → lost · → churnedدر حال دنبال‌سازی دوره‌ای
customer—→ churnedدست‌کم یک Deal «برنده»
lost—→ new (فقط دستی)ناموفق / رد شده
churned—→ new (فقط دستی)متوقف‌شده یا بی‌حرکت بلندمدت

۱۱.۲ گذار وضعیت اجرا (Execution Status)

  queued ──► running ──► success
    │           ├──────► failed ──────► (retry) ──► queued
    │           ├──────► timeout ─────► (retry) ──► queued
    │           └──────► canceled
    │                        │
    └──► canceled            └──► dead_letter (پس از اتمام retry)

  قواعد:
    ✗ هرگز running ⇄ queued  (نشانه job گم‌شده ⇒ watchdog باید تعمیر کند)
    ✗ success یک‌طرفه است (فقط با اجرای مجدد، رکورد جدید ساخته می‌شود)
    ✔ failed/canceled هر دو نهایی هستند و مسیر بازگشت، اجرای مجدد است
    ✔ stuck ⇐ اگر running بیش از timeout جریان ماند ⇒ watchdog آن را timeout می‌کند

۱۱.۳ پیاده‌سازی با Enum بومی PHP

// app/Modules/Crm/Enums/ContactStatus.php
enum ContactStatus: string
{
    case New        = 'new';
    case Qualified   = 'qualified';
    case Contacted   = 'contacted';
    case Nurturing   = 'nurturing';
    case Customer    = 'customer';
    case Lost        = 'lost';
    case Churned     = 'churned';

    private function transitions(): array
    {
        return match ($this) {
            self::New       => [self::Qualified, self::Lost],
            self::Qualified => [self::Contacted, self::Lost],
            self::Contacted => [self::Nurturing, self::Customer, self::Lost],
            self::Nurturing => [self::Customer, self::Lost, self::Churned],
            self::Customer  => [self::Churned],
            self::Lost      => [self::New],
            self::Churned   => [self::New],
        };
    }

    public function canTransitionTo(self $to): bool
    {
        return in_array($to, $this->transitions(), true);
    }
}
چرا اینقدر سخت‌گیرانه؟ اگر گذارها آزاد باشند، مشتری می‌تواند مخاطب را مستقیم از new به customer ببرد، نرخ تبدیل گزارش‌ها بی‌معنا می‌شود و داده‌های داشبورد قابل اعتماد نخواهد بود. یک Enum سخت‌گیر، داده تمیز و گزارش قابل اعتماد می‌سازد.
استثنا: عملیات ادغام (merge) و بازیابی (restore) باید با forceTransition() از route ویژه وارد شوند و در audit_logs ثبت شوند.

۱۲ حذف نرم و چرخه حیات داده

گروه دادهرفتار حذفدلیل
tenants و زیرمجموعه‌ها Hard Delete با CASCADE + تأیید دوم + بازه ۳۰ روزه پشیمانی حق حذف کامل داده + رها شدن فضای مشتری قدیمی
workflows Soft Delete (بازیابی‌پذیر ۳۰ روز) حذف اشتباهی جریان فعال، مشتری را می‌ترساند
crm_contacts و crm_deals Soft Delete + بازیابی از سطل آشغال پنل دارایی اصلی مشتری؛ حذف باید قابل بازگشت باشد
crm_merge_logs و audit_logs فقط Soft Delete، به‌درخواست صریح tenant ردیابی و حسابرسی
executions و execution_logs پاک‌سازی دوره‌ای بر اساس expires_at حجم بزرگ؛ retention تابع پلن است
idempotency_keys Hard Delete بر اساس expires_at (پس از ۲۴ ساعت) کش فنی، ارزش حسابرسی ندارد
usage_counters هیچ حذفی؛ فقط بایگانی در usage_periods پایه صورتحساب مالی
secrets حذف فوری + پاک‌سازی آثار نسخه پیشین امنیت: توکن حذف‌شده نباید جایی بماند

۱۲.۱ قواعد سطل آشغال (Trash)

  حذف توسط کاربر
       │
       ▼
  ┌──────────────────────────────────────┐
  │ Soft Delete  →  deleted_at = now()   │
  │ (به‌جز جداول بایگانی و کش)            │
  └──────────────────┬───────────────────┘
                     ▼
  ┌──────────────────────────────────────┐
  │ سطل آشغال (نمایش در فرانت)            │
  │  · مشاهده اقلام حذف‌شده               │
  │  · بازیابی (restore)                  │
  │  · حذف قطعی (Force Delete)           │
  └──────────────────┬───────────────────┘
                     ▼ پس از ۳۰ روز
  ┌──────────────────────────────────────┐
  │ Job شبانه: حذف دائمی                 │
  │  · با CASCADE از رکوردهای مرتبط       │
  │  · ثبت خلاصه در audit_logs           │
  └──────────────────────────────────────┘
موتور جست‌وجو باید حذف را ببیند: اگر روزی موتور جست‌وجو اضافه شود، حذف نرم باید فوراً بازتاب داده شود، وگرنه مخاطب حذف‌شده در نتایج باقی می‌ماند. راهکار: حذف رکورد از ایندکس در همان تراکنش، یا دریافت رویداد بعد از commit.

۱۳ حجم داده و مقیاس ۱۰۰ مشتری

تخمین بر پایه: ۱۰۰ مشتری در سال دوم، با فرض میانگین ۳۰ اجرا در روز برای هر مشتری و ۱۵ گره در هر جریان.

جدولتعداد ردیف تخمینیاندازه فیزیکیعامل رشد
execution_logs۲۵٬۰۰۰٬۰۰۰~۸–۱۲ GBبزرگ‌ترین جدول سیستم
crm_field_values۱۵٬۰۰۰٬۰۰۰~۱.۵–۲ GBتعداد فیلد × تعداد مخاطب
crm_activities۱۰٬۰۰۰٬۰۰۰~۲–۳ GBهر تعامل + هر اجرای اتوماسیون
crm_taggables۸٬۰۰۰٬۰۰۰~۰.۵–۰.۸ GBبرچسب × موجودیت
crm_deal_stage_history۶٬۰۰۰٬۰۰۰~۰.۴–۰.۶ GBهر تغییر مرحله
executions۵٬۰۰۰٬۰۰۰~۱.۵–۲.۵ GBمتناسب با تعداد اجراها
crm_tasks۵٬۰۰۰٬۰۰۰~۰.۵–۰.۸ GB—
crm_contacts۲٬۵۰۰٬۰۰۰~۰.۸–۱.۲ GBرکورد اصلی مشتری
crm_deals۱٬۵۰۰٬۰۰۰~۰.۴–۰.۶ GB—
idempotency_keys۱٬۰۰۰٬۰۰۰~۰.۳–۰.۵ GBکوتاه‌عمر، پاک می‌شود
بقیه جداول (۳۰ جدول کوچک)< ۱۰۰٬۰۰۰< ۰.۲ GB—
مجموع داده خام~۷۴ میلیون~۱۶–۲۵ GBبدون ایندکس
افزایش ایندکس‌ها (۳۰–۵۰٪)—~۵–۱۲ GB—
کل با ایندکس + WAL—~۳۰–۴۵ GBبدون احتساب پشتیبان
جمع‌بندی مقیاس: با ۱۰۰ مشتری، یک سرور متوسط (۸ گیگ رم / ۱۰۰ گیگ SSD) کاملاً پاسخگوست. گلوگاه اول، دیتابیس نیست؛ صف، CPU و I/O شبکه زودتر محدود می‌شوند. نقطه چرخش مقیاس‌پذیری، مرز ۵۰۰ مشتری یا ۱۰ میلیون اجرا در ماه است که آنجا باید پارتیشن‌بندی (بخش ۱۴) اعمال شود.
تست کارایی حداقلی: حداقل ۱۰۰ برابر این حجم را با factory بسازید و سه کوئری پرکاربرد را با EXPLAIN ANALYZE بسنجید: لیست مخاطبان، برد قیف و timeline اجرا. معیار قبولی: همه زیر ۳۰۰ میلی‌ثانیه.

۱۴ استراتژی پارتیشن‌بندی و بایگانی داده‌ها

برای جداول حجیم، عملیات DELETE سنتی پرهزینه است و سبب قفل شدن جدول و پدیده Table Bloat می‌شود. راهکار قطعی، Range Partitioning ماهانه بر مبنای تاریخ است تا آزادسازی فضا با یک DROP TABLE ارزان در کسری از ثانیه انجام شود.

جدول‌های مشمول پارتیشن

  • execution_logs (بزرگ‌ترین جدول سیستم بر مبنای created_at)
  • executions (بر مبنای created_at و ماهانه)
  • crm_activities (از فاز مقیاس بالای ۵۰۰ تننت بر مبنای ماه)

جداول مشمول هرس خودکار (Pruning)

  • idempotency_keys (بر اساس expires_at < NOW() هر ساعت)
  • لاگ‌های منقضی بر اساس سیاست پلن مشتری (مثلاً ۳۰ روزه در پلن پایه)

۱۴.۱ نمونه تعریف پارتیشن در PostgreSQL

-- تبدیل execution_logs به جدول پارتیشن‌بندی شده ماهانه
CREATE TABLE execution_logs (
    id BIGSERIAL,
    tenant_id UUID NOT NULL,
    execution_id BIGINT NOT NULL,
    node_key VARCHAR(64) NOT NULL,
    status VARCHAR(20) NOT NULL,
    input_payload JSONB,
    output_payload JSONB,
    created_at TIMESTAMPTZ NOT NULL,
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- ایجاد پارتیشن ماه فروردین / آوریل
CREATE TABLE execution_logs_y2026m04 PARTITION OF execution_logs
    FOR VALUES FROM ('2026-04-01 00:00:00+00') TO ('2026-05-01 00:00:00+00');

-- آزادسازی فضای ماه منقضی شده با صفر اثر روی دیتابیس زنده:
-- DROP TABLE execution_logs_y2026m01;

۱۵ سیاست اجرای مایگریشن‌ها در محیط عملیاتی (Zero-Downtime)

سیستم باید بدون توقف سرویس و بدون ایجاد قفل طولانی (Exclusive Locks) روی جدول‌های زنده بروزرسانی شود.

قاعده ۱ — ممنوعیت تغییرات مخرب ناگهانی

هرگز یک ستون زنده را در یک استقرار تغییر نام یا حذف نکنید. از الگوی Expand & Contract در دو یا سه نگارش پی‌درپی استفاده کنید.

قاعده ۲ — ساخت ایندکس هم‌روند

در PostgreSQL همیشه از عبارت CREATE INDEX CONCURRENTLY استفاده کنید تا جدول در حین ایندکس‌گذاری قفل نوشتن نخورد.

قاعده ۳ — مقدار پیش‌فرض بدون قفل

افزودن ستون جدید با مقدار پیش‌فرض ثابت در نسخه‌های مدرن (PG 11+ و MySQL 8+) متادیتاست، اما از DEFAULT NOW() سنگین پرهیز کنید.

۱۵.۱ چرخه سه‌مرحله‌ای تغییر ستون (Expand & Contract)

  مرحله ۱ (نسخه N):
    ├─ افزودن ستون جدید (Nullable)
    └─ کد جدید: خواندن از قدیم، نوشتن همزمان در هر دو (Dual-Write)
  
  مرحله ۲ (نسخه N+1):
    ├─ اجرای Job پس‌زمینه برای پر کردن ردیف‌های قدیمی در ستون جدید
    └─ کد: تغییر منبع خواندن به ستون جدید
  
  مرحله ۳ (نسخه N+2):
    ├─ حذف نوشتن به ستون قدیمی
    └─ DROP ستون قدیمی بدون ریسک داون‌تایم

۱۶ معماری پشتیبان‌گیری و بازیابی (Backup & Disaster Recovery)

شاخص / روشمشخصات و ابزارهدف بازیابی
RPO (نقطه بازیابی)حداکثر ۵ دقیقه از دست رفتن داده با ثبت مداوم WAL / Binary Logحداقل اتلاف داده
RTO (زمان بازیابی)کمتر از ۴۵ دقیقه برای بازگردانی کامل کلاستر اصلیبازگشت سریع سرویس
Full Backupهر ۲۴ ساعت یک‌بار (نیمه‌شب) با فشرده‌سازی و انتقال رمزنگاری‌شده به Object Storage مجزا (S3/MinIO)نسخه مبنای روزانه
PITR (Point-in-Time Recovery)آرشیو مداوم WAL به فضای ذخیره‌سازی ابری مستقل با ابزارهایی مثل pgBackRest یا WAL-Gقابلیت بازگشت به هر ثانیه
بازیابی تک‌تننت (Tenant Restore)استخراج ردیف‌های تننت مشخص از نسخه دامی دیتابیس بکاپ با اسکریپت فیلتر tenant_idبدون دستکاری سایر مشتریان

۱۷ مقایسه موتور دیتابیس: PostgreSQL vs MySQL 8

هر دو موتور گزینه‌های معتبری هستند، اما با توجه به ویژگی‌های زیرساختی، تحلیل و اولویت سیستم به شرح زیر است:

معیار ارزیابیPostgreSQL (انتخاب اول - توصیه‌شده)MySQL 8 (جایگزین پشتیبانی‌شده)
پشتیبانی از JSONبسیار قدرتمند با JSONB و ایندکس‌های تخصصی GINپشتیبانی خوب اما فاقد انعطاف‌پذیری GIN
پارتیشن‌بندی و نگهداریپارتیشن‌بندی بازه‌ای نیتیو و مدیریت تمیز با ابزارهای pg_partmanپشتیبانی می‌شود ولی ابزار اکوسیستمی محدودتر
امنیت چندمستأجریپشتیبانی نیتیو از Row-Level Security (RLS) در هسته موتورعدم پشتیبانی پیش‌فرض از RLS (متکی به لایه اپلیکیشن)
ایندکس‌گذاری هم‌روندCREATE INDEX CONCURRENTLY پایدار و بدون قفل جدولOnline DDL وجود دارد اما رفتارهای قفلی متغیر است
نتیجه‌گیری معماریپایگاه‌داده پیش‌فرض SaaS ابریپشتیبانی شده برای نصب‌های Self-Hosted سبک

۱۸ استراتژی تست و تضمین یکپارچگی داده‌ها

۱۹ ترتیب قطعی اجرای مایگریشن‌ها (Migration Dependency Order)

برای جلوگیری از خطاهای قید کلید خارجی (Foreign Key Constraint Failures)، مایگریشن‌های ۴۱ جدول باید دقیقاً بر اساس لایه‌بندی زیر اعمال گردند:

  1. لایه ۰ (زیرساخت پایه): tenants، سپس users، roles، permissions، tenant_user
  2. لایه ۱ (محیط ابری پلتفرم): plans، subscriptions، usage_counters، usage_periods، api_keys، secrets
  3. لایه ۲ (موتور اتوماسیون Core): node_types، workflows، triggers، idempotency_keys، executions، execution_logs، audit_logs
  4. لایه ۳ (پایه CRM): crm_tags، crm_custom_fields، crm_pipelines، crm_pipeline_stages
  5. لایه ۴ (موجودیت‌های اصلی CRM): crm_companies، crm_contacts، crm_deals، crm_lead_forms
  6. لایه ۵ (رابط‌ها و رویدادهای فرعی): crm_activities، crm_tasks، crm_deal_stage_history، crm_field_values، crm_taggables، crm_merge_logs، crm_segments

۲۰ نتیجه‌گیری و گام‌های بعدی پیاده‌سازی

سند دیتابیس ۴۱ جدولی با موفقیت نهایی شد: این طرح تمام نیازمندی‌های معماری ماژولار، جداسازی ابری از نسخه سازمانی، سلامت چندمستأجری (Multi-Tenancy Isolation)، عملکرد در مقیاس میلیون‌ها لاگ و سازگاری با سیستم گردش‌کار و ماژول CRM را به‌طور کامل پوشش می‌دهد.

وضعیت سند معماری

طراحی پایگاه‌داده و دیاگرام رابطه موجودیت‌ها (ERD) آماده شروع فاز کدنویسی در مخزن لاراول است.

اقدام بعدی (Next Step)

ایجاد مایگریشن‌های اولیه در لاراول ۱۱ بر مبنای جدول ترتیب لایه ۰ تا لایه ۵ و تولید Seederها برای گره‌های اولیه اتوماسیون و فیلدهای CRM.