Skip to content

معماری سیستم (Architecture) ​

این موتور گردش کار نیازی به مفسر پیچیدهٔ BPMN ندارد؛ بلکه بر پایهٔ مدل سرراست سه‌گانهٔ «تعریف (Definition)»، «نمونهٔ اجرا (Instance)» و «وظیفه (Task)» طراحی و پیاده‌سازی شده است.

معماری پروژه از الگوی Clean Architecture پیروی می‌کند؛ جهت وابستگی‌ها همواره به سمت لایه‌های درونی است و لایهٔ Domain هیچ وابستگی یا شناختی نسبت به دیتابیس، پروتکل HTTP، فرمت JSON یا فریم‌ورک‌های بیرونی ندارد.


۱. ساختار لایه‌بندی پروژه ​

WorkFlowEngine/
├── src/
│   ├── TaskFlow.Domain/
│   │   ├── Entities/
│   │   ├── ValueObjects/
│   │   ├── Common/
│   │   └── Errors/
│   ├── TaskFlow.Application/
│   │   ├── Engine.cs
│   │   ├── ProcessOrchestrator.cs
│   │   ├── Handlers/ParallelJoinHandler.cs
│   │   ├── Ports/
│   │   ├── Tenancy/
│   │   └── Results/
│   ├── TaskFlow.Infrastructure/
│   │   ├── Persistence/
│   │   └── Identity/
│   └── TaskFlow.Server/
│       ├── Program.cs
│       ├── Controllers/
│       ├── Http/
│       ├── Requests/
│       └── Contracts/
├── tests/TaskFlow.Tests/
├── examples/
└── docs/
مسیرنقش
Domain/Entitiesموجودیت‌ها: Definition، ProcessInstance، WorkflowTask
Domain/ValueObjectsAssigneeKind، TaskStatus، InstanceStatus، ProcessState
Domain/CommonTenant، Vars، Ids
Domain/ErrorsEngineException، EngineErrorKind
Application/Engine.csقوانین کسب‌وکار: شروع، ارجاع، تکمیل
Application/Handlers/ParallelJoinHandler.csبعد از join موازی، AssignTo بعدی از onAllCompleted
Application/ProcessOrchestrator.csLegacy — callback دستی برای مرحله بعد
Application/Portsرابط‌ها: IStore، IDirectory، ITenantProvider
Infrastructure/PersistenceMemoryStore، PostgresStore
Infrastructure/IdentityOpenDirectory (پیش‌فرض)، StaticDirectory
Server/Program.csComposition Root — ترکیب وابستگی‌ها
Server/Controllersکنترلرهای REST
Server/Httpمیان‌افزارها، JSON، نگاشت خطا
Server/Requestsمدل‌های بدنهٔ درخواست
Server/ContractsDTOهای خروجی و ApiMapper

جهت وابستگی لایه‌ها:

Domain  ←  Application  ←  Infrastructure
                      ←  Server (API)
Server.Program         →  wires Store / Directory / Engine

Program.cs پیاده‌سازی Store و Directory را می‌سازد و Engine را راه‌اندازی می‌کند.

پروژهنقش و مسئولیتلایه‌های مجاز جهت وابستگی
TaskFlow.Domainموجودیت‌ها و خطاهای دامنه (Definition, ProcessInstance, WorkflowTask, EngineException)بدون وابستگی به لایه‌های بیرونی
TaskFlow.Applicationهستهٔ فرایند (Engine، ParallelJoinHandler) و پورت‌ها (IStore, IDirectory)صرفاً وابسته به Domain
TaskFlow.Infrastructureپیاده‌سازی ذخیره‌ساز پایگاه داده (Postgres / حافظه) و دایرکتوری کاربرانوابسته به Application و Domain
TaskFlow.Serverرابط REST، مدیریت DTOها، Swagger و ترکیب وابستگی‌هاوابسته به Application و Infrastructure
bash
dotnet run --project src/TaskFlow.Server
متغیر محیطیمقدار پیش‌فرضتوضیحات
ADDR:8081پورت و آدرس گوش دادن؛ فقط اگر ASPNETCORE_URLS خالی باشد (داخل Docker معمولاً :8080)
ASPNETCORE_URLSخالیدر صورت تنظیم، اولویت دارد و ADDR نادیده گرفته می‌شود
DATABASE_URLخالیرشتهٔ اتصال Postgres؛ در صورت خالی بودن از حافظه موقت استفاده می‌شود
WF_USERSخالیاگر خالی باشد (و WF_GROUP_* هم نباشد)، OpenDirectory: هر شناسهٔ کاربر/گروه پذیرفته می‌شود
WF_GROUP_<id>—اعضای گروه؛ با تعریف این‌ها یا WF_USERS، دایرکتوری ایستا (StaticDirectory) عضویت را چک می‌کند
WF_API_KEYSدر Development اختیاریکلید مشترک درگاه؛ خارج از Development بدون آن فرآیند شروع نمی‌شود
ASPNETCORE_ENVIRONMENTبسته به اجرادر Development می‌توان بدون WF_API_KEYS کار کرد

شرح روان‌تر همین متغیرها: usage.md — بخش ۴.

شناسایی کاربر در درخواست‌های REST از طریق هدر X-Actor-Id انجام می‌گیرد. این مقدار شناسهٔ مات سامانهٔ میزبان است (مثلاً 102) و جایگزین لاگین نیست. قفل ورودی API در پروداکشن با WF_API_KEYS است (هدر X-API-Key). جزئیات استقرار و جلوگیری از لو رفتن کلید: بخش ۶.


۲. مدل مفهومی گردش کار ​

mermaid
flowchart RL
  start(["Start<br/>processKey + initiator"])
  start --> def["definitionKey"]
  start --> root["root instanceId"]
  root --> refer["AssignTo<br/>+ definitionKey"]
  refer --> child["child instanceId"]
  refer --> tasks["Task(s)<br/>user / group / users"]
  tasks --> inbox["کارتابل"]
  tasks --> done["Completion<br/>allCompleted"]

  style start fill:#DCCCFF,stroke:#874FFF,color:#1a1a2e
  style refer fill:#C2E5FF,stroke:#3DADFF,color:#1a1a2e
  style done fill:#CDF4D3,stroke:#66D575,color:#1a1a2e

از Start یک اینستنس ریشه و definitionKey ساخته می‌شود. هر AssignTo یک اینستنس فرزند و تسک(های) کارتابل ایجاد می‌کند.

سطحمفهوم و نقش
Definitionتعریف نوع فرایند با شناسهٔ یکتای key (بدون نیاز به دیاگرام‌های گرافیکی)
Instanceنمونهٔ اجرایی فرایند؛ عملیات Start یک اینستنس ریشه می‌سازد و هر ارجاع (AssignTo) یک اینستنس فرزند ایجاد می‌کند
Taskرکورد وظیفه در کارتابل؛ انتساب به کاربر (user) یا گروه (group)
  • وضعیت اینستنس: تا زمانی که وظایف باز داشته باشد در وضعیت running است؛ پس از تکمیل تمامی وظایفِ مرتبط با همان ارجاع به completed تغییر می‌یابد.
  • وضعیت تسک: شامل open (باز)، claimed (تحویل‌گرفته‌شده)، done (تکمیل‌شده) و cancelled (لغوشده).

رفتن خودکار به مرحله بعد (Parallel Join) ​

Engine primitiveها را دارد: Start، AssignTo، CompleteTask، Completion. برای join موازی، مرحلهٔ بعد با onAllCompleted روی همان AssignTo تعریف می‌شود و در Parameters اینستنس ذخیره می‌گردد.

بعد از هر CompleteTask، event داخلی TaskCompleted منتشر می‌شود. ParallelJoinHandler بررسی می‌کند آیا اینستنس join موازی است و allCompleted شده؛ اگر بله، AssignTo بعدی را می‌سازد:

mermaid
flowchart TD
    complete["CompleteTask"] --> event["TaskCompleted event"]
    event --> handler["ParallelJoinHandler"]
    handler --> check{"Join + AllCompleted?"}
    check -->|"خیر"| stop(["CompleteResult<br/>next = null"])
    check -->|"بله"| refer["AssignTo از onAllCompleted"]
    refer --> out(["CompleteResult<br/>با next"])

    style complete fill:#C2E5FF,stroke:#3DADFF,color:#1a1a2e
    style event fill:#DCCCFF,stroke:#874FFF,color:#1a1a2e
    style handler fill:#FFECBD,stroke:#FFC943,color:#1a1a2e
    style refer fill:#CDF4D3,stroke:#66D575,color:#1a1a2e
    style stop fill:#f5f5f5,stroke:#bdbdbd,color:#616161
    style out fill:#DCCCFF,stroke:#874FFF,color:#1a1a2e

ProcessOrchestrator / TaskFlowOrchestrator.CompleteAndAssignTo برای callback دستی (legacy) باقی مانده است.

نمونه کد: usage.md — تخصیص موازی.


۳. پایگاه داده ​

در صورت تنظیم متغیر DATABASE_URL، کلاس PostgresStore اسکیما و جداول را به‌صورت خودکار ایجاد می‌کند. شرح جامع جداول، ستون‌ها، شاخص‌ها و راهنمای مهاجرت در سند database.md آمده است.

mermaid
erDiagram
  definitions ||--o{ instances : "definition_id"
  instances ||--o{ tasks : instance_id
  instances ||--o{ instances : "parent_instance_id"

  definitions {
    text id PK
    text tenant_id
    text key
    text name
  }
  instances {
    text id PK
    text definition_id
    text definition_key
    text parent_instance_id
    text status
    jsonb vars
    text started_by
  }
  tasks {
    text id PK
    text instance_id
    text parent_instance_id
    text definition_key
    text assignee_kind
    text assignee_id
    text assigned_by
    text status
  }

متد Start در صورت عدم وجود تعریف برای processKey، آن را به‌طور خودکار ایجاد می‌نماید. اما ارجاع (AssignTo) در صورتی که تعریف فرایند از پیش ثبت نشده باشد، با خطای عدم یافتن مواجه خواهد شد.


۴. رفتار سرویس‌ها و جریان‌های کاری ​

۱. شروع فرایند (Start) ​

۱. تعریف فرایند را با processKey بازیابی کرده یا در صورت نبود ایجاد می‌کند.
۲. یک نمونهٔ اجرای ریشه با وضعیت running، ایجادکننده (initiator) و پارامترهای ارسالی می‌سازد.
۳. مقادیر { definitionKey, instanceId } را برمی‌گرداند. در این مرحله تسکی ساخته نمی‌شود.

۲. تخصیص کار (AssignTo) ​

۱. مقدار definitionKey اجباری است (یا در صورت عدم ارسال، از parentInstanceId به ارث می‌رسد).
۲. یک نمونهٔ اجرای جدید ایجاد می‌کند (اگر parentInstanceId داده شود، به اینستنس ریشه متصل می‌گردد).
۳. ایجاد وظیفه بر اساس نوع انتساب:

  • user: ایجاد یک تسک اختصاصی برای شخص مشخص‌شده.
  • group: ایجاد یک تسک گروهی؛ کلیهٔ اعضای گروه در کارتابل آن را مشاهده کرده و هر عضو پس از تحویل (Claim) می‌تواند آن را تکمیل کند.
  • users: ایجاد یک تسک مجزا به‌ازای هر شناسه، همگی ذیل یک اینستنس ارجاع یکسان.
    ۴. پاسخ خروجی شامل { instanceId, task, tasks } خواهد بود.

۳. کارتابل وظایف باز (Pending Tasks) ​

  • استعلام کاربر (user): تسک‌های باز (open) منتسب به کاربر یا گروه‌هایی که کاربر در آن‌ها عضویت دارد (Directory.UserGroups).
  • استعلام گروه (group): تسک‌های باز گروهی با شناسهٔ همان گروه (assignee_kind = group).

۴. تکمیل وظایف (Completion) ​

محاسبه بر روی instanceId ارجاع انجام می‌شود. شاخص allCompleted زمانی برابر true خواهد بود که حداقل یک تسک وجود داشته و هیچ تسکی در وضعیت open یا claimed باقی نمانده باشد.

  • تکمیل تسک شخصی صرفاً توسط همان فرد امکان‌پذیر است.
  • تکمیل تسک گروهی تنها توسط عضوی که تسک را تحویل گرفته (Claim کرده) مجاز است.
  • برای join موازی با onAllCompleted، ParallelJoinHandler بعد از allCompleted خودکار AssignTo بعدی را می‌سازد (usage.md).

۵. تکمیل و بستن کل فرایند (Complete and End) ​

متد CompleteAndEnd وظیفهٔ جاری را تکمیل کرده، سایر وظایف باز باقی‌مانده در کل درخت فرایند را به وضعیت cancelled منتقل می‌کند و اینستنس ریشه و فرزندان را در وضعیت completed قرار می‌دهد.

۶. فرایندهای کاربر (User Processes) ​

متد ListUserProcesses(user, state?) فهرست نمونه‌های ریشه‌ای که توسط کاربر آغاز شده‌اند را برمی‌گرداند:

  • notStarted: آغاز شده اما هنوز هیچ ارجاعی برای آن ثبت نشده است.
  • open: دارای تسک باز بوده و ریشه همچنان در وضعیت running قرار دارد.
  • closed: پرونده با وضعیت completed بسته شده است.

۵. مدیریت همزمانی و تراکنش‌ها ​

متد TransitionTask در پیاده‌سازی Postgres از دستور SELECT ... FOR UPDATE درون یک تراکنش مجزا بهره می‌برد. بدین ترتیب در صورتی که دو درخواست هم‌زمان برای تکمیل یک تسک ارسال شوند، یکی از آن‌ها موفق بوده و دیگری با خطای NotOpen مواجه خواهد شد.


۶. معماری پیشنهادی استقرار: React، بک‌اند و کلید API ​

انجین صفحهٔ لاگین ندارد و توکن کاربر صادر نمی‌کند. دو مقدار جدا وجود دارد:

مقدارچیستکجا نگه داشته شود
WF_API_KEYS / هدر X-API-Keyراز مشترک سرویس؛ ثابت است و مال یک کاربر نیستفقط روی سرور (بک‌اند / BFF / Gateway)
X-Actor-Idشناسهٔ کاربری که عمل را انجام می‌دهد (مثلاً mortenaho)بک‌اند آن را از جلسهٔ لاگین اپ شما می‌گذارد، نه از چیزی که React ادعا کند

اگر React کلید را در fetch بگذارد، در DevTools → Network برای هر کسی که مرورگر را باز کند دیده می‌شود. بنابراین فرانت هرگز نباید به پورت انجین وصل شود و هرگز نباید X-API-Key بفرستد.

معماری نادرست (کلید لو می‌رود) ​

mermaid
flowchart RL
  react["React / Browser"]
  engine["TaskFlow :8081"]
  react -->|"❌ fetch + X-API-Key"| engine

  style react fill:#ffcdd2,stroke:#e53935,color:#1a1a2e
  style engine fill:#fff3e0,stroke:#fb8c00,color:#1a1a2e

مسیر اشتباه: مرورگر ← اینترنت ← انجین. هدر درخواست در Network تب مرورگر قابل خواندن است؛ هر اسکریپت مخربی روی همان صفحه هم به کلید دسترسی دارد.

معماری درست (BFF) ​

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

mermaid
sequenceDiagram
  autonumber
  actor User
  participant React
  participant BFF as Your backend
  participant Engine as TaskFlow

  User->>React: open inbox
  React->>BFF: GET /api/inbox
  Note over React,BFF: app session only<br/>engine key not in browser
  BFF->>BFF: session → userId
  BFF->>Engine: GET /v1/tasks?user=mortenaho
  Note over BFF,Engine: X-API-Key (server env)<br/>X-Actor-Id (session)
  Engine-->>BFF: task list
  BFF-->>React: inbox JSON
mermaid
flowchart TB
  subgraph internet ["🌐 Internet"]
    browser["Browser / React"]
  end
  subgraph edge ["🔒 Public edge"]
    proxy["Reverse Proxy"]
    app["Backend / BFF"]
  end
  subgraph private ["🏠 Private network"]
    engine["TaskFlow Engine"]
    db[("Postgres")]
  end

  browser -->|"HTTPS + session"| proxy
  proxy --> app
  app -->|"X-API-Key (server-side)"| engine
  engine --> db

  style browser fill:#C2E5FF,stroke:#3DADFF,color:#1a1a2e
  style app fill:#CDF4D3,stroke:#66D575,color:#1a1a2e
  style engine fill:#DCCCFF,stroke:#874FFF,color:#1a1a2e
  style db fill:#e8f5e9,stroke:#43a047,color:#1a1a2e

قواعد استقرار ​

۱. پورت انجین (8081 / داخل Docker 8080) را روی اینترنت publish نکنید؛ فقط سرویس بک‌اند در شبکهٔ داخلی به آن وصل شود. انتشار پورت در docker-compose برای توسعهٔ محلی است، نه پروداکشن.
۲. WF_API_KEYS فقط در env سرور (یا Secret منیجر) باشد؛ در کد React، .env فرانت، و Git نرود. فایل .env این مخزن ignore شده است.
۳. بک‌اند X-Actor-Id را از هویت لاگین‌شده می‌سازد و فیلدهایی مثل from / initiator را از بدنهٔ درخواست مرورگر برای جعل هویت قبول نمی‌کند.
۴. ارتباط مرورگر با بک‌اند روی HTTPS باشد. ارتباط بک‌اند با انجین روی شبکهٔ خصوصی بماند.
۵. خارج از Development بدون WF_API_KEYS سرویس انجین بالا نمی‌آید تا API باز روی شبکه نماند.

نمونهٔ فراخوانی از React: usage.md. اپ کامل: employee-termination-app.

TaskFlow — موتور گردش کار WorkFlowEngine