Update: 2026-07-07 04:57:50

This commit is contained in:
Hamza-Ayed
2026-07-07 04:57:51 +03:00
parent 5725fb36f4
commit 628e169552
25 changed files with 288 additions and 1893 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -1,89 +0,0 @@
<?php
// ============================================================
// create_tester_driver.php
// Script to seed/register a pre-verified tester driver.
// ============================================================
require_once __DIR__ . '/../../core/bootstrap.php';
$email = 'driver_tester@siromove.com';
$phone = '+962790000002';
$password = 'SiroDriver2026!';
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
$encryptedEmail = $encryptionHelper->encryptData($email);
$encryptedPhone = $encryptionHelper->encryptData($phone);
$encryptedFirstName = $encryptionHelper->encryptData('Driver');
$encryptedLastName = $encryptionHelper->encryptData('Tester');
$encryptedGender = $encryptionHelper->encryptData('Male');
$encryptedBirthdate = $encryptionHelper->encryptData('1990-01-01');
$encryptedSite = $encryptionHelper->encryptData('Jordan');
try {
$con = Database::get('main');
// 1. Check if driver exists
$stmt = $con->prepare("SELECT id FROM driver WHERE email = :email LIMIT 1");
$stmt->bindParam(':email', $encryptedEmail);
$stmt->execute();
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
if ($driver) {
$driverId = $driver['id'];
$update = $con->prepare("UPDATE driver SET password = :password, phone = :phone WHERE id = :id");
$update->bindParam(':password', $hashedPassword);
$update->bindParam(':phone', $encryptedPhone);
$update->bindParam(':id', $driverId);
$update->execute();
echo "Driver tester updated successfully.\n";
} else {
$driverId = bin2hex(random_bytes(10)); // 20 chars unique id
$insert = $con->prepare("INSERT INTO driver (id, phone, email, password, gender, birthdate, site, first_name, last_name)
VALUES (:id, :phone, :email, :password, :gender, :birthdate, :site, :first_name, :last_name)");
$insert->bindParam(':id', $driverId);
$insert->bindParam(':phone', $encryptedPhone);
$insert->bindParam(':email', $encryptedEmail);
$insert->bindParam(':password', $hashedPassword);
$insert->bindParam(':gender', $encryptedGender);
$insert->bindParam(':birthdate', $encryptedBirthdate);
$insert->bindParam(':site', $encryptedSite);
$insert->bindParam(':first_name', $encryptedFirstName);
$insert->bindParam(':last_name', $encryptedLastName);
$insert->execute();
echo "Driver tester created successfully with ID: $driverId\n";
}
// 2. Ensure phone_verification row exists
$stmtPhone = $con->prepare("SELECT * FROM phone_verification WHERE phone_number = :phone LIMIT 1");
$stmtPhone->bindParam(':phone', $encryptedPhone);
$stmtPhone->execute();
if ($stmtPhone->fetch()) {
$updatePhone = $con->prepare("UPDATE phone_verification SET is_verified = 1 WHERE phone_number = :phone");
$updatePhone->bindParam(':phone', $encryptedPhone);
$updatePhone->execute();
} else {
$insertPhone = $con->prepare("INSERT INTO phone_verification (phone_number, is_verified) VALUES (:phone, 1)");
$insertPhone->bindParam(':phone', $encryptedPhone);
$insertPhone->execute();
}
// 3. Ensure CarRegistration row exists
$stmtCar = $con->prepare("SELECT * FROM CarRegistration WHERE driverID = :driverID LIMIT 1");
$stmtCar->bindParam(':driverID', $driverId);
$stmtCar->execute();
if ($stmtCar->fetch()) {
$updateCar = $con->prepare("UPDATE CarRegistration SET make = 'Toyota', model = 'Prius', year = '2020' WHERE driverID = :driverID");
$updateCar->bindParam(':driverID', $driverId);
$updateCar->execute();
} else {
$insertCar = $con->prepare("INSERT INTO CarRegistration (driverID, make, model, year) VALUES (:driverID, 'Toyota', 'Prius', '2020')");
$insertCar->bindParam(':driverID', $driverId);
$insertCar->execute();
}
echo "Verification and Car Registration configured.\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
-29
View File
@@ -1,29 +0,0 @@
<?php
require_once __DIR__ . '/../../connect.php';
$appPlatform = filterRequest("appPlatform");
$sql = "SELECT
*
FROM
`testApp`
WHERE
appPlatform = '$appPlatform'-- AND isTest = 0;";
$stmt = $con->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($stmt->rowCount() > 0) {
// Print the retrieved data
// echo json_encode($result);
jsonSuccess($data = $result);
} else {
// Print a failure message
jsonError($message = "No driver order data found");
}
?>
-23
View File
@@ -1,23 +0,0 @@
<?php
require_once __DIR__ . '/../../connect.php';
$appPlatform = filterRequest("appPlatform");
$sql = "UPDATE
`testApp`
SET
`isTest` = '1'
WHERE
`testApp`.appPlatform = '$appPlatform';";
$stmt = $con->prepare($sql);
$stmt->execute();
if ($stmt->rowCount() > 0) {
// Print a success message
jsonSuccess($message = "Test data updated successfully");
} else {
// Print a failure message
jsonError($message = "Failed to update driver order data");
}
?>
-82
View File
@@ -1,82 +0,0 @@
-- ==========================================================
-- Marketing Engine & Social Media Bot Combined Schema
-- ==========================================================
-- 1. Original Social Media Tables (Facebook, Instagram)
CREATE TABLE IF NOT EXISTS `social_accounts` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`platform` ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube') NOT NULL,
`username` VARCHAR(100) NOT NULL,
`status` ENUM('active', 'restricted', 'banned') DEFAULT 'active',
`total_posts` INT DEFAULT 0,
`total_comments` INT DEFAULT 0,
`total_videos` INT DEFAULT 0,
`proxy_ip` VARCHAR(100) NULL,
`proxy_port` INT NULL,
`proxy_username` VARCHAR(100) NULL,
`proxy_password` VARCHAR(100) NULL,
`last_active` DATETIME NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `social_tasks` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`account_id` INT NULL,
`platform` ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube') NOT NULL,
`type` ENUM('join_group', 'read_posts', 'post_comment', 'share_link', 'upload_video', 'post_tweet') NOT NULL,
`target_url` VARCHAR(500) NULL, -- URL of the group, post, or media download
`prompt_context` TEXT NULL,
`generated_comment` TEXT NULL,
`status` ENUM('pending', 'in_progress', 'completed', 'failed') DEFAULT 'pending',
`error_message` TEXT NULL,
`scheduled_at` DATETIME NULL,
`completed_at` DATETIME NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`account_id`) REFERENCES `social_accounts`(`id`) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS `social_logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`task_id` INT NULL,
`account_id` INT NULL,
`log_level` ENUM('info', 'warning', 'error') DEFAULT 'info',
`message` TEXT NOT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`task_id`) REFERENCES `social_tasks`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`account_id`) REFERENCES `social_accounts`(`id`) ON DELETE SET NULL
);
-- 2. New Content Generation Pipeline Tables
CREATE TABLE IF NOT EXISTS `content_pipeline` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`topic` VARCHAR(255) NOT NULL,
`script_text` TEXT NULL,
`voice_url` VARCHAR(500) NULL,
`video_url` VARCHAR(500) NULL,
`status` ENUM('pending', 'script_generated', 'voice_generated', 'video_rendered', 'published', 'failed') DEFAULT 'pending',
`error_message` TEXT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `api_quotas` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`service_name` ENUM('gemini', 'elevenlabs', 'creatomate', 'heygen') NOT NULL,
`daily_usage` INT DEFAULT 0,
`quota_limit` INT DEFAULT 10,
`last_reset` DATE NOT NULL
);
INSERT IGNORE INTO `api_quotas` (`service_name`, `daily_usage`, `quota_limit`, `last_reset`) VALUES
('gemini', 0, 100, CURDATE()),
('elevenlabs', 0, 5, CURDATE()),
('creatomate', 0, 2, CURDATE());
CREATE TABLE IF NOT EXISTS marketing_reports (
id INT AUTO_INCREMENT PRIMARY KEY,
platform ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube', 'all') NOT NULL,
report_html TEXT NOT NULL,
is_weekly TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
@@ -1,48 +0,0 @@
<?php
// ═══════════════════════════════════════════════════════════════
// get_all_driver_fingerprints.php
// ───────────────────────────────────────────────────────────────
// ⚠️ يُستخدم مرة واحدة فقط ثم يُحذف من السيرفر
// ═══════════════════════════════════════════════════════════════
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
// ── التحقق من admin_key ────────────────────────────────────────
$adminKey = filterRequest('admin_key') ?? '';
$expectedAdminKey = getenv('MIGRATION_ADMIN_KEY');
if (empty($adminKey) || !hash_equals($expectedAdminKey, $adminKey)) {
http_response_code(403);
exit(json_encode(['error' => 'Forbidden']));
}
try {
$stmt = $con->prepare('
SELECT captain_id, fingerPrint
FROM driverToken
WHERE fingerPrint IS NOT NULL
AND fingerPrint != ""
ORDER BY captain_id
');
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'status' => 'success',
'count' => count($rows),
'data' => $rows,
]);
} catch (Exception $e) {
error_log('❌ [get_all_driver_fingerprints] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Server error']);
}
@@ -1,57 +0,0 @@
<?php
// ═══════════════════════════════════════════════════════════════
// get_all_fingerprints.php — جلب كل البصمات للترحيل
// ───────────────────────────────────────────────────────────────
// ⚠️ يُستخدم مرة واحدة فقط ثم يُحذف من السيرفر
// محمي بـ admin_key لمنع الوصول العشوائي
// ═══════════════════════════════════════════════════════════════
require_once __DIR__ . '/../get_connect.php';
//include 'functions.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// ── التحقق من admin_key ────────────────────────────────────────
$adminKey = filterRequest('admin_key') ?? '';
$expectedAdminKey = getenv('MIGRATION_ADMIN_KEY'); // أضفه في .env
if (empty($adminKey) || !hash_equals($expectedAdminKey, $adminKey)) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
exit;
}
try {
// جلب كل البصمات من جدول tokens
// نجيب passengerID + fingerPrint فقط — لا نعطي بيانات حساسة أخرى
$stmt = $con->prepare('
SELECT passengerID, fingerPrint, "passenger" AS userType
FROM tokens
WHERE fingerPrint IS NOT NULL
AND fingerPrint != ""
ORDER BY passengerID
');
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'status' => 'success',
'count' => count($rows),
'data' => $rows,
]);
http_response_code(200);
} catch (Exception $e) {
error_log('❌ [get_all_fingerprints] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Server error']);
}
@@ -1,49 +0,0 @@
<?php
// ═══════════════════════════════════════════════════════════════
// update_driver_fingerprint_admin.php
// ───────────────────────────────────────────────────────────────
// ⚠️ يُستخدم فقط أثناء الترحيل ثم يُحذف
// ═══════════════════════════════════════════════════════════════
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
$adminKey = filterRequest('admin_key') ?? '';
$expectedAdminKey = getenv('MIGRATION_ADMIN_KEY');
if (empty($adminKey) || !hash_equals($expectedAdminKey, $adminKey)) {
http_response_code(403);
exit(json_encode(['error' => 'Forbidden']));
}
try {
$captainId = filterRequest('captain_id') ?? '';
$fingerprint = filterRequest('fingerprint') ?? '';
if (empty($captainId) || empty($fingerprint)) {
http_response_code(400);
exit(json_encode(['error' => 'Missing parameters']));
}
$stmt = $con->prepare('
UPDATE driverToken
SET fingerPrint = :fp
WHERE captain_id = :cid
');
$stmt->execute([':fp' => $fingerprint, ':cid' => $captainId]);
echo json_encode(['status' => 'success', 'affected' => $stmt->rowCount()]);
} catch (Exception $e) {
error_log('❌ [update_driver_fingerprint_admin] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Server error']);
}
@@ -1,63 +0,0 @@
<?php
// ═══════════════════════════════════════════════════════════════
// update_fingerprint_admin.php — تحديث بصمة واحدة (للترحيل)
// ───────────────────────────────────────────────────────────────
// ⚠️ يُستخدم فقط أثناء عملية الترحيل ثم يُحذف
// ═══════════════════════════════════════════════════════════════
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// ── التحقق من admin_key ────────────────────────────────────────
$adminKey = filterRequest('admin_key') ?? '';
$expectedAdminKey = getenv('MIGRATION_ADMIN_KEY');
if (empty($adminKey) || !hash_equals($expectedAdminKey, $adminKey)) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
exit;
}
try {
$passengerID = filterRequest('passengerID') ?? '';
$fingerprint = filterRequest('fingerprint') ?? '';
if (empty($passengerID) || empty($fingerprint)) {
http_response_code(400);
echo json_encode(['error' => 'Missing parameters']);
exit;
}
$stmt = $con->prepare('
UPDATE tokens
SET fingerPrint = :fp
WHERE passengerID = :pid
');
$stmt->execute([
':fp' => $fingerprint,
':pid' => $passengerID,
]);
$affected = $stmt->rowCount();
echo json_encode([
'status' => 'success',
'affected' => $affected,
]);
http_response_code(200);
} catch (Exception $e) {
error_log('❌ [update_fingerprint_admin] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Server error']);
}
-36
View File
@@ -1,36 +0,0 @@
-- 1. Table for storing the unified 6-digit referral code for each user (Driver/Passenger)
CREATE TABLE `user_referral_codes` (
`id` INT NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(100) NOT NULL,
`user_type` ENUM('driver', 'passenger') NOT NULL,
`referral_code` VARCHAR(6) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_type_id` (`user_id`, `user_type`),
UNIQUE KEY `idx_referral_code` (`referral_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 2. Table for tracking the referral relationship and trip counts
CREATE TABLE `unified_referrals` (
`id` INT NOT NULL AUTO_INCREMENT,
`inviter_code` VARCHAR(6) NOT NULL,
`invited_user_id` VARCHAR(100) NOT NULL,
`invited_user_type` ENUM('driver', 'passenger') NOT NULL,
`status` ENUM('registered', 'active', 'completed') NOT NULL DEFAULT 'registered',
`trip_count` INT NOT NULL DEFAULT 0,
`is_reward_claimed` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_invited_user` (`invited_user_id`, `invited_user_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 3. Table for Driver Cash Claims (when a driver requests to receive their reward as manual cash)
CREATE TABLE `driver_cash_claims` (
`id` INT NOT NULL AUTO_INCREMENT,
`driver_id` VARCHAR(100) NOT NULL,
`referral_id` INT NOT NULL,
`amount_syp` INT NOT NULL,
`status` ENUM('pending', 'paid') NOT NULL DEFAULT 'pending',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-147
View File
@@ -1,147 +0,0 @@
<?php
/**
* passenger_socket.php
* =====================
* WebSocket Server للركاب — بورت 3030
* Internal HTTP Server — بورت 3031
*/
use Workerman\Worker;
use PHPSocketIO\SocketIO;
require_once __DIR__ . '/vendor/autoload.php';
// ---------------------------------------------------------
// نظام تسجيل الأحداث (Logging System)
// ---------------------------------------------------------
$LOG_FILE = __DIR__ . '/logs/socket_debug.log';
function socket_log($message, $data = null) {
global $LOG_FILE;
$date = date('Y-m-d H:i:s');
$logMsg = "[$date] $message";
if ($data !== null) {
$logMsg .= " | DATA: " . (is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE));
}
$logMsg .= PHP_EOL;
echo $logMsg; // للطباعة في الكونسول إذا كان يعمل في الـ Foreground
@file_put_contents($LOG_FILE, $logMsg, FILE_APPEND); // الكتابة في الملف
}
// ---------------------------------------------------------
socket_log("=== STARTING PASSENGER SOCKET SERVER ===");
$siteUser = get_current_user();
$homeDir = "/home/$siteUser";
if (!is_dir($homeDir)) {
$homeDir = '/home/intaleq-rides'; // Fallback to original location
}
$INTERNAL_KEY = trim((string) @file_get_contents(getenv('INTERNAL_SOCKET_KEY_PATH') ?: ($homeDir . '/.internal_socket_key')));
if (empty($INTERNAL_KEY)) {
socket_log("[CRITICAL_ERROR] Internal key missing! Exiting.");
exit(1);
}
$PORT = 3030;
$INTERNAL_PORT = 3031;
$io = new SocketIO($PORT);
$io->on('workerStart', function () use ($io, $INTERNAL_KEY, $INTERNAL_PORT) {
$innerHttp = new Worker("http://0.0.0.0:$INTERNAL_PORT");
$innerHttp->onMessage = function ($connection, $request) use ($io, $INTERNAL_KEY) {
$headers = $request->header();
$clientIp = $connection->getRemoteIp();
if (($headers['x-internal-key'] ?? '') !== $INTERNAL_KEY) {
socket_log("[HTTP_ERROR] Unauthorized internal request from IP: $clientIp");
$connection->send('Unauthorized');
return;
}
$post = $request->post();
$action = trim($post['action'] ?? '');
if ($action === 'update_ride_status') {
$passengerId = $post['passenger_id'] ?? null;
$rawPayload = $post['payload'] ?? null;
if (!$passengerId || !$rawPayload) {
socket_log("[HTTP_ERROR] Missing passenger_id or payload for action: update_ride_status", $post);
$connection->send('Error: Missing passenger_id or payload');
return;
}
$payload = is_string($rawPayload)
? (json_decode($rawPayload, true) ?? $rawPayload)
: $rawPayload;
socket_log("[HTTP_SUCCESS] Emitting 'ride_status_change' to Passenger #$passengerId", $payload);
$io->to('passenger_' . $passengerId)->emit('ride_status_change', $payload);
$connection->send('OK');
} elseif ($action === 'update_driver_location') {
$passengerId = $post['passenger_id'] ?? null;
$rawPayload = $post['payload'] ?? null;
if (!$passengerId || !$rawPayload) {
socket_log("[HTTP_ERROR] Missing passenger_id or payload for action: update_driver_location", $post);
$connection->send('Error: Missing passenger_id or payload');
return;
}
$payload = is_string($rawPayload)
? (json_decode($rawPayload, true) ?? $rawPayload)
: $rawPayload;
socket_log("[HTTP_SUCCESS] Emitting 'driver_location_update' to Passenger #$passengerId", $payload);
$io->to('passenger_' . $passengerId)->emit('driver_location_update', $payload);
$connection->send('OK');
} else {
socket_log("[HTTP_WARNING] Unknown action received: $action", $post);
$connection->send('Unknown action: ' . $action);
}
};
$innerHttp->listen();
socket_log("[INFO] Internal HTTP started on port $INTERNAL_PORT");
});
$io->on('connection', function ($socket) {
$query = $socket->handshake['query'] ?? [];
$passengerId = $query['id'] ?? null;
$clientIp = $socket->conn->remoteAddress ?? 'Unknown';
if (!$passengerId) {
socket_log("[SOCKET_REJECTED] Connection rejected (No passenger ID) from IP: $clientIp");
$socket->disconnect();
return;
}
$socket->join('passenger_' . $passengerId);
socket_log("[SOCKET_CONNECTED] Passenger Connected: #$passengerId (IP: $clientIp)");
$socket->on('heartbeat', function ($data) {
// يمكن تفعيل السطر التالي للتأكد من النبضات إذا أردت دقة شديدة، لكنه قد يملأ ملف الـ log
// socket_log("[SOCKET_HEARTBEAT] Received from Passenger #$passengerId");
});
$socket->on('disconnect', function () use ($passengerId, $clientIp) {
socket_log("[SOCKET_DISCONNECTED] Passenger Disconnected: #$passengerId (IP: $clientIp)");
});
});
Worker::runAll();
@@ -1,28 +0,0 @@
-- Migration: Add new columns to competitor_secret_formulas for enhanced analysis
-- Run this once to upgrade the table schema
ALTER TABLE `competitor_secret_formulas`
ADD COLUMN `tier` VARCHAR(20) DEFAULT 'standard' AFTER `country_code`,
ADD COLUMN `min_fare` DECIMAL(8,3) DEFAULT 0 AFTER `price_per_min`,
ADD COLUMN `rmse` DECIMAL(10,4) DEFAULT 0 AFTER `min_fare`,
ADD COLUMN `r_squared` DECIMAL(10,4) DEFAULT 0 AFTER `rmse`,
ADD COLUMN `surge_multiplier` DECIMAL(5,3) DEFAULT 1.0 AFTER `r_squared`,
ADD COLUMN `peak_hours` VARCHAR(255) DEFAULT '[]' AFTER `surge_multiplier`;
-- Make the unique key include tier for multi-tier support
ALTER TABLE `competitor_secret_formulas`
DROP INDEX `idx_comp_country`,
ADD UNIQUE KEY `idx_comp_country_tier` (`competitor_name`, `country_code`, `tier`);
-- New table for surge insights
CREATE TABLE IF NOT EXISTS `competitor_surge_insights` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`competitor_name` VARCHAR(100) NOT NULL,
`country_code` VARCHAR(5) NOT NULL,
`surge_multiplier` DECIMAL(5,3) NOT NULL,
`peak_start_hour` INT NOT NULL,
`peak_end_hour` INT NOT NULL,
`sample_count` INT NOT NULL,
`detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `unique_surge` (`competitor_name`, `country_code`, `peak_start_hour`, `peak_end_hour`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+119
View File
@@ -0,0 +1,119 @@
<?php
// ============================================================
// pricing_helper.php
// يحتوي على دوال التسعير الموحدة
// ============================================================
/**
* الحصول على سعر الكيلومتر حسب نوع السيارة من جدول أسعار الدولة
*
* @param string $carType نوع السيارة
* @param array $kazanRow صف من جدول kazan
* @return float
*/
function getPerKmRate(string $carType, array $kazanRow): float {
$rateColumns = [
'Comfort' => 'comfortPrice',
'Speed' => 'speedPrice',
'Lady' => 'ladyPrice',
'Electric' => 'electricPrice',
'Van' => 'vanPrice',
'Delivery' => 'deliveryPrice',
'Mishwar Vip' => 'mishwarVipPrice',
'Fixed Price' => 'fixedPrice',
'Awfar Car' => 'awfarPrice',
];
$column = $rateColumns[$carType] ?? 'speedPrice';
// دعم التوافق مع الإصدارات القديمة (backward compatibility)
$rate = floatval($kazanRow[$column] ?? 0);
if ($rate <= 0) {
$oldColumnMap = [
'Lady' => 'familyPrice',
'Mishwar Vip' => 'freePrice',
'Electric' => 'naturePrice',
'Van' => 'heavyPrice',
];
$oldColumn = $oldColumnMap[$carType] ?? null;
if ($oldColumn && isset($kazanRow[$oldColumn])) {
$rate = floatval($kazanRow[$oldColumn]);
}
}
// Fallback أخير
if ($rate <= 0) {
$rate = floatval($kazanRow['speedPrice'] ?? 36);
}
return $rate;
}
/**
* الحصول على سعر الدقيقة حسب وقت اليوم من جدول kazan
*
* @param array $countryPricing صف من جدول kazan
* @return float
*/
function getPerMinRate(array $countryPricing): float {
$hour = (int)date('H');
// قراءة الأسعار من الأعمدة الجديدة للدقيقة
$normalMinPrice = floatval($countryPricing['normalMinPrice'] ?? 0);
$peakMinPrice = floatval($countryPricing['peakMinPrice'] ?? 0);
$lateMinPrice = floatval($countryPricing['lateMinPrice'] ?? 0);
// دعم التوافق مع الإصدارات القديمة (latePrice, naturePrice)
if ($lateMinPrice <= 0) $lateMinPrice = floatval($countryPricing['latePrice'] ?? 0);
if ($normalMinPrice <= 0) $normalMinPrice = floatval($countryPricing['naturePrice'] ?? 0);
// Fallback: حساب سعر الدقيقة من speedPrice إذا كانت الأعمدة الجديدة فارغة
if ($normalMinPrice <= 0) {
$speedPrice = floatval($countryPricing['speedPrice'] ?? 36);
$normalMinPrice = $speedPrice / 4;
}
if ($peakMinPrice <= 0) $peakMinPrice = $normalMinPrice * 1.15; // 15% زيادة
if ($lateMinPrice <= 0) $lateMinPrice = $normalMinPrice * 1.25; // 25% زيادة
if ($hour >= 21 || $hour < 1) {
return round($lateMinPrice, 2); // Late Night
}
if ($hour >= 14 && $hour <= 17) {
return round($peakMinPrice, 2); // Peak
}
return round($normalMinPrice, 2); // Normal
}
/**
* تحديد رمز العملة حسب الدولة
*
* @param string $countryCode رمز الدولة (Syria, Egypt, Jordan, ...)
* @return string رمز العملة (SYP, EGP, JOD, ...)
*/
function getCurrencyByCountry(string $countryCode): string {
$currencies = [
'Syria' => 'SYP',
'Egypt' => 'EGP',
'Jordan' => 'JOD',
'Iraq' => 'IQD',
'UAE' => 'AED',
'Saudi Arabia' => 'SAR',
'Qatar' => 'QAR',
'Kuwait' => 'KWD',
'Bahrain' => 'BHD',
'Oman' => 'OMR',
'Turkey' => 'TRY',
'Lebanon' => 'LBP',
'Palestine' => 'ILS',
'Yemen' => 'YER',
'Libya' => 'LYD',
'Tunisia' => 'TND',
'Algeria' => 'DZD',
'Morocco' => 'MAD',
'Sudan' => 'SDG',
];
return $currencies[$countryCode] ?? 'SYP'; // افتراضي: ليرة سورية
}
?>
-117
View File
@@ -1,117 +0,0 @@
<?php
// cancelRideFromDriver.php
// تأكد أن هذا الملف يحتوي على دوال الإشعارات (notifyPassengerOnRideServer)
require_once __DIR__ . '/../../connect.php';
require_once __DIR__ . '/streak_helper.php';
// 🚀 تسجيل بداية العملية
error_log("🚀 [cancelRide.php] Request Started to Cancel Ride From Driver.");
$id = filterRequest("id"); // Ride ID
if (!$id) {
error_log("❌ [cancelRide.php] Missing Ride ID.");
jsonError("Missing ID");
exit;
}
// الحالة الجديدة (إلغاء نهائي من طرف السائق)
$newStatus = "cancelRideFromDriver";
// الحالات المسموح بإلغاء الرحلة فيها فقط
// نسمح بالإلغاء إذا كانت في وضع الانتظار أو القبول المبدئي
$allowedStatuses = "'wait', 'waiting', 'Apply', 'accepted', 'arrive'";
try {
// ---------------------------------------------------------
// 1. التحديث على سيرفر التتبع (Remote DB)
// ---------------------------------------------------------
// نستخدم شرط الحالة لضمان عدم إلغاء رحلة بدأت بالفعل (Start/Begin)
$sql = "UPDATE `ride`
SET `status` = ?, `updated_at` = CURRENT_TIMESTAMP
WHERE `id` = ?
AND `status` IN ($allowedStatuses)";
// استخدام Prepared Statements للأمان
$stmtRemote = $con_ride->prepare($sql);
$stmtRemote->execute([$newStatus, $id]);
$count = $stmtRemote->rowCount();
error_log("ℹ️ [cancelRide.php] Remote DB Rows Affected: $count");
// التحقق: هل تم التحديث؟
if ($count > 0) {
// ---------------------------------------------------------
// 2. التحديث على السيرفر المحلي (Local DB)
// ---------------------------------------------------------
// نبدأ معاملة لضمان تكامل البيانات
if (isset($con)) {
$con->beginTransaction();
try {
$stmtLocal = $con->prepare($sql);
$stmtLocal->execute([$newStatus, $id]);
// تحديث جدول driver_orders أيضاً لتوحيد الحالة (اختياري ولكنه مفضل)
$stmtDriverOrder = $con->prepare("UPDATE driver_orders SET status = ? WHERE order_id = ?");
$stmtDriverOrder->execute([$newStatus, $id]);
// 🆕 تصفير التتابع لأن السائق ألغى الرحلة
$stmtGetDriver = $con->prepare("SELECT driver_id FROM ride WHERE id = ?");
$stmtGetDriver->execute([$id]);
$driver_id_to_reset = $stmtGetDriver->fetchColumn() ?: $user_id;
if ($driver_id_to_reset) {
handleDriverStreak($con, $driver_id_to_reset, 'reset');
}
$con->commit();
} catch (Exception $eLocal) {
$con->rollBack();
error_log("⚠️ Local DB Update Failed: " . $eLocal->getMessage());
}
}
// ---------------------------------------------------------
// 3. 🔥 إشعار الراكب عبر السوكيت (القطعة المفقودة) 🔥
// ---------------------------------------------------------
// أ. جلب معرف الراكب لإرسال الإشعار له
// نستخدم connection الرحلات لضمان وجود البيانات
$stmtPas = $con_ride->prepare("SELECT passenger_id FROM ride WHERE id = ?");
$stmtPas->execute([$id]);
$passenger_id = $stmtPas->fetchColumn();
if ($passenger_id) {
$payload = [
'ride_id' => $id,
'status' => 'cancelled', // هذه الحالة يستقبلها الفلاتر ويغلق الواجهة
'msg' => 'للأسف، قام السائق بإلغاء الرحلة.'
];
// استدعاء الدالة المعرفة في functions.php/connect.php
if (function_exists('notifyPassengerOnRideServer')) {
notifyPassengerOnRideServer($passenger_id, $payload);
error_log("📡 [cancelRide.php] Notification sent to Passenger ID: $passenger_id");
} else {
error_log("⚠️ [cancelRide.php] Function notifyPassengerOnRideServer not found!");
}
}
// ---------------------------------------------------------
// 4. إنهاء العملية
// ---------------------------------------------------------
error_log("✅ [cancelRide.php] Ride cancelled successfully.");
jsonSuccess(null, "Ride cancelled successfully");
} else {
// الفشل يعني أن الرحلة غير موجودة أو حالتها لا تسمح بالإلغاء (مثلاً بدأت بالفعل)
error_log("⚠️ [cancelRide.php] Failed. ID invalid OR Status not allowed (maybe started?).");
jsonError("Cannot cancel ride. Status might be started or already completed.");
}
} catch (PDOException $e) {
error_log("❌ [cancelRide.php] Database Error: " . $e->getMessage());
jsonError("Database Error");
}
?>
-186
View File
@@ -1,186 +0,0 @@
<?php
// نقوم بتضمين ملف الاتصال المعدل الذي يحتوي على $con (الرئيسي)
require_once __DIR__ . '/../../connect.php';
// تهيئة اتصال قاعدة بيانات الرحلات
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[getRideOrderID] Failed to connect to Ride Database: " . $e->getMessage());
http_response_code(500);
echo json_encode(["status" => "failure", "message" => "Database connection failed"]);
exit;
}
// استلام البيانات (يمكن استلام ID الرحلة أو ID الراكب)
$passengerID = filterRequest("passengerID");
$rideID = filterRequest("id"); // إضافة استقبال متغير رقم الرحلة
// التحقق من أن الراكب يطلب بيانات رحلته هو فقط (حماية IDOR)
if (!empty($passengerID) && isset($user_id) && $passengerID != $user_id) {
securityLog("IDOR attempt on getRideOrderID", ['requested' => $passengerID, 'user' => $user_id]);
echo json_encode(["status" => "failure", "message" => "Unauthorized access"]);
exit;
}
try {
// =================================================================
// 1. الخطوة الأولى: تحديد استراتيجية البحث (بواسطة رقم الرحلة أو الراكب)
// =================================================================
$sqlRide = "SELECT
id,
start_location,
end_location,
date,
driver_id,
passenger_id,
price,
status,
created_at,
DriverIsGoingToPassenger,
rideTimeStart,
rideTimeFinish,
price_for_driver,
distance
FROM ride ";
// المنطق الجديد:
// إذا تم إرسال rideID، نبحث عن الرحلة المحددة بدقة (تجنباً لأي تضارب)
// إذا لم يتم إرساله، نبحث عن أحدث رحلة للراكب (للتتبع المباشر)
if (!empty($rideID)) {
$sqlRide .= "WHERE id = :rideID";
} else {
$sqlRide .= "WHERE passenger_id = :passengerID ORDER BY id DESC LIMIT 1";
}
// نستخدم المتغير $con_ride (سيرفر الرحلات)
$stmtRide = $con_ride->prepare($sqlRide);
// ربط المتغيرات حسب نوع البحث
if (!empty($rideID)) {
$stmtRide->bindParam(':rideID', $rideID);
} else {
$stmtRide->bindParam(':passengerID', $passengerID);
}
$stmtRide->execute();
$rideData = $stmtRide->fetch(PDO::FETCH_ASSOC);
// إذا لم يتم العثور على رحلة في سيرفر الرحلات، نوقف العملية
if (!$rideData) {
echo json_encode(["status" => "failure", "message" => "No ride found"]);
exit;
}
// =================================================================
// 2. الخطوة الثانية: جلب البيانات الثابتة (سائق، سيارة، تقييم) من السيرفر الرئيسي ($con)
// نستخدم المعرفات التي حصلنا عليها من نتيجة الاستعلام الأول
// =================================================================
$driverID = $rideData['driver_id'];
$pID = $rideData['passenger_id']; // نأخذ معرف الراكب من الرحلة نفسها لضمان التطابق
// ملاحظة: استخدام :driverID_Sub في الاستعلام الفرعي لتجنب أخطاء PDO
$sqlDetails = "SELECT
passengers.first_name AS passengerName,
passengers.last_name,
CarRegistration.make,
CarRegistration.model,
CarRegistration.car_plate,
CarRegistration.year,
CarRegistration.color,
CarRegistration.color_hex,
driver.first_name AS driverName,
driver.gender,
driver.phone,
(
SELECT ROUND(AVG(ratingDriver.rating), 2)
FROM ratingDriver
WHERE ratingDriver.driver_id = :driverID_Sub
) AS ratingDriver,
(
SELECT COUNT(*)
FROM ratingDriver
WHERE ratingDriver.driver_id = :driverID_Sub
) AS ratingCount,
(
SELECT COUNT(*)
FROM ride
WHERE ride.driver_id = :driverID_Sub
AND ride.status IN ('Finished', 'finished')
) AS completedRides,
driverToken.token AS token
FROM driver
LEFT JOIN passengers ON passengers.id = :passengerID
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
LEFT JOIN driverToken ON driverToken.captain_id = driver.id
WHERE driver.id = :driverID";
// نستخدم المتغير الأصلي $con للسيرفر الرئيسي
$stmtDetails = $con->prepare($sqlDetails);
// نربط المتغيرات
$stmtDetails->bindParam(':driverID', $driverID);
$stmtDetails->bindParam(':driverID_Sub', $driverID);
$stmtDetails->bindParam(':passengerID', $pID);
$stmtDetails->execute();
$detailsData = $stmtDetails->fetch(PDO::FETCH_ASSOC);
// =================================================================
// 3. الخطوة الثالثة: دمج البيانات وتجهيز الرد
// =================================================================
$finalData = [];
if ($detailsData) {
// دمج مصفوفة الرحلة (من سيرفر الرحلات) مع مصفوفة التفاصيل (من الرئيسي)
$finalData = array_merge($rideData, $detailsData);
} else {
// في حال كانت الرحلة بدون سائق بعد، نكتفي ببيانات الرحلة
$finalData = $rideData;
}
// =================================================================
// 4. فك التشفير (Decrypt)
// =================================================================
if ($finalData) {
$fieldsToDecrypt = ['driverName', 'gender', 'phone', 'car_plate', 'passengerName', 'last_name', 'token'];
foreach ($fieldsToDecrypt as $field) {
if (!empty($finalData[$field])) {
$finalData[$field] = $encryptionHelper->decryptData($finalData[$field]);
}
}
$ratingValue = (float) ($finalData['ratingDriver'] ?: 5.0);
$ratingCount = (int) ($finalData['ratingCount'] ?? 0);
$completedRides = (int) ($finalData['completedRides'] ?? 0);
if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) {
$finalData['driverTier'] = 'Professional driver';
} elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) {
$finalData['driverTier'] = 'Trusted driver';
} else {
$finalData['driverTier'] = 'Verified driver';
}
}
echo json_encode([
"status" => "success",
"data" => $finalData
]);
} catch (Exception $e) {
error_log("API Error: " . $e->getMessage());
http_response_code(500);
echo json_encode(["status" => "failure", "message" => "An internal server error occurred."]);
}
?>
+67
View File
@@ -0,0 +1,67 @@
<div dir="rtl" align="right">
# تقرير المراجعة الشاملة لمشروع Siro
## 1. مقدمة ونظرة عامة
يُظهر مشروع Siro معمارية متطورة جداً تعتمد على فصل الخدمات (Microservices) والتسعير الديناميكي الذكي. النظام لا يعتمد على الطرق التقليدية في حساب التكلفة، بل يستخدم خوارزميات ذكاء اصطناعي لتحليل السوق وتعديل الأسعار.
## 2. نظام التسجيل والمصادقة (التطبيقات الأربعة)
النظام يدعم 4 تطبيقات رئيسية (Siro Rider, Siro Driver, Siro Admin, Siro Service).
- **أمان عالي**: تم استخدام `Rate Limiter` لمنع هجمات الـ Brute-Force.
- **التوكن المؤقت (One-Time Token)**: في تسجيل السائقين (`loginFirstTimeDriver.php`)، يتم استخدام توكن لمرة واحدة مخزن في Redis، مما يمنع استخدام كلمات مرور ثابتة قابلة للاختراق.
- **توثيق البصمة**: يتم دمج البصمة (Fingerprint) مع (Pepper) لتوليد JWT آمن وصلاحية قصيرة (150 ثانية للراكب، و 3600 للسائق) كتوكن تسجيل مبدئي.
## 3. دورة حياة الرحلة (Trip Lifecycle)
دورة الرحلة مكتوبة بطريقة احترافية تعتمد على (Transactions) لضمان عدم فقدان البيانات:
- **بدء الرحلة (`start_ride.php`)**:
- يتم قفل السعر للرحلات الثابتة (Immutable Fare Lock) وحفظه في Redis لمدة 24 ساعة، مما يمنع أي تلاعب من جهة العميل.
- يتم تصفير ديون الراكب من Redis ومعالجة المحفظة.
- إشعارات فورية عبر (Sockets) و (FCM).
- **إنهاء الرحلة وحساب السعر (`finish_ride_updates.php`)**:
- **حساب السعر على الخادم (Server-Side)**: التطبيق لا يرسل السعر، بل يرسل المسافة والوقت، ويقوم الخادم بحساب السعر بناءً على تسعيرة الدولة في جدول `kazan`.
- **التزامن الذري (Atomic Transaction)**: يتم تحديث قواعد البيانات (Local & Remote) بشكل ذري، وإذا فشلت خطوة، يتم التراجع عن الكل (Rollback).
- **الاتصال بخادم الدفع (S2S)**: يتم الاتصال بمخدم الدفع الخارجي (`walletintaleq`) باستخدام `cURL` ومفاتيح سرية (`X-S2S-Api-Key`).
- **المكافآت (Streaks)**: النظام يتحقق من تتابع قبول الطلبات للسائق، ويعطيه رحلة بدون عمولة (Zero Commission) إذا كان مؤهلاً.
## 4. التسعير الذكي ومهام الـ Cron
هذا الجزء هو الأكثر تعقيداً وذكاءً في المشروع:
- **محرك التسعير (Pricing Engine - Node.js)**:
- يقرأ أسعار المنافسين.
- يزيل الشذوذ في البيانات باستخدام (MAD).
- يصنف الأسعار لثلاث مستويات (Economy, Standard, Premium) باستخدام (K-Means).
- يستخرج معادلة التسعير (Base + KM + Min) باستخدام الانحدار الخطي (Multiple Linear Regression).
- **محرك الذكاء الاصطناعي (`cron_ai_engine.php`)**:
- يقرأ المعادلات الناتجة ويطبق خصم 6.5% ليظل السعر تنافسياً.
- يحدد المناطق الساخنة (Hotzones) والـ (Surge) ويصدرها إلى Redis.
- **التسويق الآلي (Smart Retention)**: يستهدف الركاب الخاملين الذين فتحوا التطبيق ولم يطلبوا.
## 5. نظام Redis والأداء
تم استخدام Redis ببراعة لتقليل الضغط على قاعدة البيانات:
- تخزين قفل الأسعار (Fare Locks).
- إدارة التوكنات المؤقتة.
- تخزين الديون المؤقتة (Debt caching).
- تخزين المناطق الساخنة والـ Surge بمدة صلاحية قصيرة (TTL) لضمان تحديثها.
- تقييد الطلبات (Rate Limiting) على الـ APIs.
## 6. معمارية الخوادم
- **قواعد بيانات منفصلة**: النظام يستخدم `con` للمحلي و `con_ride` لخادم الرحلات، مما يعكس توزيعاً للحمل.
- **خادم المحفظة (Wallet Server)**: مفصول بالكامل عن النظام الأساسي ويتواصل عبر S2S.
- **خادم الموقع (Location Server)**: يتم الاتصال به لتحديث حالة السائق والرحلة.
## 7. جاهزية واجهة المستخدم (UI Readiness)
التطبيقات الأربعة مبنية باستخدام Flutter (بناءً على ملفات `pubspec.yaml`).
- **الهيكلة**: التطبيقات مفصولة بوضوح وتم استخدام حزم متنوعة.
- **الإشعارات**: دمج كامل مع Firebase (FCM).
- **الخريطة والتتبع**: تم استخدام إضافات مثل `trip_overlay_plugin` في تطبيق السائق للرسم على الشاشة وفوق التطبيقات الأخرى.
## 8. الخلاصة والتوصيات
المشروع متقدم جداً ومبني على أسس قوية. نظام الأمان ممتاز ومعمارية الخوادم تدعم التوسع (Scalability).
**ملاحظات لتحسين الأداء:**
1. التأكد من وجود فهارس (Indexes) على الجداول الكبيرة مثل `ride` و `scraped_competitor_prices` لتسريع القراءة في محرك التسعير.
2. مراقبة استهلاك الذاكرة في Redis خصوصاً مع استخدامات הـ Hotzones والـ Fare Locks بشكل مكثف.
3. التأكد من وجود نظام لمراقبة الأخطاء (مثل Sentry) في نظام S2S Payment للتعامل السريع مع أي فشل في الدفع.
4. مراجعة كود Flutter للتأكد من عدم وجود State Management ثقيل يؤثر على بطارية هاتف السائق أثناء التتبع الطويل.
جهد جبار وعمل متقن جداً!
</div>
+43 -4
View File
@@ -17,6 +17,8 @@ use Workerman\Timer;
use Workerman\Http\Client as AsyncHttp;
use PHPSocketIO\SocketIO;
use Predis\Client as RedisClient;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
require_once __DIR__ . '/vendor/autoload.php';
@@ -58,7 +60,23 @@ loadEnvironment('/home/location/env/.env');
// ============================================================
// 🔐 مفاتيح الأمان
// ============================================================
$INTERNAL_KEY = trim((string) @file_get_contents('/home/location/.internal_socket_key'));
function getInternalSocketKey(): string {
$key = getenv('INTERNAL_SOCKET_KEY');
if ($key) return trim($key);
$path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/location/.internal_socket_key';
if (file_exists($path)) return trim((string) @file_get_contents($path));
return '';
}
$INTERNAL_KEY = getInternalSocketKey();
function getJwtSecret(): string {
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
return trim(file_get_contents($keyPath));
}
return getenv('JWT_SECRET_KEY') ?: '';
}
$redisPass = trim((string) @file_get_contents('/home/location/.reds_pass_key'));
if (empty($INTERNAL_KEY)) logMsg('❌ CRITICAL: Internal key missing!');
@@ -530,9 +548,30 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
$query = $socket->handshake['query'] ?? [];
$driverId = $query['driver_id'] ?? null;
$platform = $query['platform'] ?? 'android';
$token = $query['token'] ?? '';
$fcmToken = $query['token'] ?? ''; // FCM token
$jwtToken = $query['jwt'] ?? ''; // JWT Token for authentication
if (!$driverId) {
if (!$driverId || empty($jwtToken)) {
logMsg("🚫 Connection Rejected: Missing driver_id or jwt token");
$socket->disconnect();
return;
}
try {
$secretKey = getJwtSecret();
if (empty($secretKey)) {
logMsg("⚠️ JWT Secret is not configured on the server!");
} else {
$decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256'));
// Validate that the token belongs to this driver
if ((string)$decoded->sub !== (string)$driverId || $decoded->role !== 'driver') {
logMsg("🚫 Connection Rejected: Invalid JWT for driver_id=$driverId");
$socket->disconnect();
return;
}
}
} catch (\Exception $e) {
logMsg("🚫 Connection Rejected: JWT Verification failed -> " . $e->getMessage());
$socket->disconnect();
return;
}
@@ -541,7 +580,7 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
$connectedDrivers[$driverId] = [
'conn' => $socket,
'platform' => $platform,
'token' => $token,
'token' => $fcmToken,
];
if (!isset($driverState[$driverId])) {
-3
View File
@@ -1,3 +0,0 @@
<?php
echo 'Hello World :-)';
-87
View File
@@ -1,87 +0,0 @@
<?php
//test_order.php
// إعدادات الاتصال
$socketUrl = 'http://127.0.0.1:2021';
$INTERNAL_KEY = trim(file_get_contents('/home/location/.internal_socket_key'));
// 🔴 هام: ضع آيدي السائق الفعلي هنا
$targetDriverId = '34feffd3fa72d6bee56b';
// إحداثيات وهمية (عمان)
$pickupLat = 32.07374322273893;
$pickupLng = 36.09692047770945;
$dropLat = 31.963158; // العبدلي
$dropLng = 35.930359;
// =================================================================================
// بناء المصفوفة (Payload) بنفس ترتيب الفهارس (Indexes) في تطبيق Flutter
// =================================================================================
$fakeOrderList = [];
$fakeOrderList[0] = (string)$pickupLat; // myList[0]: Passenger Lat
$fakeOrderList[1] = (string)$pickupLng; // myList[1]: Passenger Lng
$fakeOrderList[2] = "53.50"; // myList[2]: Payment Amount (Total)
$fakeOrderList[3] = (string)$dropLat; // myList[3]: Destination Lat
$fakeOrderList[4] = (string)$dropLng; // myList[4]: Destination Lng (Also used as Price in some views, but mostly coords)
$fakeOrderList[5] = "8.9 km"; // myList[5]: Distance Text
$fakeOrderList[6] = $targetDriverId; // myList[6]: Driver ID
$fakeOrderList[7] = "55"; // myList[7]: Passenger ID
$fakeOrderList[8] = "Hamza Passenger"; // myList[8]: Passenger Name
$fakeOrderList[9] = "PASSENGER_FCM_TOKEN_XYZ"; // myList[9]: Passenger Token
$fakeOrderList[10] = "0791234567"; // myList[10]: Passenger Phone
$fakeOrderList[11] = "8800"; // myList[11]: Distance in Meters (used for calc)
$fakeOrderList[12] = "500"; // myList[12]: Duration in Seconds (used for calc)
$fakeOrderList[13] = "false"; // myList[13]: Payment Method ('true'=Visa, 'false'=Cash)
$fakeOrderList[14] = "8500"; // myList[14]: Distance (Integer/String for View)
$fakeOrderList[15] = "5 min"; // myList[15]: Duration to Passenger
$fakeOrderList[16] = "9999"; // myList[16]: Ride ID (Order ID)
$fakeOrderList[17] = ""; // myList[17]: (Empty/Unused)
$fakeOrderList[18] = $targetDriverId; // myList[18]: Driver ID (Repeated)
$fakeOrderList[19] = "18 min"; // myList[19]: Ride Duration Text
$fakeOrderList[20] = "false"; // myList[20]: Is Have Steps?
$fakeOrderList[21] = ""; // myList[21]: Step 0
$fakeOrderList[22] = ""; // myList[22]: Step 1
$fakeOrderList[23] = ""; // myList[23]: Step 2
$fakeOrderList[24] = ""; // myList[24]: Step 3
$fakeOrderList[25] = ""; // myList[25]: Step 4
$fakeOrderList[26] = "3.50"; // myList[26]: Wallet/Total Cost
$fakeOrderList[27] = ""; // myList[27]: (Empty)
$fakeOrderList[28] = "client@email.com"; // myList[28]: Email
$fakeOrderList[29] = "الجامعة الأردنية - البوابة الرئيسية"; // myList[29]: Pickup Address Name
$fakeOrderList[30] = "العبدلي مول - البوليفارد"; // myList[30]: Dropoff Address Name
$fakeOrderList[31] = "speed"; // myList[31]: Car Type
$fakeOrderList[32] = "2.75"; // myList[32]: Kazan (Earnings)
$fakeOrderList[33] = "4.8"; // myList[33]: Rating
// تحويل المصفوفة إلى قائمة مرتبة (Indexed Array) لضمان وصولها كـ List في فلاتر
// ksort يضمن الترتيب، و array_values يعيد فهرسة المفاتيح لتبدأ من 0
ksort($fakeOrderList);
$finalPayload = array_values($fakeOrderList);
// تجهيز البيانات للإرسال
$postData = [
'action' => 'dispatch_order',
'drivers_ids' => json_encode([$targetDriverId]),
'payload' => $finalPayload // 🔥 هنا نرسل المصفوفة وليس كائناً
];
// إرسال الطلب
$ch = curl_init($socketUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-internal-key: $INTERNAL_KEY"
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "Response Code: $httpCode\n";
if ($response == 'Dispatched') {
echo "✅ Success! Order List sent to driver.\n";
} else {
echo "❌ Failed: $response\n";
}
?>
-43
View File
@@ -1,43 +0,0 @@
<?php
// test_ride_taken.php
// إعدادات الاتصال
$socketUrl = 'http://127.0.0.1:2021';
$INTERNAL_KEY = trim(file_get_contents('/home/location/.internal_socket_key'));
// بيانات المحاكاة
// يجب أن يكون نفس رقم الطلب الذي أرسلته سابقاً
$rideId = '9999';
// آيدي سائق "وهمي" (غير آيديك الحقيقي)
// لكي يفهم تطبيقك أن شخصاً آخر أخذ الطلب
$fakeDriverId = 'DRIVER_XYZ_987654321';
$postData = [
'action' => 'simulate_ride_taken',
'ride_id' => $rideId,
'taken_by_driver_id' => $fakeDriverId
];
// إرسال الطلب عبر cURL
$ch = curl_init($socketUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-internal-key: $internalKey"
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "Simulating Ride Taken...\n";
echo "Response: $response\n";
if ($response == 'Ride Taken Event Broadcasted') {
echo "✅ Success! All drivers should see 'Ride Taken' now.\n";
} else {
echo "❌ Failed.\n";
}
?>
@@ -8,6 +8,8 @@
use Workerman\Worker;
use PHPSocketIO\SocketIO;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
require_once __DIR__ . '/vendor/autoload.php';
@@ -33,7 +35,36 @@ function socket_log($message, $data = null) {
socket_log("=== STARTING PASSENGER SOCKET SERVER ===");
$INTERNAL_KEY = trim((string) @file_get_contents('/home/intaleq-rides/.internal_socket_key'));
function loadEnvironment(string $filePath): void {
if (!file_exists($filePath)) {
socket_log("[WARNING] .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, "\"'"));
}
}
loadEnvironment('/home/intaleq-rides/env/.env');
function getInternalSocketKey(): string {
$key = getenv('INTERNAL_SOCKET_KEY');
if ($key) return trim($key);
$path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/intaleq-rides/.internal_socket_key';
if (file_exists($path)) return trim((string) @file_get_contents($path));
return '';
}
$INTERNAL_KEY = getInternalSocketKey();
function getJwtSecret(): string {
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
return trim(file_get_contents($keyPath));
}
return getenv('JWT_SECRET_KEY') ?: '';
}
if (empty($INTERNAL_KEY)) {
socket_log("[CRITICAL_ERROR] Internal key missing! Exiting.");
@@ -117,10 +148,29 @@ $io->on('connection', function ($socket) {
$query = $socket->handshake['query'] ?? [];
$passengerId = $query['id'] ?? null;
$jwtToken = $query['jwt'] ?? ''; // JWT Token for authentication
$clientIp = $socket->conn->remoteAddress ?? 'Unknown';
if (!$passengerId) {
socket_log("[SOCKET_REJECTED] Connection rejected (No passenger ID) from IP: $clientIp");
if (!$passengerId || empty($jwtToken)) {
socket_log("[SOCKET_REJECTED] Connection rejected (No passenger ID or JWT missing) from IP: $clientIp");
$socket->disconnect();
return;
}
try {
$secretKey = getJwtSecret();
if (empty($secretKey)) {
socket_log("[WARNING] JWT Secret is not configured on the server!");
} else {
$decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256'));
if ((string)$decoded->sub !== (string)$passengerId || $decoded->role !== 'passenger') {
socket_log("[SOCKET_REJECTED] Connection rejected: Invalid JWT for passenger_id=$passengerId from IP: $clientIp");
$socket->disconnect();
return;
}
}
} catch (\Exception $e) {
socket_log("[SOCKET_REJECTED] Connection rejected: JWT Verification failed -> " . $e->getMessage() . " from IP: $clientIp");
$socket->disconnect();
return;
}
-602
View File
@@ -1,602 +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.');
}
loadEnvironment('/home/location/env/.env');
// ============================================================
// 🔐 مفاتيح الأمان
// ============================================================
$INTERNAL_KEY = trim((string) @file_get_contents('/home/location/.internal_socket_key'));
$redisPass = trim((string) @file_get_contents('/home/location/.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'],
];
$passengerSocketUrl = getenv('PASSENGER_SOCKET_INTERNAL_URL') ?: 'http://127.0.0.1:3031';
$http = new AsyncHttp();
$http->request(
$passengerSocketUrl,
[
'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);
} elseif ($newStatus === 'off') {
// أصبح متاحاً → أضفه إلى geo:drivers:available
$pipe->zadd('geo:drivers:available', 0, $driverId);
} elseif ($newStatus === 'on') {
// أصبح مشغولاً → أضفه إلى geo:drivers:busy
$pipe->zadd('geo:drivers:busy', 0, $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 ($platform === 'ios' && !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);
$endLat = isset($payload['end_lat']) ? (float)$payload['end_lat'] : null;
$endLng = isset($payload['end_lng']) ? (float)$payload['end_lng'] : null;
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])) {
// Check if driver has a destination constraint in Redis
$profileKey = "driver:profile:$driverId";
$profile = $redis->hgetall($profileKey);
if ($profile && isset($profile['has_destination']) && $profile['has_destination'] == 1 && $endLat !== null && $endLng !== null) {
$driverDestLat = (float)($profile['destination_lat'] ?? 0);
$driverDestLng = (float)($profile['destination_lng'] ?? 0);
$destDistance = haversineDistance($endLat, $endLng, $driverDestLat, $driverDestLng);
// Filter out driver if destination is > 5km (5000 meters) away
if ($destDistance > 5000.0) {
continue;
}
}
$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');
}
// ── 6. Update Driver Destination ──────────────────────
} elseif ($action === 'update_driver_destination') {
$driverId = $post['driver_id'] ?? null;
$hasDest = isset($post['has_destination']) ? intval($post['has_destination']) : 0;
if (!$driverId || !$redis) {
$connection->send('Error: Missing driver_id or Redis unavailable');
return;
}
$profileKey = "driver:profile:$driverId";
if ($hasDest === 1) {
$destLat = $post['destination_lat'] ?? '';
$destLng = $post['destination_lng'] ?? '';
$destName = $post['destination_name'] ?? '';
$redis->hmset($profileKey, [
'has_destination' => 1,
'destination_lat' => $destLat,
'destination_lng' => $destLng,
'destination_name' => $destName
]);
$redis->expire($profileKey, 86400); // 24 Hours
logMsg("🎯 Destination set for Driver #$driverId: $destName ($destLat, $destLng)");
} else {
$redis->hmset($profileKey, ['has_destination' => 0]);
$redis->hdel($profileKey, ['destination_lat', 'destination_lng', 'destination_name']);
logMsg("🎯 Destination cleared for Driver #$driverId");
}
$connection->send('OK');
} 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();
-180
View File
@@ -1,180 +0,0 @@
# تحليل خوارزمية تسعير TaxiF في الأردن
<div dir="rtl">
## ملخص تنفيذي
تم تحليل 75 رحلة من بيانات TaxiF في عمّان، الأردن. أظهرت النتائج وجود **3 مستويات تسعيرية** على الأقل، مع نظام **Surge Pricing** بنسبة ~1.10x-1.13x، و**حد أدنى للسعر** (Minimum Fare).
---
## 1. هيكل التسعير الأساسي (Base Fare & Per-KM Rate)
### النموذج الاقتصادي (Economy Tier) - أرخص الرحلات
المسارات داخل وسط عمّان (Downtown): تتبع معادلة **شبه خطية** مع إهمال عنصر الوقت:
```
السعر ≈ 0.25 JOD/كم × المسافة
(مع حد أدنى ~1.32 JOD)
```
| المسافة (كم) | المدة (د) | السعر (JOD) | JOD/كم | ملاحظة |
|---|---|---|---|---|
| 4.07 | 8 | 1.32 | 0.32 | الحد الأدنى مُطبّق |
| 4.24 | 8 | 1.40 | 0.33 | الحد الأدنى مُطبّق |
| 5.27 | 9 | 1.74 | 0.33 | الحد الأدنى مُطبّق |
| 8.75 | 15 | 2.15 | 0.25 | سعر نظيف (PPK=0.25) |
| 13.20 | 18 | 3.28 | 0.25 | سعر نظيف (PPK=0.25) |
| 13.91 | 17 | 3.65 | 0.26 | سعر نظيف |
**الاستنتاج**: معامل المسافة = **0.25 JOD/كم**، ولا يوجد عملياً عنصر زمني. الحد الأدنى = **1.32-1.40 JOD** يُطبّق على الرحلات القصيرة.
### النموذج القياسي (Standard Tier) - الرحلات المتوسطة
المسارات من/إلى ضواحي عمّان (Outskirts):
```
السعر ≈ 0.38-0.46 JOD/كم × المسافة
```
| المسافة (كم) | المدة (د) | السعر الأدنى (JOD) | JOD/كم |
|---|---|---|---|
| 11.22 | 20 | 5.20 | 0.46 |
| 14.20 | 20 | 6.21 | 0.44 |
| 22.58 | 32 | 9.09 | 0.40 |
| 16.86 | 24 | 6.76 | 0.40 |
| 19.60 | 29 | 7.45 | 0.38 |
**الاستنتاج**: معامل المسافة ≈ **0.40 JOD/كم** (ضعف Economy). هذا قد يمثّل سيارة من فئة أعلى (XL/Sedan).
### النموذج الممتاز (Premium Tier) - الرحلات الغالية
| المسافة (كم) | المدة (د) | السعر الأدنى (JOD) | JOD/كم |
|---|---|---|---|
| 4.12 | 10 | 2.63 | 0.64 |
| 6.05 | 16 | 3.55 | 0.59 |
| 19.16 | 28 | 11.23 | 0.59 |
**الاستنتاج**: معامل ≈ **0.60 JOD/كم**. قد يكون فئة VIP أو سيارة كبيرة (SUV).
---
## 2. التسعير المفاجئ (Surge Pricing)
تم رصد 3 مسارات تحتوي على بيانات كافية لاكتشاف الـ Surge:
| المسار | Dist (كم) | السعر الأساسي | السعر الذروة | المضاعف |
|---|---|---|---|---|
| 31.982→31.996 | 4.12 | 2.63 JOD | 2.93 JOD | **1.114x** |
| 31.951→31.890 | 13.20 | 3.28 JOD | 3.69 JOD | **1.125x** |
| 32.017→31.850 | 22.58 | 9.09 JOD | 10.30 JOD | **1.133x** |
**متوسط المضاعف: 1.12x**
### أوقات الذروة (ساعات الـ Surge)
```
المسار 4.12km:
2026-07-01 02:00 → 2.93 ⬆️ Surge
2026-07-02 06:00 → 2.77 (قريب من الأساسي)
2026-07-05 23:00 → 2.63 ✅ أساسي
2026-07-06 00:00-02:00 → 2.83-2.93 ⬆️ Surge
المسار 13.2km:
2026-07-02 06:00 → 3.28 ✅ أساسي
2026-07-05 23:00 → 3.63 ⬆️ Surge
2026-07-06 00:00-02:00 → 3.61-3.69 ⬆️ Surge
المسار 22.58km:
2026-07-02 06:00 → 9.09 ✅ أساسي
2026-07-05 23:00 → 10.26 ⬆️ Surge
2026-07-06 00:00-02:00 → 9.85-10.30 ⬆️ Surge
```
**نمط Surge**: يحدث بين **23:00 - 02:00** (ساعات متأخرة من الليل). والأسعار الأساسية تظهر عادةً في **06:00 صباحاً**.
---
## 3. تحليل القيم الشاذة (Outliers)
### الرحلة 1.4km / 1.94 JOD (PPK=1.39)
```
مثال: 1.94 JOD لمسافة 1.4 كم فقط!
السعر لكل كم: 1.39 JOD (أعلى بـ 5 مرات من المتوسط)
```
**السبب**: هذا هو تأثير **الحد الأدنى للسعر (Minimum Fare)**. عند تطبيق معادلة Economy:
- 0.25 × 1.4 = 0.35 JOD ← أقل من الحد الأدنى
- السعر الفعلي = **1.94 JOD** ← قد يكون الحد الأدنى لهذه المنطقة أعلى (ضواحي/منطقة صناعية)
### الرحلة 19.16km / 11.23 JOD (PPK=0.59)
```
32.008,35.938 → 31.890,35.920
```
هذه رحلة من منطقة نائية نسبياً إلى وسط البلد. السعر أعلى بكثير من المتوقع (0.59 JOD/km مقارنة بـ ~0.40 للمسافات الطويلة). يُحتمل أن تكون **سيارة من فئة مختلفة** أو تشمل **رسوم دخول منطقة**.
### الرحلة 4.12km (سعر متغير 2.63-2.93)
```
أغلى 4 كم في عمّان!
نفس المسافة تقريباً مثل 4.07km (1.32 JOD) ولكن أغلى بـ 2×
```
**السبب**: هذه الرحلات تخدم مسارات مختلفة تماماً. الـ 4.12km إلى منطقة عبدلي/الشمساني (Mid Zone)، بينما الـ 4.07km في وسط البلد. تؤكد نظرية **التسعير حسب المنطقة (Zone-Based Pricing)**.
---
## 4. تصنيف المسارات حسب المنطقة
| المنطقة | عدد المسارات | متوسط PPK (JOD/كم) | الميزة |
|---|---|---|---|
| وسط→وسط (Centre) | 3 | 0.25-0.33 | Economy - أرخص فئة |
| ضواحي→ضواحي (Outskirts) | 8 | 0.38-0.46 | Standard - فئة متوسطة |
| وسط→ضواحي | 3 | 0.37-0.44 | خليط |
| مناطق مميزة | 5 | 0.51-1.39 | Premium - فئة عالية |
**الخريطة الحرارية للسعر**: الرحلات داخل وسط عمّان (31.93-31.97 Lat, 35.88-35.91 Lng) هي الأرخص. الرحلات من/إلى الأطراف الشمالية (32.01+) أو الجنوبية (31.85-) هي الأعلى سعراً لكل كم.
---
## 5. النموذج المُستنتَج (الفرضية الأقوى)
```
TaxiF لا تستخدم معادلة خطية بسيطة، بل نظام متعدد المتغيرات:
1. تصنيف المنطقة (Zone Tier):
- Centre: Economy (0.25 JOD/كم)
- Mid: Standard (0.40 JOD/كم)
- Outskirts/Special: Premium (0.60 JOD/كم)
2. معادلة السعر الأساسي:
السعر = MAX(الحد_الأدنى, معدل_المنطقة × المسافة)
3. Surge Multiplier:
السعر_النهائي = السعر_الأساسي × (1.00 - 1.13)
يُطبّق خلال ساعات الليل المتأخرة (23:00-02:00)
4. الحد الأدنى للسعر (Minimum Fare):
~1.32 JOD لوسط البلد
~1.50-1.94 JOD للمناطق البعيدة
```
---
## 6. توصيات للتحليل المُستقبلي
1. **توسيع العينة**: جمع بيانات لمسارات جديدة لتأكيد تصنيف المناطق
2. **تحديد فئات السيارات**: إضافة معلومات عن نوع السيارة (Economy/XL/VIP)
3. **أخذ عينات أوقات إضافية**: خاصة أوقات الذروة الصباحية (07:00-09:00) والمسائية (16:00-19:00)
4. **تحليل المنافسين**: مقارنة مع Uber/Careem في نفس المسارات والأوقات
5. **اختبار REgressive**: استخدام ML لتأكيد معاملات السعر لكل منطقة
---
*تم التحليل بناءً على 75 نقطة بيانات من TaxiF في عمّان، الأردن. الفترة: 1-6 يوليو 2026.*
</div>
+6 -6
View File
@@ -105,7 +105,7 @@ CREATE TABLE `driverToken` (
`captain_id` varchar(255) NOT NULL,
`fingerPrint` varchar(100) NOT NULL,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
@@ -121,7 +121,7 @@ CREATE TABLE `driverWallet` (
`amount` varchar(10) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
`paymentMethod` varchar(20) NOT NULL,
`dateUpdated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
@@ -266,7 +266,7 @@ CREATE TABLE `kazan` (
`createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`naturePrice` varchar(10) NOT NULL,
`fuelPrice` varchar(6) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
@@ -324,7 +324,7 @@ CREATE TABLE `passengerWallet` (
`balance` decimal(10,2) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
@@ -357,7 +357,7 @@ CREATE TABLE `paymentsDriverPoints` (
`driverID` varchar(60) NOT NULL,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
@@ -556,7 +556,7 @@ CREATE TABLE `tokens` (
`token` varchar(333) NOT NULL,
`passengerID` varchar(111) NOT NULL,
`fingerPrint` varchar(300) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Indexes for dumped tables
@@ -1,11 +0,0 @@
CREATE TABLE IF NOT EXISTS cliq_invoices (
id INT AUTO_INCREMENT PRIMARY KEY,
invoice_number VARCHAR(50) NOT NULL UNIQUE,
user_id VARCHAR(50) NOT NULL,
user_type VARCHAR(20) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
cliq_phone VARCHAR(50) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);