Files
intaleq/backend/ride/location/save_driver_destination.php
T
Hamza-AyedandClaude Opus 5 92dc6b3641 chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل
نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق».
نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا
`cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules ·
.dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore.

هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً
مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ.

⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا
صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم
توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:10:29 +03:00

219 lines
8.2 KiB
PHP

<?php
// ============================================================
// ride/location/save_driver_destination.php
// API Endpoint for Captains to set their destination (max 2 times daily)
// ============================================================
require_once __DIR__ . '/../../connect.php';
// 1. Authorize Driver
if ($role !== 'driver') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized. Driver role required.']);
exit;
}
// 2. Filter Inputs
$action = filterRequest('action') ?? 'set';
$destLat = filterRequest('destination_lat') ?? filterRequest('target_latitude');
$destLng = filterRequest('destination_lng') ?? filterRequest('target_longitude');
$destName = filterRequest('destination_name') ?? 'Destination';
function notifySocketServerDestination($userId, $hasDestination, $destLat = '', $destLng = '', $destName = '') {
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
if (strpos($url, 'localhost') !== false || strpos($url, '127.0.0.1') !== false) {
if (file_exists('/.dockerenv')) {
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
}
}
$internalKey = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => 'update_driver_destination',
'driver_id' => (string)$userId,
'has_destination' => (int)$hasDestination,
'destination_lat' => (string)$destLat,
'destination_lng' => (string)$destLng,
'destination_name' => (string)$destName,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 400);
if ($internalKey) {
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
}
curl_exec($ch);
curl_close($ch);
}
try {
if ($action === 'get') {
// 🟥 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.");
} else {
jsonSuccess(null, "No active destination set.");
}
exit;
}
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
WHERE driver_id = :did
AND is_active = 1
");
$stmtDeactivate->execute([':did' => $user_id]);
// Sync with Socket Server
notifySocketServerDestination($user_id, 0);
jsonSuccess(null, "تم إلغاء تفعيل الوجهة الشخصية بنجاح.");
exit;
}
// Default action: set
if (empty($destLat) || empty($destLng)) {
jsonError("Missing required parameters: destination_lat and destination_lng are required.");
}
// 3. Enforce Limit: Max 3 times daily
// Check local Redis rate limit first
if (isset($redis)) {
$redisKey = "driver:dest_count:" . $user_id;
$redisCount = intval($redis->get($redisKey));
if ($redisCount >= 3) {
jsonError("حسناً كابتن، لقد وصلت للحد الأقصى المسموح به لتحديد الوجهة اليوم (3 مرات في اليوم).");
}
}
// Check MySQL database limit
$stmtCount = $con->prepare("
SELECT COUNT(*)
FROM driver_destinations
WHERE driver_id = :did
AND usage_date = CURDATE()
");
$stmtCount->execute([':did' => $user_id]);
$dailyCount = intval($stmtCount->fetchColumn());
if ($dailyCount >= 3) {
// Sync Redis counter just in case
if (isset($redis)) {
$redisKey = "driver:dest_count:" . $user_id;
$redis->set($redisKey, $dailyCount);
$redis->expire($redisKey, 86400); // 24 hours TTL
}
jsonError("حسناً كابتن، لقد وصلت للحد الأقصى المسموح به لتحديد الوجهة اليوم (3 مرات في اليوم).");
}
// 4. Deactivate previous active destinations for this driver
$stmtDeactivate = $con->prepare("
UPDATE driver_destinations
SET is_active = 0
WHERE driver_id = :did
AND is_active = 1
");
$stmtDeactivate->execute([':did' => $user_id]);
// 🟥 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)
VALUES (:did, :lat, :lng, :name, 1, CURDATE())
");
$stmtInsert->execute([
':did' => $user_id,
':lat' => (float)$destLat,
':lng' => (float)$destLng,
':name' => $destName
]);
// Sync with Socket Server
notifySocketServerDestination($user_id, 1, $destLat, $destLng, $destName);
// Increment local Redis counter
if (isset($redis)) {
$redisKey = "driver:dest_count:{$user_id}";
$redis->incr($redisKey);
$redis->expire($redisKey, 86400); // 24 hours TTL
}
jsonSuccess(null, "تم حفظ وجهتك كابتن بنجاح! سيتم توجيه الطلبات المطابقة لوجهتك.");
} catch (Exception $e) {
error_log("[save_driver_destination.php] Error: " . $e->getMessage());
jsonError("Failed to save driver destination: " . $e->getMessage());
}