getMessage() . "\n"); } echo "[PredictiveDemand] Starting...\n"; $GRID_SIZE = 0.01; // حجم الخلية (~1.1km) $RADIUS_DEGREES = 0.045; // ~5km $ANTI_SPAM_TTL = 7200; // 2 ساعة $TOP_ZONES = 10; // أفضل 10 مناطق $WEEKS_HISTORY = 4; // تحليل آخر 4 أسابيع // ───────────────────────────────────────────────────────────── // 1. تحليل الطلبات التاريخية (نفس الساعة + نفس اليوم من الأسبوع) // ───────────────────────────────────────────────────────────── $currentHour = (int)date('H'); $currentDow = (int)date('N'); // 1=Mon ... 7=Sun $targetHourNext = ($currentHour + 1) % 24; // الساعة القادمة (المتوقع) $sqlHistory = " SELECT ROUND(SUBSTRING_INDEX(start_location, ',', 1) / {$GRID_SIZE}) * {$GRID_SIZE} AS cell_lat, ROUND(SUBSTRING_INDEX(start_location, ',', -1) / {$GRID_SIZE}) * {$GRID_SIZE} AS cell_lng, 'JO' AS country_code, COUNT(*) AS demand_score FROM ride WHERE status IN ('completed', 'cancelled_by_driver', 'timeout') AND HOUR(created_at) = :next_hour AND DAYOFWEEK(created_at) = :dow AND created_at >= DATE_SUB(NOW(), INTERVAL :weeks WEEK) AND start_location != '' AND start_location IS NOT NULL GROUP BY cell_lat, cell_lng, country_code HAVING demand_score >= 2 ORDER BY demand_score DESC LIMIT :top "; try { $stmt = $conRide->prepare($sqlHistory); $stmt->bindValue(':next_hour', $targetHourNext, PDO::PARAM_INT); $stmt->bindValue(':dow', $currentDow, PDO::PARAM_INT); $stmt->bindValue(':weeks', $WEEKS_HISTORY, PDO::PARAM_INT); $stmt->bindValue(':top', $TOP_ZONES * 3, PDO::PARAM_INT); // نأخذ أكثر لنصفيها لاحقاً $stmt->execute(); $hotZones = $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { die("[PredictiveDemand] Query error: " . $e->getMessage() . "\n"); } if (empty($hotZones)) { echo "[PredictiveDemand] Not enough historical data for this hour. Exiting.\n"; exit; } echo "[PredictiveDemand] Found " . count($hotZones) . " hot zones.\n"; // خذ أفضل N منطقة $hotZones = array_slice($hotZones, 0, $TOP_ZONES); // ───────────────────────────────────────────────────────────── // 2. تسمية المناطق باستخدام geofence_zones الموجودة أو اسم // عام بناءً على الإحداثيات // ───────────────────────────────────────────────────────────── function getZoneName($con, $lat, $lng, $radius) { try { $stmt = $con->prepare(" SELECT zone_name FROM geofence_zones WHERE is_active = 1 AND ABS(latitude - :lat) < :r AND ABS(longitude - :lng) < :r ORDER BY ABS(latitude - :lat) + ABS(longitude - :lng) ASC LIMIT 1 "); $stmt->execute([':lat' => $lat, ':lng' => $lng, ':r' => $radius]); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row ? $row['zone_name'] : null; } catch (Exception $e) { return null; } } // ───────────────────────────────────────────────────────────── // 3. حفظ النتائج في Redis للـ API // ───────────────────────────────────────────────────────────── $redisData = []; foreach ($hotZones as &$zone) { $zoneName = getZoneName($con, $zone['cell_lat'], $zone['cell_lng'], $RADIUS_DEGREES); $zone['zone_name'] = $zoneName ?? "منطقة ({$zone['cell_lat']}, {$zone['cell_lng']})"; $redisData[] = [ 'lat' => (float)$zone['cell_lat'], 'lng' => (float)$zone['cell_lng'], 'zone_name' => $zone['zone_name'], 'demand_score' => (int)$zone['demand_score'], 'predicted_hour' => $targetHourNext, 'country_code' => $zone['country_code'], ]; } unset($zone); if ($redis) { $redis->setex( 'siro:cache:predictive_demand', 3600, // TTL ساعة واحدة json_encode([ 'last_updated' => date('Y-m-d H:i:s'), 'predicted_hour' => $targetHourNext, 'zones' => $redisData, ], JSON_UNESCAPED_UNICODE) ); echo "[PredictiveDemand] Zones cached in Redis.\n"; } // ───────────────────────────────────────────────────────────── // 4. إيجاد السائقين المتاحين قرب كل منطقة وإرسال إشعار // ───────────────────────────────────────────────────────────── $encryptionHelper = new EncryptionHelper(); $totalSent = 0; $totalSkipped = 0; $notifiedDrivers = []; // لتجنب إرسال نفس السائق أكثر من إشعار foreach ($hotZones as $zone) { $cellLat = (float)$zone['cell_lat']; $cellLng = (float)$zone['cell_lng']; $zoneName = $zone['zone_name']; $score = (int)$zone['demand_score']; $minutes = 30; // التوقع دائماً 30 دقيقة للأمام // استخرج رسالة مناسبة بناءً على حجم الطلب if ($score >= 10) { $intensity = 'مرتفع جداً 🔥🔥'; } elseif ($score >= 5) { $intensity = 'مرتفع 🔥'; } else { $intensity = 'متوسط 📈'; } $title = "🔮 توقع ذكي من سيرو"; $body = "نتوقع طلب {$intensity} من منطقة «{$zoneName}» خلال {$minutes} دقيقة. اتجه الآن! 🚗"; // ── جلب السائقين المتاحين في نطاق 5km ────────────────── $sqlDrivers = " SELECT d.id AS driver_id, t.token, dl.lat, dl.lng FROM drivers d JOIN driver_live_location dl ON dl.driver_id = d.id JOIN tokens t ON t.driverID = d.id WHERE d.status_driver = 'free' AND d.verified = 1 AND dl.updated_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND ABS(dl.lat - :lat) < :radius AND ABS(dl.lng - :lng) < :radius LIMIT 50 "; try { $stmtD = $con->prepare($sqlDrivers); $stmtD->execute([ ':lat' => $cellLat, ':lng' => $cellLng, ':radius' => $RADIUS_DEGREES, ]); $nearbyDrivers = $stmtD->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { // جدول driver_live_location قد يختلف اسمه error_log("[PredictiveDemand] Driver query error: " . $e->getMessage()); continue; } echo "[PredictiveDemand] Zone [{$zoneName}] score={$score} → " . count($nearbyDrivers) . " nearby drivers.\n"; foreach ($nearbyDrivers as $driver) { $driverId = $driver['driver_id']; // تخطّى إذا أرسلنا لهذا السائق سابقاً في نفس الـ cron run if (isset($notifiedDrivers[$driverId])) { $totalSkipped++; continue; } // Anti-spam: مرة كل ساعتين لكل سائق $redisKey = "predictive:sent:{$driverId}"; if ($redis && $redis->exists($redisKey)) { $totalSkipped++; continue; } $decryptedToken = $encryptionHelper->decryptData($driver['token'] ?? ''); if (!$decryptedToken) continue; sendFCM_Internal( $decryptedToken, $title, $body, [ 'type' => 'predictive_demand', 'zone_name' => $zoneName, 'zone_lat' => $cellLat, 'zone_lng' => $cellLng, 'demand_score' => $score, 'predicted_hour' => $targetHourNext, ], 'System', false, 'ding' ); // حفظ في Redis anti-spam if ($redis) { $redis->setex($redisKey, $ANTI_SPAM_TTL, '1'); } $notifiedDrivers[$driverId] = true; $totalSent++; } } echo "[PredictiveDemand] Done ✅ — Sent: {$totalSent}, Skipped: {$totalSkipped}\n"; ?>