Update: 2026-07-09 03:41:48

This commit is contained in:
Hamza-Ayed
2026-07-09 03:41:48 +03:00
parent 7dff7b6973
commit 70718946f5
10 changed files with 295 additions and 639 deletions
+9 -15
View File
@@ -66,26 +66,20 @@ if ($avgPricePerKm <= 0) {
}
// Calculate the competitor's total price based on distance and average market per-km rate
// 🔥 لا يوجد أي تعديل صناعي على سعر المنافس هنا — الرقم المعروض للراكب
// يجب أن يعكس بيانات السوق الحقيقية فقط، حتى لو لم نكن أرخص فعلياً في هذه الرحلة.
$competitorTotalPrice = round($distance * $avgPricePerKm, 2);
// Make sure competitor is slightly higher than us for psychological effect
// if their raw math somehow ended up lower due to straight per-km multiplication
if ($siroPrice > 0 && $competitorTotalPrice <= $siroPrice) {
// Force a dynamic difference based on country
$multiplier = ($countryCode === 'JO') ? 1.15 : 1.10;
$competitorTotalPrice = round($siroPrice * $multiplier, 2);
}
// Calculate savings
$savingsPct = 0;
if ($competitorTotalPrice > 0 && $siroPrice < $competitorTotalPrice) {
$savingsPct = (($competitorTotalPrice - $siroPrice) / $competitorTotalPrice) * 100;
}
// Format the labels
$compNameAr = 'التطبيقات الأخرى';
$savingsLabel = "أوفر بـ " . number_format($savingsPct, 1) . "% من $compNameAr ⚡";
// نعرض شارة "أوفر" فقط إذا كنا أرخص فعلياً حسب البيانات الحقيقية — لا تلاعب بالأرقام
$savingsPct = 0;
$savingsLabel = null;
if ($competitorTotalPrice > 0 && $siroPrice > 0 && $siroPrice < $competitorTotalPrice) {
$savingsPct = (($competitorTotalPrice - $siroPrice) / $competitorTotalPrice) * 100;
$savingsLabel = "أوفر بـ " . number_format($savingsPct, 1) . "% من $compNameAr ⚡";
}
$siroCommissionRate = 0.14; // Default 14% commission
if ($countryCode === 'JO') $siroCommissionRate = 0.14;
+13 -1
View File
@@ -119,7 +119,18 @@ try {
// --- Location Server Redis ---
$redisLocation = new Redis();
$locHost = getenv('REDIS_LOCATION_HOST') ?: $redisHost;
// 🔥 [Fix Silent Fallback] إذا لم تُضبط REDIS_LOCATION_HOST صراحة، نسقط
// على Redis الرئيسي — وهذا يجعل استعلامات كثافة السائقين (geo:drivers:*)
// ترجع فارغة بصمت لأن تلك المفاتيح تُكتب فقط على Redis الخاص بلوكيشن
// سيرفر. نسجّل تحذيراً واضحاً حتى لا يمر هذا دون ملاحظة في اللوجز.
$locHostConfigured = getenv('REDIS_LOCATION_HOST');
if (!$locHostConfigured) {
error_log('[REDIS] ⚠️ REDIS_LOCATION_HOST is not set — $redisLocation is falling back to the MAIN redis host (' . $redisHost . '). ' .
'geo:drivers:available / driver:profile:* / driver:public:* keys live only on the location-server Redis, ' .
'so driver-density lookups (getSpeed.php, heatmap_live.php, pricing/get.php) will silently return empty results ' .
'unless REDIS_LOCATION_HOST/PORT/PASSWORD are configured correctly in .env.');
}
$locHost = $locHostConfigured ?: $redisHost;
$locPort = (int)(getenv('REDIS_LOCATION_PORT') ?: $redisPort);
$locPass = getenv('REDIS_LOCATION_PASSWORD') ?: $redisPass;
@@ -127,6 +138,7 @@ try {
if ($locPass) $redisLocation->auth($locPass);
// No prefix for location server
} else {
error_log("[REDIS] ⚠️ Failed to connect \$redisLocation to $locHost:$locPort — driver-density features will be degraded.");
$redisLocation = null;
}
}
-553
View File
@@ -1,553 +0,0 @@
<?php
/**
* driver_socket.php
* ==================
* WebSocket Server للسائقين — بورت 2020
* Internal HTTP Server — بورت 2021
*
* 🚀 Level 2 Architecture (Production Ready):
* - Event Buffering (Batching)
* - Redis Pipelines (تقليل الـ I/O والـ Latency بشكل كبير)
* - Memory State Cache للسائقين
* - جميع طرق HTTP (Dispatch, Market, Force Disconnect...) موجودة بالكامل
*/
use Workerman\Worker;
use Workerman\Timer;
use Workerman\Http\Client as AsyncHttp;
use PHPSocketIO\SocketIO;
use Predis\Client as RedisClient;
require_once __DIR__ . '/vendor/autoload.php';
// ============================================================
// ⚙️ إعدادات عامة
// ============================================================
ini_set('memory_limit', '512M');
date_default_timezone_set('Asia/Amman');
// ── Tunables (إعدادات الأداء) ──────────────────────────────────
const MIN_MOVE_METERS = 10.0; // GEOADD فقط إذا تحرك أكثر من 10 متر
const HMSET_SPEED_DELTA = 1.0; // فرق السرعة المطلوب لتحديث Redis
const HMSET_HEADING_DELTA = 5.0; // فرق الاتجاه المطلوب لتحديث Redis
const EXPIRE_REFRESH_SECONDS = 300; // 5 دقائق لتجديد الـ TTL
const FORWARD_MIN_METERS = 15.0; // HTTP forward للراكب
const FORWARD_MAX_SECONDS = 3; // أقصى مدة للـ Forward
const REDIS_BATCH_INTERVAL = 0.5; // تنفيذ مجمّع (Batch) كل نصف ثانية (500ms)
// ─────────────────────────────────────────────────────────────
function logMsg(string $msg): void {
echo '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
}
function loadEnvironment(string $filePath): void {
if (!file_exists($filePath)) {
logMsg("⚠️ .env not found: $filePath");
return;
}
foreach (file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue;
[$name, $value] = explode('=', $line, 2);
putenv(trim($name) . '=' . trim($value, "\"'"));
}
logMsg('✅ Environment loaded.');
}
$siteUser = get_current_user();
$homeDir = "/home/$siteUser";
if (!is_dir($homeDir)) {
$homeDir = '/home/location'; // Fallback to original location
}
loadEnvironment(getenv('ENV_FILE_PATH') ?: ($homeDir . '/.env'));
// ============================================================
// 🔐 مفاتيح الأمان
// ============================================================
$INTERNAL_KEY = trim((string) @file_get_contents(getenv('INTERNAL_SOCKET_KEY_PATH') ?: ($homeDir . '/.internal_socket_key')));
$redisPass = trim((string) @file_get_contents(getenv('REDIS_PASS_KEY_PATH') ?: ($homeDir . '/.reds_pass_key')));
if (empty($INTERNAL_KEY)) logMsg('❌ CRITICAL: Internal key missing!');
if (empty($redisPass)) logMsg('❌ CRITICAL: Redis password missing!');
// ============================================================
// 🗄️ Redis Singleton
// ============================================================
$redis = null;
function getRedis(): ?RedisClient {
global $redis, $redisPass;
if ($redis !== null) {
try {
$redis->ping();
return $redis;
} catch (\Exception $e) {
logMsg('⚠️ Redis ping failed, reconnecting...');
$redis = null;
}
}
try {
$client = new RedisClient([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'password' => $redisPass,
'read_write_timeout' => 0,
]);
$client->connect();
$redis = $client;
return $redis;
} catch (\Exception $e) {
logMsg('❌ Redis Error: ' . $e->getMessage());
return null;
}
}
// ============================================================
// 📐 Haversine Distance (متر)
// ============================================================
function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371000;
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat / 2) ** 2
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
}
// ============================================================
// 📡 Forward موقع السائق → سيرفر الراكب (ASYNC)
// ============================================================
function forwardLocationToPassengerSocket(
string $driverId,
string $passengerId,
array $payload,
string $internalKey,
array &$fwdThrottle
): void {
if (empty($passengerId)) return;
$now = time();
$last = $fwdThrottle[$driverId] ?? null;
if ($last !== null) {
$timeDiff = $now - $last['ts'];
$dist = haversineDistance(
$last['lat'], $last['lng'],
(float)$payload['lat'], (float)$payload['lng']
);
if ($dist < FORWARD_MIN_METERS && $timeDiff < FORWARD_MAX_SECONDS) return;
}
$fwdThrottle[$driverId] = [
'ts' => $now,
'lat' => (float)$payload['lat'],
'lng' => (float)$payload['lng'],
];
$http = new AsyncHttp();
$http->request(
'http://127.0.0.1:3031',
[
'method' => 'POST',
'data' => http_build_query([
'action' => 'update_driver_location',
'passenger_id' => $passengerId,
'payload' => json_encode($payload),
]),
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'x-internal-key' => $internalKey,
'Connection' => 'close',
],
'timeout' => 3,
],
null,
fn(\Exception $e) => logMsg('⚠️ Forward failed: ' . $e->getMessage())
);
}
// ============================================================
// 📲 FCM (ASYNC)
// ============================================================
function sendFCM_Async(string $token, string $title, string $body, array $rideData): void {
if (empty($token)) return;
$http = new AsyncHttp();
$http->request(
'https://api.intaleq.xyz/siro/ride/firebase/send_fcm.php',
[
'method' => 'POST',
'data' => json_encode([
'target' => $token,
'title' => $title,
'body' => $body,
'isTopic' => false,
'category' => 'Order',
'tone' => 'start',
'passengerList' => json_encode($rideData),
]),
'headers' => ['Content-Type' => 'application/json; charset=UTF-8'],
'timeout' => 5,
],
null,
fn(\Exception $e) => logMsg('⚠️ FCM failed: ' . $e->getMessage())
);
}
// ============================================================
// 🧠 Memory State & Event Buffer
// ============================================================
$connectedDrivers = [];
$active_orders_drivers = [];
$driverState = [];
$fwdThrottle = [];
$eventBuffer = []; // 🚀 Level 2: مصفوفة تجميع الأحداث لـ Redis
// ============================================================
// 🚀 Socket.IO — بورت 2020
// ============================================================
$io = new SocketIO(2020);
// ============================================================
// A. Internal HTTP Server & Redis Batch Processor (Worker Start)
// ============================================================
$io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
// 🚀 1. Redis Pipeline Batch Processor (Level 2)
// يعمل كل نصف ثانية، يجمع كل الأوامر ويرسلها لـ Redis دفعة واحدة
Timer::add(REDIS_BATCH_INTERVAL, function() {
global $eventBuffer;
if (empty($eventBuffer)) return;
$redis = getRedis();
if (!$redis) return;
try {
$pipe = $redis->pipeline();
$processedCount = 0;
foreach ($eventBuffer as $driverId => $ops) {
$profileKey = "driver:profile:$driverId";
$processedCount++;
if (isset($ops['hmset'])) {
$pipe->hmset($profileKey, $ops['hmset']);
}
if (isset($ops['expire'])) {
$pipe->expire($profileKey, $ops['expire']);
}
if (isset($ops['status_change'])) {
$oldStatus = $ops['status_change']['old'];
$newStatus = $ops['status_change']['new'];
if ($oldStatus === 'on') $pipe->zrem('geo:drivers:busy', $driverId);
if ($oldStatus === 'off') $pipe->zrem('geo:drivers:available', $driverId);
if ($newStatus === 'close' || $newStatus === 'blocked') {
$pipe->zrem('geo:drivers:available', $driverId);
$pipe->zrem('geo:drivers:busy', $driverId);
}
}
if (isset($ops['geoadd'])) {
$st = $ops['geoadd']['status'];
$lng = $ops['geoadd']['lng'];
$lat = $ops['geoadd']['lat'];
if ($st === 'off') {
$pipe->geoadd('geo:drivers:available', $lng, $lat, $driverId);
} elseif ($st === 'on') {
$pipe->geoadd('geo:drivers:busy', $lng, $lat, $driverId);
}
}
}
$pipe->execute();
$eventBuffer = []; // إفراغ المصفوفة بعد التنفيذ الناجح
// logMsg("⚡ Processed Redis Batch: $processedCount drivers updated in 1 network call.");
} catch (\Exception $e) {
logMsg("⚠️ Redis Pipeline Error: " . $e->getMessage());
}
});
// 🌐 2. Internal HTTP Server — بورت 2021
$innerHttp = new Worker('http://0.0.0.0:2021');
$innerHttp->onMessage = function ($connection, $request) use ($io, $INTERNAL_KEY) {
global $active_orders_drivers, $connectedDrivers;
$headers = $request->header();
if (($headers['x-internal-key'] ?? '') !== $INTERNAL_KEY) {
$connection->send('Unauthorized');
return;
}
$post = $request->post();
$action = trim($post['action'] ?? '');
$redis = getRedis();
// ── 1. Dispatch Order ────────────────────────────────
if ($action === 'dispatch_order') {
$rideId = $post['ride_id'] ?? null;
$drivers = json_decode($post['drivers_ids'] ?? '[]', true);
$payload = $post['payload'] ?? [];
if (is_array($payload)) $payload = array_values($payload);
if ($rideId && !empty($drivers)) {
$active_orders_drivers[$rideId] = $drivers;
logMsg("🚀 Dispatch Ride #$rideId → " . count($drivers) . ' drivers.');
}
foreach ($drivers as $driverId) {
if (!isset($connectedDrivers[$driverId])) continue;
$io->to('driver_' . $driverId)->emit('new_ride_request', $payload);
$platform = $connectedDrivers[$driverId]['platform'] ?? 'android';
$token = $connectedDrivers[$driverId]['token'] ?? '';
if (!empty($token)) {
sendFCM_Async($token, 'طلب جديد', 'لديك رحلة جديدة قريبة منك', $payload);
}
}
$connection->send('Dispatched');
// ── 2. Market New Ride ────────────────────────────────
} elseif ($action === 'market_new_ride') {
$payload = $post['payload'] ?? [];
$rideId = $payload['id'] ?? null;
$lat = (float)($payload['start_lat'] ?? 0);
$lng = (float)($payload['start_lng'] ?? 0);
if (!$redis || !$rideId || $lat == 0 || $lng == 0) {
$connection->send('Error: Redis unavailable or invalid coords');
return;
}
$redis->geoadd('geo:rides:waiting', $lng, $lat, $rideId);
$nearbyDrivers = $redis->georadius('geo:drivers:available', $lng, $lat, 50, 'km');
$count = 0;
foreach ($nearbyDrivers as $driverId) {
if (isset($connectedDrivers[$driverId])) {
$io->to('driver_' . $driverId)->emit('market_new_ride', $payload);
$count++;
}
}
logMsg("📢 Market Ride #$rideId → $count drivers.");
$connection->send("Broadcasted to $count drivers");
// ── 3. Get Nearby Ride IDs ────────────────────────────
} elseif ($action === 'get_nearby_ride_ids') {
$lat = (float)($post['lat'] ?? 0);
$lng = (float)($post['lng'] ?? 0);
$radius = (float)($post['radius'] ?? 9);
if (!$redis) { $connection->send(json_encode([])); return; }
$results = $redis->georadius(
'geo:rides:waiting', $lng, $lat, $radius, 'km',
['WITHDIST' => true, 'SORT' => 'ASC', 'COUNT' => 40]
);
$connection->send(json_encode($results));
// ── 4. Ride Taken ─────────────────────────────────────
} elseif ($action === 'ride_taken_event') {
$rideId = $post['ride_id'] ?? null;
$winnerDriverId = $post['taken_by_driver_id'] ?? null;
if (!$rideId) { $connection->send('Error: Missing ride_id'); return; }
if ($redis) $redis->zrem('geo:rides:waiting', $rideId);
$io->emit('ride_taken', [
'ride_id' => $rideId,
'taken_by_driver_id' => $winnerDriverId,
]);
unset($active_orders_drivers[$rideId]);
logMsg("✅ Ride #$rideId taken by #$winnerDriverId.");
$connection->send('OK');
// ── 5. Force Disconnect ───────────────────────────────
} elseif ($action === 'force_disconnect') {
$driverId = $post['driver_id'] ?? null;
if ($driverId && isset($connectedDrivers[$driverId])) {
$connectedDrivers[$driverId]['conn']->disconnect();
unset($connectedDrivers[$driverId]);
if ($redis) {
$redis->zrem('geo:drivers:available', $driverId);
$redis->zrem('geo:drivers:busy', $driverId);
}
logMsg("🚫 Driver #$driverId force-disconnected.");
$connection->send('Disconnected');
} else {
$connection->send('Driver not connected');
}
} else {
$connection->send('Unknown action');
}
};
$innerHttp->listen();
});
// ============================================================
// B. WebSocket Events للسائقين
// ============================================================
$io->on('connection', function ($socket) use ($INTERNAL_KEY) {
global $connectedDrivers, $driverState, $fwdThrottle, $eventBuffer;
$query = $socket->handshake['query'] ?? [];
$driverId = $query['driver_id'] ?? null;
$platform = $query['platform'] ?? 'android';
$token = $query['token'] ?? '';
if (!$driverId) {
$socket->disconnect();
return;
}
$socket->join('driver_' . $driverId);
$connectedDrivers[$driverId] = [
'conn' => $socket,
'platform' => $platform,
'token' => $token,
];
if (!isset($driverState[$driverId])) {
$driverState[$driverId] = [
'lat' => 0.0,
'lng' => 0.0,
'speed' => -999.0,
'heading' => -999.0,
'status' => '',
'expire_ts' => 0,
];
}
logMsg("✅ Driver Connected: #$driverId ($platform)");
$socket->on('ping_alive', function () {
// Socket.IO handles pong automatically
});
$socket->on('update_location', function ($data)
use ($driverId, $INTERNAL_KEY, &$driverState, &$fwdThrottle, &$eventBuffer)
{
global $connectedDrivers;
$data = (array) $data;
$lat = isset($data['lat']) ? (float)$data['lat'] : null;
$lng = isset($data['lng']) ? (float)$data['lng'] : null;
$heading = (float)($data['heading'] ?? 0);
$speed = (float)($data['speed'] ?? 0);
$status = (string)($data['status'] ?? 'off');
$distance = (float)($data['distance'] ?? 0);
$passengerId = (string)($data['passenger_id'] ?? '');
$rideId = $data['ride_id'] ?? null;
if ($lat === null || $lng === null) return;
$state = &$driverState[$driverId];
$now = time();
// 1. Forward للراكب (ASYNC + throttle)
if (!empty($passengerId)) {
forwardLocationToPassengerSocket(
$driverId, $passengerId,
[
'latitude' => $lat,
'longitude' => $lng,
'heading' => $heading,
'speed' => $speed,
'ride_id' => $rideId,
'driver_id' => $driverId,
],
$INTERNAL_KEY, $fwdThrottle
);
}
// 2. حساب ماذا تغيّر لتجنب ضغط Redis
$movedMeters = ($state['lat'] == 0.0 && $state['lng'] == 0.0)
? 999.0
: haversineDistance($state['lat'], $state['lng'], $lat, $lng);
$didMove = $movedMeters >= MIN_MOVE_METERS;
$speedMs = $speed / 3.6;
$speedChanged = abs($speedMs - $state['speed']) >= HMSET_SPEED_DELTA;
$headingChanged = abs($heading - $state['heading']) >= HMSET_HEADING_DELTA;
$statusChanged = ($status !== $state['status']);
$needHmset = $speedChanged || $headingChanged || $statusChanged;
$needGeoadd = $didMove;
$needExpireRefresh = ($now - $state['expire_ts']) >= EXPIRE_REFRESH_SECONDS;
if (!$needHmset && (!$needGeoadd && !$statusChanged) && !$needExpireRefresh) {
return; // لم يتغير شيء مهم، تجاهل تماماً (0 عمليات Redis)
}
// 🚀 3. Buffering Event بدل الإرسال المباشر لـ Redis (Level 2 Magic)
if (!isset($eventBuffer[$driverId])) {
$eventBuffer[$driverId] = [];
}
if ($needHmset) {
$eventBuffer[$driverId]['hmset'] = [
'id' => $driverId, 'heading' => $heading, 'speed' => $speed, 'status' => $status, 'updated_at' => $now
];
$state['speed'] = $speedMs;
$state['heading'] = $heading;
}
if ($needExpireRefresh || $needHmset) {
$eventBuffer[$driverId]['expire'] = 900;
$state['expire_ts'] = $now;
}
if ($statusChanged) {
$eventBuffer[$driverId]['status_change'] = [
'old' => $state['status'],
'new' => $status
];
$state['status'] = $status;
// Auto disconnect if blocked
if ($status === 'blocked') {
if (isset($connectedDrivers[$driverId])) {
$connectedDrivers[$driverId]['conn']->disconnect();
unset($connectedDrivers[$driverId]);
}
}
}
if ($needGeoadd || $statusChanged) {
$eventBuffer[$driverId]['geoadd'] = [
'status' => $status,
'lng' => $lng,
'lat' => $lat
];
if ($needGeoadd) {
$state['lat'] = $lat;
$state['lng'] = $lng;
}
}
});
$socket->on('disconnect', function () use ($driverId) {
global $connectedDrivers, $driverState, $fwdThrottle;
unset($connectedDrivers[$driverId]);
unset($driverState[$driverId]);
unset($fwdThrottle[$driverId]);
logMsg("❌ Driver Disconnected: #$driverId");
});
});
Worker::runAll();
+4 -1
View File
@@ -79,7 +79,10 @@ function sendToLocationServer($action, $data) {
function findBestDrivers($con, $lat, $lng, $carType, $endLat = null, $endLng = null) {
// 1. الاتصال بـ Redis لجلب الأقرب
$locationServerUrl = "https://location.intaleq.xyz/api_get_nearby.php";
// 🔥 [Fix Hardcoded URL] كان مثبتاً على رابط الإنتاج مباشرة بخلاف كل استدعاء
// آخر لسيرفر اللوكيشن في هذا الملف (يعتمد على env)، فأي بيئة غير إنتاج كانت
// تضرب سيرفر الإنتاج الحقيقي بالخطأ.
$locationServerUrl = getenv('LOCATION_API_URL') ?: "https://location.intaleq.xyz/api_get_nearby.php";
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = ['lat' => $lat, 'lng' => $lng, 'radius' => 5, 'limit' => 100];
+24 -23
View File
@@ -77,31 +77,32 @@ try {
}
// =================================================================
// 2. إشعار السائق (Socket + FCM)
// 2. إشعار السائق/السائقين (Socket + FCM)
// =================================================================
// 🔥 يُرسل دائماً بغض النظر عن driver_id — إذا كانت الرحلة لم تُقبل بعد
// (driver_id = 0)، لوكيشن سيرفر يستخدم ride:offered_drivers:{rideId}
// من Redis لإشعار كل السائقين الذين وصلهم عرض هذه الرحلة أصلاً.
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
$internalKeyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '';
$internalKey = ($internalKeyPath && file_exists($internalKeyPath)) ? trim(file_get_contents($internalKeyPath)) : (getenv('INTERNAL_SOCKET_KEY') ?: '');
$ch = curl_init($socketUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'cancel_ride',
'driver_id' => $driverId,
'ride_id' => $rideId,
'reason' => $reason
]));
if (!empty($internalKey)) curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500);
curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
@curl_exec($ch);
curl_close($ch);
if ($driverId > 0) {
// أ) Socket (إشعار السائق في التطبيق فوراً)
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
$internalKeyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '';
$internalKey = ($internalKeyPath && file_exists($internalKeyPath)) ? trim(file_get_contents($internalKeyPath)) : (getenv('INTERNAL_SOCKET_KEY') ?: '');
$ch = curl_init($socketUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'cancel_ride',
'driver_id' => $driverId,
'ride_id' => $rideId,
'reason' => $reason
]));
if (!empty($internalKey)) curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500);
curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
@curl_exec($ch);
curl_close($ch);
// ب) FCM (باستخدام الدالة الجديدة مع فك التشفير)
// FCM للسائق الذي قَبِل الرحلة فعلاً (باستخدام الدالة الجديدة مع فك التشفير)
$driverToken = filterRequest("driver_token");
if (empty($driverToken)) {
+70 -39
View File
@@ -1,53 +1,85 @@
<?php
// cron_cleanup_waiting.php
// مسار الاتصال بقاعدة البيانات
require_once __DIR__ . '/../../get_connect.php';
// cron_ride_timeout.php
// 🔥 أُعيدت كتابته بالكامل: كان يعتمد على جدول waitingRides الذي لم يعد
// يُملأ من تدفق add_ride.php الحقيقي (addWaitingRide.php لا يُستدعى من هناك)،
// فكانت الرحلات العالقة بحالة 'waiting' لا تُصفَّر أبداً. الآن يعمل مباشرة
// على جدول ride باستخدام أعمدة date/time المكتوبة فعلياً عند الإنشاء.
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
// تسجيل في اللوج لبداية العملية
error_log("⏰ [Cleanup Cron] Started cleaning old waiting rides...");
$minutesLimit = 15;
try {
// المدة المسموحة بالدقائق
$minutesLimit = 15;
$con = Database::get('main');
// =========================================================
// الخطوة 1: تحديث الحالة في الجدول الرئيسي (الأرشيف)
// الخطوة 1: جلب الرحلات العالقة (status='waiting' منذ أكثر من 15 دقيقة)
// =========================================================
// نقوم بتحديث حالة الرحلات التي ستُحذف ليعلم الراكب أنها انتهت (Time Out)
// بدلاً من أن تبقى 'waiting' للأبد في سجلات الراكب
$sqlUpdate = "UPDATE ride
SET status = 'timeout'
WHERE id IN (
SELECT id FROM waitingRides
WHERE created_at < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)
) AND status = 'waiting'"; // نتأكد أننا نحدث ما هو معلق فقط
$sqlSelect = "SELECT id, passenger_id FROM ride
WHERE status = 'waiting'
AND TIMESTAMP(`date`, `time`) < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)";
$stmtSelect = $con->prepare($sqlSelect);
$stmtSelect->execute();
$staleRides = $stmtSelect->fetchAll(PDO::FETCH_ASSOC);
$stmtUpdate = $con->prepare($sqlUpdate);
$stmtUpdate->execute();
$updatedCount = $stmtUpdate->rowCount();
$updatedCount = 0;
if (!empty($staleRides)) {
$sqlUpdate = "UPDATE ride SET status = 'timeout', updated_at = NOW() WHERE id = ? AND status = 'waiting'";
$stmtUpdate = $con->prepare($sqlUpdate);
// نسخة أرشيفية (best-effort)
$con_ride = null;
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[cron_ride_timeout] Secondary ride DB unavailable: " . $e->getMessage());
}
$stmtUpdate2 = $con_ride ? $con_ride->prepare(
"UPDATE ride SET status = 'timeout', updated_at = NOW() WHERE id = ? AND status = 'waiting'"
) : null;
foreach ($staleRides as $ride) {
$rideId = $ride['id'];
$stmtUpdate->execute([$rideId]);
$updatedCount += $stmtUpdate->rowCount();
if ($stmtUpdate2) {
try {
$stmtUpdate2->execute([$rideId]);
} catch (PDOException $e) {
error_log("[cron_ride_timeout] Secondary DB update failed for #$rideId: " . $e->getMessage());
}
}
// 🆕 تنظيف Redis + إعلام أي سائقين ما زالوا يرون هذا الطلب على أنه ملغى
sendToLocationServer('cancel_ride', [
'ride_id' => $rideId,
'driver_id' => 0,
'reason' => 'timeout',
]);
}
}
// =========================================================
// الخطوة 2: الحذف من جدول الانتظار (تنظيف Hot Data)
// الخطوة 2: تنظيف جدول waitingRides القديم (إن وُجدت صفوف فيه من مسار قديم)
// =========================================================
$sqlDelete = "DELETE FROM waitingRides
WHERE created_at < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)";
$deletedCount = 0;
try {
$sqlDelete = "DELETE FROM waitingRides WHERE created_at < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)";
$stmtDelete = $con->prepare($sqlDelete);
$stmtDelete->execute();
$deletedCount = $stmtDelete->rowCount();
} catch (PDOException $e) {
error_log("[cron_ride_timeout] waitingRides cleanup skipped: " . $e->getMessage());
}
$stmtDelete = $con->prepare($sqlDelete);
$stmtDelete->execute();
$deletedCount = $stmtDelete->rowCount();
// =========================================================
// الخطوة 3: (اختياري) تنظيف الريدز
// =========================================================
// بما أنك تستخدم Redis، المفترض أن تحذفها منه أيضاً.
// لكن بما أن الريدز يعتمد على TTL (Expire) أو سيتم تحديثه عند الطلب القادم،
// فالحذف من الـ MySQL يكفي لأن getRideWaiting سيفحص MySQL ولن يجدها.
// تقرير العملية
if ($deletedCount > 0) {
$msg = "✅ [Cleanup Cron] Success: Timed out $updatedCount rides in Main DB, and Deleted $deletedCount rides from Waiting DB.";
if ($updatedCount > 0 || $deletedCount > 0) {
$msg = "✅ [Cleanup Cron] Success: Timed out $updatedCount ride(s) in ride table, and cleaned $deletedCount legacy waitingRides row(s).";
error_log($msg);
echo json_encode(["status" => "success", "message" => $msg]);
} else {
@@ -59,7 +91,6 @@ try {
} catch (PDOException $e) {
$errorMsg = "❌ [Cleanup Cron] Error: " . $e->getMessage();
error_log($errorMsg);
error_log("[cron_ride_timeout] Error: " . $e->getMessage());
echo json_encode(["status" => "failure", "message" => "An internal error occurred."]);
}
?>
?>