Update: 2026-08-07 01:35:27
This commit is contained in:
@@ -69,6 +69,32 @@ try {
|
||||
error_log("[accept_ride] تعذّر فحص الإيقاف: " . $eBlock->getMessage());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP A0b — حارس الحصرية في الإسناد بالدفعات
|
||||
//
|
||||
// بدون هذا الفحص تبقى الحصرية شكلية: سائق تلقّى العرض سابقاً (أو صاغ
|
||||
// الطلب يدوياً) يستطيع القبول خارج دوره فيعود السباق كما كان.
|
||||
//
|
||||
// وجود المفتاح هو ما يحدد الوضع: البثّ الحر لا يكتبه أصلاً، فالفحص
|
||||
// يمرّ بلا أثر — لا حاجة لقراءة DISPATCH_MODE هنا.
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
try {
|
||||
if (isset($redisLocation) && $redisLocation) {
|
||||
$currentOffer = $redisLocation->get("ride:$rideId:current_offer");
|
||||
if ($currentOffer !== false && $currentOffer !== null
|
||||
&& (string) $currentOffer !== (string) $driverId) {
|
||||
error_log("[accept_ride] DriverID=$driverId حاول قبول RideID=$rideId"
|
||||
. " والعرض الحالي لـ $currentOffer — رُفض");
|
||||
printFailure("Ride not available");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $eOffer) {
|
||||
// فشل الفحص لا يمنع القبول — القفل التفاؤلي في STEP A يبقى الضامن
|
||||
// ضد الازدواج، وأسوأ نتيجة هنا هي عودة سلوك البثّ الحر لهذه الرحلة.
|
||||
error_log("[accept_ride] تعذّر فحص حصرية العرض: " . $eOffer->getMessage());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP A — القفل على ride DB (المرجع الأساسي)
|
||||
// Optimistic lock: نغير فقط إذا status لا يزال 'waiting' أو 'wait'
|
||||
@@ -93,6 +119,17 @@ try {
|
||||
|
||||
error_log("[accept_ride] ride DB locked. RideID=$rideId → DriverID=$driverId");
|
||||
|
||||
// عدّاد القبول — بسط معدّل القبول الذي يرتّب به محرك الإسناد.
|
||||
// يُعدّ هنا بعد نجاح القفل حصراً: العدّ قبله كان سيحسب للخاسرين في
|
||||
// السباق قبولاً لم يحدث.
|
||||
try {
|
||||
if (isset($redisLocation) && $redisLocation) {
|
||||
$redisLocation->hIncrBy("driver:score:$driverId", 'accepts', 1);
|
||||
}
|
||||
} catch (Throwable $eScore) {
|
||||
error_log("[accept_ride] تعذّر تحديث عدّاد القبول: " . $eScore->getMessage());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP B — تزامن primary DB (بعد نجاح القفل)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
@@ -16,22 +16,20 @@ try {
|
||||
// =================================================================================
|
||||
// 🛠️ دالة مساعدة: إرسال الرحلة لسوق السائقين (Marketplace Broadcast)
|
||||
// =================================================================================
|
||||
function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketData = []) {
|
||||
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
|
||||
if (strpos($url, 'localhost') !== false || strpos($url, '127.0.0.1') !== false) {
|
||||
if (file_exists('/.dockerenv')) {
|
||||
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
|
||||
}
|
||||
}
|
||||
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
||||
// ⚠️ هذه الحمولة تُبَثّ لكل سائق قريب (وليس للفائز فقط) — لا نضع فيها أي
|
||||
// بيانات راكب حسّاسة (هاتف/إيميل/FCM token). ما ينقص السائق من تفاصيل
|
||||
// الراكب يجلبه بعد نجاح القبول، والسيرفر يحلّ التوكن بنفسه في acceptRide.php.
|
||||
//
|
||||
// أسماء الحقول تطابق getRideWaiting.php لأن available_rides_page.dart يحشر
|
||||
// رحلات الـ socket في نفس القائمة ويقرأ منها بنفس المفاتيح — أي حقل ناقص
|
||||
// يصل للسيرفر لاحقاً كنص "null".
|
||||
$marketPayload = array_merge([
|
||||
/**
|
||||
* حمولة الرحلة كما يراها السائق قبل القبول.
|
||||
*
|
||||
* مشتركة بين البثّ الحر والإسناد بالدفعات عمداً: أي حقل يظهر في أحدهما
|
||||
* ولا يظهر في الآخر يصل للسيرفر لاحقاً كنص "null".
|
||||
*
|
||||
* ⚠️ لا تضع هنا أي بيانات راكب حسّاسة (هاتف/إيميل/FCM token) — هذه الحمولة
|
||||
* تصل سائقين لن يقودوا الرحلة. التفاصيل تُجلب بعد نجاح القبول.
|
||||
*
|
||||
* أسماء الحقول تطابق getRideWaiting.php لأن available_rides_page.dart يحشر
|
||||
* رحلات الـ socket في نفس القائمة ويقرأ منها بنفس المفاتيح.
|
||||
*/
|
||||
function buildMarketPayload($rideId, $lat, $lng, $payloadData, $extraMarketData = []) {
|
||||
return array_merge([
|
||||
'id' => (string)$rideId,
|
||||
'start_lat' => $lat,
|
||||
'start_lng' => $lng,
|
||||
@@ -48,6 +46,52 @@ function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketDa
|
||||
'passengerRate' => $payloadData[33],
|
||||
'passengerId' => $payloadData[7],
|
||||
], $extraMarketData);
|
||||
}
|
||||
|
||||
/**
|
||||
* يوقظ العرض المتسلسل في السوكِت بعد أن بنى dispatchStartBatched الطابور.
|
||||
* السوكِت هو صاحب المؤقّت لأنه العملية الوحيدة طويلة العمر (Workerman
|
||||
* Timer) — كرون كل دقيقة لا يصلح لمهلة عشر ثوانٍ.
|
||||
*/
|
||||
function notifySocketStartBatchedDispatch($rideId) {
|
||||
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
|
||||
if (file_exists('/.dockerenv')) {
|
||||
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
|
||||
}
|
||||
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
|
||||
'action' => 'start_batched_dispatch',
|
||||
'ride_id' => $rideId,
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 2000);
|
||||
if ($INTERNAL_KEY) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
|
||||
}
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketData = []) {
|
||||
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
|
||||
if (strpos($url, 'localhost') !== false || strpos($url, '127.0.0.1') !== false) {
|
||||
if (file_exists('/.dockerenv')) {
|
||||
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
|
||||
}
|
||||
}
|
||||
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
||||
// ⚠️ هذه الحمولة تُبَثّ لكل سائق قريب (وليس للفائز فقط) — لا نضع فيها أي
|
||||
// بيانات راكب حسّاسة (هاتف/إيميل/FCM token). ما ينقص السائق من تفاصيل
|
||||
// الراكب يجلبه بعد نجاح القبول، والسيرفر يحلّ التوكن بنفسه في acceptRide.php.
|
||||
//
|
||||
// أسماء الحقول تطابق getRideWaiting.php لأن available_rides_page.dart يحشر
|
||||
// رحلات الـ socket في نفس القائمة ويقرأ منها بنفس المفاتيح — أي حقل ناقص
|
||||
// يصل للسيرفر لاحقاً كنص "null".
|
||||
$marketPayload = buildMarketPayload($rideId, $lat, $lng, $payloadData, $extraMarketData);
|
||||
|
||||
$postData = [
|
||||
'action' => 'market_new_ride',
|
||||
@@ -428,15 +472,44 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($driversData)) {
|
||||
dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper, $extraDispatchData);
|
||||
error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers.");
|
||||
} else {
|
||||
error_log("[add_ride] No direct drivers found for RideID=$insertedId — market only.");
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// الإسناد — بالدفعات أو بالبثّ الحر
|
||||
//
|
||||
// batched: عرض حصري متسلسل على مرشحين مرتبين. لا بثّ للسوق، ولا
|
||||
// إشعار جماعي — وإلا لضاع معنى الحصرية.
|
||||
// broadcast (الافتراضي): السلوك القائم حرفياً بلا أي تغيير.
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
require_once __DIR__ . '/../dispatch/dispatch_helper.php';
|
||||
|
||||
$batchedStarted = false;
|
||||
if (dispatchIsBatched((string) ($payload[31] ?? ''))) {
|
||||
$batchedStarted = dispatchStartBatched(
|
||||
$redisLocation ?? null,
|
||||
$insertedId,
|
||||
$driversData ?: [],
|
||||
buildMarketPayload($insertedId, $startLat, $startLng, $payload, $extraDispatchData)
|
||||
);
|
||||
|
||||
if ($batchedStarted) {
|
||||
notifySocketStartBatchedDispatch($insertedId);
|
||||
error_log("[add_ride] RideID=$insertedId أُسند بالدفعات.");
|
||||
} else {
|
||||
error_log("[add_ride] تعذّر الإسناد بالدفعات RideID=$insertedId"
|
||||
. " — السقوط للبثّ الحر.");
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast للـ marketplace دائماً
|
||||
broadcastRideToMarket($insertedId, $startLat, $startLng, $payload, $extraDispatchData);
|
||||
if (!$batchedStarted) {
|
||||
if (!empty($driversData)) {
|
||||
dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper, $extraDispatchData);
|
||||
error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers.");
|
||||
} else {
|
||||
error_log("[add_ride] No direct drivers found for RideID=$insertedId — market only.");
|
||||
}
|
||||
|
||||
// Broadcast للـ marketplace دائماً
|
||||
broadcastRideToMarket($insertedId, $startLat, $startLng, $payload, $extraDispatchData);
|
||||
}
|
||||
|
||||
// رد النجاح للتطبيق
|
||||
printSuccess($insertedId);
|
||||
|
||||
@@ -328,6 +328,17 @@ try {
|
||||
'passenger_id' => $passengerId,
|
||||
]);
|
||||
|
||||
// ختم نهاية الرحلة — مقياس الخمول في ترتيب محرك الإسناد (وزن ١٥٪،
|
||||
// إنصافاً لمن انتظر أطول). يُكتب هنا لا عند القبول: السائق يبدأ
|
||||
// "الخمول" حين يفرغ فعلاً، لا حين ينشغل.
|
||||
try {
|
||||
if (isset($redisLocation) && $redisLocation) {
|
||||
$redisLocation->hSet("driver:score:$driver_id", 'last_ride_end', time());
|
||||
}
|
||||
} catch (Throwable $eScore) {
|
||||
error_log("[finish_ride_updates] تعذّر ختم نهاية الرحلة: " . $eScore->getMessage());
|
||||
}
|
||||
|
||||
// 🔥 [Fix Split-Brain] تحديث القاعدة البعيدة الآن فقط، بعد أن أصبح الدفع
|
||||
// والتحديث المحلي مؤكدَين نجاحهما — يبقي الحالتين متطابقتين دائماً.
|
||||
// فشل هذا التحديث best-effort فقط (لا يُرجع الرحلة المحلية المُنجَزة فعلاً).
|
||||
|
||||
Reference in New Issue
Block a user