Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
+152
View File
@@ -46,6 +46,64 @@ if (!in_array($status, $allowedStatuses, true)) {
error_log("[accept_ride] DriverID=$driverId attempting RideID=$rideId");
try {
// ═══════════════════════════════════════════════════════════
// STEP A0 — تطبيق الإيقاف على الخادم
// ‏كان الإيقاف بعد ٣ إلغاءات يُكتب في تخزين تطبيق السائق وحده، فيُفكّ
// ‏بمسح بيانات التطبيق أو إعادة تثبيته. هذه هي البوابة الحقيقية: سائق
// ‏موقوف لا يستطيع أن يفوز برحلة مهما فعل بتطبيقه.
// ═══════════════════════════════════════════════════════════
// ‏الفحص من Redis لا MySQL: هذا المسار يُستدعى على كل محاولة قبول من كل
// ‏سائق، والإيقاف حالة نادرة قصيرة العمر. مفتاح بـ TTL يجعل انتهاء
// ‏الإيقاف تلقائياً بلا كرون ولا استعلام تنظيف.
// ‏MySQL يبقى السجل الدائم للمراجعة الإدارية، ولا يُقرأ هنا.
try {
if (isset($redisLocation) && $redisLocation) {
$blockedUntil = $redisLocation->get("driver:$driverId:blocked");
if ($blockedUntil) {
error_log("[accept_ride] DriverID=$driverId موقوف حتى $blockedUntil — رُفض القبول");
printFailure("Your account is temporarily suspended until $blockedUntil");
exit;
}
// ‏تجميد سلامة — بلا TTL: لا ينتهي بالوقت بل برفع إنسان له.
// ‏سائق عليه بلاغ سلامة مفتوح لا يُسند له راكب جديد.
$safetyHold = $redisLocation->get("driver:$driverId:safety_hold");
if ($safetyHold) {
error_log("[accept_ride] DriverID=$driverId مجمّد ببلاغ سلامة #$safetyHold — رُفض القبول");
printFailure("Your account is under review. Please contact support.");
exit;
}
}
} catch (Throwable $eBlock) {
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'
@@ -70,6 +128,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 (بعد نجاح القفل)
// ═══════════════════════════════════════════════════════════
@@ -87,6 +156,53 @@ try {
error_log("[accept_ride] primary DB sync WARNING: " . $eSync->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP B1 — تثبيت مسافة الاقتراب لحظة القبول (في Redis لا MySQL)
//
// ‏هذه المسافة أساس تعويض عدم الحضور لاحقاً، ويجب أن تُقاس الآن حصراً:
// ‏عند الإلغاء يكون السائق واقفاً عند نقطة الالتقاط فتصير صفراً.
//
// ‏لكنها قيمة عابرة تُقرأ مرة واحدة عند نهاية الرحلة — فلا تستحق كتابتين
// ‏في MySQL على أسخن مسار في النظام. تُحفظ في نفس الهاش الذي يستخدمه
// ‏السوكِت لحالة الرحلة (ride:$id:state)، وتُثبَّت في MySQL كتابةً واحدة
// ‏مع تحديث الحالة النهائي عند الإلغاء.
//
// ‏hSet لا يمسح حقول السوكِت الأخرى في نفس الهاش، وTTL الـ24 ساعة الذي
// ‏يضبطه السوكِت يغطي هذه الحقول تلقائياً.
// ═══════════════════════════════════════════════════════════
try {
$approachKm = 0.0;
if (isset($redisLocation) && $redisLocation) {
$stmtPickup = $con->prepare("SELECT start_location FROM ride WHERE id = ?");
$stmtPickup->execute([$rideId]);
[$pickupLat, $pickupLng] = array_pad(
array_map('trim', explode(',', (string) $stmtPickup->fetchColumn(), 2)), 2, null
);
if (is_numeric($pickupLat) && is_numeric($pickupLng)) {
$loc = $redisLocation->hGetAll("driver:public:$driverId");
if (!empty($loc['lat']) && !empty($loc['lng'])) {
$approachKm = round(getDistanceBetweenPoints(
(float) $loc['lat'], (float) $loc['lng'],
(float) $pickupLat, (float) $pickupLng
), 3);
}
}
if ($approachKm > 0) {
$redisLocation->hSet("ride:$rideId:state", 'pickup_approach_km', $approachKm);
}
}
if ($approachKm <= 0) {
error_log("[accept_ride] تعذّر قياس مسافة الاقتراب RideID=$rideId"
. " — التعويض سيحتسب زمن الانتظار وحده");
}
} catch (Throwable $eApproach) {
// ‏قياس التعويض لا يُسقط قبول الرحلة أبداً.
error_log("[accept_ride] pickupApproachKm WARNING: " . $eApproach->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP C — driver_orders (INSERT أو UPDATE بسطر واحد آمن)
// ON DUPLICATE KEY يمنع race condition ثانية على هذا الجدول
@@ -275,6 +391,42 @@ try {
'passenger_id' => $passengerIdValue ?? '',
]);
// ═══════════════════════════════════════════════════════════
// ‏الراكب الذي طلب برسالة نصية لا يملك إنترنت: السوكيت لن يصله،
// ‏والإشعار الصامت أعلاه يذهب إلى العدم. الرسالة النصية هي قناته
// ‏الوحيدة لمعرفة أن سائقاً قَبِل، ومن هو، وكيف يعرف سيارته.
// ═══════════════════════════════════════════════════════════
try {
$stmtSms = $con->prepare("
SELECT sender_norm FROM sms_ride_requests
WHERE ride_id = ? AND status = 'dispatched' LIMIT 1
");
$stmtSms->execute([$rideId]);
$smsSender = $stmtSms->fetchColumn();
if ($smsSender) {
require_once __DIR__ . '/../../sms/helpers.php';
$plate = trim((string) ($driverInfo['car_plate'] ?? ''));
$car = trim(($driverInfo['make'] ?? '') . ' ' . ($driverInfo['model'] ?? ''));
$color = trim((string) ($driverInfo['color'] ?? ''));
$lines = ['سيرو: وجدنا لك سائقاً.'];
$lines[] = 'السائق: ' . ($driverInfo['driverName'] ?? '—');
if (!empty($driverInfo['phone'])) $lines[] = 'هاتفه: ' . $driverInfo['phone'];
if ($car !== '' || $color !== '') $lines[] = 'السيارة: ' . trim("$car $color");
if ($plate !== '') $lines[] = 'اللوحة: ' . $plate;
$lines[] = 'سيتصل بك عند وصوله.';
smsEnqueue($con, (string) $smsSender, implode("\n", $lines),
'driver_found', (string) $rideId);
}
} catch (Throwable $eSms) {
// ‏لا نُسقط قبول الرحلة من أجل رسالة: السائق قَبِل فعلاً،
// ‏والرسالة الفائتة أهون من رحلة تُلغى بخطأ.
error_log('[accept_ride] تعذّر تجهيز رسالة SMS للراكب: ' . $eSms->getMessage());
}
error_log("[accept_ride] SUCCESS. RideID=$rideId accepted by DriverID=$driverId");
// ═══════════════════════════════════════════════════════════