Files
Siro/backend/ride/rides/getRideOrderID.php
T
Hamza-AyedandClaude Opus 5 ef1240b130 تصحيح مفتاح Redis لموقع السائق: driver:location ← driver:public
الموضعان كانا يقرآن مفتاحاً لا يُكتب في أي مكان في المشروع:
  $redisLocation->hGetAll("driver:location:$driverId")

الكاتب الفعلي هو معالج الدفعات في loction_server/driver_socket.php، وهو
يكتب hmset على driver:profile:{id} و driver:public:{id} معاً بنفس الحقول
(lat/lng/heading/speed/status/updated_at). و driver:public له TTL 86400
بينما driver:profile له 900 فقط، فالعام هو الأنسب للقراءة.

الأثر: كانت حمولة القبول تصل الراكب بلا إحداثيات أولية للسائق، فلا يظهر
الماركر إلا بعد أول تحديث موقع من السوكيت أو الـ polling.

أحد الموضعين ملف getRideOrderID.php الذي أضفته في f66db7db — نسخت النمط
من acceptRide.php فنقلت الخطأ معه.

مثبَّت على الإنتاج: redis-cli --scan --pattern 'driver:*' أرجع
driver:public:<id> فقط، ولا شيء باسم driver:location.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:14:26 +03:00

205 lines
8.9 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// ═══════════════════════════════════════════════════════════════
// ride/rides/getRideOrderID.php
// PURPOSE : بيانات السائق/السيارة للرحلة الحالية — المسار الاحتياطي للراكب
// حين لا يوصل السوكيت driver_info (ride_lifecycle_controller.dart
// → getUpdatedRideForDriverApply).
//
// لماذا هذا الملف: التطبيق يطلب getRideOrderID.php منذ البداية، والموجود
// كان getRideOrderIDNew.php فقط ⇒ 404 صامت. ونتيجته أن الـ polling يسبق
// الـ FCM فيضبط _isAcceptanceProcessed=true بلا بيانات، فيُرفض لاحقاً
// payload الـ FCM الذي يحمل driver_info كاملاً ("Already processed")،
// فتظهر الرحلة مقبولة بلا معلومات سائق، ويبقى driverToken فارغاً فلا
// تُرسَل رسائل الراكب للسائق.
//
// getRideOrderIDNew.php لا يصلح بديلاً مباشراً: هو داخلي (get_connect.php →
// validateInternalKey) ولا يستطيع التطبيق مناداته، ولا يرجع
// ratingCount/completedRides/driverTier.
//
// الرد يطابق شكل driverInfo في acceptRide.php حرفياً حتى يقرأه
// _fillDriverDataLocally و getUpdatedRideForDriverApply بنفس المفاتيح.
// ═══════════════════════════════════════════════════════════════
include "../../connect.php";
// الراكب من الـ JWT فقط — لا نثق بأي passengerID قادم من الطلب (حماية IDOR)
$passengerId = $user_id;
$rideIdParam = filterRequest("id");
if (empty($passengerId)) {
printFailure("Unauthorized");
exit;
}
try {
// ── 1. الرحلة: ride DB هو المرجع، مع fallback على primary ──────
$rideRow = null;
$sqlRide = "SELECT id, driver_id, passenger_id, status
FROM `ride`
WHERE passenger_id = ? ";
$params = [$passengerId];
if (!empty($rideIdParam)) {
$sqlRide .= "AND id = ? ";
$params[] = $rideIdParam;
}
$sqlRide .= "ORDER BY id DESC LIMIT 1";
try {
$con_ride = Database::get('ride');
$stmtRide = $con_ride->prepare($sqlRide);
$stmtRide->execute($params);
$rideRow = $stmtRide->fetch(PDO::FETCH_ASSOC);
} catch (Exception $eRideDb) {
error_log("[getRideOrderID] ride DB unavailable: " . $eRideDb->getMessage());
}
if (!$rideRow) {
$stmtRide = $con->prepare($sqlRide);
$stmtRide->execute($params);
$rideRow = $stmtRide->fetch(PDO::FETCH_ASSOC);
}
if (!$rideRow) {
printFailure("No ride found");
exit;
}
$driverId = $rideRow['driver_id'] ?? '';
if (empty($driverId)) {
// رحلة لم يقبلها سائق بعد — ليست حالة خطأ
echo json_encode([
"status" => "success",
"message" => "No driver assigned yet",
"data" => ["ride_id" => (string)$rideRow['id'], "status" => $rideRow['status']],
]);
exit;
}
// ── 2. بيانات السائق — نفس استعلام acceptRide.php ──────────────
$stmtDriver = $con->prepare("
SELECT
d.id AS driver_id,
d.first_name,
d.last_name,
d.gender,
d.phone,
c.make,
c.model,
c.car_plate,
c.year,
c.color,
c.color_hex,
(SELECT ROUND(AVG(rating), 2) FROM ratingDriver WHERE driver_id = d.id) AS ratingDriver,
(SELECT COUNT(*) FROM ratingDriver WHERE driver_id = d.id) AS ratingCount,
(SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status IN ('Finished', 'finished')) AS completedRides,
dt.token
FROM driver d
LEFT JOIN CarRegistration c ON c.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
WHERE d.id = ?
LIMIT 1
");
$stmtDriver->execute([$driverId]);
$driverRaw = $stmtDriver->fetch(PDO::FETCH_ASSOC);
if (!$driverRaw) {
printFailure("Driver not found");
exit;
}
// جدول driverToken يخزّن التوكن مشفّراً — بلا فك تشفير يصل التطبيق
// blob يستخدمه كـ FCM target فيرفضه FCM بـ 400 ولا تصل رسائل الراكب.
$driverInfo = [];
$encryptedFields = ['first_name', 'last_name', 'gender', 'phone', 'car_plate', 'token'];
foreach ($driverRaw as $key => $value) {
if (in_array($key, $encryptedFields, true) && !empty($value)) {
$decrypted = false;
try {
$decrypted = $encryptionHelper->decryptData($value);
} catch (\Throwable $e) {
$decrypted = false;
}
$driverInfo[$key] = ($decrypted !== false && $decrypted !== null && $decrypted !== '')
? $decrypted
: $value;
} else {
$driverInfo[$key] = $value;
}
}
$driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? ''));
if (empty($driverInfo['driverName']) && !empty($driverInfo['phone'])) {
$driverInfo['driverName'] = $driverInfo['phone'];
}
$driverInfo['ratingDriver'] = !empty($driverInfo['ratingDriver']) ? (string)$driverInfo['ratingDriver'] : "5.0";
$ratingValue = (float) $driverInfo['ratingDriver'];
$ratingCount = (int) ($driverInfo['ratingCount'] ?? 0);
$completedRides = (int) ($driverInfo['completedRides'] ?? 0);
if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) {
$driverInfo['driverTier'] = 'Professional driver';
} elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) {
$driverInfo['driverTier'] = 'Trusted driver';
} else {
$driverInfo['driverTier'] = 'Verified driver';
}
// ── 3. آخر موقع للسائق من Redis (اختياري) ──────────────────────
if (isset($redisLocation) && $redisLocation) {
try {
// ‏المفتاح driver:public هو ما يكتبه driver_socket.php فعلاً
// ‏(driver:location لا وجود له — كان خطأً منسوخاً من acceptRide.php).
$driverLoc = $redisLocation->hGetAll("driver:public:$driverId");
if (!empty($driverLoc)) {
$driverInfo['lat'] = (float)($driverLoc['lat'] ?? 0);
$driverInfo['lng'] = (float)($driverLoc['lng'] ?? 0);
$driverInfo['heading'] = (float)($driverLoc['heading'] ?? 0);
}
} catch (Exception $eLoc) {
// تجاهل — الموقع يصل بالسوكيت أصلاً
}
}
// ⚠️ تصادم مفاتيح: getUpdatedRideForDriverApply يقرأ passengerName + last_name
// كاسم **الراكب**. لو تركنا last_name للسائق يظهر لقب السائق كاسم الراكب.
// ننقل لقب السائق إلى driver_last_name (وهو مفتاح يفهمه
// _fillDriverDataLocally أصلاً) ونرجّع اسم الراكب الحقيقي في مكانه.
$driverInfo['driver_last_name'] = $driverInfo['last_name'] ?? '';
unset($driverInfo['last_name']);
try {
$stmtPas = $con->prepare("SELECT first_name, last_name FROM passengers WHERE id = ? LIMIT 1");
$stmtPas->execute([$passengerId]);
$pasRow = $stmtPas->fetch(PDO::FETCH_ASSOC);
if ($pasRow) {
foreach (['first_name' => 'passengerName', 'last_name' => 'last_name'] as $src => $dst) {
$val = $pasRow[$src] ?? '';
if ($val === '' || $val === null) { $driverInfo[$dst] = ''; continue; }
$dec = false;
try {
$dec = $encryptionHelper->decryptData($val);
} catch (\Throwable $e) {
$dec = false;
}
$driverInfo[$dst] = ($dec !== false && $dec !== null && $dec !== '') ? $dec : $val;
}
}
} catch (PDOException $ePas) {
error_log("[getRideOrderID] passenger name lookup WARNING: " . $ePas->getMessage());
}
$driverInfo['ride_id'] = (string)$rideRow['id'];
$driverInfo['status'] = $rideRow['status'];
echo json_encode([
"status" => "success",
"message" => "Driver info",
"data" => $driverInfo,
]);
} catch (PDOException $e) {
error_log("[getRideOrderID] CRITICAL: " . $e->getMessage());
printFailure("Server error");
}