Files
tripz-llc/docs/17-backend-backlog.md
T
Hamza-AyedandClaude Opus 4.8 a83dd7e5e5 feat: المجموعة G — Redis خط أول والقاعدة خط احتياط
الأساس: common/cache — كاش-جانبي فوق Redis. مبدأ صارم: فشل Redis لا يُسقط
الطلب (يُسجَّل ويُرجَع للقاعدة) — الكاش تحسين أداء لا مصدر حقيقة.

- G1: لغة المستخدم في Redis (كانت استعلاماً قبل كل إشعار)؛ تُكتب عند
  PATCH /users/me فيصير الإصابة دائمة
- G2/G3: توكنات FCM في Redis — يُبطَل عند register وعند اكتشاف توكن ميت.
  البث الجماعي صار بلا استعلامات قاعدة
- G4: كاش tenant resolve/config + إبطال عند الإنشاء
- G5: كاش التعرفة و ride-types + إبطال صريح عند أي إنشاء
- G6: التقييم يتراكم في Redis، والقاعدة تُكتب مرة واحدة يومياً لكل طرف
  (حجز الكتابة ذرّي بـLua). التأخير مقصود ليبعد الاحتكاك بين الطرفين.
  أُضيف ratings.target_user_id و users.rating — تقييم السائق للراكب كان
  يُخزَّن بلا هدف فلا يُجمَّع أبداً
- كاش الخرائط (route/reverse/geocode) بإحداثيات مقرَّبة 4 خانات (~11م):
  نداء انطلق كان يهيمن على p50 (2.9s). لا يُخزَّن الرجوع لخط مستقيم ولا
  استجابة فاشلة

تصحيح: ادّعيت سابقاً أن tenants.resolve يعمل على كل request — خطأ.
tenant_id يأتي من الـJWT؛ resolve يُستدعى عند الدخول و3 كنترولرات فقط.
الحمل الأثقل فعلياً هو رفع موقع السائق (المجموعة H).

هجرة: RatingsBothParties.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:39:34 +03:00

22 KiB
Raw Blame History

17 — Backlog الباك إند (مراجعة المالك + مقارنة سيرو)

مصدره: مراجعة المالك (2026-07-17) + قراءة مباشرة لجداول سيرو (schema_primary.sql, schema_ride.sql). الترتيب حسب الأثر. نمشي مجموعة مجموعة.


المجموعة A — الزمن الحقيقي والإشعارات (الأعلى أولوية) — ✅ منفَّذة

شكوى المالك الأساسية: «تسأل القاعدة كثيراً، أقرب للـ polling»، والإشعارات ناقصة.

# البند التفصيل الحالة
A1 FCM على كل حالات الرحلة حالياً push عند assigned فقط. المطلوب: FCM + WebSocket على كل انتقال. ضروري للخلفية (background). ✅ notifyParties تُنادى من كل انتقال + priority: high + حذف التوكنات الميتة
A2 ترجمة الإشعارات الإشعارات تُرسل إنجليزي والهاتف عربي → نصوص الإشعارات في ملفات ترجمة وتُرسل حسب لغة المستخدم. ✅ common/i18n (ar/en) + عمود users.language
A3 تقليل استعلامات القاعدة حالة الرحلة الجارية + المواقع في Redis؛ القاعدة للحقيقة الدائمة فقط. (الآن كل انتقال يقرأ/يكتب عدة مرات + يقرأ السائق ثانيةً). ✅ TripStateService (hash لكل رحلة نشطة، TTL 6س) — الانتقال صار UPDATE شرطي + قراءة واحدة
A4 Race condition عند القبول سائقان يقبلان بنفس اللحظة → قبول ذرّي (Redis SETNX / UPDATE شرطي WHERE status='searching'). أول قبول يفوز، والثاني يُرفض بوضوح. ✅ CAS بـLua في Redis + UPDATE … WHERE status='searching' كحَكَم نهائي
A5 إلغاء العرض عند القبول فور القبول: بث WebSocket + FCM لبقية السائقين المعروض عليهم → «الرحلة لم تعد متاحة» فتختفي من شاشتهم/الـ overlay. ✅ مجموعة عروض في Redis + trip:offer_taken + FCM
A6 الرحلات المتاحة (available rides) قائمة طلبات متاحة يسحبها السائق (بديل/مكمّل للعرض المباشر). ✅ GET /trips/available
A7 overlay أندرويد معلومات الرحلة للقبول/الرفض فوق التطبيقات — يحتاج FCM data-message + payload كامل. ✅ dataOnly + payload كامل (نقاط، مسافة، أجرة، فئة، دفع)

مؤجَّل من A: سجل الأحداث (trip_events) ما زال يُكتب متزامناً داخل الطلب — نقله إلى BullMQ يبقى تحسيناً مفتوحاً (worker.ts لا يزال هيكلاً فارغاً).


المجموعة B — اكتمال نموذج الرحلة

مقارنة بجدول ride في سيرو + قواعد التشغيل.

# البند التفصيل
B1 حالة started وفصل الوصول عن البدء السائق وصل ≠ الرحلة بدأت. الراكب قد لا يرد/يلغي. نحتاج تمييزاً صريحاً.
B2 عدّاد انتظار 5 دقائق (قانون) يبدأ عند وصول السائق (بعد تأكيد المسافة من فلاتر). عند انتهائه: خيار للراكب — يبقى السائق منتظراً، أو تعويض فترة الانتظار.
B3 احتساب مشوار الوصول للراكب المسافة/الزمن من موقع السائق حتى الراكب تُحتسب بالتعرفة (دقائق + مسافة).
B4 نقاط توقف (stops) نقطتا توقف ضمن الرحلة (كما في سيرو/شير).
B5 حجز مسبق/جدولة جداول الرحلة (date/time/endtime في سيرو).
B6 فصل السعر price_for_driver مقابل price_for_passenger (العمولة) — ناقص عندنا.
B7 طوابع زمنية دقيقة DriverIsGoingToPassenger · rideTimeStart · rideTimeFinish (سيرو) — عندنا assigned_at/completed_at فقط.
B8 مطابقة الوجهة is_destination_match + خصم الراكب.
B9 تعديل التعرفة من لوحة الأدمن جداول تعرفة قابلة للتحرير (موجودة كـ jsonb — نحتاج واجهة/نقاط CRUD).
B10 كتالوج أنواع الرحلات العالمي ride_types فكرته سليمة (الأدمن/المستأجر يضيف أنواعه). المطلوب: كتالوج جاهز بما هو شائع عالمياً كخيارات جاهزة للاختيار (عندنا 6 فقط الآن).
B11 تعرفة بالوزن بُعد تسعير إضافي بالوزن (للشحن/التوصيل) بجانب المسافة والزمن. الاستعلام عن التعرفة من Redis خط أول (G5).

المجموعة C — بيانات السائق والمركبة (من سيرو)

# البند التفصيل
C1 CarRegistration كامل vin · car_plate · make · model · year · expiration_date · color + color_hex (لتلوين السيارة في فلاتر) · owner · fuel · isDefault · vehicle_category_id · fuel_type_id · status.
C2 حقول السائق gender · national_number (فريد) · name_arabic · first/last_name · birthdate · license_type/categories/issue/expiry · address · accountBank/bankCode · employmentType · maritalStatus · rejected_reason (سبب رفض خدمة العملاء).
C3 ai_data + user_input تخزين مخرجات Gemini الخام و ما أدخله السائق — للمقارنة والتدقيق (نمط سيرو).
C4 صور السيارة ×2 صورتان للمركبة (لا واحدة).
C5 فيديو/liveness للوجه تأكيد حيّ للوجه (غير السيلفي الثابت).

المجموعة D — الأمان والمصادقة

# البند التفصيل
D1 تطبيع أرقام الهاتف (JO/EG/SY) خصوصاً مصر: الناس تكتب 01… بدل 1… وتظن المفتاح 2 لا 20. نحتاج normalize احترافي لكل دولة.
D2 بصمة الجهاز (device fingerprint) تُرسل من فلاتر مع كل request وتُربط بالجلسة (نمط سيرو: التوكن المسروق لا يعمل على جهاز آخر).
D3 HMAC للعمليات الحساسة خصوصاً المدفوعات — توقيع الطلب.

المجموعة E — كشف الاحتيال (من driver_ride_scam)

# البند التفصيل
E1 تسجيل زر الاتصال isDriverCallPassenger لكل رحلة (سيرو).
E2 ربط الاتصال بالإلغاء اتصال ثم إلغاء = مؤشر اتفاق خارج التطبيق. 3 إلغاءات/يوم → إنذار؛ التكرار → إجراء.

المجموعة F — الدردشة

| F1 | إشعار الرسالة يُرسل فقط إذا لم تكن صفحة الدردشة مفتوحة (المعالجة الأساسية في فلاتر؛ الباك إند يرسل دائماً ويترك القرار للعميل أو عبر presence). |


المجموعة G — Redis خط أول والقاعدة خط احتياط — ✅ منفَّذة

مراجعة المالك (2026-07-17، الجولة الثانية): «كل ما بدي أبعث notification أستعلم من القاعدة — هذا ثقيل. الريدز خط أول، القاعدة نقطة الاحتياط». القاعدة: البيانات الساخنة والمتكررة تُقرأ من Redis؛ القاعدة تُقرأ مرة واحدة عند البرود (cache miss) ثم تُكتب في Redis. الأساس: common/cache/cache.service.ts — كاش-جانبي مع مبدأ فشل Redis لا يُسقط الطلب (يُسجَّل ويُرجَع للقاعدة).

# البند التفصيل الحالة
G1 لغة المستخدم في Redis فلاتر يفحص لغة الجهاز عند الفتح ويرفعها عبر PATCH /users/me → تُكتب في Redis مباشرة → الإشعار يقرأ من هناك. ✅ user:lang:{tenant}:{user} (TTL يوم) + كتابة عند التحديث
G2 توكنات الأجهزة (FCM) في Redis كان استعلام device_tokens قبل كل إرسال. ✅ user:fcm:{tenant}:{user}؛ يُبطَل عند register وعند اكتشاف توكن ميت
G3 البث الجماعي بلا استعلام لكل مستخدم — ✅ التوكنات واللغة من الكاش، فالبث الجماعي صار بلا استعلامات قاعدة أصلاً
G4 tenant في Redis كاش resolve (slug↔UUID) + config مع إبطال عند الإنشاء/التعديل. ✅ tenant:{slugOrId} (TTL ساعة)
G5 التعرفة و ride-types في Redis كاش مع إبطال صريح عند إنشاء تعرفة/نوع جديد. ✅ tariff:{t}:{city}:{class} و ridetypes:{t} (TTL 15د)
G6 التقييم بتراكم يومي مجموع/عدد في Redis، والقاعدة تُكتب مرة واحدة يومياً لكل طرف (حجز الكتابة ذرّي بـLua). التأخير مقصود ليبعد الاحتكاك. للطرفين معاً. ✅ + أُضيف ratings.target_user_id وusers.rating — تقييم السائق للراكب كان يُخزَّن بلا هدف فلا يُجمَّع أبداً
— كاش الخرائط route/reverse/geocode في Redis بإحداثيات مقرَّبة 4 خانات (~11م). ✅ TTL يوم. لا يُخزَّن الرجوع لخط مستقيم ولا استجابة فاشلة
G7 سعة Redis رصد مساحة أكبر أو Redis منفصل عند الحاجة (خصوصاً dispatch + المواقع). حالياً DB 3 مشترك — راجع docs/14. ⏳ قرار تشغيلي — يُراجَع بعد المجموعة H

تصحيح لادّعاء سابق في هذا المستند: كُتب أن TenantsService.resolve يعمل على كل request وأنه «أعلى نسبة قراءات في النظام». هذا خطأ — الـmiddleware يضع نص الهيدر في السياق فقط، وtenant_id يأتي جاهزاً من داخل الـJWT. resolve يُستدعى عند الدخول و3 كنترولرات فقط. كاشه مفيد لكن أثره أصغر بكثير مما ادّعيت. الحمل الحقيقي الأثقل هو رفع موقع السائق (استعلام + كتابة قاعدة كل نبضة) — وهو المجموعة H.


المجموعة H — المواقع والتتبع (من loction_server في سيرو)

ملاحظة المالك: «لحد الآن مش شايف السائق أو الراكب يرفع موقعه، ولا جداول location». عندنا الآن: driver:location عبر WebSocket → Redis GEO + حفظ صف السائق في Postgres على كل نبضة (drivers.updateLocation يعمل repo.save) — هذا أثقل حتى من سيرو.

ما يفعله سيرو (مقروء من الكود):

  • driver_socket.php — سوكيت مخصص للمواقع، لا يلمس القاعدة إطلاقاً؛ Redis فقط عبر pipeline كل 500ms (REDIS_BATCH_INTERVAL).
  • عتبات لتقليل الكتابة: MIN_MOVE_METERS=10 (GEOADD فقط عند تحرّك >10م)، HMSET_SPEED_DELTA=1.0، HMSET_HEADING_DELTA=5، FORWARD_MIN_METERS=15 / FORWARD_MAX_SECONDS=3 للبث للراكب.
  • فهرسان منفصلان: geo:drivers:available وgeo:drivers:busy (عندنا فهرس واحد لكل فئة خدمة، بلا تمييز مشغول/متاح).
  • driver:profile:{id} hash في Redis (heading/speed/status) — المطابقة تقرأ منه بلا قاعدة.
  • عروض الرحلة: setex للعرض + sadd لمجموعة المعروض عليهم + expire — نفس نمطنا في A5 ✅.
# البند التفصيل
H1 إيقاف كتابة الموقع على القاعدة لكل نبضة drivers.updateLocation يكتب Postgres كل ثانية/ثلاث — يُنقل إلى Redis فقط.
H2 عتبات + batching تبنّي عتبات سيرو (10م/1.0 سرعة/5 اتجاه) + pipeline كل 500ms.
H3 جدول car_locations مكافئ صف واحد لكل سائق (آخر موقع) — يُكتب دورياً من worker لا من الطلب. عند سيرو: ON DUPLICATE KEY UPDATE + عمود point SRID 4326 عبر trigger (عندنا PostGIS متاح).
H4 جدول car_tracks (المسار) سجل نقاط تاريخي للتتبع/النزاعات — إدراج مجمّع (batch insert) من worker.
H5 فهرس available/busy تمييز السائق المشغول عن المتاح في فهرس GEO.
H6 driver_behavior max_speed · avg_speed · hard_brakes · total_distance · behavior_score لكل رحلة.
H7 driver_daily_work / driver_daily_summary ساعات عمل السائق (total_seconds باليوم + last_point_at + last_status).
H8 ربط المواقع بالمطابقة والـdispatch المطابقة تقرأ driver:profile من Redis؛ لوحة dispatch ترى الأسطول حياً.
— Geofence مؤجَّل بقرار المالك («خليها لوقتها») — موجود في سيرو (get_location_area_links, LocationIntelligenceEngine).

المجموعة I — المدفوعات والمحفظة (من payment_server في سيرو) 🔴 أمني

طلب المالك: تدقيق أمني على المدفوعات، خصوصاً الـpayout: OTP عبر نبيه + بصمة (وجه/إصبع) في فلاتر + HMAC.

بنية سيرو (مقروءة من WalletDB.sql + sms_webhook/):

  • جدول لكل طريقة دفع (كما قال المالك): cliq_invoices · ecash_transactions (+_driver) · invoices_shamcash (+_passenger) · mtn_invoices · invoices_sms (+_passenger) · kazan.
  • محافظ منفصلة: driverWallet · passengerWallet · siroWallet (محفظة الشركة) — نمط دفتر قيود append-only، الرصيد = SUM(amount).
  • نمط ذكي جداً: raw_sms_log + process_with_gemini.php — رسالة SMS من مزوّد الدفع تُرفع خاماً، Gemini يقرأها ويستخرج المبلغ/المرجع، ثم finalize_wallet_payment. يحلّ غياب الـAPI الرسمي في سوريا.
  • payment_tokens (+_passenger) للتتبع/عدم التكرار · admin_audit_log · paymentsLogSyria(Driver).

🔴 ثغرات وجدتها في كود سيرو — لا تُنقل كما هي:

الثغرة التفصيل
IDOR في request_payout.php driverId وphone يُؤخذان من الطلب لا من الـJWT → سائق يطلب سحب رصيد سائق آخر إلى هاتفه. يجب اشتقاق الهوية من التوكن حصراً.
لا خصم عند الطلب الطلب يفحص الرصيد ثم يُدرج سجلاً فقط بلا حجز → طلبات متعددة متزامنة تمرّ كلها (double-spend).
عمولة متناقضة request_payout يفحص balance >= amount + 3500 (العمولة فوق المبلغ)، وfinalize_payout يحسب netAmount = amount - 3500 ويخصم الصافي فقط (العمولة داخله) → تسريب مال. والرقم 3500 مكرر حرفياً في الملفين.
finalizePayout بلا معاملة 5 عمليات كتابة بلا beginTransaction/rollBack → فشل في المنتصف = خصم بلا تسجيل عمولة (حالة نصفية).
UPDATE payments SET isGiven=TRUE WHERE driverID=… AND isGiven=FALSE يعلّم كل الدفعات المعلّقة كمدفوعة بغضّ النظر عن مبلغ السحب.
driverWallet.amount = varchar(10) المال مخزَّن كنص (وحساب SUM على varchar).
لا OTP على الـpayout phone_verification موجود لكنه مستخدم في التسجيل/الدخول فقط — لا شيء يحمي السحب. (ملاحظة المالك صحيحة.)
جدولان متداخلان payout_requests وdriver_withdrawal_requests لنفس المفهوم.

🔴 ثغرة في كودنا نحن (wallet.service.ts): credit/debit تعمل read-modify-write على عمود balance بلا قفل ولا معاملة → سباق حقيقي: خصمان متزامنان يقرآن نفس الرصيد ويكتبان فوق بعض = مال مفقود/مخلوق. سيرو هنا أفضل منّا (دفتر قيود + FOR UPDATE).

# البند التفصيل
I1 ✅ إصلاح سباق المحفظة منفَّذ ومُثبَت على السيرفر (commit 9d6b752): UPDATE … SET balance = balance ± :delta WHERE tenant_id … AND balance >= :amount RETURNING * — عبارة واحدة ذرّية، والقيد+الرصيد في معاملة واحدة. أُضيف wallet_txns.balance_after وقيد CHECK (balance >= 0) كشبكة أمان. إنشاء المحفظة عبر ON CONFLICT DO NOTHING.
نتيجة wallet-race-test.mjs 100 5 على Postgres حقيقي: 100 خصم متزامن → نجح 50 بالضبط، رُفض 50، الرصيد 250→0، المخصوم = 250 (لا مال ضائع ولا مخلوق)، الزمن 1492ms. ✅
I2 محفظة لكل طرف + محفظة المنصة راكب · سائق · المستأجر/المنصة (مكافئ siroWallet) — أساس العمولة (B6).
I3 جدول لكل طريقة دفع شام كاش · كليك · إي كاش · بيموب · MTN · فوري … لكل واحدة جدولها + محوّل (adapter) موحّد. حالياً عندنا tripz_pay_payments عام.
I4 OTP على الـpayout عبر نبيه إرسال كود + تحقّق قبل تنفيذ السحب (لا يوجد في سيرو).
I5 بصمة (وجه/إصبع) في فلاتر تأكيد حيوي قبل السحب — يُربط بالطلب (device fingerprint من D2).
I6 HMAC على العمليات المالية توقيع الطلب (D3) — إلزامي على topup/payout.
I7 سجل تدقيق مالي مكافئ admin_audit_log — من فعل ماذا ومتى على كل عملية.
I8 webhook SMS + Gemini تبنّي نمط سيرو للأسواق بلا API رسمي (سوريا): raw_sms_log + استخراج بالـAI + تسوية.

قرار: هل يخاطب فلاتر خرائط انطلق مباشرة؟

القرار المتّخذ (2026-07-17): تقسيم حسب نوع النداء — لا «كله مباشر» ولا «كله عبر السيرفر».

النوع المسار السبب
البلاطات (tiles) فلاتر → انطلق مباشرة حجم كبير ومتكرر، لا يحمل أسراراً، ويُخزَّن مؤقتاً على الجهاز. تمريره عبر سيرفرنا = تضخيم عرض النطاق بلا فائدة.
geocode / reverse / route / places فلاتر → سيرفرنا → انطلق يحمي مفتاح API (مفتاح داخل التطبيق = مسروق)، يتيح كاش Redis، حصص لكل مستأجر، وتبديل المزوّد بلا تحديث التطبيق.

السرعة ليست مقايضة هنا — بالعكس: اختبار التحميل عندنا أظهر أن p50 قفز إلى 2.9 ثانية والسبب المهيمن هو نداء HTTP الخارجي إلى انطلق. كاش النتائج في Redis (نفس العنوان يُطلب آلاف المرات) يجعل المسار عبر سيرفرنا أسرع من النداء المباشر، لا أبطأ. الإضافة الصافية للسيرفر ~10–30ms مقابل توفير ~2.9s على كل إصابة كاش. → البند المطلوب: كاش Redis لنتائج geocode/route/places (يُضاف للمجموعة G).


مؤجَّل عمداً (قرار المالك)

المفاوض الذكي · تدرّج السائق · خصم العمولة — آخر شيء (جديدة حتى على سيرو). Geofence — مؤجَّل («لوقتها»)، موجود في سيرو للاستئناس.


ترتيب التنفيذ المقترح

  1. A (الزمن الحقيقي + FCM + Redis + race) ✅ منفَّذة ومُثبَتة على السيرفر.
  2. I1 (سباق المحفظة) ✅ منفَّذة ومُثبَتة بتزامن حقيقي.
  3. G (Redis خط أول: لغة/توكنات/tenant/تعرفة/تقييم/كاش الخرائط) ✅ منفَّذة.
  4. H (المواقع: إيقاف الكتابة لكل نبضة + batching + tracks) — التالي، وهو الحمل الأثقل فعلياً.
  5. B (نموذج الرحلة: started/انتظار/فصل السعر/الطوابع/stops).
  6. I الباقي (المدفوعات: جداول لكل طريقة + OTP/بصمة/HMAC للسحب).
  7. C (بيانات المركبة والسائق + ai_data).
  8. D (تطبيع الهاتف + بصمة الجهاز + HMAC).
  9. E (الاحتيال) ثم F.