تحلیل کامل اولین ماژول تجاری پلتفرم اتوماسیون: دامنه کسبوکار، مدل داده، قوانین کسبوکار، گرههای اتوماسیون (Trigger / Action / Condition)، سناریوهای واقعی، API، مجوزها، متریک مصرف و نقشه پیادهسازی.
تقریباً هر کسبوکار B2B و B2C به مخاطب، پیگیری و قیف فروش نیاز دارد. بستر تست ایده برای همه صنایع.
«وقتی لید جدید ثبت شد → پیامک بفرست → تسک پیگیری بساز → به کارشناس تخصیص بده» روشنترین و پرفروشترین سناریوی اتوماسیون است.
CRM مشتری را نگه میدارد و سپس ماژولهای پیامرسان، پرداخت و گزارش روی آن سوار میشوند.
module-messagingmodule-paymentmodule-ecommercemodule-analytics ┌──────────────────────────────────────────────┐
│ automation-core (هسته) │
│ موتور اجرا · رجیستری گره · بیلینگ · tenant │
└───────────────────┬──────────────────────────┘
│ (ثبت گرهها و متریکها)
┌────────────────────┼────────────────────┬───────────────────┐
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐
│ module-crm │◄──│ module-messaging│ │ module-payment │ │ module- │
│ (این سند) │ │ پیامک/ایمیل/ │ │ درگاه/فاکتور │ │ analytics │
│ │ │ واتساپ/تلگرام │ │ │ │ گزارشها │
└─────────────┘ └─────────────────┘ └─────────────────┘ └──────────────┘
▲
│ گرههای CRM که ماژولهای دیگر مصرف میکنند:
│ crm.contact.create · crm.deal.move_stage · crm.activity.log
└──────────────────────────────────────────────────────────────
طراحی دامنه CRM بر پایه ۱۱ موجودیت اصلی انجام میشود. هر موجودیت یک مفهوم کسبوکاری مستقل است و در گرههای اتوماسیون قابل ارجاع خواهد بود.
| موجودیت | مفهوم کسبوکاری | کاربرد در اتوماسیون |
|---|---|---|
| Contact | شخص: مشتری، لید، سرنخ | محرک اصلی (ایجاد/تغییر) و هدف اکثر اقدامات |
| Company | سازمان/شرکت مرتبط با مخاطبان | مبنای سگمنتبندی B2B و تخصیص حساب |
| Deal | فرصت فروش با مبلغ و تاریخ بستن | محرک تغییر مرحله، یادآوری، پیشبینی درآمد |
| Pipeline | قیف فروش (مثلاً «فروش مستقیم»، «همکاری») | انتخاب مسیر در گرههای حرکت مرحله |
| Stage | مرحله داخل قیف (لید، مذاکره، برنده، باخت) | شرط و اقدام؛ ترتیب و احتمال موفقیت |
| Activity | رویداد تعامل: تماس، ایمیل، جلسه، یادداشت | ثبت خودکار؛ محرک پیگیری بعدی |
| Task | کار زماندار برای کارشناس | ایجاد خودکار با سررسید نسبی (SLAs) |
| Tag | برچسب چندگانه روی مخاطب/فرصت | شرط فیلتر و ورودی سگمنت |
| Segment | گروه داینامیک بر اساس فیلتر ذخیرهشده | مخاطب هدف اقدامات گروهی و کمپین |
| CustomField | فیلد اختصاصی هر tenant روی موجودیتها | شخصیسازی برای هر صنعت بدون تغییر کد |
| LeadForm | فرم جذب لید با endpoint عمومی | محرک ورود داده از سایت مشتری |
┌───────────────┐
│ companies │
│ name, domain │
└───────┬───────┘
│ 1:N
▼
┌───────────────┐ N:1 ┌───────────────┐ N:M ┌──────────┐
│ segments │◄──────►│ contacts │◄───────►│ tags │
│ (dynamic) │ │ email, mobile │ │ color │
└───────────────┘ │ score, status │ └──────────┘
└───┬───────┬───┘
│ │
1:N │ │ 1:N
▼ ▼
┌────────────────┐ ┌──────────────────┐
│ deals │ │ activities │
│ amount, stage │ │ type, body, ts │
│ close_date │ └──────────────────┘
└───────┬────────┘
│ N:1
▼
┌────────────────┐ 1:N ┌──────────────────┐
│ pipelines │────────►│ stages │
│ name, is_default│ │ order, prob. │
└────────────────┘ └──────────────────┘
┌────────────────┐ ┌──────────────────┐
│ custom_fields │ │ lead_forms │
│ entity, type │ │ public_token │
└────────────────┘ └──────────────────┘
┌────────────────┐ ┌──────────────────┐
│ assignment_ │ │ crm_tasks │
│ rules │ │ due_at, assignee │
└────────────────┘ └──────────────────┘
┌────────────────┐
│ field_values │ (مقادیر فیلد سفارشی — JSON)
└────────────────┘
[ورود از فرم/وبهوک/API/ورود دستی]
│
▼
┌─────────────┐
│ new (لید) │ ← امتیاز اولیه = 0
└──────┬──────┘
│ (قوانین امتیازدهی)
┌─────────┴─────────┐
▼ ▼
┌─────────┐ ┌──────────────┐
│ cold │ │ qualified │ ← MQL (امتیاز ≥ آستانه)
└────┬────┘ └──────┬───────┘
│ │ (تخصیص به کارشناس)
│ ▼
│ ┌──────────────┐
│ │ lead → │
│ │ contacted │ ← اولین فعالیت ثبت شد
│ └──────┬───────┘
│ ▼
│ ┌──────────────┐
│ │ nurturing │ ← دنبالسازی زمانبندیشده
│ └──────┬───────┘
│ │ (ایجاد Deal)
└────────►┌──────────▼──────────┐
│ customer │ ← Deal به مرحله «برنده» رسید
└──────────┬──────────┘
▼
┌─────────────┐
│ churned / │ ← عدم خرید مجدد پس از N روز
│ inactive │
└─────────────┘
crm_)| جدول | ستونهای کلیدی | توضیح |
|---|---|---|
crm_contacts | tenant_id, first_name, last_name, email, mobile, company_id, status, score, owner_user_id, source, last_activity_at, meta JSON | موجودیت مرکزی. ایندکس یکتا روی (tenant_id, email) و (tenant_id, mobile) |
crm_companies | tenant_id, name, domain, industry, size, owner_user_id, meta JSON | ایندکس یکتا روی (tenant_id, domain) |
crm_pipelines | tenant_id, name, is_default, sort_order | هر tenant میتواند چند قیف داشته باشد |
crm_stages | pipeline_id, name, sort_order, probability, is_won, is_lost, rotting_days | rotting_days برای هشدار رکود فرصت |
crm_deals | tenant_id, pipeline_id, stage_id, contact_id, company_id, title, amount, currency, expected_close_date, status, owner_user_id, stage_changed_at | ایندکس روی (tenant_id, stage_id) و (tenant_id, expected_close_date) |
crm_deal_stage_history | deal_id, from_stage_id, to_stage_id, changed_by, changed_at, duration_seconds | برای گزارش قیف و محاسبه مدت هر مرحله |
crm_activities | tenant_id, contact_id, deal_id, type (call/email/meeting/note/whatsapp), subject, body, occurred_at, user_id, meta JSON | type قابل توسعه توسط ماژولهای دیگر |
crm_tasks | tenant_id, contact_id, deal_id, title, due_at, assignee_user_id, status, priority, completed_at | مبنای گره «ایجاد تسک پیگیری» |
crm_tags | tenant_id, name, slug, color | یکتا روی (tenant_id, slug) |
crm_taggables | tag_id, taggable_type, taggable_id | رابطه چندبهچند (Polymorphic) |
crm_segments | tenant_id, name, entity (contact/deal), filters JSON, is_dynamic, last_count | فیلترها در قالب JSON استاندارد تعریف میشوند |
crm_custom_fields | tenant_id, entity, key, label, type, options JSON, is_required, sort_order | هر tenant فیلدهای خودش را میسازد |
crm_field_values | tenant_id, custom_field_id, entity_type, entity_id, value_text, value_number, value_date, value_json | مقادیر با ستونهای typed برای کوئری و ایندکسپذیری |
crm_assignment_rules | tenant_id, name, strategy (round_robin/load_balanced/fixed/manual), conditions JSON, user_pool JSON, is_active, last_assigned_index | موتور تخصیص خودکار |
crm_scoring_rules | tenant_id, name, entity, expression JSON, points, is_active | مثلاً «ایمیل شرکتی = +۱۰ امتیاز» |
crm_lead_forms | tenant_id, name, public_token, fields JSON, redirect_url, workflow_id, honeypot, is_active | public_token برای endpoint عمومی بدون احراز هویت |
crm_merge_logs | tenant_id, primary_id, merged_ids JSON, merged_by, merged_at | قابلیت Audit برای ادغام مخاطبان تکراری |
crm_ شروع میشوند. این کار
همزیستی چند ماژول در یک دیتابیس و مهاجرتهای مستقل را ممکن میکند.
// app/Modules/Crm/Database/Migrations/..._create_crm_contacts_table.php Schema::create('crm_contacts', function (Blueprint $t) { $t->id(); $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('mobile', 32)->nullable(); $t->string('status', 24)->default('new'); // new|qualified|contacted|nurturing|customer|churned $t->integer('score')->default(0); $t->string('source', 40)->nullable(); // form|webhook|api|manual|import $t->timestamp('last_activity_at')->nullable(); $t->json('meta')->nullable(); $t->timestamps(); $t->softDeletes(); // حذف منطقی برای احترام به داده مشتری $t->unique(['tenant_id', 'email']); $t->index(['tenant_id', 'mobile']); $t->index(['tenant_id', 'status', 'score']); $t->index(['tenant_id', 'owner_user_id']); });
| گزینه | مزیت | عیب | حکم |
|---|---|---|---|
فقط ستون meta JSON | سادگی کامل، بدون join | کوئری و ایندکسگذاری روی فیلد خاص دشوار | کافی نیست |
| EAV خالص (کلید-مقدار) | انعطاف بینهایت | کوئری سنگین، پیچیدگی گزارشگیری | تنها |
ترکیبی: تعریف در crm_custom_fields + مقدار در crm_field_values با ستون typed | انعطاف + قابلیت فیلتر و ایندکس روی فیلدهای پرکاربرد | کمی پیچیدگی بیشتر در سرویس | انتخابشده |
الگوی مقداردهی: بر اساس type فیلد، مقدار در ستون متناظر نوشته میشود:
text → value_text، number/currency → value_number،
date → value_date، multi-select/json → value_json.
این قوانین درون ماژول پیاده میشوند (نه در Workflow) چون رفتار پیشفرض و قابل پیشبینی مورد انتظار یک CRM هستند. اتوماسیون روی این قوانین سوار میشود، جایگزین آنها نمیشود.
ورودی جدید (فرم / وبهوک / API / دستی)
│
▼
┌─────────────────────────────────────┐
│ ۱) نرمالسازی │
│ email → lowercase + trim │
│ mobile → حذف +98 / 0 / فاصلهها │
└──────────────┬──────────────────────┘
▼
┌─────────────────────────────────────┐
│ ۲) جستوجو در (tenant_id, email) │
│ یا (tenant_id, mobile) │
└──────────────┬──────────────────────┘
┌─────────┴─────────┐
▼ ▼
┌─────────────┐ ┌──────────────────────────┐
│ یافت نشد │ │ یافت شد │
│ → ایجاد جدید│ │ → سیاست قابل تنظیم tenant │
└─────────────┘ │ (a) فقط بهروزرسانی │
│ (b) رد و لاگ conflict │
│ (c) ادغام + ثبت در │
│ crm_merge_logs │
└──────────────────────────┘
| استراتژی | منطق | مناسب برای |
|---|---|---|
round_robin | چرخشی بین اعضای pool با ذخیره ایندکس آخر در last_assigned_index | تیم فروش با حجم مشابه |
load_balanced | به کاربری که کمترین مخاطب/فرصت باز دارد | تیم ناهمگون با ظرفیت متفاوت |
fixed | همیشه یک کاربر/تیم مشخص | حسابهای کلیدی سازمانی |
rule_based | بر اساس شرط: منبع، استان، مبلغ، صنعت | چند تیم موازی با تخصص متفاوت |
manual | بدون تخصیص؛ انتظار در صف «بدون مالک» | کسبوکار کوچک |
// مقدار اولویتبندی: اولین قاعدهی منطبق برنده است { "strategy": "rule_based", "rules": [ { "when": { "deal.amount": { "$gte": 100000000 } }, "assign_to": "user:12", "priority": 1 }, { "when": { "contact.source": "form", "contact.city": "tehran" }, "assign_to": "pool:tehran_sales", "strategy": "round_robin" }, { "when": {}, "assign_to": "pool:general", "priority": 999 } ] }
crm_scoring_rules) تا مشتری بتواند بدون کد، وزنها را تغییر دهد.
اجرای محاسبه در یک Job مجزا تا سرعت ثبت مخاطب کاهش نیابد.
rotting_days است (مثلاً «مذاکره» = ۷ روز).now() > stage_changed_at + rotting_days.crm.deal.rotting و امکان اتوماسیون (هشدار به مدیر، تسک پیگیری).rotting_notified_at تا از ارسال تکراری هشدار جلوگیری کند.softDeletes برای همه موجودیتهای اصلی.ماژول CRM مجموعاً ۲۲ گره در هسته ثبت میکند: ۸ محرک (Trigger)، ۹ اقدام (Action) و ۵ شرط (Condition). این گرهها تنها چیزیست که مشتری در Builder میبیند.
crm.contacts_writtenstatus_changed با پرچم cascade_depth محدود میشودcrm.deals_writtenlast_activity_at ⇒ بازنشانی شمارنده امتیازدهی3 days یا 2 hours) · due_at (مطلق) · assignee · priority{{ }})true → مخاطب یافتشده · false → مسیر ایجادcrm.contact.score gte 60 ⇒ مسیر تیم فروشmodule.entity.operation با حروف کوچک و
جداکننده نقطه. این قاعده باعث میشود پنل بتواند گرهها را خودکار دستهبندی و فیلتر کند
(مثلاً همه گرههای crm.contact.* در یک گروه).
هر گره باید بتواند خودش را توصیف کند. پنل Nuxt از همین توصیف، فرم پیکربندی، اعتبارسنجی، auto-complete متغیرها و پیشنمایش را میسازد. بنابراین افزودن گره جدید هیچ تغییری در فرانت لازم ندارد.
{
"slug": "crm.contact.create",
"label": "ایجاد مخاطب",
"category": "crm.contact",
"kind": "action", // trigger | action | condition
"icon": "user-plus",
"color": "#5b9dff",
"docs_url": "https://docs.example.com/modules/crm/contact-create",
"fields": [
{ "key": "email", "label": "ایمیل", "type": "email",
"required": false, "supports_variables": true },
{ "key": "mobile", "label": "موبایل", "type": "text",
"required": false, "supports_variables": true,
"hint": "فرمتهای ۰۹۱۲... و +۹۸... خودکار نرمالسازی میشوند" },
{ "key": "owner_user_id", "label": "کارشناس مسئول",
"type": "select", "options_source": "crm.users" },
{ "key": "tags", "label": "برچسبها", "type": "multiselect",
"options_source": "crm.tags", "allow_new": true },
{ "key": "on_conflict", "label": "در صورت تکراری بودن",
"type": "select", "default": "update",
"options": [
{ "value": "skip", "label": "رد کن" },
{ "value": "update", "label": "بهروزرسانی کن" },
{ "value": "merge", "label": "ادغام کن" },
{ "value": "fail", "label": "خطا بده" }
] },
{ "key": "custom", "label": "فیلدهای سفارشی",
"type": "key_value",
"dynamic_options_source": "crm.custom_fields:contact" }
],
"validation": {
"at_least_one": ["email", "mobile"],
"message": "حداقل یکی از ایمیل یا موبایل باید وارد شود."
},
"output": {
"contact": { "id": "string", "email": "string",
"mobile": "string", "status": "string" },
"created": "boolean",
"matched_by": "string"
},
"retry": { "max_attempts": 3, "backoff": "exponential" },
"timeout_seconds": 10,
"metering": "crm.contacts_written",
"required_capability": "module.crm.write"
}
| type | نمایش در پنل | نکته پیادهسازی |
|---|---|---|
text / textarea | ورودی متن | پشتیبانی از درج متغیر {{ }} |
email / url | ورودی با اعتبارسنجی | اعتبارسنجی دوطرفه (فرانت + سرور) |
number / currency | عدد با فرمتبندی | واحد پول از تنظیمات tenant خوانده میشود |
date / datetime | تقویم | منطقه زمانی tenant اعمال میشود |
select / multiselect | لیست انتخابی | گزینهها از سرور میآید (options_source) |
key_value | جدول کلید/مقدار | برای فیلدهای سفارشی و هدرهای HTTP |
expression | ادیتور Monaco | فقط در گرههای Condition |
json | ادیتور Monaco + اعتبارسنجی | برای مقادیر ساختاریافته |
user_picker | جستوجوی کاربر | کوئری زنده با debounce |
contact_picker | جستوجوی مخاطب | محدود به tenant جاری |
// app/Modules/Crm/Actions/CreateContact.php class CreateContact implements NodeContract { public static function slug(): string { return 'crm.contact.create'; } public static function schema(): array { return [ 'kind' => 'action', 'label' => 'ایجاد مخاطب', 'fields' => [ /* ... مطابق بخش ۶.۱ ... */ ], 'output' => ['contact' => 'object', 'created' => 'boolean'], ]; } public function __construct( private readonly ContactService $contacts ) {} public function execute(NodeContext $ctx): NodeResult { $config = $ctx->config(); // متغیرها قبلاً resolve شدهاند if (blank($config['email'] ?? null) && blank($config['mobile'] ?? null)) { throw new NodeValidationException( 'حداقل یکی از ایمیل یا موبایل الزامی است.' ); } [$contact, $created] = $this->contacts->upsert( tenant: $ctx->tenantId(), data: $config, policy: $config['on_conflict'] ?? 'update', source: 'automation', ); if ($created) { // تولید رویداد برای اتوماسیونهای زنجیرهای بعدی ContactCreated::dispatch($contact, 'automation'); } return NodeResult::make([ 'contact' => $contact->toNodeArray(), 'created' => $created, ]); } public function retryPolicy(): RetryPolicy { return RetryPolicy::exponential(attempts: 3); } }
// app/Modules/Crm/CrmServiceProvider.php public function register(): void { NodeRegistry::register(module: 'crm', version: '1.0.0', nodes: [ // ── Triggers ────────────────────────────────────────── 'crm.contact.created' => Triggers\ContactCreated::class, 'crm.contact.updated' => Triggers\ContactUpdated::class, 'crm.contact.status_changed' => Triggers\ContactStatusChanged::class, 'crm.deal.created' => Triggers\DealCreated::class, 'crm.deal.stage_changed' => Triggers\DealStageChanged::class, 'crm.deal.rotting' => Triggers\DealRotting::class, 'crm.task.due' => Triggers\TaskDue::class, 'crm.lead_form.submitted' => Triggers\LeadFormSubmitted::class, // ── Actions ─────────────────────────────────────────── 'crm.contact.create' => Actions\CreateContact::class, 'crm.contact.update' => Actions\UpdateContact::class, 'crm.contact.assign' => Actions\AssignContact::class, 'crm.contact.change_status' => Actions\ChangeContactStatus::class, 'crm.contact.add_tag' => Actions\AddTag::class, 'crm.contact.remove_tag' => Actions\RemoveTag::class, 'crm.deal.create' => Actions\CreateDeal::class, 'crm.deal.move_stage' => Actions\MoveDealStage::class, 'crm.activity.log' => Actions\LogActivity::class, 'crm.task.create' => Actions\CreateTask::class, // ── Conditions ──────────────────────────────────────── 'crm.contact.exists' => Conditions\ContactExists::class, 'crm.contact.has_tag' => Conditions\ContactHasTag::class, 'crm.contact.score' => Conditions\ContactScore::class, 'crm.deal.in_stage' => Conditions\DealInStage::class, 'crm.segment.contains' => Conditions\SegmentContains::class, ]); // متریکهای مصرف این ماژول (برای بیلینگ پلنمحور) UsageMeter::define('crm.contacts_written'); UsageMeter::define('crm.deals_written'); UsageMeter::define('crm.contacts_stored'); // gauge — نه شمارنده // منو و مجوزهای پنل AdminMenu::add('crm', 'CRM', capability: 'module.crm'); }
ecommerce.order.paid را اضافه کند،
این گره بهطور خودکار در پالت Builder کنار گرههای CRM ظاهر میشود — بدون یک خط
تغییر در کد Nuxt.
ماژول CRM زیرمجموعهای از قرارداد کلی /api/v1 است و همه قواعد سند ۰۱ (نسخهبندی،
ساختار ثابت پاسخ، PlanGate، Idempotency) بر آن حاکم است.
| متد | مسیر | توضیح |
|---|---|---|
| GET | /api/v1/crm/contacts | لیست با فیلتر، مرتبسازی و صفحهبندی |
| POST | /api/v1/crm/contacts | ایجاد مخاطب (با اعمال Dedupe) |
| GET | /api/v1/crm/contacts/{id} | جزئیات + برچسبها + فیلدهای سفارشی |
| PUT | /api/v1/crm/contacts/{id} | ویرایش |
| DELETE | /api/v1/crm/contacts/{id} | حذف منطقی |
| POST | /api/v1/crm/contacts/merge | ادغام دو یا چند مخاطب تکراری |
| POST | /api/v1/crm/contacts/{id}/tags | افزودن برچسب |
| DELETE | /api/v1/crm/contacts/{id}/tags/{tag} | حذف برچسب |
| GET | /api/v1/crm/contacts/{id}/timeline | تایملاین فعالیتها، تسکها، فرصتها |
| GET | /api/v1/crm/companies | لیست شرکتها |
| POST | /api/v1/crm/companies | ایجاد شرکت |
| GET | /api/v1/crm/pipelines | لیست قیفها به همراه مراحل |
| POST | /api/v1/crm/pipelines | ایجاد قیف |
| POST | /api/v1/crm/pipelines/{id}/stages | ایجاد مرحله |
| GET | /api/v1/crm/deals | لیست فرصتها (فیلتر قیف/مرحله/مالک) |
| POST | /api/v1/crm/deals | ایجاد فرصت |
| POST | /api/v1/crm/deals/{id}/move | انتقال به مرحله دیگر |
| GET | /api/v1/crm/deals/{id}/history | تاریخچه مراحل |
| GET | /api/v1/crm/activities | لیست فعالیتها |
| POST | /api/v1/crm/activities | ثبت فعالیت |
| GET | /api/v1/crm/tasks | لیست تسکها |
| POST | /api/v1/crm/tasks | ایجاد تسک |
| PATCH | /api/v1/crm/tasks/{id} | تکمیل یا تغییر وضعیت |
| GET | /api/v1/crm/segments | لیست سگمنتها + تعداد تخمینی |
| POST | /api/v1/crm/segments | ساخت سگمنت با فیلتر JSON |
| GET | /api/v1/crm/custom-fields | لیست فیلدهای سفارشی |
| POST | /api/v1/crm/custom-fields | ساخت فیلد سفارشی |
| GET | /api/v1/crm/lead-forms | لیست لیدفرمها |
| POST | /api/v1/crm/lead-forms | ساخت لیدفرم (تولید public_token) |
| POST | /api/v1/crm/lead-forms/{token}/submit | endpoint عمومی دریافت لید |
| GET | /api/v1/crm/stats | آمار خلاصه: نرخ تبدیل قیف، میانگین زمان هر مرحله |
// POST /api/v1/crm/contacts // Authorization: Bearer ak_live_xxxxxxxxxxxx // Idempotency-Key: 4f1c9a7e-... (اختیاری اما توصیهشده) { "first_name": "مریم", "last_name": "احمدی", "email": "maryam@acme.ir", "mobile": "09121234567", "source": "api", "tags": ["وبینار-مهر"], "custom": { "city": "تهران", "budget": 50000000 } } // 201 Created { "data": { "id": "ctc_01J8X...", "status": "new", "score": 25, "created": true, "matched_by": null, "owner": { "id": 12, "name": "کارشناس ۳" } }, "meta": { "request_id": "req_...", "tenant": "acme" } }
status · tag · owner · score_gte · created_after?fields=id,email,score برای کاهش حجم پاسخcrm:read — خواندن مخاطب و فرصتcrm:write — ایجاد و ویرایشcrm:delete — حذف (پیشفرض غیرفعال)crm:admin — مدیریت قیف، فیلد سفارشی و قوانینak_test_* روی یک tenant سندباکس کار میکنند که دادههایش
هر شب پاک میشود و اجراهایش در متریک مصرف پلن حساب نمیشود. این برای فرآیند یکپارچهسازی
مشتری حیاتی است.
این سناریوها در پنل بهصورت قالب آماده (Template) ارائه میشوند تا مشتری با یک کلیک آنها را نصب و شخصیسازی کند. هر قالب، ارزش فروش ملموس ایجاد میکند.
secrets رمزنگاریشده ذخیره میشود، نه در متن Workflowcrm_merge_logs قابل بازگشت استماژول CRM علاوه بر گرههای اتوماسیون، یک پنل عملیاتی مستقل هم دارد؛ چون بدون محیط کار روزمره، مشتری CRM را قبول نمیکند. این پنل در Nuxt ساخته میشود و از همان REST API تغذیه میکند.
| صفحه | مسیر | امکانات کلیدی |
|---|---|---|
| داشبورد CRM | /crm | KPI خلاصه: مخاطب جدید، فرصت باز، ارزش قیف، تسکهای عقبافتاده، نمودار تبدیل |
| لیست مخاطبان | /crm/contacts | جدول با فیلتر پیشرفته، انتخاب گروهی، عملیات دستهای، صادرات CSV |
| پروفایل مخاطب | /crm/contacts/[id] | تایملاین یکپارچه، فیلدهای سفارشی، برچسبها، فرصتهای مرتبط، دکمه اجرای جریان |
| شرکتها | /crm/companies | لیست + مخاطبان و فرصتهای هر شرکت |
| برد قیف فروش | /crm/pipeline | نمای Kanban با Drag&Drop؛ تغییر مرحله ⇒ تولید رویداد اتوماسیون در لحظه |
| فرصت فروش | /crm/deals/[id] | جزئیات، تاریخچه مراحل، فعالیتها، تایمر رکود |
| فعالیتها | /crm/activities | لیست زمانمحور همه تعاملات تیم |
| تسکها | /crm/tasks | نمای «امروز / این هفته / عقبافتاده»، تکمیل سریع |
| سگمنتها | /crm/segments | سگمنتساز بصری (شرطهای AND/OR)، پیشنمایش تعداد، اجرای جریان روی سگمنت |
| لیدفرمها | /crm/forms | فرمساز، تولید کد Embed، اتصال به Workflow، آمار ارسال |
| تنظیمات CRM | /crm/settings | قیفها و مراحل، فیلدهای سفارشی، قوانین تخصیص، امتیازدهی، سیاست Dedupe |
| قالبهای آماده | /crm/templates | گالری ۱۰ سناریوی بخش ۸ با نصب یککلیکی |
┌───────────────────────────────────────────────────────────────────────┐ │ CRM › قیف فروش [قیف اصلی ▾] [فیلتر: مالک ▾] [+ فرصت] [⚙ تنظیمات] │ ├───────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─ لید (۱۲) ─────┐ ┌─ مذاکره (۷) ────┐ ┌─ پیشفاکتور (۴) ─┐ ┌─ برده (۳) ┐│ │ ├────────────────┤ ├─────────────────┤ ├──────────────────┤ ├───────────┤│ │ │ ▢ شرکت آلفا │ │ ▢ شرکت بتا │ │ ▢ شرکت دلتا │ │ ▢ شرکت ...││ │ │ ۵۰ م · ۳ روز │ │ ۱۲۰ م·۱۲ روز⚠│ │ ۸۰ م · ۲ روز │ │ ۲۰۰ م ││ │ ├────────────────┤ ├─────────────────┤ ├──────────────────┤ ├───────────┤│ │ │ ▢ شرکت گاما │ │ ▢ شرکت زتا │ │ ... │ │ ... ││ │ │ ۳۰ م · ۱ روز │ │ ۹۰ م · ۱ روز │ │ │ │ ││ │ └────────────────┘ └─────────────────┘ └──────────────────┘ └───────────┘│ │ │ │ ⚠ علامت هشدار = فرصت راکدتر از حد مجاز مرحله │ │ Drag & Drop یک کارت ⇒ API فراخوانی میشود ⇒ رویداد crm.deal.stage_ │ │ changed منتشر میشود ⇒ اتوماسیونهای فعال در لحظه اجرا میشوند. │ └───────────────────────────────────────────────────────────────────────┘
کارت خلاصه مخاطب با امتیاز، وضعیت، آخرین فعالیت.
تایملاین یکپارچه با آیکون نوع فعالیت و گروهبندی زمانی.
سازنده بصری شرط با گروهبندی AND/OR و پیشنمایش زنده تعداد.
نمایش امتیاز با رنگبندی (سرد/گرم/داغ) و توضیح اجزای امتیاز.
رندر داینامیک فیلد سفارشی بر اساس type — مشترک بین فرم و جدول.
اجرای دستی جریان روی یک رکورد مشخص، با نمایش نتیجه همان لحظه.
هر قابلیت یک اسلاگ دارد. نقشها فقط مجموعهای از این اسلاگها هستند و مشتری میتواند نقش سفارشی بسازد.
| Capability | معنی | سطح ریسک |
|---|---|---|
module.crm | دسترسی به ماژول CRM (پیشنیاز همه موارد زیر) | پایه |
crm.contacts.view | مشاهده لیست و پروفایل مخاطبان | پایه |
crm.contacts.create | ایجاد مخاطب | پایه |
crm.contacts.update | ویرایش مخاطب | متوسط |
crm.contacts.delete | حذف مخاطب | حساس |
crm.contacts.merge | ادغام مخاطبان تکراری | حساس |
crm.contacts.export | صادرات داده مخاطبان | حساس |
crm.contacts.view_others | مشاهده مخاطبان سایر کارشناسان | متوسط |
crm.deals.view / .manage | مشاهده و مدیریت فرصتها | متوسط |
crm.pipeline.manage | ساخت و ویرایش قیف و مراحل | متوسط |
crm.settings.manage | قوانین تخصیص، امتیازدهی، فیلد سفارشی | متوسط |
crm.forms.manage | ساخت و انتشار لیدفرم عمومی | حساس |
crm.automation.manage | ساخت و فعالسازی جریانهای اتوماسیون | حساس |
| نقش | مجموعه دسترسی | کاربرد |
|---|---|---|
| Owner | همه قابلیتها + مدیریت کاربران و صورتحساب | مالک کسبوکار (صاحب tenant) |
| Sales Manager | مشاهده همه + مدیریت قیف و قوانین + گزارشها + اتوماسیون (بدون صورتحساب) | مدیر فروش |
| Sales Rep | مشاهده و ویرایش مخاطبان خودش، فرصتها، تسکهای خودش | کارشناس فروش |
| Marketing | مشاهده همه مخاطبان + ساخت لیدفرم + سگمنت + اتوماسیون بازاریابی (بدون حذف) | تیم بازاریابی |
| Read Only | فقط مشاهده، بدون هیچ نوشتن | حسابرس یا مدیرعامل |
| Automation Builder | مشاهده داده + ساخت و ویرایش جریانها (بدون حذف داده) | تیم فنی مشتری |
// app/Modules/Crm/Http/Controllers/ContactController.php public function update(UpdateContactRequest $request, Contact $contact) { $this->authorize('crm.contacts.update'); // محدودیت مالکیت: کارشناس فقط مخاطب خودش را میبیند if (! $request->user()->can('crm.contacts.view_others') && $contact->owner_user_id !== $request->user()->id) { throw new AuthorizationException('دسترسی به این مخاطب مجاز نیست.'); } $contact->update($request->validated()); ContactUpdated::dispatch($contact, 'manual'); return ContactResource::make($contact); }
| متریک | نوع | واحد | کاربرد در بیلینگ |
|---|---|---|---|
crm.contacts_stored | Gauge (سطح) | تعداد رکورد فعال | سقف پلن — کنترل حجم دیتابیس |
crm.contacts_written | Counter (ماهانه) | تعداد نوشتن | مصرف عملیاتی + overage |
crm.deals_written | Counter (ماهانه) | تعداد نوشتن | مصرف عملیاتی |
crm.custom_fields | Gauge | تعداد فیلد | سقف پلن (محافظت از کارایی) |
crm.pipelines | Gauge | تعداد قیف | سقف پلن |
crm.lead_forms | Gauge | تعداد فرم | سقف پلن |
crm.api_requests | Counter (روزانه) | درخواست | Rate limit و پلن API |
| محدودیت | Free | Starter | Business | Enterprise |
|---|---|---|---|---|
| مخاطب فعال (Gauge) | ۲۰۰ | ۵٬۰۰۰ | ۵۰٬۰۰۰ | نامحدود* |
| نوشتن ماهانه مخاطب | ۱۰۰ | ۲٬۰۰۰ | ۲۰٬۰۰۰ | نامحدود |
| قیف فروش | ۱ | ۲ | ۵ | نامحدود |
| فیلد سفارشی | ۳ | ۱۰ | ۲۵ | نامحدود |
| لیدفرم عمومی | ۱ | ۳ | ۱۰ | نامحدود |
| قوانین تخصیص | — | ✔ | ✔ | ✔ |
| امتیازدهی سرنخ | — | — | ✔ | ✔ |
| صادرات داده | — | ۵٬۰۰۰ ردیف | ✔ | ✔ |
| فیلد سفارشی فرمولدار | — | — | ✔ | ✔ |
| قالبهای آماده | ۳ قالب | همه | همه | همه + سفارشی |
* «نامحدود» در عمل با سیاست منع استفاده غیرمتعارف (Fair Usage) و پایش خودکار محدود میشود.
مصرف ماهانه = ۸۰٪ سقف
│
└─► بنر هشدار در پنل + ایمیل به Owner (یکبار در دوره)
مصرف ماهانه = ۱۰۰٪ سقف
│
├─► اجراهای جدید اتوماسیون مرتبط با CRM متوقف میشوند ← 402
├─► خواندن داده همچنان آزاد است (مشتری دادهاش را از دست نمیدهد)
├─► ورود داده دستی همچنان ممکن است (تجربه کار روزمره قطع نشود)
└─► اعلان ارتقا + لینک مستقیم به صفحه صورتحساب
گزینه Overage (اختیاری، فقط پلن Business و Enterprise)
│
└─► اجرای بیشتر مجاز است و هزینه مازاد در فاکتور دوره بعد محاسبه میشود
usage:{tenant_id}:{metric}:{YYYY-MM}.ماژول CRM خودش پیام ارسال نمیکند؛ اما برای اینکه سناریوهای بخش ۸ کار کنند، باید بفهمیم CRM با چه سرویسهایی و از چه راهی گفتگو میکند.
┌────────────────────┐ ┌────────────────────────┐
│ module-crm │ │ module-messaging │
│ رویداد تولید میکند│───────►│ پیام ارسال میکند │
│ crm.contact.created│ گره │ sms.send · email.send │
└────────────────────┘ └───────────┬────────────┘
│
┌───────────┼───────────┬──────────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌────────────┐
│ کاوهنگار│ │ SMTP │ │ واتساپ │ │ تلگرام │
│ پیامک │ │ اختصاصی │ │ Business │ │ Bot API │
└─────────┘ └─────────┘ └──────────┘ └────────────┘
(کانکتورها در ماژول messaging پیاده میشوند)
http.request کار کند.
CRM هرگز گروگان یک سرویس بیرونی نمیشود.
| کانکتور | هدف | محل پیادهسازی |
|---|---|---|
| وبهوک خروجی (Outbound Webhook) | اعلام رویداد CRM به سیستمهای دیگر | هسته — action.webhook |
| ایمپورت CSV | ورود اولیه داده مشتری | داخل CRM (لازم است) |
| Google Calendar | همگامسازی تسکهای زماندار | ماژول scheduler (فاز ۲) |
| VoIP / تماس | ثبت خودکار تماس در تایملاین | فاز ۴ |
هر وبهوک خروجی با امضای HMAC-SHA256 ارسال میشود تا مشتری بتواند اصالت آن را بررسی کند:
// هدرهای ارسالی X-Automation-Event: crm.contact.created X-Automation-Delivery: dlv_01J8X... X-Automation-Timestamp: 1758880000 X-Automation-Signature: sha256=<hmac> // محاسبه امضا (سمت مشتری) $signature = hash_hmac( 'sha256', $timestamp . '.' . $rawBody, $tenantWebhookSecret ); // بررسی: امضا برابر است و اختلاف زمانی کمتر از ۵ دقیقه (ضد Replay Attack)
| تلاش | زمان انتظار | شرط ادامه |
|---|---|---|
| ۱ | فوری | پاسخ 2xx ⇒ پایان موفق |
| ۲ | +۳۰ ثانیه | خطای 5xx یا Timeout |
| ۳ | +۵ دقیقه | خطای 5xx یا Timeout |
| ۴ | +۳۰ دقیقه | خطای 5xx یا Timeout |
| ۵ | +۲ ساعت | خطای 5xx یا Timeout |
| پایان | — | ثبت در Dead Letter Queue + اعلان به مشتری |
نکته: خطاهای 4xx (بهجز 429) تکرار نمیشوند، چون نشانه
اشکال در تنظیمات است نه مشکل موقت. این تفکیک از هدر رفتن منابع و سردرگمی مشتری جلوگیری میکند.
| دسته | نمونه تست | نوع | اولویت |
|---|---|---|---|
| Dedupe | ایجاد دو مخاطب با یک ایمیل ⇒ فقط یک رکورد + رویداد updated | Feature | بحرانی |
| Dedupe موبایل | +989121234567 و 09121234567 تکراری تشخیص داده شوند | Unit | بحرانی |
| جداسازی tenant | کاربر tenant A نتواند مخاطب tenant B را بخواند (۳۲ تست برای همه endpointها) | Feature | بحرانی |
| تخصیص چرخشی | ۱۰ مخاطب متوالی بین ۳ کاربر بهدرستی توزیع شوند | Feature | مهم |
| امتیازدهی | ترکیب قوانین ⇒ امتیاز مورد انتظار + گذر از آستانه MQL | Unit | مهم |
| رکود فرصت | فرصت با ۸ روز سکون ⇒ رویداد rotting یکبار تولید شود (نه بیشتر) | Feature | مهم |
| گره create | اجرای گره با ایمیل نامعتبر ⇒ خطای اعتبارسنجی + retry نشود | Unit | مهم |
| گره move_stage | انتقال به مرحلهای از قیف دیگر ⇒ رد شود | Unit | مهم |
| حلقه زنجیرهای | A → B → A با cascade_depth ⇒ پس از ۳ سطح متوقف شود | Feature | بحرانی |
| سقف مصرف | رسیدن به سقف نوشتن ⇒ پاسخ 402 + خواندن همچنان کار کند | Feature | بحرانی |
| محدودیت پلن | Free با ۲۰۱ مخاطب ⇒ ایجاد مخاطب جدید رد شود | Feature | مهم |
| لیدفرم عمومی | ارسال ۱۰۰ درخواست در دقیقه ⇒ Rate Limit فعال شود | Feature | مهم |
| Idempotency | دو درخواست با یک Idempotency-Key ⇒ فقط یک رکورد | Feature | مهم |
| ادغام | ادغام مخاطب با ۱۰ فعالیت ⇒ همه فعالیتها منتقل + قابل بازگشت | Feature | مهم |
| صادرات CSV | فایل خروجی فقط داده tenant جاری را داشته باشد | Feature | بحرانی |
| وبهوک خروجی | مشتری خطای 500 بدهد ⇒ ۵ تلاش با backoff صحیح | Feature | مهم |
| کارایی | لیست ۵۰٬۰۰۰ مخاطب با فیلتر ⇒ زیر ۳۰۰ میلیثانیه | Performance | مهم |
// tests/Feature/Tenancy/CrmIsolationTest.php class CrmIsolationTest extends TestCase { public function test_tenant_cannot_read_other_tenant_contacts(): void { [$a, $b] = Tenant::factory()->count(2)->create(); $contactB = Contact::factory()->for($b)->create(); $this->actingAsTenant($a) ->getJson("/api/v1/crm/contacts/{$contactB->id}") ->assertNotFound(); // 404 — نه 403 (عدم افشای وجود) } public function test_export_never_includes_foreign_rows(): void { /* بررسی میشود که فایل CSV هیچ tenant_id دیگری نداشته باشد */ } // این تست برای «همه» مدلهای CRM بهصورت DataProvider اجرا میشود }
schema()، outputSchema() و retryPolicy() باشد./usage بهدرستی نمایش داده شوند.ماژول CRM در ۶ اسپرینت دوهفتهای (~۳ ماه) با یک توسعهدهنده تماموقت بههمراه هسته قابل تحویل است. ترتیب اسپرینتها بر پایه «اول محصول قابل فروش» است، نه «اول معماری کامل».
crm_contacts، crm_companies، crm_tags، crm_taggablesBelongsToTenant trait + Global ScopeContactService::upsert() با نرمالسازی email/mobileNodeContract + NodeRegistry در هسته (پیادهسازی مرجع)contact.created، contact.create، contact.update، contact.exists، activity.logGET /nodes/types و تست رندر داینامیک فرم در پنلpipelines، stages، deals، deal_stage_historyDealService::moveStage() با ثبت تاریخچهdeal.created، deal.stage_changed، deal.create، deal.move_stage، deal.in_stagedeal.rottingcontact.score + segment.containscontact.assign، add_tag، remove_tag، change_statuslead_form.submitted + contact.updated + contact.has_tagtask.create و task.due + نمای SLA/crm/tasks، /crm/activities، /crm/forms| ریسک | احتمال | اثر | راهکار کاهش |
|---|---|---|---|
| CRM ما در برابر CRMهای جاافتاده ضعیف بهنظر برسد | بالا | بالا | تمرکز روی موتور اتوماسیون بهعنوان تمایز اصلی، نه تکرار CRMهای موجود. پیام فروش: «CRM + اتوماسیون در یک جا». |
| انتظار مشتری از «CRM کامل» فراتر از محدوده MVP باشد | بالا | متوسط | محدوده بخش ۱.۲ در قرارداد فروش صریح ذکر شود + نقشه راه ماژولهای بعدی ارائه شود. |
| مهاجرت داده مشتری از سیستم قدیمی سخت باشد | متوسط | بالا | ایمپورت CSV با نقشهبرداری ستون + سرویس مهاجرت در فاز ۱ بهعنوان خدمت پرداختی. |
| نشت داده بین tenantها بهدلیل فراموشی Global Scope | متوسط | بحرانی | تست خودکار نشت داده در CI + بازبینی کد اجباری برای هر مدل جدید. |
| گرههای زیاد، رابط Builder را برای مشتری غیرفنی پیچیده کند | متوسط | متوسط | دستهبندی، جستوجو، «گرههای پیشنهادی» بر اساس زمینه و مخفیسازی گرههای پیشرفته. |
| هزینه ارسال پیامک/ایمیل، حاشیه سود پلنهای ارزان را بخورد | بالا | متوسط | تفکیک کامل هزینه پیام از اشتراک پلتفرم (اعتبار پیامکی جداگانه) یا سقف پیام در پلن. |
| مشتری جریان اشتباه بسازد و سیستم را بهخاطر خرابی سرزنش کند | بالا | متوسط | اعتبارسنجی پیش از فعالسازی، اجرای آزمایشی، تشخیص حلقه، هشدار حجم غیرعادی اجرا. |
| پیچیدگی نگهداری و افزایش هزینه پشتیبانی | متوسط | متوسط | داشبورد سلامت جریانها (نرخ خطا، جریانهای شکسته) + هشدار خودکار به مشتری پیش از تماس او. |
| # | سؤال | چرا مهم است | پیشنهاد من |
|---|---|---|---|
| ۱ | صنعت هدف اول برای ماژول CRM چیست؟ (خدماتی / B2B / آموزش / کلینیک / املاک) | روی قالبهای آماده، اصطلاحات فارسی و پیام فروش اثر مستقیم دارد | شروع با کسبوکارهای خدماتی B2B — بهترین تناسب با قیف فروش و اتوماسیون |
| ۲ | پیامک و ایمیل در پلن پایه رایگان باشد یا اعتبار جداگانه فروخته شود؟ | مستقیماً بر حاشیه سود و سادگی قیمتگذاری اثر دارد | اعتبار جداگانه برای پیامک؛ ایمیل در سقف پلن |
| ۳ | آیا CRM باید «ایمپورت از سیستم قدیمی» داشته باشد؟ | بدون آن، مهاجرت مشتری سخت و هزینه جذب بالا میرود | بله — CSV با نقشهبرداری ستون + خدمت مهاجرت پرداختی |
| ۴ | آیا فیلد سفارشی فرمولدار (امتیاز خودکار، مبلغ محاسباتی) در فاز ۱ لازم است؟ | قابلیت قدرتمند ولی پیچیده — تعادل زمان MVP | به پلنهای Business و Enterprise منتقل شود |
| ۵ | ویجت جاسازی CRM در سایت مشتری در فاز ۱ باشد؟ | ارزش فروش دارد اما زمانبر است | فقط لیدفرم + کد Embed؛ ویجت کامل در فاز ۳ |
| ۶ | آیا مشتری میتواند داده CRM خود را کامل حذف کند؟ | الزام حقوقی در برخی صنایع | بله — صفحه «حذف کامل tenant» با تأیید دو مرحلهای |
۸ محرک · ۹ اقدام · ۵ شرط
همه با پیشوند crm_
زیر /api/v1/crm/*
آماده ارائه در فرآیند فروش
Nuxt 4 + Vue 3
شامل تست اجباری نشت داده
~۳ ماه با یک توسعهدهنده
متصل به PlanGate و بیلینگ