getMessage() . "\n"); exit(1); } // ═══════════════════════════════════════════════════════════════ // ١) التقييمات — استعلام واحد مجمّع لكل السائقين // ‏نافذة ٩٠ يوماً: تقييم قبل سنة لا يصف السائق اليوم. // ═══════════════════════════════════════════════════════════════ $ratings = []; try { $stmt = $con->query(" SELECT driver_id, AVG(rating) AS avg_rating, COUNT(*) AS cnt FROM ratingDriver WHERE driver_id IS NOT NULL AND rating IS NOT NULL AND created_at >= NOW() - INTERVAL 90 DAY GROUP BY driver_id "); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { // ‏تقييم واحد أو اثنان لا يكفيان لحكم — ننعّمه نحو المحايد (٣٫٥/٥) $n = (int) $row['cnt']; $avg = (float) $row['avg_rating']; $ratings[$row['driver_id']] = round((($avg * $n) + (3.5 * 3)) / ($n + 3), 3); } } catch (PDOException $e) { fwrite(STDERR, "[driver_score] تعذّر جلب التقييمات: " . $e->getMessage() . "\n"); } // ═══════════════════════════════════════════════════════════════ // ٢) المرور على مفاتيح الدرجات الموجودة // ‏scan لا keys: keys تحجب Redis بالكامل على قاعدة كبيرة. // ═══════════════════════════════════════════════════════════════ $processed = 0; $iterator = null; // ‏بلا هذا الضبط يرجع phpredis false عند دفعة فارغة فتتوقف الحلقة مبكراً $redisLocation->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); while (($keys = $redisLocation->scan($iterator, 'driver:score:*', 200)) !== false) { foreach ($keys as $key) { $driverId = substr($key, strlen('driver:score:')); if ($driverId === '') continue; try { $row = $redisLocation->hGetAll($key) ?: []; $offers = (float) ($row['offers'] ?? 0); $accepts = (float) ($row['accepts'] ?? 0); // ‏لا يتجاوز المقبول المعروض — يحدث لو أُعيد ضبط أحدهما وحده. $accepts = min($accepts, $offers); $rate = ($accepts + SCORE_PRIOR_ACCEPTS) / ($offers + SCORE_PRIOR_OFFERS); $update = ['accept_rate' => round($rate, 4)]; if (isset($ratings[$driverId])) { $update['rating'] = $ratings[$driverId]; } // ‏الاضمحلال: نافذة متحرّكة بدل سجل العمر كله. $update['offers'] = round($offers * SCORE_DECAY, 3); $update['accepts'] = round($accepts * SCORE_DECAY, 3); $redisLocation->hMSet($key, $update); $redisLocation->expire($key, SCORE_KEY_TTL); $processed++; } catch (Throwable $e) { fwrite(STDERR, "[driver_score] خطأ على $key: " . $e->getMessage() . "\n"); } } } // ═══════════════════════════════════════════════════════════════ // ٣) سائقون لهم تقييم ولم يُعرض عليهم شيء بعد // ‏بلا هذه الخطوة يظل التقييم معطّلاً حتى أول عرض — أي أن السائق // ‏الممتاز الجديد لا يستفيد من تقييمه في أول ترتيب له. // ═══════════════════════════════════════════════════════════════ $seeded = 0; foreach ($ratings as $driverId => $rating) { $key = "driver:score:$driverId"; try { if (!$redisLocation->exists($key)) { $redisLocation->hMSet($key, [ 'rating' => $rating, 'accept_rate' => round(SCORE_PRIOR_ACCEPTS / SCORE_PRIOR_OFFERS, 4), ]); $redisLocation->expire($key, SCORE_KEY_TTL); $seeded++; } } catch (Throwable $e) { // تجاهُل — المرور القادم يعالجه } } echo "[driver_score] حُدِّث $processed سائق، وأُنشئ $seeded من التقييمات." . " تقييمات محمّلة: " . count($ratings) . "\n";