إصلاح سوكيت الراكب وتوحيد مسارات الإشعارات

سوكيت الراكب (سبب عدم ظهور معلومات السائق عند القبول):
- تطبيق الراكب كان يرسل id فقط بلا jwt، و passenger_socket.php يرفض أي
  اتصال بلا jwt ⇒ الراكب لا ينضم لغرفته أبداً ولا يستلم ride_status_change
  ولا driver_location_update. تظهر حالة القبول عبر الـ polling فقط بينما
  driver_info يصل بالسوكيت وحده. (سوكيت السائق يعتبر الـ jwt اختيارياً،
  ومن هنا جاء التباين بين التطبيقين.)
- cancelled_by_driver كان يسقط من switch حالات الراكب فيبقى معلّقاً بعد
  إلغاء السائق.
- حماية socket (late) من القراءة قبل التهيئة عند الانسحاب بلا jwt.

الإشعارات والرسائل (سبب "مرات توصل ومرات لا"):
- جدول tokens يخزّن توكن الراكب مشفّراً، و getRideWaiting.php كان يرجعه
  بلا فك تشفير ⇒ من يقبل من قائمة السوق يحمل blob مشفّراً يستخدمه كـ FCM
  target فيرفضه FCM بـ 400: لا إشعار قبول ولا رسائل. ومن يقبل من الـ
  dispatch/FCM يحمل نصاً صريحاً فتعمل. الفرق كان في طريقة القبول.
- acceptRide.php يحلّ التوكن من القاعدة دائماً ولا يثق بالعميل (أصحّ أمنياً).
- market_new_ride كان لا يحمل passengerId ولا الإحداثيات فتصل "null"؛
  أُضيفت بلا أي PII لأن الحمولة تُبَثّ لكل سائق قريب لا للفائز فقط.
- send_fcm.php: مهلة على OAuth (كان يعلّق حتى مهلة PHP فتُسقط الرسالة
  بصمت)، توحيد ding→default لأندرويد، وحقن title/body/tone في data
  مطابقةً لـ FcmService.
- تطبيق السائق يقرأ title/body من data أولاً مثل الراكب، ولا يعرض فقاعة
  فارغة للرسائل الصامتة.

السوكيت والإعدادات:
- forwardLocationToPassengerSocket كان يقرأ lat/lng والحمولة فيها
  latitude/longitude ⇒ المسافة تخرج ضخمة والـ throttle معطّل تماماً
  فيُعاد التوجيه مع كل نبضة GPS.
- notifyPassengerOnRideServer كان يرجع null بصمت مطلق عند حجب العنوان.
- العنوان الافتراضي لسيرفر الموقع كان nginx/loction_server/driver_socket.php
  وهو ديمون Workerman لا يُخدَم عبر nginx ⇒ صار socket_driver:2021.
  (LOCATION_API_URL بقي على nginx لأن api_get_nearby.php سكربت عادي.)
- ride_server/passenger_socket.php (النسخة التي يشغّلها Docker) كانت ناقصة
  كل كود مواصلاتي الموجود في passenger_server/ ⇒ نُقل مع REDIS_HOST.
- .env.example: ALLOWED_SOCKET_URLS يغطّي أسماء حاويات Docker، وإضافة
  PASSENGER_SOCKET_INTERNAL_URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-26 15:47:23 +03:00
co-authored by Claude Opus 5
parent e269cb9b70
commit 143146c1b4
11 changed files with 258 additions and 39 deletions
@@ -6,8 +6,10 @@ import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../main.dart'; // contains global 'box'
import '../../../env/env.dart';
import '../../../main.dart'; // contains global 'box' + 'storage'
import '../../../print.dart';
import '../../functions/encrypt_decrypt.dart'; // r()
import 'ride_lifecycle_controller.dart';
import 'nearby_drivers_controller.dart';
import 'map_engine_controller.dart';
@@ -29,10 +31,38 @@ class MapSocketController extends GetxController {
DateTime? get lastDriverLocationTime => _lastSocketLocationTime;
int get socketLocationUpdatesCount => _socketLocationUpdatesCount;
void initConnectionWithSocket() {
/// يقرأ الـ JWT بنفس آلية CRUD._getJwt (SecureStorage أولاً ثم GetStorage).
/// passenger_socket.php يرفض أي اتصال بلا jwt، فبدونه لا تصل أي أحداث للراكب.
Future<String> _getJwtForSocket() async {
try {
final String? encryptedJwt = await storage.read(key: BoxName.jwt);
if (encryptedJwt != null && encryptedJwt.isNotEmpty) {
return r(encryptedJwt).toString().split(Env.addd)[0];
}
} catch (e) {
Log.print('Error reading JWT from SecureStorage for socket: $e');
}
final String? fallback = box.read(BoxName.jwt);
if (fallback != null && fallback.toString().isNotEmpty) {
try {
return r(fallback).toString().split(Env.addd)[0];
} catch (e) {
Log.print('Error decrypting fallback JWT for socket: $e');
}
}
return '';
}
Future<void> initConnectionWithSocket() async {
if (isSocketConnected) return;
String passengerId = box.read(BoxName.passengerID).toString();
final String jwt = await _getJwtForSocket();
if (jwt.isEmpty) {
Log.print(
"⚠️ No JWT available — passenger socket would be rejected by the server. Aborting connect.");
return;
}
Log.print("🔌 Initializing Socket for Passenger: $passengerId");
socket = io_client.io(
@@ -40,7 +70,7 @@ class MapSocketController extends GetxController {
io_client.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.setQuery({'id': passengerId})
.setQuery({'id': passengerId, 'jwt': jwt})
.setReconnectionAttempts(20)
.setReconnectionDelay(2000)
.setReconnectionDelayMax(10000)
@@ -171,13 +201,15 @@ class MapSocketController extends GetxController {
// ── مواصلاتي: اشتراك/إلغاء اشتراك ببثّ موقع خط ─────────────
// يُستدعى عند فتح/إغلاق شاشة تتبع الباص الحي. يضمن السوكيت متصلاً أولاً.
void subscribeToTransitRoute(int routeId) {
Future<void> subscribeToTransitRoute(int routeId) async {
_subscribedTransitRouteId = routeId;
if (!isSocketConnected) {
initConnectionWithSocket();
await initConnectionWithSocket();
// سيُعاد الاشتراك تلقائياً من onConnect إن أضفنا ذلك، لكن نحاول فوراً أيضاً
}
if (socket.connected) {
// socket هو late — لو انسحب initConnectionWithSocket (بلا JWT مثلاً) يبقى
// غير مُهيّأ، فقراءته مباشرة ترمي LateInitializationError.
if (_isSocketInitialized && socket.connected) {
socket.emit('subscribe_transit_route', {'route_id': routeId});
Log.print('🚌 Subscribed to transit route #$routeId');
}
@@ -185,7 +217,7 @@ class MapSocketController extends GetxController {
void unsubscribeFromTransitRoute(int routeId) {
if (_subscribedTransitRouteId == routeId) _subscribedTransitRouteId = null;
if (isSocketConnected && socket.connected) {
if (_isSocketInitialized && isSocketConnected && socket.connected) {
socket.emit('unsubscribe_transit_route', {'route_id': routeId});
Log.print('🚌 Unsubscribed from transit route #$routeId');
}
@@ -243,7 +275,12 @@ class MapSocketController extends GetxController {
_onRideFinishedWithSocket(data);
break;
// cancel_ride_by_driver.php يبعث 'cancelled_by_driver' حرفياً،
// و cancel_ride_by_passenger.php يبعث 'cancelled_by_passenger'.
// كانت الحالتان تسقطان من الـ switch فيبقى الراكب معلّقاً على الشاشة.
case 'cancelled':
case 'cancelled_by_driver':
case 'canceled_by_driver':
rideLifecycle.processRideCancelledByDriver(data, source: "Socket");
break;