Update: 2026-08-07 01:06:09
This commit is contained in:
+12
-2
@@ -4,13 +4,13 @@
|
|||||||
-- النموذج: التعويض = مسافة الاقتراب إلى الراكب × سعر الكيلومتر لنوع السيارة
|
-- النموذج: التعويض = مسافة الاقتراب إلى الراكب × سعر الكيلومتر لنوع السيارة
|
||||||
-- + دقائق الانتظار الفعلية × سعر الدقيقة حسب وقت اليوم.
|
-- + دقائق الانتظار الفعلية × سعر الدقيقة حسب وقت اليوم.
|
||||||
-- يُخصم من محفظة الراكب ويُضاف لمحفظة السائق. الأسعار من kazan نفسه الذي
|
-- يُخصم من محفظة الراكب ويُضاف لمحفظة السائق. الأسعار من kazan نفسه الذي
|
||||||
-- تُسعَّر به الرحلة، عبر ride/rides/pricing_helper.php.
|
-- تُسعَّر به الرحلة، عبر ride/pricing/pricing_helper.php.
|
||||||
--
|
--
|
||||||
-- يستبدل هذا الملف مقاربة "الرسم الثابت" السابقة: المبلغ لم يعد يُقرأ من
|
-- يستبدل هذا الملف مقاربة "الرسم الثابت" السابقة: المبلغ لم يعد يُقرأ من
|
||||||
-- طلب العميل (كان filterRequest("penalty_fee") بلا سقف ولا تحقق)، ولم يعد
|
-- طلب العميل (كان filterRequest("penalty_fee") بلا سقف ولا تحقق)، ولم يعد
|
||||||
-- رقماً ثابتاً — صار محسوباً من بيانات مسجَّلة على الخادم.
|
-- رقماً ثابتاً — صار محسوباً من بيانات مسجَّلة على الخادم.
|
||||||
--
|
--
|
||||||
-- ⚠️ يُنفَّذ على قاعدتَي primary و ride معاً: الكود يكتب جدول ride في الاثنتين.
|
-- ⚠️ هذا الملف لقاعدة primary (jorSiroDB) حصراً. الملف التوأم _ride.sql يُنفَّذ على قاعدة الرحلات (intaleq-ridesDB).
|
||||||
|
|
||||||
-- ══════════════════════════════════════════════════════════════
|
-- ══════════════════════════════════════════════════════════════
|
||||||
-- ١) سياسة التعويض لكل دولة (قاعدة primary فقط — kazan يُقرأ عبر $con)
|
-- ١) سياسة التعويض لكل دولة (قاعدة primary فقط — kazan يُقرأ عبر $con)
|
||||||
@@ -28,6 +28,12 @@ ALTER TABLE `kazan`
|
|||||||
|
|
||||||
-- ══════════════════════════════════════════════════════════════
|
-- ══════════════════════════════════════════════════════════════
|
||||||
-- ٢) بيانات الوصول والاقتراب (قاعدتا primary و ride)
|
-- ٢) بيانات الوصول والاقتراب (قاعدتا primary و ride)
|
||||||
|
--
|
||||||
|
-- هذان العمودان للتوثيق النهائي فقط ولا يُكتبان أثناء الرحلة: القيمتان
|
||||||
|
-- تعيشان في هاش Redis نفسه الذي يستخدمه السوكِت لحالة الرحلة
|
||||||
|
-- (ride:$id:state، الحقول pickup_approach_km و driver_arrived_at)،
|
||||||
|
-- وتُثبَّتان هنا كتابةً واحدة مدمجة في جملة تحديث الحالة عند الإلغاء.
|
||||||
|
-- أي: صفر كتابات إضافية على المسار الحار (القبول والوصول).
|
||||||
-- ══════════════════════════════════════════════════════════════
|
-- ══════════════════════════════════════════════════════════════
|
||||||
ALTER TABLE `ride`
|
ALTER TABLE `ride`
|
||||||
ADD COLUMN `driverArrivedAt` DATETIME NULL DEFAULT NULL
|
ADD COLUMN `driverArrivedAt` DATETIME NULL DEFAULT NULL
|
||||||
@@ -38,6 +44,10 @@ ALTER TABLE `ride`
|
|||||||
-- ══════════════════════════════════════════════════════════════
|
-- ══════════════════════════════════════════════════════════════
|
||||||
-- ٣) تطبيق الإيقاف على الخادم (قاعدتا primary و ride)
|
-- ٣) تطبيق الإيقاف على الخادم (قاعدتا primary و ride)
|
||||||
-- كان الإيقاف يُكتب في تخزين تطبيق السائق وحده، فيُفكّ بإعادة التثبيت.
|
-- كان الإيقاف يُكتب في تخزين تطبيق السائق وحده، فيُفكّ بإعادة التثبيت.
|
||||||
|
--
|
||||||
|
-- مسار الفحص في acceptRide.php هو مفتاح Redis بـ TTL (driver:$id:blocked)
|
||||||
|
-- فينتهي الإيقاف تلقائياً بلا كرون تنظيف وبلا استعلام على كل قبول.
|
||||||
|
-- هذا العمود سجل دائم للمراجعة الإدارية، يُكتب في الإلغاء الثالث فقط.
|
||||||
-- ══════════════════════════════════════════════════════════════
|
-- ══════════════════════════════════════════════════════════════
|
||||||
ALTER TABLE `driver`
|
ALTER TABLE `driver`
|
||||||
ADD COLUMN `blockedUntil` DATETIME NULL DEFAULT NULL
|
ADD COLUMN `blockedUntil` DATETIME NULL DEFAULT NULL
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
-- 2026_08_07_driver_cancel_compensation.sql
|
||||||
|
--
|
||||||
|
-- تعويض السائق عند عدم حضور الراكب (إلغاء السائق بعد الوصول والانتظار).
|
||||||
|
-- النموذج: التعويض = مسافة الاقتراب إلى الراكب × سعر الكيلومتر لنوع السيارة
|
||||||
|
-- + دقائق الانتظار الفعلية × سعر الدقيقة حسب وقت اليوم.
|
||||||
|
-- يُخصم من محفظة الراكب ويُضاف لمحفظة السائق. الأسعار من kazan نفسه الذي
|
||||||
|
-- تُسعَّر به الرحلة، عبر ride/pricing/pricing_helper.php.
|
||||||
|
--
|
||||||
|
-- يستبدل هذا الملف مقاربة "الرسم الثابت" السابقة: المبلغ لم يعد يُقرأ من
|
||||||
|
-- طلب العميل (كان filterRequest("penalty_fee") بلا سقف ولا تحقق)، ولم يعد
|
||||||
|
-- رقماً ثابتاً — صار محسوباً من بيانات مسجَّلة على الخادم.
|
||||||
|
--
|
||||||
|
-- ⚠️ هذا الملف لقاعدة ride (intaleq-ridesDB) حصراً. جدول kazan لا يُمسّ هنا: بنيته في قاعدة الرحلات أقدم ومختلفة الأعمدة، ولا يُقرأ في هذا المسار.
|
||||||
|
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
-- ٢) بيانات الوصول والاقتراب (قاعدتا primary و ride)
|
||||||
|
--
|
||||||
|
-- هذان العمودان للتوثيق النهائي فقط ولا يُكتبان أثناء الرحلة: القيمتان
|
||||||
|
-- تعيشان في هاش Redis نفسه الذي يستخدمه السوكِت لحالة الرحلة
|
||||||
|
-- (ride:$id:state، الحقول pickup_approach_km و driver_arrived_at)،
|
||||||
|
-- وتُثبَّتان هنا كتابةً واحدة مدمجة في جملة تحديث الحالة عند الإلغاء.
|
||||||
|
-- أي: صفر كتابات إضافية على المسار الحار (القبول والوصول).
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
ALTER TABLE `ride`
|
||||||
|
ADD COLUMN `driverArrivedAt` DATETIME NULL DEFAULT NULL
|
||||||
|
COMMENT 'لحظة تأكيد وصول السائق لنقطة الالتقاط — يختمها الخادم بعد تحقق الموقع',
|
||||||
|
ADD COLUMN `pickupApproachKm` FLOAT NOT NULL DEFAULT 0
|
||||||
|
COMMENT 'المسافة من موقع السائق لحظة القبول إلى نقطة الالتقاط (كم)';
|
||||||
|
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
-- ٣) تطبيق الإيقاف على الخادم (قاعدتا primary و ride)
|
||||||
|
-- كان الإيقاف يُكتب في تخزين تطبيق السائق وحده، فيُفكّ بإعادة التثبيت.
|
||||||
|
--
|
||||||
|
-- مسار الفحص في acceptRide.php هو مفتاح Redis بـ TTL (driver:$id:blocked)
|
||||||
|
-- فينتهي الإيقاف تلقائياً بلا كرون تنظيف وبلا استعلام على كل قبول.
|
||||||
|
-- هذا العمود سجل دائم للمراجعة الإدارية، يُكتب في الإلغاء الثالث فقط.
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
ALTER TABLE `driver`
|
||||||
|
ADD COLUMN `blockedUntil` DATETIME NULL DEFAULT NULL
|
||||||
|
COMMENT 'إيقاف استقبال الرحلات حتى هذا الوقت (إلغاءات متكررة). NULL = غير موقوف';
|
||||||
|
|
||||||
|
-- للتراجع:
|
||||||
|
-- ALTER TABLE `ride` DROP COLUMN `driverArrivedAt`, DROP COLUMN `pickupApproachKm`;
|
||||||
|
-- ALTER TABLE `driver` DROP COLUMN `blockedUntil`;
|
||||||
@@ -52,21 +52,21 @@ try {
|
|||||||
// بمسح بيانات التطبيق أو إعادة تثبيته. هذه هي البوابة الحقيقية: سائق
|
// بمسح بيانات التطبيق أو إعادة تثبيته. هذه هي البوابة الحقيقية: سائق
|
||||||
// موقوف لا يستطيع أن يفوز برحلة مهما فعل بتطبيقه.
|
// موقوف لا يستطيع أن يفوز برحلة مهما فعل بتطبيقه.
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// الفحص من Redis لا MySQL: هذا المسار يُستدعى على كل محاولة قبول من كل
|
||||||
|
// سائق، والإيقاف حالة نادرة قصيرة العمر. مفتاح بـ TTL يجعل انتهاء
|
||||||
|
// الإيقاف تلقائياً بلا كرون ولا استعلام تنظيف.
|
||||||
|
// MySQL يبقى السجل الدائم للمراجعة الإدارية، ولا يُقرأ هنا.
|
||||||
try {
|
try {
|
||||||
$stmtBlock = $con->prepare("SELECT blockedUntil FROM driver WHERE id = ?");
|
if (isset($redisLocation) && $redisLocation) {
|
||||||
$stmtBlock->execute([$driverId]);
|
$blockedUntil = $redisLocation->get("driver:$driverId:blocked");
|
||||||
$blockedUntil = $stmtBlock->fetchColumn();
|
if ($blockedUntil) {
|
||||||
|
error_log("[accept_ride] DriverID=$driverId موقوف حتى $blockedUntil — رُفض القبول");
|
||||||
if (!empty($blockedUntil) && strtotime($blockedUntil) > time()) {
|
printFailure("Your account is temporarily suspended until $blockedUntil");
|
||||||
error_log("[accept_ride] DriverID=$driverId موقوف حتى $blockedUntil — رُفض القبول");
|
exit;
|
||||||
printFailure("Your account is temporarily suspended until $blockedUntil");
|
}
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
} catch (PDOException $eBlock) {
|
} catch (Throwable $eBlock) {
|
||||||
// العمود غير موجود بعد (قبل تشغيل ترحيل 2026_08_07) — لا نمنع القبول
|
error_log("[accept_ride] تعذّر فحص الإيقاف: " . $eBlock->getMessage());
|
||||||
// بسبب فحصٍ لا يمكن إجراؤه، لكن نسجّله بوضوح.
|
|
||||||
error_log("[accept_ride] تعذّر فحص blockedUntil (شغّل"
|
|
||||||
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eBlock->getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -111,44 +111,49 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// STEP B1 — تثبيت مسافة الاقتراب لحظة القبول
|
// STEP B1 — تثبيت مسافة الاقتراب لحظة القبول (في Redis لا MySQL)
|
||||||
// هذه هي المسافة التي سيقطعها السائق نحو الراكب، وهي أساس تعويض عدم
|
//
|
||||||
// الحضور لاحقاً. تُقاس الآن حصراً: عند الإلغاء يكون السائق واقفاً عند
|
// هذه المسافة أساس تعويض عدم الحضور لاحقاً، ويجب أن تُقاس الآن حصراً:
|
||||||
// نقطة الالتقاط فتصير المسافة صفراً، فلا يمكن استنتاجها بأثر رجعي.
|
// عند الإلغاء يكون السائق واقفاً عند نقطة الالتقاط فتصير صفراً.
|
||||||
// مصدر الموقع هو Redis لا الطلب — حتى لا يتحكم العميل بالمبلغ.
|
//
|
||||||
|
// لكنها قيمة عابرة تُقرأ مرة واحدة عند نهاية الرحلة — فلا تستحق كتابتين
|
||||||
|
// في MySQL على أسخن مسار في النظام. تُحفظ في نفس الهاش الذي يستخدمه
|
||||||
|
// السوكِت لحالة الرحلة (ride:$id:state)، وتُثبَّت في MySQL كتابةً واحدة
|
||||||
|
// مع تحديث الحالة النهائي عند الإلغاء.
|
||||||
|
//
|
||||||
|
// hSet لا يمسح حقول السوكِت الأخرى في نفس الهاش، وTTL الـ24 ساعة الذي
|
||||||
|
// يضبطه السوكِت يغطي هذه الحقول تلقائياً.
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
try {
|
try {
|
||||||
$stmtPickup = $con->prepare("SELECT start_location FROM ride WHERE id = ?");
|
|
||||||
$stmtPickup->execute([$rideId]);
|
|
||||||
$pickupRaw = (string) $stmtPickup->fetchColumn();
|
|
||||||
|
|
||||||
[$pickupLat, $pickupLng] = array_pad(
|
|
||||||
array_map('trim', explode(',', $pickupRaw, 2)), 2, null
|
|
||||||
);
|
|
||||||
|
|
||||||
$approachKm = 0.0;
|
$approachKm = 0.0;
|
||||||
if (isset($redisLocation) && $redisLocation
|
if (isset($redisLocation) && $redisLocation) {
|
||||||
&& is_numeric($pickupLat) && is_numeric($pickupLng)) {
|
$stmtPickup = $con->prepare("SELECT start_location FROM ride WHERE id = ?");
|
||||||
$loc = $redisLocation->hGetAll("driver:public:$driverId");
|
$stmtPickup->execute([$rideId]);
|
||||||
if (!empty($loc['lat']) && !empty($loc['lng'])) {
|
[$pickupLat, $pickupLng] = array_pad(
|
||||||
$approachKm = round(getDistanceBetweenPoints(
|
array_map('trim', explode(',', (string) $stmtPickup->fetchColumn(), 2)), 2, null
|
||||||
(float) $loc['lat'], (float) $loc['lng'],
|
);
|
||||||
(float) $pickupLat, (float) $pickupLng
|
|
||||||
), 3);
|
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) {
|
if ($approachKm <= 0) {
|
||||||
$sqlApproach = "UPDATE ride SET pickupApproachKm = ? WHERE id = ?";
|
|
||||||
$con->prepare($sqlApproach)->execute([$approachKm, $rideId]);
|
|
||||||
try {
|
|
||||||
$con_ride->prepare($sqlApproach)->execute([$approachKm, $rideId]);
|
|
||||||
} catch (PDOException $e) { /* primary يكفي لحساب التعويض */ }
|
|
||||||
} else {
|
|
||||||
error_log("[accept_ride] تعذّر قياس مسافة الاقتراب RideID=$rideId"
|
error_log("[accept_ride] تعذّر قياس مسافة الاقتراب RideID=$rideId"
|
||||||
. " — التعويض سيحتسب زمن الانتظار وحده");
|
. " — التعويض سيحتسب زمن الانتظار وحده");
|
||||||
}
|
}
|
||||||
} catch (PDOException $eApproach) {
|
} catch (Throwable $eApproach) {
|
||||||
|
// قياس التعويض لا يُسقط قبول الرحلة أبداً.
|
||||||
error_log("[accept_ride] pickupApproachKm WARNING: " . $eApproach->getMessage());
|
error_log("[accept_ride] pickupApproachKm WARNING: " . $eApproach->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,23 +43,23 @@ try {
|
|||||||
// الحالة تُحدَّث والراكب يُشعَر كالمعتاد — لكن لا نختم وقتاً يترتب عليه
|
// الحالة تُحدَّث والراكب يُشعَر كالمعتاد — لكن لا نختم وقتاً يترتب عليه
|
||||||
// مال لا نستطيع إثباته. النتيجة: لا تعويض لهذه الرحلة، وسجلّ واضح.
|
// مال لا نستطيع إثباته. النتيجة: لا تعويض لهذه الرحلة، وسجلّ واضح.
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// الختم في Redis لا MySQL — قيمة عابرة تُقرأ مرة واحدة عند الإلغاء،
|
||||||
|
// وتُثبَّت في MySQL كتابةً واحدة هناك. hSetNx يجعله ذرّياً: نداء ثانٍ
|
||||||
|
// لا يعيد ضبط عدّاد الانتظار.
|
||||||
try {
|
try {
|
||||||
$stmtRide = $con->prepare("SELECT * FROM ride WHERE id = ? AND driver_id = ?");
|
$stateKey = "ride:$rideId:state";
|
||||||
$stmtRide->execute([$rideId, $driverId]);
|
|
||||||
$rideRow = $stmtRide->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (is_array($rideRow)
|
if (isset($redisLocation) && $redisLocation
|
||||||
&& array_key_exists('driverArrivedAt', $rideRow)
|
&& !$redisLocation->hExists($stateKey, 'driver_arrived_at')) {
|
||||||
&& empty($rideRow['driverArrivedAt'])) {
|
|
||||||
|
|
||||||
|
$stmtPickup = $con->prepare("SELECT start_location FROM ride WHERE id = ? AND driver_id = ?");
|
||||||
|
$stmtPickup->execute([$rideId, $driverId]);
|
||||||
[$pLat, $pLng] = array_pad(
|
[$pLat, $pLng] = array_pad(
|
||||||
array_map('trim', explode(',', (string) $rideRow['start_location'], 2)), 2, null
|
array_map('trim', explode(',', (string) $stmtPickup->fetchColumn(), 2)), 2, null
|
||||||
);
|
);
|
||||||
|
|
||||||
$canStamp = false;
|
$canStamp = false;
|
||||||
if (isset($redisLocation) && $redisLocation
|
if (is_numeric($pLat) && is_numeric($pLng)) {
|
||||||
&& is_numeric($pLat) && is_numeric($pLng)) {
|
|
||||||
|
|
||||||
$loc = $redisLocation->hGetAll("driver:public:$driverId");
|
$loc = $redisLocation->hGetAll("driver:public:$driverId");
|
||||||
if (!empty($loc['lat']) && !empty($loc['lng'])) {
|
if (!empty($loc['lat']) && !empty($loc['lng'])) {
|
||||||
$gap = getDistanceBetweenPoints(
|
$gap = getDistanceBetweenPoints(
|
||||||
@@ -75,23 +75,20 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($canStamp) {
|
if ($canStamp) {
|
||||||
// WHERE driverArrivedAt IS NULL يجعله ذرّياً: نداء ثانٍ لا
|
$redisLocation->hSetNx($stateKey, 'driver_arrived_at', time());
|
||||||
// يعيد ضبط عدّاد الانتظار.
|
// تأمين TTL: لو وصل هذا الطلب قبل أول update_ride_state من
|
||||||
$sqlStamp = "UPDATE ride SET driverArrivedAt = NOW()
|
// السوكِت، الهاش يكون بلا انتهاء صلاحية.
|
||||||
WHERE id = ? AND driver_id = ? AND driverArrivedAt IS NULL";
|
if ($redisLocation->ttl($stateKey) < 0) {
|
||||||
$con->prepare($sqlStamp)->execute([$rideId, $driverId]);
|
$redisLocation->expire($stateKey, 86400);
|
||||||
try {
|
}
|
||||||
$con_ride->prepare($sqlStamp)->execute([$rideId, $driverId]);
|
|
||||||
} catch (PDOException $e) { /* primary هو المرجع لهذا الحقل */ }
|
|
||||||
} else {
|
} else {
|
||||||
error_log("[arrive_ride] تعذّر إثبات الوصول — لا تعويض لهذه"
|
error_log("[arrive_ride] تعذّر إثبات الوصول — لا تعويض لهذه"
|
||||||
. " الرحلة (ride=$rideId driver=$driverId)");
|
. " الرحلة (ride=$rideId driver=$driverId)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (PDOException $eStamp) {
|
} catch (Throwable $eStamp) {
|
||||||
// العمود غير مُرحَّل بعد — الوصول يعمل، التعويض معطّل.
|
// فشل الختم لا يمنع إشعار الراكب بالوصول.
|
||||||
error_log("[arrive_ride] تعذّر ختم driverArrivedAt (شغّل"
|
error_log("[arrive_ride] تعذّر ختم وقت الوصول: " . $eStamp->getMessage());
|
||||||
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eStamp->getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. جلب بيانات الراكب للإرسال
|
// 3. جلب بيانات الراكب للإرسال
|
||||||
|
|||||||
@@ -67,6 +67,22 @@ try {
|
|||||||
// نوع السيارة تحت أي من الاسمين
|
// نوع السيارة تحت أي من الاسمين
|
||||||
$rideCarType = (string) ($rideRow['carType'] ?? $rideRow['car_type'] ?? 'Speed');
|
$rideCarType = (string) ($rideRow['carType'] ?? $rideRow['car_type'] ?? 'Speed');
|
||||||
|
|
||||||
|
// مدخلا التعويض عاشا في Redis طوال الرحلة ولم يلمسا MySQL. نقرأهما
|
||||||
|
// هنا قراءةً واحدة يشترك فيها الحساب (القسم ٤٫١) والتوثيق (القسم ٣).
|
||||||
|
$approachKm = 0.0;
|
||||||
|
$arrivedTs = 0;
|
||||||
|
try {
|
||||||
|
if (isset($redisLocation) && $redisLocation) {
|
||||||
|
$state = $redisLocation->hMGet("ride:$rideId:state",
|
||||||
|
['pickup_approach_km', 'driver_arrived_at']) ?: [];
|
||||||
|
$approachKm = (float) ($state['pickup_approach_km'] ?? 0);
|
||||||
|
$arrivedTs = (int) ($state['driver_arrived_at'] ?? 0);
|
||||||
|
}
|
||||||
|
} catch (Throwable $eState) {
|
||||||
|
error_log("[cancel_ride_by_driver] تعذّرت قراءة حالة الرحلة من Redis: "
|
||||||
|
. $eState->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
if ($assignedDriver === false) {
|
if ($assignedDriver === false) {
|
||||||
$con->rollBack();
|
$con->rollBack();
|
||||||
jsonError("Ride not found");
|
jsonError("Ride not found");
|
||||||
@@ -118,15 +134,32 @@ try {
|
|||||||
|
|
||||||
// تثبيت الإيقاف على الخادم. كان يُكتب في تخزين تطبيق السائق وحده
|
// تثبيت الإيقاف على الخادم. كان يُكتب في تخزين تطبيق السائق وحده
|
||||||
// (statusDriverLocation='blocked')، فيُفكّ بمسح البيانات أو إعادة
|
// (statusDriverLocation='blocked')، فيُفكّ بمسح البيانات أو إعادة
|
||||||
// التثبيت. الآن acceptRide.php يرفض القبول ما دام blockedUntil مستقبلياً.
|
// التثبيت. الآن acceptRide.php يرفض القبول ما دام المفتاح حياً.
|
||||||
// GREATEST يمنع إلغاءً جديداً من تقصير إيقاف سارٍ أطول.
|
//
|
||||||
|
// Redis هو مسار الفحص (TTL ينهي الإيقاف تلقائياً بلا كرون تنظيف)،
|
||||||
|
// وMySQL سجل دائم للمراجعة الإدارية لا يُقرأ في المسار الحار.
|
||||||
|
// هذه الكتابة تحدث في الإلغاء الثالث فقط — لا على كل رحلة.
|
||||||
|
try {
|
||||||
|
if (isset($redisLocation) && $redisLocation) {
|
||||||
|
$blockKey = "driver:$driverId:blocked";
|
||||||
|
$remaining = $redisLocation->ttl($blockKey);
|
||||||
|
// لا نُقصّر إيقافاً سارياً أطول بإلغاء جديد.
|
||||||
|
if ($remaining < 4 * 3600) {
|
||||||
|
$redisLocation->setex($blockKey, 4 * 3600, $blockUntil);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable $eRedisBlock) {
|
||||||
|
error_log("[cancel_ride_by_driver] تعذّر تثبيت الإيقاف في Redis: "
|
||||||
|
. $eRedisBlock->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$con->prepare("UPDATE driver
|
$con->prepare("UPDATE driver
|
||||||
SET blockedUntil = GREATEST(COALESCE(blockedUntil, ?), ?)
|
SET blockedUntil = GREATEST(COALESCE(blockedUntil, ?), ?)
|
||||||
WHERE id = ?")
|
WHERE id = ?")
|
||||||
->execute([$blockUntil, $blockUntil, $driverId]);
|
->execute([$blockUntil, $blockUntil, $driverId]);
|
||||||
} catch (PDOException $eBlock) {
|
} catch (PDOException $eBlock) {
|
||||||
error_log("[cancel_ride_by_driver] تعذّر تثبيت الإيقاف (شغّل"
|
error_log("[cancel_ride_by_driver] تعذّر تسجيل الإيقاف في MySQL (شغّل"
|
||||||
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eBlock->getMessage());
|
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eBlock->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,14 +167,35 @@ try {
|
|||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 3. تحديث حالة الرحلة في جدول ride
|
// 3. تحديث حالة الرحلة في جدول ride
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
$sqlRide = "UPDATE ride SET status = ?, driver_id = 0 WHERE id = ?";
|
// التوثيق النهائي: بيانات التعويض التي عاشت في Redis طوال الرحلة
|
||||||
|
// تُثبَّت هنا داخل نفس جملة تحديث الحالة — لا كتابة إضافية.
|
||||||
|
// تُقرأ من Redis قبل هذه النقطة (القسم ٤٫١ يقرأها للحساب)، لذا نقرأها
|
||||||
|
// مرة واحدة هنا ونمرّرها للطرفين.
|
||||||
|
$stampArrivedAt = $arrivedTs > 0 ? date('Y-m-d H:i:s', $arrivedTs) : null;
|
||||||
|
|
||||||
|
$sqlRide = "UPDATE ride SET status = ?, driver_id = 0,
|
||||||
|
pickupApproachKm = ?, driverArrivedAt = ?
|
||||||
|
WHERE id = ?";
|
||||||
|
$sqlRideLegacy = "UPDATE ride SET status = ?, driver_id = 0 WHERE id = ?";
|
||||||
|
$rideParams = [$statusText, $approachKm, $stampArrivedAt, $rideId];
|
||||||
|
|
||||||
// Local DB
|
// Local DB
|
||||||
$con->prepare($sqlRide)->execute([$statusText, $rideId]);
|
try {
|
||||||
|
$con->prepare($sqlRide)->execute($rideParams);
|
||||||
|
} catch (PDOException $eCols) {
|
||||||
|
// أعمدة التعويض غير مُرحَّلة بعد — الإلغاء نفسه لا يتوقف.
|
||||||
|
error_log("[cancel_ride_by_driver] أعمدة التعويض غير موجودة (شغّل"
|
||||||
|
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eCols->getMessage());
|
||||||
|
$con->prepare($sqlRideLegacy)->execute([$statusText, $rideId]);
|
||||||
|
}
|
||||||
|
|
||||||
// Remote DB (إن وجد)
|
// Remote DB (إن وجد)
|
||||||
if (isset($con_ride)) {
|
if (isset($con_ride)) {
|
||||||
$con_ride->prepare($sqlRide)->execute([$statusText, $rideId]);
|
try {
|
||||||
|
$con_ride->prepare($sqlRide)->execute($rideParams);
|
||||||
|
} catch (PDOException $eCols) {
|
||||||
|
$con_ride->prepare($sqlRideLegacy)->execute([$statusText, $rideId]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 تصفير التتابع لأن السائق ألغى الرحلة
|
// 🆕 تصفير التتابع لأن السائق ألغى الرحلة
|
||||||
@@ -180,21 +234,20 @@ try {
|
|||||||
$compensationCap = (float) ($kazan['cancelCompensationCap'] ?? 0);
|
$compensationCap = (float) ($kazan['cancelCompensationCap'] ?? 0);
|
||||||
$minWaitMinutes = (int) ($kazan['cancelMinWaitMinutes'] ?? 5);
|
$minWaitMinutes = (int) ($kazan['cancelMinWaitMinutes'] ?? 5);
|
||||||
|
|
||||||
$compensation = 0.0;
|
// $approachKm و$arrivedTs قُرئا من Redis قراءةً واحدة في القسم صفر.
|
||||||
|
$compensation = 0.0;
|
||||||
$waitedMinutes = 0;
|
$waitedMinutes = 0;
|
||||||
$approachKm = (float) ($rideRow['pickupApproachKm'] ?? 0);
|
|
||||||
$arrivedAt = $rideRow['driverArrivedAt'] ?? null;
|
|
||||||
|
|
||||||
if ($compensationCap <= 0) {
|
if ($compensationCap <= 0) {
|
||||||
// التعويض معطّل لهذه الدولة — الافتراضي حتى تضبطه الإدارة.
|
// التعويض معطّل لهذه الدولة — الافتراضي حتى تضبطه الإدارة.
|
||||||
error_log("[cancel_ride_by_driver] التعويض معطّل (cancelCompensationCap=0)"
|
error_log("[cancel_ride_by_driver] التعويض معطّل (cancelCompensationCap=0)"
|
||||||
. " ride=$rideId");
|
. " ride=$rideId");
|
||||||
} elseif (empty($arrivedAt)) {
|
} elseif ($arrivedTs <= 0) {
|
||||||
// لا ختم وصول ⇒ السائق ألغى قبل أن يصل. لا انتظار، فلا تعويض.
|
// لا ختم وصول ⇒ السائق ألغى قبل أن يصل، أو تعذّر إثبات وصوله.
|
||||||
error_log("[cancel_ride_by_driver] إلغاء قبل تسجيل الوصول — بلا تعويض"
|
error_log("[cancel_ride_by_driver] إلغاء بلا ختم وصول — بلا تعويض"
|
||||||
. " ride=$rideId driver=$driverId");
|
. " ride=$rideId driver=$driverId");
|
||||||
} else {
|
} else {
|
||||||
$waitedMinutes = (int) floor((time() - strtotime($arrivedAt)) / 60);
|
$waitedMinutes = (int) floor((time() - $arrivedTs) / 60);
|
||||||
|
|
||||||
if ($waitedMinutes < $minWaitMinutes) {
|
if ($waitedMinutes < $minWaitMinutes) {
|
||||||
error_log("[cancel_ride_by_driver] انتظار $waitedMinutes د < الحد"
|
error_log("[cancel_ride_by_driver] انتظار $waitedMinutes د < الحد"
|
||||||
|
|||||||
Reference in New Issue
Block a user