Update: 2026-07-02 16:52:25

This commit is contained in:
Hamza-Ayed
2026-07-02 16:52:25 +03:00
parent 75e6fc42d7
commit 5d14493ff4
9 changed files with 213 additions and 20 deletions
+9 -1
View File
@@ -371,9 +371,17 @@ function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNa
'DriverList' => $payloadForDriver,
'order_id' => (string)$rideId
];
// 🆕 Destination Match Indicator
$title = "طلب جديد 🔔";
if (!empty($driver['is_destination_match'])) {
$title = "في طريقك! 📍";
$fcmData['is_destination_match'] = "1";
}
$fcmResult = sendFcmNotification(
$driverToken,
"طلب جديد 🔔",
$title,
"هناك رحلة جديدة من " . $startNameLoc,
$fcmData,
"Order",
@@ -52,15 +52,31 @@ function notifySocketServerDestination($userId, $hasDestination, $destLat = '',
try {
if ($action === 'get') {
$stmtGet = $con->prepare("
SELECT target_latitude, target_longitude, destination_name, created_at
FROM driver_destinations
WHERE driver_id = :did
AND is_active = 1
LIMIT 1
");
$stmtGet->execute([':did' => $user_id]);
$activeDest = $stmtGet->fetch(PDO::FETCH_ASSOC);
// 🟥 Read from Redis FIRST
$activeDest = null;
if (isset($redis)) {
try {
$cached = $redis->get("driver:destination:{$user_id}");
if ($cached) {
$activeDest = json_decode($cached, true);
}
} catch (Exception $e) {
error_log("[save_driver_destination] Redis GET error: " . $e->getMessage());
}
}
// 🟦 Fallback to SQL if Redis miss
if (!$activeDest) {
$stmtGet = $con->prepare("
SELECT target_latitude, target_longitude, destination_name, created_at
FROM driver_destinations
WHERE driver_id = :did
AND is_active = 1
LIMIT 1
");
$stmtGet->execute([':did' => $user_id]);
$activeDest = $stmtGet->fetch(PDO::FETCH_ASSOC);
}
if ($activeDest) {
jsonSuccess($activeDest, "Active destination retrieved.");
@@ -71,6 +87,18 @@ try {
}
if ($action === 'clear') {
// 🟥 Clear from Redis FIRST
if (isset($redis)) {
try {
$redis->del("driver:destination:{$user_id}");
// Remove from the Geo Index (used by GEORADIUS search in pricing engine)
$redis->zRem("geo:driver:destinations", (string)$user_id);
} catch (Exception $e) {
error_log("[save_driver_destination] Redis DEL error: " . $e->getMessage());
}
}
// 🟦 Then clear from SQL
$stmtDeactivate = $con->prepare("
UPDATE driver_destinations
SET is_active = 0
@@ -79,7 +107,7 @@ try {
");
$stmtDeactivate->execute([':did' => $user_id]);
// Sync with Redis on Socket Server
// Sync with Socket Server
notifySocketServerDestination($user_id, 0);
jsonSuccess(null, "تم إلغاء تفعيل الوجهة الشخصية بنجاح.");
@@ -130,7 +158,36 @@ try {
");
$stmtDeactivate->execute([':did' => $user_id]);
// 5. Insert new destination
// 🟥 5a. Write to Redis FIRST (primary read path for pricing engine)
if (isset($redis)) {
try {
$destData = [
'driver_id' => $user_id,
'target_latitude' => (float)$destLat,
'target_longitude' => (float)$destLng,
'destination_name' => $destName,
'is_active' => 1,
'usage_date' => date('Y-m-d')
];
// TTL: end of day (midnight)
$secondsUntilMidnight = strtotime('tomorrow') - time();
// A) Store details as JSON string (for reading driver info)
$redis->setex("driver:destination:{$user_id}", $secondsUntilMidnight, json_encode($destData));
// B) Add to Geo Index for ultra-fast GEORADIUS proximity search
// GEOADD key longitude latitude member
$redis->geoAdd("geo:driver:destinations", (float)$destLng, (float)$destLat, (string)$user_id);
// Geo index has no native TTL per-member, so we set TTL on the whole key if not already set
if ($redis->ttl("geo:driver:destinations") < 0) {
$redis->expire("geo:driver:destinations", $secondsUntilMidnight);
}
} catch (Exception $e) {
error_log("[save_driver_destination] Redis SET error: " . $e->getMessage());
}
}
// 🟦 5b. Insert into SQL as secondary (persistent store)
$stmtInsert = $con->prepare("
INSERT INTO driver_destinations
(driver_id, target_latitude, target_longitude, destination_name, is_active, usage_date)
@@ -143,12 +200,12 @@ try {
':name' => $destName
]);
// Sync with Redis on Socket Server
// Sync with Socket Server
notifySocketServerDestination($user_id, 1, $destLat, $destLng, $destName);
// Increment local Redis counter
if (isset($redis)) {
$redisKey = "driver:dest_count:" . $user_id;
$redisKey = "driver:dest_count:{$user_id}";
$redis->incr($redisKey);
$redis->expire($redisKey, 86400); // 24 hours TTL
}
+82
View File
@@ -449,6 +449,86 @@ if (!empty($passenger_id)) {
}
}
// 🆕 المطابقة الذكية للوجهة (Destination Matching)
// Architecture: GEORADIUS (O(log N+K)) → detail fetch → SQL fallback
$isDestinationMatch = false;
$matchedDriverId = 0;
try {
$bestDriverId = 0;
$foundViaRedis = false;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🟥 Step 1: Redis GEORADIUS — ultra-fast O(log N + K) search
// Finds all drivers whose destination is within 3.5 km of
// the passenger's drop-off point. No PHP math. No full scan.
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if (isset($redis)) {
try {
// geoRadius(key, longitude, latitude, radius, unit, [options])
$nearbyDriverIds = $redis->geoRadius(
'geo:driver:destinations',
(float)$destLng,
(float)$destLat,
3.5,
'km',
['COUNT' => 1, 'ASC'] // closest first, stop after 1
);
if (!empty($nearbyDriverIds)) {
$candidateId = (string)$nearbyDriverIds[0];
// Verify destination is still active today (read detail JSON)
$detailJson = $redis->get("driver:destination:{$candidateId}");
$detail = $detailJson ? json_decode($detailJson, true) : null;
if ($detail
&& !empty($detail['is_active'])
&& ($detail['usage_date'] ?? '') === date('Y-m-d')
) {
$bestDriverId = $candidateId;
$foundViaRedis = true;
} else {
// Stale entry — clean up from geo index
$redis->zRem('geo:driver:destinations', $candidateId);
}
}
} catch (Exception $e) {
error_log("[Destination Matching] Redis GEORADIUS error: " . $e->getMessage());
}
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🟦 Step 2: SQL Fallback (only if Redis unavailable or empty)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if (!$foundViaRedis) {
$stmtDest = $con->prepare("
SELECT driver_id,
( 6371 * acos( cos( radians(:endLat) ) * cos( radians( target_latitude ) )
* cos( radians( target_longitude ) - radians(:endLng) ) + sin( radians(:endLat) )
* sin( radians( target_latitude ) ) ) ) AS dest_distance
FROM driver_destinations
WHERE is_active = 1 AND usage_date = CURDATE()
HAVING dest_distance <= 3.5
ORDER BY dest_distance ASC
LIMIT 1
");
$stmtDest->execute([':endLat' => $destLat, ':endLng' => $destLng]);
$destMatch = $stmtDest->fetch(PDO::FETCH_ASSOC);
if ($destMatch) {
$bestDriverId = $destMatch['driver_id'];
}
}
if ($bestDriverId) {
$isDestinationMatch = true;
$matchedDriverId = $bestDriverId;
// Passenger gets 14% discount for Destination Match
$discount = max((float)$discount, 14.0);
}
} catch (Exception $e) {
error_log("[Destination Matching] Error: " . $e->getMessage());
}
// Calculate prices for all categories
foreach ($categories as $key => $carType) {
$result = calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType);
@@ -497,6 +577,8 @@ if (isset($encryptionHelper)) {
// ✅ FIX R6: تضمين distance و duration في الـ token لمنع التلاعب
'distance' => $distance,
'duration' => $duration,
'is_destination_match' => $isDestinationMatch ? 1 : 0,
'matched_driver_id' => $matchedDriverId,
'expires' => time() + 420, // Valid for 7 minutes
'prices' => $pricesRaw
];
+39 -4
View File
@@ -160,6 +160,10 @@ $price = $tokenData['prices'][$tokenCarType]['price'];
$price_for_driver = $tokenData['prices'][$tokenCarType]['driver_price'];
$price_for_passenger = $price;
// 🆕 Destination Matching
$is_destination_match = isset($tokenData['is_destination_match']) ? (int)$tokenData['is_destination_match'] : 0;
$matched_driver_id = isset($tokenData['matched_driver_id']) ? $tokenData['matched_driver_id'] : 0;
// ── 2. تنسيق التواريخ ─────────────────────────────────────────
$date_formatted = date("Y-m-d");
$time_formatted = date("H:i:s");
@@ -191,16 +195,17 @@ $insertData = [
':price_for_driver' => $price_for_driver,
':price_for_passenger' => $price_for_passenger,
':distance' => $distance,
':is_destination_match' => $is_destination_match,
];
$sqlInsert = "INSERT INTO `ride`
(`start_location`,`end_location`,`date`,`time`,`endtime`,
`price`,`passenger_id`,`driver_id`,`status`,`carType`,
`price_for_driver`,`price_for_passenger`,`distance`)
`price_for_driver`,`price_for_passenger`,`distance`,`is_destination_match`)
VALUES
(:start_location,:end_location,:date,:time,:endtime,
:price,:passenger_id,:driver_id,:status,:carType,
:price_for_driver,:price_for_passenger,:distance)";
:price_for_driver,:price_for_passenger,:distance,:is_destination_match)";
try {
// ═══════════════════════════════════════════════════════════
@@ -224,11 +229,11 @@ try {
$sqlInsertWithId = "INSERT INTO `ride`
(`id`,`start_location`,`end_location`,`date`,`time`,`endtime`,
`price`,`passenger_id`,`driver_id`,`status`,`carType`,
`price_for_driver`,`price_for_passenger`,`distance`)
`price_for_driver`,`price_for_passenger`,`distance`,`is_destination_match`)
VALUES
(:id,:start_location,:end_location,:date,:time,:endtime,
:price,:passenger_id,:driver_id,:status,:carType,
:price_for_driver,:price_for_passenger,:distance)";
:price_for_driver,:price_for_passenger,:distance,:is_destination_match)";
try {
$primaryData = $insertData;
@@ -285,6 +290,36 @@ try {
// Direct dispatch للسائقين القريبين
$driversData = findBestDrivers($con, $startLat, $startLng, $carType, $endLat, $endLng);
// 🆕 Destination Matching Priority
if ($is_destination_match && $matched_driver_id) {
$found = false;
foreach ($driversData as $k => $d) {
if ($d['captain_id'] == $matched_driver_id) {
// Remove from current position and move to the very front
unset($driversData[$k]);
$d['is_destination_match'] = 1;
array_unshift($driversData, $d);
$found = true;
break;
}
}
// If driver wasn't nearby but we still want to give it to them because it's their route
if (!$found) {
$stmtD = $con->prepare("SELECT token FROM driverToken WHERE captain_id = :d");
$stmtD->execute([':d' => $matched_driver_id]);
$dT = $stmtD->fetchColumn();
if ($dT) {
array_unshift($driversData, [
'captain_id' => $matched_driver_id,
'token' => $dT,
'is_destination_match' => 1
]);
}
}
}
if (!empty($driversData)) {
dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper);
error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers.");
+1
View File
@@ -1458,6 +1458,7 @@ CREATE TABLE `ride` (
`carType` varchar(20) NOT NULL DEFAULT 'Speed',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_destination_match` tinyint(1) NOT NULL DEFAULT 0,
`DriverIsGoingToPassenger` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`rideTimeStart` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`rideTimeFinish` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+1
View File
@@ -1361,6 +1361,7 @@ CREATE TABLE `ride` (
`carType` varchar(20) NOT NULL DEFAULT 'Speed',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_destination_match` tinyint(1) NOT NULL DEFAULT 0,
`DriverIsGoingToPassenger` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`rideTimeStart` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`rideTimeFinish` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -157,7 +157,8 @@ class FirebaseMessagesController extends GetxController {
Get.toNamed('/OrderRequestPage', arguments: {
'myListString': myListString,
'DriverList': myList,
'body': body
'body': body,
'is_destination_match': message.data['is_destination_match'] ?? '0',
});
}
break;
@@ -405,7 +405,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Get.toNamed('/OrderRequestPage', arguments: {
'myListString': jsonEncode(driverList),
'DriverList': driverList,
'body': 'New Trip Request via Socket ⚡'
'body': 'New Trip Request via Socket ⚡',
'is_destination_match': rideData['is_destination_match'] ?? '0',
});
} else {
Log.print(
@@ -63,6 +63,7 @@ class OrderRequestController extends GetxController
String totalTripDistance = "--";
String totalTripDuration = "--";
String tripPrice = "--";
bool isDestinationMatch = false; // 🆕 Destination Match flag
String timeToPassenger = "Calculating...".tr;
String distanceToPassenger = "--";
@@ -158,6 +159,12 @@ class OrderRequestController extends GetxController
print("Error decoding DriverList: $e");
}
}
// 🆕 Check for Destination Match flag from FCM data
if (args['is_destination_match'] == '1' ||
args['is_destination_match'] == 1 ||
args['is_destination_match'] == true) {
isDestinationMatch = true;
}
}
// ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟
else {