طراحی کامل دادهای پلتفرم اتوماسیون: اصول نامگذاری، استراتژی کلیدها، اسکیمای هسته و پلتفرم و ماژول CRM، ایندکسگذاری، پارتیشنبندی، مهاجرت بدونوقفه، حجم داده در مقیاس ۱۰۰ مشتری و استراتژی پشتیبانگیری.
پیش از طراحی هر جدول، این ۱۲ قاعده بهعنوان قانون غیرقابل تغییر پروژه تعیین میشوند. هر جدول جدید باید از این قواعد تبعیت کند؛ در غیر این صورت در بازبینی رد میشود.
| # | قاعده | تعیینشده | دلیل |
|---|---|---|---|
| ۱ | نام جدول | 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؛ بدون این، کارایی اسکوپ فاجعه میشود |
tenant_id در ایندکس ترکیبی — کوئریهای پرتکرار بعد از ۱۰٬۰۰۰ رکورد کند میشوند.
۳۸ جدول در سه گروه مستقل سازمان مییابند. مرز گروهها دقیقاً با مرز کد (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_*
هیچ جدول هستهای آسیب نمیبیند و نیازی به تغییر نیست.
subscriptions وابسته بود، نسخه نصبشده روی سرور مشتری
از کار میافتاد. بررسی مجوزها همیشه از طریق سرویس PlanGate انجام میشود
که در حالت Self-Hosted یک پیادهسازی ساده «همهچیز مجاز» دارد.
در تمام جداول این سند از نمادهای زیر استفاده میشود:
| نوع داده | معادل 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) سرو میشود.
| جدول | هدف | ستونهای کلیدی |
|---|---|---|
| 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_. همه دارای tenant_id با ایندکس ترکیبی
هستند (بهجز crm_stages که از طریق pipeline_id به tenant میرسد).
گروهبندی در سه کارت زیر آمده است: ارتباطی (۴)، قیف و فرصت (۴)، عملیاتی و پیکربندی (۹).
| جدول | هدف | ستونهای کلیدی | حجم تخمینی |
|---|---|---|---|
| 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_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_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 | نشان | توضیح |
|---|---|---|---|---|
id | bigint | خیر | PK | کلید داخلی |
public_id | char(26) | خیر | UQ | ULID برای API (ex_01J...) |
tenant_id | bigint | خیر | FK | حذف با CASCADE از tenants |
workflow_id | bigint | خیر | FK IX | حذف با CASCADE — با حذف جریان، اجراهایش هم میروند |
workflow_version | unsignedInteger | خیر | — | کدام نسخه اجرا شد (برای دیباگ با نسخه قدیمی) |
status | varchar(20) | خیر | IX | queued/running/success/failed/canceled/timeout |
trigger_type | varchar(24) | خیر | — | webhook/schedule/event/manual/api |
trigger_context | json | آری | — | ورودی محرک (payload وبهوک، خروجی cron) |
current_node_key | varchar(16) | آری | — | گره در حال اجرا — برای نمایش زنده در پنل |
nodes_executed | unsignedInteger | خیر | — | تعداد گرههای تکمیلشده |
nodes_total | unsignedInteger | خیر | — | کل گرههای نسخه اجرا (برای نوار پیشرفت) |
duration_ms | unsignedInteger | آری | — | زمان کل اجرا — NULL یعنی هنوز در حال اجراست |
error_code | varchar(40) | آری | IX | کد ماشینخوان: node_failed/timeout/quota/invalid_graph |
error_message | text | آری | — | پیام انسانی — باید ماسک شود و بدون داده حساس باشد |
failed_node_key | varchar(16) | آری | — | گره شکستخورده برای جهتدهی مستقیم کاربر |
retry_of_id | bigint | آری | FK | اگر «اجرای مجدد» بود، رکورد قبلی — تاریخچه قابل ردیابی |
started_at | timestamp | آری | IX | شروع اجرا |
finished_at | timestamp | آری | — | پایان اجرا |
expires_at | timestamp | خیر | IX | مبنای پاکسازی دورهای (مطابق retention پلن) |
expires_at از روز اول؟ چون مدت نگهداری لاگ یک محدودیت پلن است
(Free = ۳۰ روز، Business = ۹۰ روز). اگر این ستون بعداً اضافه شود، پرکردنش روی میلیونها
ردیف مستقیم بهمعنای UPDATE سنگین خواهد بود. با یک ایندکس، پاکسازی به
DELETE ... WHERE expires_at < now() ساده تبدیل میشود.
crm_contacts
۲۱ ستون
| ستون | نوع | NULL | نشان | توضیح |
|---|---|---|---|---|
id | bigint | خیر | PK | — |
public_id | char(26) | خیر | UQ | ctc_01J... در پاسخ API |
tenant_id | bigint | خیر | FK | بدون آن جدول معنا ندارد |
company_id | bigint | آری | FK IX | nullOnDelete — حذف شرکت، مخاطب را نمیبرد |
owner_user_id | bigint | آری | FK IX | مالک داخلی؛ NULL یعنی «بدون مالک» (صف انتظار) |
first_name | varchar(80) | آری | — | — |
last_name | varchar(80) | آری | — | — |
email | varchar(190) | آری | — | به شکل اصلی برای نمایش |
email_normalized | varchar(190) | آری | UQ(tenant_id,email_normalized) | lowercase + trim — کلید تشخیص تکراری |
mobile | varchar(32) | آری | — | بهشکل واردشده |
mobile_normalized | varchar(32) | آری | IX(tenant_id,mobile_normalized) | بدون پیشوند کشوری و بدون صفر اول |
status | varchar(24) | خیر | IX(tenant_id,status,score) | ماشین وضعیت — بخش ۱۱ |
score | integer | خیر | IX | مقدار فعلی امتیاز (۰ تا ۱۰۰۰) |
source | varchar(32) | آری | IX | form/webhook/api/manual/import/automation |
source_detail | varchar(120) | آری | — | مثل utm_campaign=webinar-01 |
last_activity_at | timestamp | آری | IX | بهروزرسانی خودکار با هر crm.activity.log |
next_activity_at | timestamp | آری | IX | سررسید اولین تسک باز — مبنای نمای «کارهای امروز» |
scored_at | timestamp | آری | — | آخرین بازمحاسبه امتیاز (برای decay شبانه) |
external_id | varchar(64) | آری | UQ(tenant_id,external_id) | شناسه در سیستم بیرونی (ERP/حسابداری) |
meta | json | آری | — | فیلدهای فرار — هرگز ستون فیلتر اصلی نباشد |
deleted_at | timestamp | آری | IX | حذف منطقی + شرط WHERE deleted_at IS NULL |
email_normalized
و ایندکس معمولی روی mobile_normalized. یعنی تکراری بر اساس
ایمیل امکانناپذیر است (جبران دیتابیس) و تکراری بر اساس موبایل توسط کد تشخیص
داده میشود (سیاستمند). اگر یکتا را هم روی موبایل میگذاشتیم، نمیتوانستیم در حالت
«فقط ایمیل» بهصورت خودکار رکورد بسازیم.
چهار 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']); });
// 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 یکتا هستند نه در هر جدول.
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 و یک تست
کارایی واقعی اضافه کنید — نه از روی حدس.
SELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0;
JSON قدرتمند است و خطرناک. بدون سیاست روشن، به مرور «جای فیلد شکسته» تبدیل میشود.
workflows.trigger_config، settingsnode_types.schema، secrets نه، ولی manifest بلهexecution_logs.input/outputcrm_segments.filters، crm_lead_forms.fieldscrm_merge_logs.merged_ids، invoices.linesusage_periods.breakdowntenant_id داخل JSON سؤال ۱: آیا باید روی این داده WHERE یا ORDER BY زده شود؟
├─ بله ⇒ ستون مستقل بساز
└─ خیر ↓
سؤال ۲: آیا ساختار هر رکورد متفاوت است (Schema های ناهمگون)؟
├─ خیر ⇒ آیا نیاز به فیلتر در آینده دارم؟ اگر بله ⇒ ستون مستقل
└─ بله ↓
سؤال ۳: آیا فقط برای خواندن و نمایش استفاده میشود؟
├─ بله ⇒ 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); } }
settings فاقد اعتبارسنجی سمت سرور باشد،
یک API-Client میتواند ساختار را بشکند و جریان در لحظه اجرا از کار بیفتد.
قاعده: هر ستون JSON حتماً یک Schema validator دارد که هنگام ذخیره و هنگام
اجرا هر دو بررسی میشوند.
ADD COLUMN + UPDATE ... (data ->>'k')::type
در یک مهاجرت و بدون داونتایم (PostgreSQL) انجام دهید. سپس یک ماه هر دو را بنویسید
(Dual-Write) و در پایان JSON را حذف کنید.
هستهایترین تصمیم مدل داده: هر وضعیت، یک ماشین با گذارهای مجاز است. این نهتنها اعتبارسنجی، بلکه گزارشگیری و حتی طراحی API را شکل میدهد.
┌────────────────────────────────────────┐
│ 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 (فقط دستی) | متوقفشده یا بیحرکت بلندمدت |
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 میکند
// 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 | حذف فوری + پاکسازی آثار نسخه پیشین | امنیت: توکن حذفشده نباید جایی بماند |
حذف توسط کاربر
│
▼
┌──────────────────────────────────────┐
│ Soft Delete → deleted_at = now() │
│ (بهجز جداول بایگانی و کش) │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ سطل آشغال (نمایش در فرانت) │
│ · مشاهده اقلام حذفشده │
│ · بازیابی (restore) │
│ · حذف قطعی (Force Delete) │
└──────────────────┬───────────────────┘
▼ پس از ۳۰ روز
┌──────────────────────────────────────┐
│ Job شبانه: حذف دائمی │
│ · با CASCADE از رکوردهای مرتبط │
│ · ثبت خلاصه در audit_logs │
└──────────────────────────────────────┘
تخمین بر پایه: ۱۰۰ مشتری در سال دوم، با فرض میانگین ۳۰ اجرا در روز برای هر مشتری و ۱۵ گره در هر جریان.
| جدول | تعداد ردیف تخمینی | اندازه فیزیکی | عامل رشد |
|---|---|---|---|
| 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 | بدون احتساب پشتیبان |
factory بسازید و سه
کوئری پرکاربرد را با EXPLAIN ANALYZE بسنجید: لیست مخاطبان، برد قیف و
timeline اجرا. معیار قبولی: همه زیر ۳۰۰ میلیثانیه.
برای جداول حجیم، عملیات DELETE سنتی پرهزینه است و سبب قفل شدن جدول و پدیده Table Bloat میشود. راهکار قطعی، Range Partitioning ماهانه بر مبنای تاریخ است تا آزادسازی فضا با یک DROP TABLE ارزان در کسری از ثانیه انجام شود.
execution_logs (بزرگترین جدول سیستم بر مبنای created_at)executions (بر مبنای created_at و ماهانه)crm_activities (از فاز مقیاس بالای ۵۰۰ تننت بر مبنای ماه)idempotency_keys (بر اساس expires_at < NOW() هر ساعت)-- تبدیل 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;
سیستم باید بدون توقف سرویس و بدون ایجاد قفل طولانی (Exclusive Locks) روی جدولهای زنده بروزرسانی شود.
هرگز یک ستون زنده را در یک استقرار تغییر نام یا حذف نکنید. از الگوی Expand & Contract در دو یا سه نگارش پیدرپی استفاده کنید.
در PostgreSQL همیشه از عبارت CREATE INDEX CONCURRENTLY استفاده کنید تا جدول در حین ایندکسگذاری قفل نوشتن نخورد.
افزودن ستون جدید با مقدار پیشفرض ثابت در نسخههای مدرن (PG 11+ و MySQL 8+) متادیتاست، اما از DEFAULT NOW() سنگین پرهیز کنید.
مرحله ۱ (نسخه N):
├─ افزودن ستون جدید (Nullable)
└─ کد جدید: خواندن از قدیم، نوشتن همزمان در هر دو (Dual-Write)
مرحله ۲ (نسخه N+1):
├─ اجرای Job پسزمینه برای پر کردن ردیفهای قدیمی در ستون جدید
└─ کد: تغییر منبع خواندن به ستون جدید
مرحله ۳ (نسخه N+2):
├─ حذف نوشتن به ستون قدیمی
└─ DROP ستون قدیمی بدون ریسک داونتایم
| شاخص / روش | مشخصات و ابزار | هدف بازیابی |
|---|---|---|
| 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 (انتخاب اول - توصیهشده) | MySQL 8 (جایگزین پشتیبانیشده) |
|---|---|---|
| پشتیبانی از JSON | بسیار قدرتمند با JSONB و ایندکسهای تخصصی GIN | پشتیبانی خوب اما فاقد انعطافپذیری GIN |
| پارتیشنبندی و نگهداری | پارتیشنبندی بازهای نیتیو و مدیریت تمیز با ابزارهای pg_partman | پشتیبانی میشود ولی ابزار اکوسیستمی محدودتر |
| امنیت چندمستأجری | پشتیبانی نیتیو از Row-Level Security (RLS) در هسته موتور | عدم پشتیبانی پیشفرض از RLS (متکی به لایه اپلیکیشن) |
| ایندکسگذاری همروند | CREATE INDEX CONCURRENTLY پایدار و بدون قفل جدول | Online DDL وجود دارد اما رفتارهای قفلی متغیر است |
| نتیجهگیری معماری | پایگاهداده پیشفرض SaaS ابری | پشتیبانی شده برای نصبهای Self-Hosted سبک |
404 ModelNotFound مواجه میشود.usage_counters) با استفاده از تراکنشهای ایزوله و قفلهای بدبینانه lockForUpdate().برای جلوگیری از خطاهای قید کلید خارجی (Foreign Key Constraint Failures)، مایگریشنهای ۴۱ جدول باید دقیقاً بر اساس لایهبندی زیر اعمال گردند:
tenants، سپس users، roles، permissions، tenant_userplans، subscriptions، usage_counters، usage_periods، api_keys، secretsnode_types، workflows، triggers، idempotency_keys، executions، execution_logs، audit_logscrm_tags، crm_custom_fields، crm_pipelines، crm_pipeline_stagescrm_companies، crm_contacts، crm_deals، crm_lead_formscrm_activities، crm_tasks، crm_deal_stage_history، crm_field_values، crm_taggables، crm_merge_logs، crm_segmentsطراحی پایگاهداده و دیاگرام رابطه موجودیتها (ERD) آماده شروع فاز کدنویسی در مخزن لاراول است.
ایجاد مایگریشنهای اولیه در لاراول ۱۱ بر مبنای جدول ترتیب لایه ۰ تا لایه ۵ و تولید Seederها برای گرههای اولیه اتوماسیون و فیلدهای CRM.