سوكيت الراكب (سبب عدم ظهور معلومات السائق عند القبول): - تطبيق الراكب كان يرسل 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>
288 lines
14 KiB
PHP
288 lines
14 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// driver/ride/accept_ride.php
|
|
// PURPOSE : قبول رحلة — ride DB هو المرجع، primary DB يتزامن بعده
|
|
// RACE : Optimistic lock عبر WHERE status IN ('waiting','wait')
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
include "../../connect.php";
|
|
|
|
try {
|
|
$con_ride = Database::get('ride');
|
|
} catch (Exception $e) {
|
|
error_log("[accept_ride] Failed to connect to Ride Database: " . $e->getMessage());
|
|
printFailure("Database connection failed");
|
|
exit;
|
|
}
|
|
|
|
// ── 1. Input & Validation ──────────────────────────────────────
|
|
$rideId = filterRequest("id");
|
|
// Force driver_id from JWT — never trust user-supplied driver_id
|
|
$driverId = $user_id;
|
|
$status = filterRequest("status"); // القيمة التي يرسلها التطبيق: 'accepted'
|
|
$passengerToken = filterRequest("passengerToken");
|
|
$passengerFingerprint = filterRequest("passengerFingerprint");
|
|
$passengerIdValue = filterRequest("passenger_id");
|
|
|
|
if (empty($rideId) || empty($driverId)) {
|
|
printFailure("Missing required parameters");
|
|
exit;
|
|
}
|
|
|
|
// Self-ride validation
|
|
$driverFingerprint = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : '';
|
|
if (!empty($driverFingerprint) && $driverFingerprint === $passengerFingerprint) {
|
|
error_log("[accept_ride] Self-ride attempt blocked. DriverID=$driverId, Fingerprint=$driverFingerprint");
|
|
printFailure("Self-matching is not allowed");
|
|
exit;
|
|
}
|
|
|
|
// status whitelist — لا نقبل قيمة عشوائية من التطبيق
|
|
$allowedStatuses = ['accepted', 'Apply'];
|
|
if (!in_array($status, $allowedStatuses, true)) {
|
|
$status = 'accepted'; // fallback آمن
|
|
}
|
|
|
|
error_log("[accept_ride] DriverID=$driverId attempting RideID=$rideId");
|
|
|
|
try {
|
|
// ═══════════════════════════════════════════════════════════
|
|
// STEP A — القفل على ride DB (المرجع الأساسي)
|
|
// Optimistic lock: نغير فقط إذا status لا يزال 'waiting' أو 'wait'
|
|
// السائق الأول الذي يصل يربح — الباقي يجدون rowCount=0
|
|
// ═══════════════════════════════════════════════════════════
|
|
$stmtLock = $con_ride->prepare("
|
|
UPDATE `ride`
|
|
SET `status` = ?,
|
|
`driver_id` = ?,
|
|
`rideTimeStart` = NOW()
|
|
WHERE `id` = ?
|
|
AND `status` IN ('waiting', 'wait')
|
|
");
|
|
$stmtLock->execute([$status, $driverId, $rideId]);
|
|
|
|
if ($stmtLock->rowCount() === 0) {
|
|
// الرحلة غير متاحة — سائق آخر سبق أو الرحلة ألغيت
|
|
error_log("[accept_ride] RideID=$rideId not available for DriverID=$driverId (rowCount=0)");
|
|
printFailure("Ride not available");
|
|
exit;
|
|
}
|
|
|
|
error_log("[accept_ride] ride DB locked. RideID=$rideId → DriverID=$driverId");
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// STEP B — تزامن primary DB (بعد نجاح القفل)
|
|
// ═══════════════════════════════════════════════════════════
|
|
try {
|
|
$con->prepare("
|
|
UPDATE `ride`
|
|
SET `driver_id` = ?,
|
|
`status` = ?,
|
|
`rideTimeStart` = NOW()
|
|
WHERE `id` = ?
|
|
")->execute([$driverId, $status, $rideId]);
|
|
error_log("[accept_ride] primary DB synced. RideID=$rideId");
|
|
} catch (PDOException $eSync) {
|
|
// لا نوقف — ride DB هو المرجع
|
|
error_log("[accept_ride] primary DB sync WARNING: " . $eSync->getMessage());
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// STEP C — driver_orders (INSERT أو UPDATE بسطر واحد آمن)
|
|
// ON DUPLICATE KEY يمنع race condition ثانية على هذا الجدول
|
|
// ═══════════════════════════════════════════════════════════
|
|
try {
|
|
$con->prepare("
|
|
INSERT INTO `driver_orders` (`driver_id`, `order_id`, `status`, `created_at`)
|
|
VALUES (?, ?, ?, NOW())
|
|
ON DUPLICATE KEY UPDATE
|
|
`driver_id` = VALUES(`driver_id`),
|
|
`status` = VALUES(`status`),
|
|
`created_at` = NOW()
|
|
")->execute([$driverId, $rideId, $status]);
|
|
} catch (PDOException $eOrders) {
|
|
error_log("[accept_ride] driver_orders WARNING: " . $eOrders->getMessage());
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// STEP C.1 — تحديث جدول waitingRides حتى لا تظهر للكباتن الآخرين
|
|
// ═══════════════════════════════════════════════════════════
|
|
try {
|
|
$con->prepare("UPDATE `waitingRides` SET `status` = 'Apply' WHERE `id` = ?")->execute([$rideId]);
|
|
} catch (PDOException $eWaiting) {
|
|
error_log("[accept_ride] waitingRides WARNING: " . $eWaiting->getMessage());
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// STEP D — جلب بيانات السائق للراكب
|
|
// ═══════════════════════════════════════════════════════════
|
|
$driverInfo = [];
|
|
|
|
$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) {
|
|
$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';
|
|
}
|
|
|
|
if (isset($redisLocation) && $redisLocation) {
|
|
try {
|
|
$driverLoc = $redisLocation->hGetAll("driver:location:$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) {
|
|
// Ignore location fetch error
|
|
}
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// STEP E — جلب passenger_id وإرسال الإشعارات
|
|
// ═══════════════════════════════════════════════════════════
|
|
if (empty($passengerIdValue)) {
|
|
$passengerId = $con->prepare("SELECT passenger_id FROM ride WHERE id = ? LIMIT 1");
|
|
$passengerId->execute([$rideId]);
|
|
$passengerIdValue = $passengerId->fetchColumn();
|
|
}
|
|
|
|
// 🆕 نحلّ توكن الراكب من القاعدة دائماً ولا نعتمد على ما يرسله التطبيق:
|
|
// • مسار الـ dispatch/FCM يرسل التوكن سليماً (نص صريح من add_ride.php).
|
|
// • مسار "سوق الرحلات" يرسله كما خرج من getRideWaiting.php أي **مشفّراً**
|
|
// (جدول tokens يخزّنه مشفّراً — انظر ride/firebase/addToken.php)، فكان
|
|
// FCM يرفضه بـ 400 ولا يصل الراكب أي إشعار قبول.
|
|
// القيمة القادمة من العميل تبقى fallback أخيراً فقط.
|
|
$passengerTokenFromClient = $passengerToken;
|
|
$passengerToken = '';
|
|
if ($passengerIdValue) {
|
|
try {
|
|
$stmtTok = $con->prepare("SELECT token FROM tokens WHERE passengerID = ? LIMIT 1");
|
|
$stmtTok->execute([$passengerIdValue]);
|
|
$rawPassengerToken = $stmtTok->fetchColumn();
|
|
if (!empty($rawPassengerToken)) {
|
|
$decryptedToken = false;
|
|
try {
|
|
$decryptedToken = $encryptionHelper->decryptData($rawPassengerToken);
|
|
} catch (\Throwable $eTok) {
|
|
$decryptedToken = false;
|
|
}
|
|
$passengerToken = ($decryptedToken !== false && $decryptedToken !== null && $decryptedToken !== '')
|
|
? trim($decryptedToken)
|
|
: $rawPassengerToken;
|
|
}
|
|
} catch (PDOException $eTok) {
|
|
error_log("[accept_ride] passenger token lookup WARNING: " . $eTok->getMessage());
|
|
}
|
|
}
|
|
if (empty($passengerToken)) {
|
|
$passengerToken = $passengerTokenFromClient;
|
|
}
|
|
|
|
if ($passengerIdValue) {
|
|
// Socket — real-time update على خريطة الراكب
|
|
if (function_exists('notifyPassengerOnRideServer')) {
|
|
notifyPassengerOnRideServer($passengerIdValue, [
|
|
'status' => 'accepted',
|
|
'ride_id' => $rideId,
|
|
'driver_id' => $driverId,
|
|
'driver_info' => $driverInfo,
|
|
]);
|
|
}
|
|
|
|
// FCM — push notification صامت
|
|
if (!empty($passengerToken)) {
|
|
sendFCM_Internal(
|
|
$passengerToken,
|
|
"", // تفريغ العنوان للإرسال الصامت
|
|
"", // تفريغ المحتوى للإرسال الصامت
|
|
['ride_id' => (string) $rideId, 'driver_info' => $driverInfo, 'status' => 'accepted'],
|
|
"Accepted Ride",
|
|
false
|
|
);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// STEP F — تنظيف السوق + Cache ride state (أبلغ location server)
|
|
// ═══════════════════════════════════════════════════════════
|
|
sendToLocationServer('ride_taken_event', [
|
|
'ride_id' => $rideId,
|
|
'taken_by_driver_id' => $driverId,
|
|
]);
|
|
|
|
// 🆕 Cache ride state in Redis
|
|
sendToLocationServer('update_ride_state', [
|
|
'ride_id' => $rideId,
|
|
'status' => $status,
|
|
'driver_id' => $driverId,
|
|
'passenger_id' => $passengerIdValue ?? '',
|
|
]);
|
|
|
|
error_log("[accept_ride] SUCCESS. RideID=$rideId accepted by DriverID=$driverId");
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// STEP G — رد النجاح للسائق (نفس بنية الرد القديمة)
|
|
// ═══════════════════════════════════════════════════════════
|
|
echo json_encode([
|
|
"status" => "success",
|
|
"message" => "Ride Accepted",
|
|
"data" => $driverInfo,
|
|
]);
|
|
|
|
} catch (PDOException $e) {
|
|
error_log("[accept_ride] CRITICAL: " . $e->getMessage());
|
|
printFailure("Server error");
|
|
}
|