getMessage() . "\n"); exit(1); } // ═══════════════════════════════════════════════════════════════ // ١) الرحلات المشتبهة في النافذة الأخيرة // // ‏ملاحظة: cancel_ride_by_driver.php يصفّر ride.driver_id عند الإلغاء، // ‏فالسائق لا يُعرف من جدول ride. driver_orders هو من يحتفظ بالربط. // ═══════════════════════════════════════════════════════════════ $cancelPlaceholders = implode(',', array_fill(0, count(LEAK_CANCEL_STATUSES), '?')); $startedPlaceholders = implode(',', array_fill(0, count(LEAK_STARTED_STATUSES), '?')); $sql = " SELECT dord.driver_id, dord.order_id AS ride_id, dord.status AS cancel_status, r.passenger_id FROM driver_orders dord JOIN ride r ON r.id = dord.order_id WHERE dord.status IN ($cancelPlaceholders) AND dord.created_at >= NOW() - INTERVAL " . LEAK_SCAN_HOURS . " HOUR AND r.status NOT IN ($startedPlaceholders) AND dord.driver_id <> '0' AND r.passenger_id <> '' "; try { $stmt = $con->prepare($sql); $stmt->execute(array_merge(LEAK_CANCEL_STATUSES, LEAK_STARTED_STATUSES)); $suspects = $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { fwrite(STDERR, "[leak] فشل الفحص: " . $e->getMessage() . "\n"); exit(1); } // ═══════════════════════════════════════════════════════════════ // ٢) تقييم كل حالة وتسجيلها // ═══════════════════════════════════════════════════════════════ $pairStmt = $con->prepare(" SELECT COUNT(*) FROM driver_leak_events WHERE driver_id = ? AND passenger_id = ? AND created_at >= NOW() - INTERVAL " . LEAK_WINDOW_DAYS . " DAY "); $appStmt = $con->prepare(" SELECT COUNT(*) FROM driver_ride_scam WHERE driverID = ? AND rideID = ? "); $insertStmt = $con->prepare(" INSERT IGNORE INTO driver_leak_events (driver_id, passenger_id, ride_id, cancel_status, pair_repeat, app_reported, confidence) VALUES (?, ?, ?, ?, ?, ?, ?) "); $inserted = 0; $flagged = 0; foreach ($suspects as $s) { try { $pairStmt->execute([$s['driver_id'], $s['passenger_id']]); $pairRepeat = ((int) $pairStmt->fetchColumn()) + 1; $appStmt->execute([$s['driver_id'], $s['ride_id']]); $appReported = ((int) $appStmt->fetchColumn()) > 0 ? 1 : 0; // ── الثقة ── // ‏حالة منفردة ضعيفة عمداً: الإلغاء بعد القبول له أسباب مشروعة // ‏كثيرة (عطل، خطأ في العنوان، راكب غيّر رأيه). التكرار مع نفس // ‏الراكب هو ما يحوّلها من حادثة إلى نمط. $confidence = 0.25; if ($pairRepeat >= 2) $confidence = 0.60; if ($pairRepeat >= 3) $confidence = 0.85; // ‏بلاغ التطبيق (اتصل ثم أُلغيت) يرفع الثقة ولا يصنعها وحده. if ($appReported) $confidence = min(1.0, $confidence + 0.15); $insertStmt->execute([ $s['driver_id'], $s['passenger_id'], $s['ride_id'], $s['cancel_status'], $pairRepeat, $appReported, round($confidence, 3), ]); if ($insertStmt->rowCount() > 0) { $inserted++; if ($confidence >= LEAK_REVIEW_THRESHOLD) $flagged++; } } catch (PDOException $e) { fwrite(STDERR, "[leak] خطأ على الرحلة {$s['ride_id']}: " . $e->getMessage() . "\n"); } } // ═══════════════════════════════════════════════════════════════ // ٣) تحويل السجل إلى عقوبة في محرك الإسناد // // ‏العقوبة تُحسب من مجموع ثقة أحداث السائق في النافذة، لا من عددها — // ‏فحالتان ضعيفتان لا تساويان حالة واحدة مؤكدة. // ═══════════════════════════════════════════════════════════════ $penalized = 0; try { $scoreStmt = $con->prepare(" SELECT driver_id, SUM(confidence) AS total FROM driver_leak_events WHERE created_at >= NOW() - INTERVAL " . LEAK_WINDOW_DAYS . " DAY GROUP BY driver_id "); $scoreStmt->execute(); foreach ($scoreStmt->fetchAll(PDO::FETCH_ASSOC) as $row) { // ‏يُشبَع عند مجموع 3 — أي ثلاث حالات مؤكدة تقريباً = العقوبة كاملة. $penalty = min(LEAK_PENALTY_MAX, ((float) $row['total']) / 3.0); $redisLocation->hSet("driver:score:{$row['driver_id']}", 'leak_penalty', round($penalty, 3)); $penalized++; } } catch (Throwable $e) { fwrite(STDERR, "[leak] تعذّر تطبيق العقوبات: " . $e->getMessage() . "\n"); } // ‏تصفير عقوبة من خرج من النافذة: بلا هذا تبقى العقوبة أبدية في Redis // ‏حتى لو نظُف سجل السائق تماماً — أي عقوبة بلا انتهاء. try { $iterator = null; $redisLocation->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); $activeStmt = $con->prepare(" SELECT DISTINCT driver_id FROM driver_leak_events WHERE created_at >= NOW() - INTERVAL " . LEAK_WINDOW_DAYS . " DAY "); $activeStmt->execute(); $active = array_flip($activeStmt->fetchAll(PDO::FETCH_COLUMN)); while (($keys = $redisLocation->scan($iterator, 'driver:score:*', 200)) !== false) { foreach ($keys as $key) { $driverId = substr($key, strlen('driver:score:')); if ($driverId === '' || isset($active[$driverId])) continue; if ($redisLocation->hExists($key, 'leak_penalty')) { $redisLocation->hDel($key, 'leak_penalty'); } } } } catch (Throwable $e) { fwrite(STDERR, "[leak] تعذّر تنظيف العقوبات المنتهية: " . $e->getMessage() . "\n"); } echo "[leak] فُحص " . count($suspects) . " حالة، سُجّل $inserted جديدة،" . " منها $flagged تستحق مراجعة بشرية. عوقب $penalized سائق.\n";