Update: 2026-07-02 16:52:25
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user