Update: 2026-07-31 03:00:06
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
# خطة إضافة «طلبات الطعام» إلى منصة سيرو — المعمارية والدوكر والتنفيذ
|
||||
|
||||
> الحالة: مقترح للتنفيذ — لم يُكتب أي كود بعد.
|
||||
> التاريخ: 2026-07-30
|
||||
> المرجع المعماري: [docker/docker-compose.yml](../../docker/docker-compose.yml) و [docs/30-siro-port-plan.md](../30-siro-port-plan.md)
|
||||
|
||||
---
|
||||
|
||||
## ١. الخلاصة التنفيذية (القرار في عشرة أسطر)
|
||||
|
||||
- «طلبات الطعام» تُضاف كـ **وحدة (module) مستقلة داخل نفس منصة سيرو**، لا كمشروع منفصل ولا كخدمة مصغّرة (microservice) بقاعدة مستخدمين خاصة بها.
|
||||
- الحاويات: نضيف **حاويتين فقط** إلى نفس `docker compose`: حاوية `php_food` (fpm للطلبات المتزامنة) وحاوية `socket_food` (عملية Workerman دائمة لحالة الطلب اللحظية).
|
||||
- المشترَك يبقى مشترَكاً: **نفس nginx، نفس MySQL، نفس Redis، نفس JWT، نفس المحفظة** — لأن هذا هو ما يجعل التطبيق «متكاملاً» بدل أن يكون تطبيقين ملصوقين.
|
||||
- المعزول يبقى معزولاً: **قاعدة بيانات `siro_food` مستقلة**، مساحة أسماء مفاتيح Redis مستقلة `food:*`، وحدود ذاكرة ولوغات مستقلة.
|
||||
- الفائدة الحقيقية من العزل ليست الأداء (الحاوية على لينكس = عملية عادية)، بل: انهيار الطعام لا يُسقط الرحلات، ونشر الطعام لا يتطلب إعادة تشغيل الباك إند، وأي عميل وايت-ليبل يشغّل الطعام أو يطفئه بمتغيّر واحد.
|
||||
|
||||
---
|
||||
|
||||
## ٢. المبدأ الحاكم: ما الذي يُشارَك وما الذي يُعزل؟
|
||||
|
||||
القاعدة التي بُني عليها هذا التصميم — وهي نفس فلسفة `docker-compose.yml` الحالية «التقسيم بالدور، لا حاوية-واحدة-فيها-كل-شيء ولا تفتيت microservices»:
|
||||
|
||||
| المكوّن | القرار | السبب |
|
||||
|---|---|---|
|
||||
| هوية المستخدم (JWT) | **مشترك** | الراكب نفسه هو الزبون نفسه. حسابان لشخص واحد = كارثة منتج ودعم فني. |
|
||||
| المحفظة والدفع | **مشترك** (`payment_server/v2`) | رصيد واحد للراكب يُنفَق على الرحلة والوجبة. تكرار المحفظة يعني تسويتين ماليتين متضاربتين. |
|
||||
| الإشعارات (FCM) | **مشترك** (`core/Services/FcmService.php`) | مسار الرسائل مُشخَّص ومُسجَّل بالفعل. |
|
||||
| Redis | **مشترك، بمساحة أسماء منفصلة** | نفس المثيل، لكن كل مفاتيح الطعام تبدأ بـ `food:` — لا تصادم مع حالة الرحلة. |
|
||||
| قاعدة البيانات | **معزولة: `siro_food`** | نفس نمط `siro_transit`. يمنع أن يقفل جدول طلبات مزدحم استعلامات الرحلات. |
|
||||
| كود PHP وحاوية fpm | **معزولة: `backend/food/` + `php_food`** | نشر مستقل، حد ذاكرة مستقل، وانهيار مستقل. |
|
||||
| السوكيت | **معزول: `socket_food`** | العملية الدائمة لا تُخلَط أبداً مع fpm (نفس سبب فصل `socket_driver`). |
|
||||
| أسطول التوصيل | **مشترك مع تمييز بالدور** | نفس السائقين، مع علم `can_deliver`. تفصيل هذا في §٦. |
|
||||
|
||||
**ما لن نفعله (قرارات مرفوضة صراحةً):**
|
||||
1. لن نبني تطبيق Flutter خامساً للزبون — الطعام يدخل داخل `siro_rider` كتبويب.
|
||||
2. لن ننشئ جدول مستخدمين جديداً في `siro_food`؛ نخزّن `passenger_id` كمرجع منطقي فقط.
|
||||
3. لن نضع الطعام داخل حاوية `php` الحالية — لأن أي خطأ فادح في الطعام سيستهلك حوض fpm نفسه الذي يخدم الرحلات.
|
||||
4. لن نفتح بورت السوكيت للعالم مباشرة (انظر §٤ — درس مؤلم مدفوع الثمن سابقاً).
|
||||
|
||||
---
|
||||
|
||||
## ٣. طبقة الدوكر — الشكل النهائي
|
||||
|
||||
### ٣.١ الحاويات الجديدة
|
||||
|
||||
تُضاف إلى نفس `docker/docker-compose.yml` (لا ملف compose ثانٍ — ملفّان يعنيان شبكتين وحيرة تشغيلية):
|
||||
|
||||
```yaml
|
||||
# وحدة الطعام — fpm مستقلة عن fpm الرحلات عمداً:
|
||||
# انهيار الطعام يجب ألا يبتلع حوض العمليات الذي يخدم الرحلات.
|
||||
php_food:
|
||||
build:
|
||||
context: ./php
|
||||
dockerfile: Dockerfile.fpm # نفس الصورة تماماً — لا صيانة مزدوجة
|
||||
args:
|
||||
PHP_VERSION: "${PHP_VERSION:-8.2}"
|
||||
volumes:
|
||||
- ../backend:/var/www/backend # يحتاج core/ و functions.php المشتركة
|
||||
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro
|
||||
- ./php/food-pool.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
|
||||
- ./keys:/keys:ro
|
||||
env_file: .env
|
||||
depends_on: [mysql, redis]
|
||||
mem_limit: 1g
|
||||
restart: unless-stopped
|
||||
|
||||
# سوكيت الطعام — WS بورت 4040 + HTTP داخلي 4041
|
||||
socket_food:
|
||||
build:
|
||||
context: ./php
|
||||
dockerfile: Dockerfile.socket
|
||||
args:
|
||||
PHP_VERSION: "${PHP_VERSION:-8.2}"
|
||||
command: ["php", "food_socket.php", "start"]
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../food_server:/app
|
||||
- ./keys:/keys:ro
|
||||
env_file: .env
|
||||
ports:
|
||||
# لا نفتح 4040 للعالم: Workerman نصّ صريح والتطبيق يطلب TLS فتتجمّد المصافحة.
|
||||
# nginx على المضيف يستمع 4040 بالشهادة ويمرّر إلى 14040 هنا.
|
||||
- "127.0.0.1:14040:4040"
|
||||
# و4041 داخلي فقط: الباك إند يناديه عبر http://socket_food:4041
|
||||
depends_on: [redis]
|
||||
mem_limit: 512m
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### ٣.٢ تعديل nginx (حاوية البوابة)
|
||||
|
||||
في [docker/nginx/default.conf](../../docker/nginx/default.conf) يُضاف توجيه مسار الطعام إلى حوض fpm الخاص به:
|
||||
|
||||
```nginx
|
||||
# كل ما تحت /backend/food/ يذهب إلى حوض fpm المستقل
|
||||
location ~ ^/backend/food/.*\.php$ {
|
||||
try_files $uri =404;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass php_food:9000; # ← لا php:9000
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_read_timeout 60s;
|
||||
}
|
||||
```
|
||||
> ترتيب البلوكات مهم: هذا البلوك يجب أن يسبق `location ~ \.php$` العام، وإلا ابتلعه الأخير وذهبت الطلبات إلى الحوض الخطأ بصمت.
|
||||
|
||||
وتُضاف نقطة حالة الحوض `/(fpm-status-food)` بنفس قيود العنوان الحالية، وإلا سنراقب حوضاً واحداً ونظن أننا نراقب اثنين.
|
||||
|
||||
### ٣.٣ nginx المضيف (TLS للسوكيت) — إلزامي لا اختياري
|
||||
|
||||
يُضاف مقطع في `nginx/siro-sockets-tls.conf` على المضيف: استماع `4040` بالشهادة → تمرير إلى `127.0.0.1:14040`. هذا **ليس تحسيناً**: تكرار خطأ الماضي (نشر بورت السوكيت خاماً) ينتج مصافحة مجمّدة و timeout في التطبيق بلا أي سطر خطأ يدل عليه.
|
||||
|
||||
### ٣.٤ جدار الحماية
|
||||
|
||||
لا تعتمد على UFW لحجب بورت الطعام: `docker-proxy` يلتفّ على قواعد UFW. الحجب يتم بربط البورت بـ `127.0.0.1` في compose كما هو مكتوب أعلاه — وهذا كافٍ وحده.
|
||||
|
||||
### ٣.٥ المتغيّرات الجديدة في `docker/.env.example`
|
||||
|
||||
```
|
||||
FOOD_ENABLED=true
|
||||
DB_FOOD_NAME=siro_food
|
||||
DB_FOOD_HOST=mysql
|
||||
DB_FOOD_USER=siro_food
|
||||
DB_FOOD_PASS=
|
||||
FOOD_SOCKET_URL=http://socket_food:4041
|
||||
FOOD_COMMISSION_PERCENT=15
|
||||
FOOD_DELIVERY_BASE_FEE=
|
||||
FOOD_MAX_ACTIVE_ORDERS_PER_USER=3
|
||||
```
|
||||
|
||||
### ٣.٦ فخ النشر الذي يجب تفاديه مسبقاً
|
||||
|
||||
`vendor/` و `composer.lock` مُستثنيان من git في بعض مسارات المشروع — وهذا ما قتل `payment_server/v2` على نشر نظيف سابقاً. لذلك:
|
||||
- `food_server/composer.lock` **يُلتزم به في git إلزامياً**.
|
||||
- تُضاف إلى [docker/README.md](../../docker/README.md) خطوة صريحة:
|
||||
`docker compose run --rm socket_food composer install`
|
||||
|
||||
---
|
||||
|
||||
## ٤. طبقة البيانات — قاعدة `siro_food`
|
||||
|
||||
### ٤.١ التسجيل في طبقة الاتصال
|
||||
|
||||
يُضاف مدخل واحد إلى الخريطة في [backend/core/Database/Database.php](../../backend/core/Database/Database.php):
|
||||
|
||||
```php
|
||||
'food' => [
|
||||
'name' => 'DB_FOOD_NAME',
|
||||
'host' => 'DB_FOOD_HOST',
|
||||
'user' => 'DB_FOOD_USER',
|
||||
'pass' => 'DB_FOOD_PASS',
|
||||
],
|
||||
```
|
||||
وقاعدة صارمة تُوثَّق وتُراجَع في كل PR — نفس قاعدة transit: **ممنوع `Database::get('main')` داخل `backend/food/`**. أي حاجة لبيانات الراكب تُقرأ عبر دالة خدمة واحدة معرّفة في `food/functions.php`، لا باستعلامات متفرقة عبر القواعد.
|
||||
|
||||
### ٤.٢ المخطط `schema_food.sql` (الجداول الأساسية)
|
||||
|
||||
```
|
||||
merchants المتجر/المطعم: الاسم، الموقع (lat/lng)، الحالة، نسبة العمولة، أوقات العمل
|
||||
merchant_users حساب دخول صاحب المطعم (هوية منفصلة عن الراكب — دور merchant)
|
||||
menu_categories أقسام القائمة داخل المطعم
|
||||
menu_items الصنف: السعر، الصورة، متاح/غير متاح، وقت التحضير التقديري
|
||||
item_options الإضافات والخيارات (حجم، إضافات) وسعر كل خيار
|
||||
orders الطلب: passenger_id، merchant_id، courier_id، الحالة، الإجماليات، العنوان
|
||||
order_items أصناف الطلب بسعر **مجمّد وقت الطلب** (لا JOIN على menu_items للسعر)
|
||||
order_status_log كل انتقال حالة مع الوقت والفاعل — مصدر الحقيقة للنزاعات
|
||||
order_payments مرجع معاملة المحفظة/الدفع + حالة التسوية
|
||||
courier_assignments محاولات إسناد الطلب للسائق: عُرض/قُبل/رُفض/انتهت المهلة
|
||||
merchant_payouts مستحقات المطاجر ودورات التسوية
|
||||
food_promo_codes أكواد الخصم (منفصلة عن أكواد الرحلات)
|
||||
```
|
||||
|
||||
**قواعد مخطط غير قابلة للتفاوض:**
|
||||
1. **تجميد الأسعار**: `order_items` يحمل `unit_price` و `option_price_json` منسوخين وقت الإنشاء. تغيير المطعم لسعره لاحقاً يجب ألا يعيد كتابة تاريخ طلب مكتمل.
|
||||
2. **المال بالأعداد الصحيحة**: كل المبالغ `BIGINT` بأصغر وحدة نقدية، لا `FLOAT` مطلقاً.
|
||||
3. **مفتاح تعطيل التكرار (idempotency)**: عمود `client_order_uuid UNIQUE` على `orders` — الضغط المزدوج على «تأكيد» أو إعادة محاولة الشبكة يجب ألا ينتج طلبين ولا خصمين.
|
||||
4. **الحالة كسلسلة محكومة**: `pending → merchant_accepted → preparing → ready → courier_assigned → picked_up → delivered` وفروع `cancelled_by_*` و `rejected`. الانتقالات تُفرَض في دالة واحدة `food_transition_status()` — لا `UPDATE orders SET status` مبعثرة في الملفات.
|
||||
5. **الفهارس منذ اليوم الأول**: `(passenger_id, created_at)`، `(merchant_id, status)`، `(courier_id, status)`، ومكاني على `merchants(lat,lng)`.
|
||||
|
||||
---
|
||||
|
||||
## ٥. طبقة الـ API — البنية والمسارات
|
||||
|
||||
نتبع نمط «بوابة لكل جمهور» المستخدَم في `backend/transit/` حرفياً، لأنه ما يجعل الوحدة تبدو جزءاً أصيلاً من المشروع لا طُعماً غريباً.
|
||||
|
||||
```
|
||||
backend/food/
|
||||
├── connect_app.php بوابة الزبون (JWT الراكب)
|
||||
├── connect_merchant.php بوابة المطعم (JWT بدور merchant)
|
||||
├── connect_courier.php بوابة السائق (JWT السائق)
|
||||
├── connect_admin.php بوابة الإدارة (JWT admin/super_admin)
|
||||
├── functions.php منطق مشترك: انتقال الحالة، الحسابات، صياغة الردود
|
||||
├── schema_food.sql
|
||||
├── merchant/ browse.php details.php menu.php search.php
|
||||
├── cart/ validate.php quote.php
|
||||
├── order/ create.php status.php cancel.php rate.php history.php
|
||||
├── merchant_ops/ incoming.php accept.php reject.php ready.php items_toggle.php
|
||||
├── courier/ offer_respond.php picked_up.php delivered.php active.php
|
||||
├── admin/ merchants.php orders.php payouts.php settings.php
|
||||
└── cron_*.php انتهاء مهلة الطلبات، التسويات، تنبيهات التأخير
|
||||
```
|
||||
|
||||
كل بوابة تكرّر نفس الافتتاحية المثبتة في المشروع: `core/bootstrap.php` ثم `RateLimiter` ثم `JwtService::authenticate()` ثم `Database::get('food')` مع رد `503` نظيف عند فشل الاتصال.
|
||||
|
||||
**قواعد التعامل مع API الحالي — مأخوذة من سلوكه الفعلي:**
|
||||
- لا تعتمد `$_GET`؛ المدخلات تُقرأ كما يقرؤها باقي المشروع (جسم الطلب)، وترويسة `X-Device-FP` مطلوبة.
|
||||
- شكل الرد يطابق مغلّف الرسائل القائم (`status` + `message` + الحمولة) — التطبيق الحالي يفكّه بهذا الشكل، وأي شكل جديد سيكسر المُحلِّل المشترك.
|
||||
- تعدد اللغات: نصوص الحالة تُعاد كمفاتيح، والترجمة في التطبيق.
|
||||
|
||||
**الأمن — البنود التي أُخذت من إخفاقات وحدة مواصلاتي، فلا تتكرر:**
|
||||
1. **IDOR أولاً وقبل كل شيء**: كل نقطة تأخذ `order_id` تتحقق أن الطلب يخص الفاعل (زبونه أو مطعمه أو سائقه). يُكتب اختبار سلبي واحد على الأقل لكل نقطة قبل الدمج.
|
||||
2. **تفويض الدور على مستوى البوابة**: `connect_merchant.php` يرفض توكن الراكب حتى لو كان صالحاً.
|
||||
3. **الخوادم لا تثق بالأسعار**: السعر النهائي يُحسب في الخادم من `menu_items` — إجمالي يرسله العميل يُتجاهل ويُسجَّل كإشارة احتيال.
|
||||
4. **تحديد المعدل**: حد أشدّ على `order/create.php` (منع فيض الطلبات الوهمية) وعلى `merchant_ops/*`.
|
||||
5. **رفع صور القائمة**: تحقق من النوع والحجم، إعادة ترميز، أسماء عشوائية، ونشر من مسار لا ينفّذ PHP.
|
||||
6. **بيانات شخصية**: عنوان الزبون ورقمه يظهران للسائق **فقط** بعد `courier_assigned` و**يُحجبان** بعد `delivered`.
|
||||
|
||||
---
|
||||
|
||||
## ٦. التوصيل — إعادة استخدام أسطول الرحلات
|
||||
|
||||
هذا أهم قرار منتجي/تقني في الخطة.
|
||||
|
||||
**القرار:** لا أسطول ثانٍ. نستخدم نفس السائقين مع علم `can_deliver` وحالة تفرّغ، اعتماداً على بنية `geo:drivers:available` القائمة في Redis.
|
||||
|
||||
**الآلية:**
|
||||
1. عند `ready` (أو قبله بوقت التحضير التقديري)، يستدعي الباك إند بحثاً جغرافياً حول موقع المطعم — نفس نمط `georadius` في [loction_server/find_drivers_redis.php](../../loction_server/find_drivers_redis.php)، لكن على مفتاح `geo:couriers:available` الذي يُملأ بالسائقين ذوي `can_deliver=1` وليسوا في رحلة.
|
||||
2. العرض يُرسل لسائق واحد في كل مرة بمهلة قصيرة (15–20 ثانية)، ويُسجَّل كل عرض في `courier_assignments`. الصمت = رفض ضمني وانتقال للتالي. هذا يمنع «سباق القبول» الذي ينتج طلباً بسائقين.
|
||||
3. القفل: `SET food:order:{id}:lock <courier_id> NX EX 20` — القابل الأول فقط يفوز، ذرّياً.
|
||||
4. سائق في رحلة نقل لا يظهر لعروض التوصيل والعكس — حالة السائق مصدر حقيقة واحد في Redis، لا علمان متنافسان.
|
||||
5. الأثر على أرباح السائق: التوصيل يدخل نفس دفتر الأرباح ونفس تلميح الأرباح المعتمد في تطبيق السائق، لا شاشة أرباح موازية.
|
||||
|
||||
**المخاطرة الواجب مراقبتها:** في ساعة الذروة تتنافس الوجبات والرحلات على الأسطول نفسه. المؤشر الحارس: نسبة الطلبات التي لم تجد سائقاً خلال 5 دقائق. إن تجاوزت ١٠٪ نُفعّل تخصيص جزء من الأسطول للتوصيل في نطاق زمني/جغرافي — لكن **لا نبني هذا التعقيد قبل أن يثبت الرقم أنه لازم**.
|
||||
|
||||
---
|
||||
|
||||
## ٧. المال — الدفع والعمولة والتسوية
|
||||
|
||||
- الدفع يمر عبر `payment_server/v2` نفسه؛ الطعام لا يفتح قناة دفع جديدة.
|
||||
- **الحجز ثم الالتقاط**: عند إنشاء الطلب يُحجز المبلغ من المحفظة (`hold`)، ويُلتقط عند `delivered`، ويُفكّ الحجز فوراً عند `rejected` أو `cancelled`. أي مسار إلغاء لا يفكّ الحجز يعني مالاً محتجزاً بلا سبب — وهذا أسرع طريق لفقدان ثقة المستخدم.
|
||||
- الدفع نقداً عند الاستلام: يُحصّله السائق، فيُقيَّد ديناً على محفظته ويُسوّى مع المطعم في `merchant_payouts` — نفس آلية تسوية النقد القائمة للرحلات.
|
||||
- تفكيك كل طلب مسجَّل صراحةً: `items_total + delivery_fee + service_fee − discount`، ونصيب المنصة = `commission_percent` من `items_total` فقط (لا من رسوم التوصيل)، ونصيب السائق من رسوم التوصيل.
|
||||
- رسوم التوصيل تُحسب في **محرك التسعير القائم** [backend/pricing-engine](../../backend/pricing-engine) بمعامل خاص بالطعام، لا بمعادلة جديدة مكرّرة — تكرار منطق التسعير هو المصدر التاريخي لتذبذب الأسعار في هذا المشروع.
|
||||
- التسعير **مثبّت لحظة عرض السلة**: عرض السعر يُوقَّع ويصلح لمدة 10 دقائق. لا يجوز أن يتغير الإجمالي بين شاشة التأكيد وشاشة الدفع.
|
||||
|
||||
---
|
||||
|
||||
## ٨. الزمن الحقيقي — سوكيت الطعام
|
||||
|
||||
- قنوات الاشتراك: `food:order:{id}` (الزبون)، `food:merchant:{id}` (لوحة المطعم)، `food:courier:{id}` (السائق).
|
||||
- المصدر الوحيد للحقيقة هو قاعدة البيانات؛ السوكيت **ناقل إشعار لا مخزن حالة**. عند إعادة الاتصال يسحب التطبيق `order/status.php` ويُصحّح نفسه — هذا ما يمنع «الطلب معلّق للأبد» بعد انقطاع شبكة.
|
||||
- موقع السائق أثناء التوصيل يُبثّ من نفس تدفق المواقع القائم؛ لا مسار تتبّع ثانٍ.
|
||||
- كل حدث سوكيت **مصحوب بإشعار FCM** لحالات المفصل (قُبل، جاهز، خرج للتوصيل، وصل) — لأن التطبيق في الخلفية لا يملك سوكيتاً حياً.
|
||||
- تشغيل السوكيت **داخل الحاوية فقط**: لا يُطلق أبداً على المضيف مباشرة (المضيف لا يصل إلى Redis داخل الشبكة، والنتيجة انقطاع صامت). إعادة التشغيل: `docker compose restart socket_food`.
|
||||
|
||||
---
|
||||
|
||||
## ٩. طبقة التطبيقات
|
||||
|
||||
| التطبيق | العمل المطلوب |
|
||||
|---|---|
|
||||
| `siro_rider` | تبويب «طعام»: تصفح المطاعم، القائمة، السلة، الدفع، تتبّع الطلب، السجل والتقييم. خلف علم `FOOD_ENABLED` يأتي من إعدادات الخادم — لا نسخة تطبيق جديدة لإطفائه. |
|
||||
| `siro_driver` | نوع مهمة جديد «توصيل» داخل تدفّق العروض القائم: بطاقة عرض، استلام من المطعم، تسليم، إثبات تسليم. |
|
||||
| لوحة المطعم | **ويب متجاوب داخل `dashboard/`** لا تطبيق أصلي. صاحب المطعم يعمل على شاشة المحل، والويب يُنشر فوراً بلا دورة متجر. هذا يوفّر أشهر عمل. |
|
||||
| `siro_admin` | إدارة المطاعم والاعتماد، مراقبة الطلبات، التسويات، إعدادات العمولة والرسوم. |
|
||||
|
||||
قاعدة توحيد الواجهة: الطعام يستخدم نفس نظام الألوان والمكوّنات وطبقة الشبكة الحالية في التطبيق. أي مكوّن «مقتبس من تطبيق طعام آخر» بمظهر مختلف يجعل الميزة تبدو ملصقة.
|
||||
|
||||
---
|
||||
|
||||
## ١٠. خطة التنفيذ على مراحل
|
||||
|
||||
كل مرحلة تنتهي بشيء **قابل للتشغيل والاختبار**، لا بكود على الرف.
|
||||
|
||||
**المرحلة صفر — الأساس (بلا منطق منتج)**
|
||||
حاويتان جديدتان في compose، مسار nginx، `Database::get('food')`، `schema_food.sql`، بوابة `connect_app.php` ترد على `ping`. معيار الإنجاز: `docker compose up -d` يرفع ثماني خدمات، ونداء ping يرد 200 من الحوض الجديد (يُتحقق من الحوض عبر `fpm-status-food`).
|
||||
|
||||
**المرحلة الأولى — الكتالوج (للقراءة فقط)**
|
||||
المطاعم والأقسام والأصناف، تصفح وبحث، لوحة الإدارة لإنشاء مطعم. معيار الإنجاز: مطعم حقيقي واحد بقائمة كاملة يظهر في التطبيق.
|
||||
|
||||
**المرحلة الثانية — الطلب بلا مال**
|
||||
السلة، التسعير من الخادم، إنشاء الطلب، آلة الحالة، لوحة المطعم، السوكيت والإشعارات. الدفع نقداً فقط. معيار الإنجاز: طلب حقيقي يمر `pending → delivered` وسجل الحالات مكتمل.
|
||||
|
||||
**المرحلة الثالثة — التوصيل**
|
||||
`geo:couriers:available`، حلقة العروض والقفل، مهام السائق، تتبّع الموقع. معيار الإنجاز: ٢٠ طلباً تجريبياً بلا طلب واحد بسائقين ولا طلب يتيم.
|
||||
|
||||
**المرحلة الرابعة — المال**
|
||||
حجز/التقاط المحفظة، العمولة، تسويات المطاعم، تسوية نقد السائق، تقارير الإدارة. معيار الإنجاز: مطابقة مالية لمئة طلب تجريبي بفرق صفر.
|
||||
|
||||
**المرحلة الخامسة — التقسية والإطلاق**
|
||||
مراجعة أمنية (تركيزها IDOR والتفويض)، اختبار ضغط، سجلات ومؤشرات، إطلاق تدريجي على منطقة واحدة ومطاعم محدودة.
|
||||
|
||||
---
|
||||
|
||||
## ١١. الاختبار ومعايير القبول
|
||||
|
||||
- **اختبار ضغط** بنفس أدوات [stress_test](../../stress_test) وبنفس قاعدة القراءة الصادقة المعتمدة في `docker/README.md`: الرقم المُلتزَم به هو الرقم الذي عبر الاختبار فعلاً، لا أكثر. الهدف الابتدائي: ١٠٠٠ طلب/ساعة بـ p95 < 500ms، مع التحقق أن **زمن استجابة الرحلات لم يتأثر** أثناء الحمل — هذا هو اختبار العزل الحقيقي.
|
||||
- **حالات حافة إلزامية**: ضغط مزدوج على التأكيد، انقطاع الشبكة بين الحجز والإنشاء، رفض المطعم بعد الدفع، صنف نفد أثناء التحضير، إلغاء الزبون بعد استلام السائق، سائق تعطّل تطبيقه وهو حامل الطلب.
|
||||
- **اختبارات سلبية للتفويض** لكل نقطة نهاية — تُدمج مع الكود لا بعده.
|
||||
- **مراقبة**: لوحة تحمل الأربعة أرقام التي تصف صحة الخدمة فعلاً — نسبة قبول المطاعم، زمن التحضير، زمن إيجاد سائق، نسبة الإلغاء ومصدره.
|
||||
|
||||
---
|
||||
|
||||
## ١٢. النشر والتراجع
|
||||
|
||||
- `FOOD_ENABLED=false` يخفي الميزة من التطبيق كلياً بلا نشر جديد — هذا هو مفتاح التراجع الأول والأسرع.
|
||||
- التراجع الكامل: `docker compose stop php_food socket_food`. الرحلات لا تتأثر إطلاقاً — وهذا بالضبط ما اشتريناه بالعزل.
|
||||
- لا تُشغَّل ترحيلات مخطط الطعام على قواعد الرحلات؛ `siro_food` منفصلة تماماً ونسخها الاحتياطي منفصل.
|
||||
- **تنبيه بيئي**: توجد مهمة مجدولة تلتزم وتدفع كل تعديل تلقائياً على `main`. أي عمل على وحدة الطعام يجب أن يجري على فرع مستقل، وإلا وصل كود نصف مكتمل إلى `main` برسالة التزام آلية لا تصف شيئاً.
|
||||
- تسجيل الأخطاء: تأكد أن `error_log` و `access_log` فعّالان لحاويات الطعام منذ اليوم الأول — الخدمة غير المسجَّلة تبدو سليمة حتى تكذب عليك في أول عطل.
|
||||
|
||||
---
|
||||
|
||||
## ١٣. المخاطر المفتوحة والقرارات التي تحتاج حسماً
|
||||
|
||||
1. **تنازع الأسطول** بين الرحلات والتوصيل في الذروة — مقاسة بمؤشر، والحل يؤجَّل حتى يثبت الرقم لزومه.
|
||||
2. **مصدر السعر النهائي**: مثبَّت في محرك التسعير القائم؛ أي استثناء يُطلب لاحقاً يجب رفضه.
|
||||
3. **هوية صاحب المطعم**: جدول `merchant_users` مستقل بدور `merchant`. القرار البديل (توسيع جدول المستخدمين الرئيسي) مرفوض لأنه يخلط نطاقات التفويض في قاعدة الرحلات.
|
||||
4. **حدّ الاعتماد**: هل تُنشر المطاعم بعد اعتماد إداري يدوي؟ الافتراض في هذه الخطة: **نعم**، اعتماد يدوي إلزامي في الإطلاق الأول — والقرار قابل للمراجعة من صاحب المنتج.
|
||||
5. **بند مفتوح خارج نطاق هذه الوحدة لكنه يمسّها**: مسائل تعرّض بيانات شخصية موثّقة في [08_security](../08_security) يجب ألا تتكرر في أي نقطة نهاية للطعام — خصوصاً في نقاط تُعيد عناوين وأرقاماً.
|
||||
|
||||
---
|
||||
|
||||
## ١٤. قائمة تحقق قبل أول دمج
|
||||
|
||||
- [ ] `docker compose config` يمرّ، والحاويتان تعملان بحدود ذاكرة معلنة
|
||||
- [ ] بلوك nginx للطعام **قبل** البلوك العام، ومُتحقق منه بنداء فعلي
|
||||
- [ ] nginx المضيف يخدم 4040 بـ TLS → 14040
|
||||
- [ ] `food_server/composer.lock` ملتزم به في git
|
||||
- [ ] لا استدعاء لـ `Database::get('main')` داخل `backend/food/`
|
||||
- [ ] كل نقطة تأخذ `order_id` تملك اختبار تفويض سلبياً
|
||||
- [ ] كل المبالغ أعداد صحيحة، و`client_order_uuid` فريد
|
||||
- [ ] انتقالات الحالة تمر جميعها عبر `food_transition_status()` فقط
|
||||
- [ ] العمل على فرع مستقل لا على `main`
|
||||
Reference in New Issue
Block a user