Motoshub · Technical Assessment & Product Blueprint
گزارشی که بعد از خواندن کامل سورس هر دو مخزن (پلتفرم PHP/Oxwall و پروتوتایپ React) نوشته شده — از «چه چیزی امروز آماده است» تا «چطور این را به یک موتور SaaS چندمشتری تبدیل کنیم».
دو مخزن مستقل، دو دنیای جدا که هنوز به هم وصل نشدهاند.
یک فورک Oxwall با ~۱۴۱ پلاگین (اکثراً سفارشیِ iis*). کل سطح API در
یک پلاگین بهنام iismobilesupport جمع شده که همان «Gateway» است.
روی گیتلب داخلی، جریان MR-محور با ~۲۵۰ برنچ.
API اجتماعی آماده بدون tenant/license
React 19 + Vite 8، روی demo.shub.ir دیپلوی میشود.
همهی دادهها mock است؛ تنها نقطهی اتصال واقعی، فایل
src/lib/api.ts است که هنوز پیاده نشده. ماژولهای سازمانی
(پروژه/قرارداد/صندوق) فقط اینجا وجود دارند.
داکس /docs آماده به API واقعی وصل نیست
استخراجشده از dispatcher واقعی (web_service_general.php) و انوتیشنهای @OA.
| دامنه | وضعیت | تعداد op | توضیح |
|---|---|---|---|
| Auth / Session | آماده | ~12 | login/logout/join، فراموشی رمز، کد تأیید، CAPTCHA، کد ملی |
| Users / Profile | آماده | 42 | پروفایل، ویرایش، آواتار/کاور، follow/block، sessionها |
| Groups (+files) | آماده | 28 | CRUD گروه، عضویت، دعوت، مدیران، فایل/پوشهی گروه |
| Messaging / Chat | آماده | 13 | پیام، فوروارد، mute، رسانهی چت، جستجو |
| Newsfeed | آماده | 9 | داشبورد، پست، لایک، فوروارد، privacy |
| Events / Forum / Blogs / News | آماده | 35 | CRUD کامل هر چهار دامنه |
| Media (photo/video) | آماده | 11 | آلبوم، عکس، ویدیو |
| Comments / Friends / Notifications / Privacy / Search | آماده | ~18 | سطح پایهی اجتماعی |
آپلود فایل (upload_single_file) | آماده | 1 | چندنوعی، با اسکن آنتیویروس ClamAV |
| iispors (پرسمانساز/کوییز) | فعال، بدون داکس | ~82 | در dispatcher هست ولی صفر انوتیشن @OA |
| iispors_judgement (داوری) | فعال، بدون داکس | ~0 doc | CRUD داوری، معیار، امتیازدهی |
| iislandingcreator (لندینگ) | فعال، بدون داکس | ~10 | ساخت صفحه/سکشن |
| پروژه / قرارداد / صندوق / پژوهش / گزارش | API ندارد | 0 | ماژولهای سازمانی — فقط در پروتوتایپ mock هستند |
| iisgrant، iiscfp، iiscompetition، iisticketing… | بیرون از Gateway | 0 | پلاگینهای دامنهای که به لایهی API وصل نشدهاند |
openapi.json در پروتوتایپ ۱۵۶ عملیات دارد (کمی قدیمی)؛ و رجیستری دستی مدعی
۲۷۱ عملیاتِ «قابلفراخوانی» است (شامل iispors/لندینگ بدون schema). قدم اول داکس: بازتولید
openapi.json از سورس فعلی تا این اعداد همتراز شوند و tagها اضافه شوند (الان خالیاند و Swagger UI دستهبندی ندارد).
یک مونولیت Oxwall بهازای هر مشتری؛ همهچیز روی یک دیسک، یک دیتابیس.
flowchart TB M["Mobile apps
iOS / Android (FCM)"]:::client W["Web browsers"]:::client M --> IMS W --> IMS subgraph OX["Oxwall PHP 7.4 monolith — ONE per customer"] IMS["iismobilesupport
= API gateway
mobile/services/{information,action}/:type"]:::acc PL["~141 iis* plugins
(social + domain + SMS + SSO)"] CORE["Oxwall core
ow_base_config (settings)"] IMS --> PL --> CORE end CORE --> DB[("MariaDB 10.5
ow_* tables")]:::store CORE --> FS[("ow_userfiles/
local disk, per-plugin")]:::store classDef client fill:#eef1f4,stroke:#9aa6b4,color:#1b2431; classDef acc fill:#b45e28,stroke:#8a4620,color:#fff; classDef store fill:#e3f3ec,stroke:#2e9e6b,color:#14432f;
Fig 1 — امروز: هر مشتری = یک کپی کامل از کد + یک دیتابیس + یک پوشهی فایل، بهصورت دستی نصب میشود.
access_token در واقع همان کوکی ow_login اکسوال است؛ هر ریکوئست دوباره لاگین میشود.config.php (per-instance، pepper هاردکد)، جدول ow_base_config (قابلویرایش از ادمین)، و .env (فقط نصب/داکر).یک frontend، یک Gateway، و دو بکاند (قدیمی PHP + جدید Django) پشت یک هویت واحد.
flowchart TB FE["New React frontend
(from prototype)"]:::acc FE --> GW["API Gateway / BFF
یک host، JWT auth واحد"]:::acc IDP["Identity Provider
Keycloak / OIDC"]:::idp IDP -. validate JWT .- GW GW --> BR{"Router / Bridge"} BR -->|"social: users, groups,
feed, chat, media, forum"| LEG["Legacy Oxwall API
iismobilesupport"] BR -->|"projects, contracts, funds,
research, reports,
settings, storage"| DJ["Django + DRF
services (new)"]:::new LEG --> LDB[("tenant MariaDB")]:::store DJ --> PGDB[("tenant PostgreSQL")]:::store DJ --> OBJ[("Object storage
MinIO / S3")]:::store classDef acc fill:#b45e28,stroke:#8a4620,color:#fff; classDef new fill:#2e9e6b,stroke:#1f6f4b,color:#fff; classDef idp fill:#3e6cb0,stroke:#2c4d80,color:#fff; classDef store fill:#e3f3ec,stroke:#2e9e6b,color:#14432f;
Fig 2 — هدف: Gateway تصمیم میگیرد هر درخواست به دنیای قدیمی برود یا جدید؛ فرانت فقط یک API میبیند.
تا فرانت هیچوقت نفهمد کدام قابلیت از PHP میآید و کدام از Django. این یعنی میتوانی فیچرها را تکبهتک از PHP به Django مهاجرت دهی بدون اینکه فرانت عوض شود — دقیقاً همان استراتژی «Strangler Fig».
دامنههای سازمانی (قرارداد، صندوق…) state-machine و approval-flow سنگین دارند. Django/DRF + مدل چندمشتری، RBAC و Celery برای اینها بسیار مناسبتر از افزودن پلاگین به Oxwall PHP 7.4 است.
پاسخ مستقیم به سؤالت: فرانت چطور به بک وصل میشود، bridge چیست، git کجاست.
flowchart LR
subgraph SRC["Source control"]
GH["GitHub
frontend + docs
demo.shub.ir"]:::git
GL["GitLab (internal)
motoshub-web
+ Django (new repo)"]:::git
end
GH -->|"GitHub Actions"| FEBUILD["Frontend build
(static SPA)"]
GL -->|"GitLab CI"| IMG["Versioned images
+ registry"]
FEBUILD --> RUNFE["Frontend (served)"]
RUNFE -->|"HTTPS /api/*"| GW["API Gateway"]
GW -->|"REST"| DJ["Django services"]
GW -->|"HTTP form-POST
access_token bridge"| LEG["Oxwall iismobilesupport"]
IMG --> DJ
IMG --> LEG
classDef git fill:#3e6cb0,stroke:#2c4d80,color:#fff;
Fig 3 — Git دو جا زندگی میکند: فرانت روی GitHub، بکاندها روی GitLab. CI هر کدام artifact نسخهدار میسازد.
یک آداپتور کوچک داخل Gateway که درخواست REST مدرن فرانت را به قرارداد قدیمی Oxwall ترجمه میکند:
فرانت GET /api/social/feed میفرستد؛ bridge آن را به
POST mobile/services/information/get_dashboard با فیلد access_token تبدیل میکند،
JWT کاربر را به session/کوکی اکسوال map میکند، و جواب JSON را نرمال برمیگرداند. فرانت از وجود این ترجمه بیخبر است.
همین حالا سه SSO در کد هست؛ باید یکیشان را «منبع حقیقت هویت» کنیم.
در سورس اینها را داری: iishajsso (OpenID Connect کامل، با کلاینت Jumbojett)، iissso (تیکتی/CAS با single-logout)، و iisgmailconnect (OAuth گوگل). یعنی زیرساخت OIDC از قبل وجود دارد.
iishajsso که الان OIDC است، بهعنوان یک realm/federation به Keycloak وصل میشود. این هم SSO واقعی بین دنیای قدیم و جدید میدهد، هم برای مدل چندمشتری (هر tenant یک realm) تمیز است.
sequenceDiagram participant U as User participant FE as Frontend participant GW as Gateway participant IDP as Keycloak (OIDC) participant OX as Oxwall API U->>FE: open app FE->>IDP: OIDC login IDP-->>FE: id_token + access (JWT) FE->>GW: request + Bearer JWT GW->>GW: validate JWT (per-tenant realm) GW->>OX: bridge map to ow_login session OX-->>GW: data GW-->>FE: normalized JSON
Fig 4 — لاگین یکبار در Keycloak؛ همان توکن هم برای Django و هم (از طریق bridge) برای Oxwall کار میکند.
قلب مدل کسبوکار تو: یک تیم maintain، N مشتری، فروش فیچر به همه.
واقعیت سخت از سورس: هیچ مفهوم tenant/license/feature-flag در کد نیست. اکسوال ذاتاً single-tenant است (یک DB، یک config.php، یک پوشهی فایل بهازای هر نصب). پس دو مسیر داری:
| مدل | ایزولاسیون | مزیت | هزینه |
|---|---|---|---|
| Instance-per-tenant (پیشنهادی) | کانتینر + DB مجزا برای هر مشتری | ایزولاسیون کامل، بکاپ/بیلینگ ساده، منطبق با واقعیت اکسوال، ریسک پایین | اورکستریشن و منابع بیشتر |
| Shared-app + tenant column | یک اپ، ستون tenant در DB | مصرف منابع کمتر | نیازمند بازنویسی عمیق اکسوال — ریسک بالا |
توصیه: مدل Instance-per-tenant با یک Control Plane مرکزی. اکسوال را دستنخورده نگه میداری و multi-tenancy را در لایهی اورکستریشن حل میکنی، نه داخل کد قدیمی.
flowchart TB GL["GitLab: یک codebase
یک تیم maintenance"]:::git --> REG["Image registry
نسخههای immutable (semver)"] CP["Control Plane
tenants · licenses · enabled features"]:::acc REG --> PROV["Provisioner
(scripted / IaC)"] CP --> PROV PROV --> T1["Tenant A
app + db + files + settings"]:::t PROV --> T2["Tenant B
features: {social, projects}"]:::t PROV --> T3["Tenant C
features: {social, funds, SSO}"]:::t classDef acc fill:#b45e28,stroke:#8a4620,color:#fff; classDef git fill:#3e6cb0,stroke:#2c4d80,color:#fff; classDef t fill:#e3f3ec,stroke:#2e9e6b,color:#14432f;
Fig 5 — فیچر جدید یکبار روی GitLab merge میشود یک image نسخهدار Control Plane تعیین میکند کدام مشتری کدام فیچر را (طبق لایسنسی که خریده) روشن دارد.
«فروش فیچر به مشتری دیگر» یعنی صرفاً یک feature-flag/entitlement در Control Plane روشن شود — نه یک دیپلوی جدید. برای این کار باید یک لایهی سبک tenant + license + entitlement ساخته شود (در Django، بهعنوان بخشی از فاز ۳).
دو خواستهی مشخص تو — و اینکه در سورس کجا قلاب میشوند.
مشکل فعلی: سقف واقعی آپلود در php-fpm.conf/www.conf/nginx روی ۲۰M هاردکد است. راهحل حرفهای:
ow_base_config (که از قبل زیرساخت admin-editable دارد) منتقل کن و در کد آپلود، بهجای ini_get() این مقدار DB را بخوان.SystemSection (که الان mock است) را به این API واقعی وصل کن: GET/PUT /api/core/settings. متغیرهایی که خودت طراحی کردهای (upload_max_mb, chunk_size_mb, user_quota_gb, session_hours…) دقیقاً همینها هستند.الان فایلها زیر ow_userfiles/<plugin>/… ذخیره میشوند (لوکال، cloudfiles خاموش)، ولی هیچ quota/آمار/پاکسازی مرکزی نیست. برای داشبوردی که در پروتوتایپ (StorageSection) طراحی کردهای:
GET /api/storage/stats و POST /api/storage/cleanup (retention). پلاگین iisdatabackup نقطهی شروع خوبی برای بخش بکاپ است.OW_USE_CLOUDFILES دارد.پنج فاز؛ هر فاز یک خروجی قابلعرضه دارد. تخمین برای یک تیم کوچک بکاند.
بازتولید openapi.json از سورس فعلی (۱۶۸ op + tagها) و کاملکردن داکس برای تیم فرانت. افزودن Sentry برای دیدن خطاهای ساکت (مثل باگ فایلمنیجر). راهاندازی registry ایمیج + تگ semver در CI. دیسیپلین dev/staging/prod.
بالا آوردن API Gateway/BFF و Keycloak. پیادهسازی bridge به Oxwall. جایگزینی src/lib/api.ts پروتوتایپ با fetch واقعی. خروجی: فرانت واقعی روی دادهی زندهی اجتماعی.
ساخت پروژه Django/DRF: هسته + Auth (JWT/Keycloak)، تنظیمات سیستم، سرویس فایل/دیسک، سپس ماژولهای سازمانی بهترتیب: پروژه قرارداد صندوق پژوهش گزارشساز. اینها همانهاییاند که هیچ API ندارند.
مدل Tenant/License/Entitlement، اتوماسیون provisioning، داشبورد per-tenant تنظیمات و storage، و مکانیزم «فروش فیچر = روشنکردن flag».
رویدادهای Django (تسک اساین شد، قسط سررسید شد) فید/اعلان اکسوال. سپس مهاجرت آرام باقی فیچرها از PHP به Django با الگوی Strangler، بدون تغییر فرانت.
دو گروه: آمادهها (مستندسازی/اتصال) و ساختنیها (Django جدید).
| حوزه | مسیر نمونه (قرارداد فعلی) | اقدام |
|---|---|---|
| Auth/Users/Groups/Feed/Chat/Media/Forum/Blog/News/Events | POST mobile/services/{information,action}/:type | مستندسازی کامل + wrapper REST در Gateway |
| iispors / iispors_judgement / landing | (بدون @OA) | افزودن انوتیشن + داکس |
| ماژول | Endpointهای کلیدی | اولویت |
|---|---|---|
| Core / Settings | GET/PUT /core/settings · /core/me | ۱ |
| Files / Storage | POST /files (chunked) · GET /storage/stats · POST /storage/cleanup | ۲ |
| Projects | /projects · /projects/:id/tasks · /milestones · /board | ۳ |
| Contracts | /contracts · /:id/stages · /payments · /approvals | ۴ |
| Funds | /funds · /:id/reviews · /installments · /kpi | ۵ |
| Research | /research · /:id/applicants · /outputs | ۶ |
| Reports | POST /reports/aggregate · /reports/saved | ۷ |
| Tenant / License | /tenants · /licenses · /entitlements | ۸ |
تخمین «مهندسی خالص» برای یک بکاند سینیور؛ تست/ریویو جداست.
| # | تسک | فاز | تخمین |
|---|---|---|---|
| 1 | بازتولید openapi.json (۱۶۸ op) + tag + انتشار داکس | ۰ | ۲–۳ روز |
| 2 | Sentry + registry ایمیج + تگ semver در CI | ۰ | ۳–۴ روز |
| 3 | راهاندازی Gateway/BFF (اسکلت + routing) | ۱ | ۴–۵ روز |
| 4 | Keycloak + اعتبارسنجی JWT در Gateway | ۱ | ۴–۶ روز |
| 5 | Bridge اکسوال (map JWTow_login، نرمالسازی JSON) | ۱ | ۵–۸ روز |
| 6 | اتصال پروتوتایپ به API واقعی (جایگزینی api.ts) | ۱ | ۵–۷ روز |
| 7 | اسکلت Django/DRF + Auth + Tenant model پایه | ۲ | ۵–۷ روز |
| 8 | سرویس Settings (DB-backed) + وصل به SystemSection | ۲ | ۳–۴ روز |
| 9 | سرویس File/Storage + داشبورد حجم (StorageSection) | ۲ | ۶–۹ روز |
| 10 | ماژول Projects (board/task/milestone) | ۲ | ۸–۱۲ روز |
| 11 | ماژول Contracts (state-machine + approval) | ۲ | ۱۰–۱۴ روز |
| 12 | ماژول Funds + Research (روی الگوی قرارداد) | ۲ | ۱۰–۱۴ روز |
| 13 | گزارشساز (aggregate + saved + خروجی) | ۲ | ۶–۹ روز |
| 14 | Control Plane: License/Entitlement + provisioning | ۳ | ۱۰–۱۵ روز |
| 15 | Notification bridge (Django events فید اکسوال) | ۴ | ۵–۷ روز |
جمع تقریبی تا پایان فاز ۳: ~۴ تا ۵ ماه کار مهندسی خالص برای یک نفر؛ با تیم دو-سهنفره و موازیسازی کمتر.
آنچه امروز شکسته است و فرایند حرفهای پیشنهادی.
الان: نسخه در ow_version.xml (2.0.0/build 11233) + تگهای گیت متروکه (۲۰۲۰) + CI بدون تگ. پراکنده و غیرقابلاتکا.
پیشنهاد: semver واقعی روی هر release، ایمیج immutable در registry بهازای هر نسخه، جریان staging prod با promotion، changelog خودکار. هر مشتری نسخهی پینشده دارد؛ آپدیت = rollout کنترلشدهی همان image.
الان: فقط پلاگین iisdatabackup، بدون سیاست منظم.
پیشنهاد: بهازای هر tenant: dump زمانبندیشدهی DB + snapshot از ow_userfiles، ذخیرهی offsite (S3)، retention مشخص (مثلاً روزانه×۷ + هفتگی×۴)، و تست بازیابی دورهای. با مهاجرت به object storage، فایلها ذاتاً نسخهدار/replicated میشوند.
در مدل instance-per-tenant، هر کانتینر limit CPU/RAM دارد (staging الان 1cpu/1G).
یک لایهی مانیتورینگ (Prometheus/Grafana) مصرف هر tenant را میبیند؛ همین داده مبنای بیلینگ maintenance و ظرفیتسنجی میشود.
quota فایل هر مشتری از همان سرویس Storage اعمال میشود.
motoshub_salt_SK750) بین همهی نصبها؛ اجرای کانتینر staging با کاربر root؛
پیشفرض دیتای MySQL روی /tmp؛ و دو استک داکر واگرا (docker/Dockerfile در برابر docker/php/Dockerfile).
Motoshub Technical Blueprint · تهیهشده پس از مطالعهی کامل سورس motoshub-web و motoshub-prototype · نسخهی زنده و قابلویرایش