feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
<?php
// ═══════════════════════════════════════════════════════════════
// driver/ride/accept_ride.php
// PURPOSE : قبول رحلة — ride DB هو المرجع، primary DB يتزامن بعده
// RACE : Optimistic lock عبر WHERE status IN ('waiting','wait')
// ═══════════════════════════════════════════════════════════════
include "../../connect.php";
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[accept_ride] Failed to connect to Ride Database: " . $e->getMessage());
printFailure("Database connection failed");
exit;
}
// ── 1. Input & Validation ──────────────────────────────────────
$rideId = filterRequest("id");
// Force driver_id from JWT — never trust user-supplied driver_id
$driverId = $user_id;
$status = filterRequest("status"); // القيمة التي يرسلها التطبيق: 'accepted'
$passengerToken = filterRequest("passengerToken");
$passengerFingerprint = filterRequest("passengerFingerprint");
$passengerIdValue = filterRequest("passenger_id");
if (empty($rideId) || empty($driverId)) {
printFailure("Missing required parameters");
exit;
}
// Self-ride validation
$driverFingerprint = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : '';
if (!empty($driverFingerprint) && $driverFingerprint === $passengerFingerprint) {
error_log("[accept_ride] Self-ride attempt blocked. DriverID=$driverId, Fingerprint=$driverFingerprint");
printFailure("Self-matching is not allowed");
exit;
}
// status whitelist — لا نقبل قيمة عشوائية من التطبيق
$allowedStatuses = ['accepted', 'Apply'];
if (!in_array($status, $allowedStatuses, true)) {
$status = 'accepted'; // fallback آمن
}
error_log("[accept_ride] DriverID=$driverId attempting RideID=$rideId");
try {
// ═══════════════════════════════════════════════════════════
// STEP A — القفل على ride DB (المرجع الأساسي)
// Optimistic lock: نغير فقط إذا status لا يزال 'waiting' أو 'wait'
// السائق الأول الذي يصل يربح — الباقي يجدون rowCount=0
// ═══════════════════════════════════════════════════════════
$stmtLock = $con_ride->prepare("
UPDATE `ride`
SET `status` = ?,
`driver_id` = ?,
`rideTimeStart` = NOW()
WHERE `id` = ?
AND `status` IN ('waiting', 'wait')
");
$stmtLock->execute([$status, $driverId, $rideId]);
if ($stmtLock->rowCount() === 0) {
// الرحلة غير متاحة — سائق آخر سبق أو الرحلة ألغيت
error_log("[accept_ride] RideID=$rideId not available for DriverID=$driverId (rowCount=0)");
printFailure("Ride not available");
exit;
}
error_log("[accept_ride] ride DB locked. RideID=$rideId → DriverID=$driverId");
// ═══════════════════════════════════════════════════════════
// STEP B — تزامن primary DB (بعد نجاح القفل)
// ═══════════════════════════════════════════════════════════
try {
$con->prepare("
UPDATE `ride`
SET `driver_id` = ?,
`status` = ?,
`rideTimeStart` = NOW()
WHERE `id` = ?
")->execute([$driverId, $status, $rideId]);
error_log("[accept_ride] primary DB synced. RideID=$rideId");
} catch (PDOException $eSync) {
// لا نوقف — ride DB هو المرجع
error_log("[accept_ride] primary DB sync WARNING: " . $eSync->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP C — driver_orders (INSERT أو UPDATE بسطر واحد آمن)
// ON DUPLICATE KEY يمنع race condition ثانية على هذا الجدول
// ═══════════════════════════════════════════════════════════
try {
$con->prepare("
INSERT INTO `driver_orders` (`driver_id`, `order_id`, `status`, `created_at`)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
`driver_id` = VALUES(`driver_id`),
`status` = VALUES(`status`),
`created_at` = NOW()
")->execute([$driverId, $rideId, $status]);
} catch (PDOException $eOrders) {
error_log("[accept_ride] driver_orders WARNING: " . $eOrders->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP C.1 — تحديث جدول waitingRides حتى لا تظهر للكباتن الآخرين
// ═══════════════════════════════════════════════════════════
try {
$con->prepare("UPDATE `waitingRides` SET `status` = 'Apply' WHERE `id` = ?")->execute([$rideId]);
} catch (PDOException $eWaiting) {
error_log("[accept_ride] waitingRides WARNING: " . $eWaiting->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP D — جلب بيانات السائق للراكب
// ═══════════════════════════════════════════════════════════
$driverInfo = [];
$stmtDriver = $con->prepare("
SELECT
d.id AS driver_id,
d.first_name,
d.last_name,
d.gender,
d.phone,
c.make,
c.model,
c.car_plate,
c.year,
c.color,
c.color_hex,
(SELECT ROUND(AVG(rating), 2) FROM ratingDriver WHERE driver_id = d.id) AS ratingDriver,
(SELECT COUNT(*) FROM ratingDriver WHERE driver_id = d.id) AS ratingCount,
(SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status IN ('Finished', 'finished')) AS completedRides,
dt.token
FROM driver d
LEFT JOIN CarRegistration c ON c.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
WHERE d.id = ?
LIMIT 1
");
$stmtDriver->execute([$driverId]);
$driverRaw = $stmtDriver->fetch(PDO::FETCH_ASSOC);
if ($driverRaw) {
$encryptedFields = ['first_name', 'last_name', 'gender', 'phone', 'car_plate', 'token'];
foreach ($driverRaw as $key => $value) {
if (in_array($key, $encryptedFields, true) && !empty($value)) {
$decrypted = false;
try {
$decrypted = $encryptionHelper->decryptData($value);
} catch (\Throwable $e) {
$decrypted = false;
}
$driverInfo[$key] = ($decrypted !== false && $decrypted !== null && $decrypted !== '') ? $decrypted : $value;
} else {
$driverInfo[$key] = $value;
}
}
$driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? ''));
if (empty($driverInfo['driverName']) && !empty($driverInfo['phone'])) {
$driverInfo['driverName'] = $driverInfo['phone'];
}
$driverInfo['ratingDriver'] = !empty($driverInfo['ratingDriver']) ? (string)$driverInfo['ratingDriver'] : "5.0";
$ratingValue = (float) $driverInfo['ratingDriver'];
$ratingCount = (int) ($driverInfo['ratingCount'] ?? 0);
$completedRides = (int) ($driverInfo['completedRides'] ?? 0);
if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) {
$driverInfo['driverTier'] = 'Professional driver';
} elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) {
$driverInfo['driverTier'] = 'Trusted driver';
} else {
$driverInfo['driverTier'] = 'Verified driver';
}
if (isset($redisLocation) && $redisLocation) {
try {
// ‏المفتاح الصحيح driver:public — و driver:location لا يُكتب في أي
// ‏مكان إطلاقاً فكانت القراءة ترجع فارغة دائماً، فيصل الراكب بلا
// ‏إحداثيات أولية للسائق. الكاتب driver_socket.php في معالج
// ‏الدفعات (hmset على driver:profile و driver:public معاً، بحقول
// ‏lat/lng/heading نفسها، وTTL أربعٍ وعشرين ساعة للعام).
$driverLoc = $redisLocation->hGetAll("driver:public:$driverId");
if (!empty($driverLoc)) {
$driverInfo['lat'] = (float)($driverLoc['lat'] ?? 0);
$driverInfo['lng'] = (float)($driverLoc['lng'] ?? 0);
$driverInfo['heading'] = (float)($driverLoc['heading'] ?? 0);
}
} catch (Exception $eLoc) {
// Ignore location fetch error
}
}
}
// ═══════════════════════════════════════════════════════════
// STEP E — جلب passenger_id وإرسال الإشعارات
// ═══════════════════════════════════════════════════════════
if (empty($passengerIdValue)) {
$passengerId = $con->prepare("SELECT passenger_id FROM ride WHERE id = ? LIMIT 1");
$passengerId->execute([$rideId]);
$passengerIdValue = $passengerId->fetchColumn();
}
// 🆕 نحلّ توكن الراكب من القاعدة دائماً ولا نعتمد على ما يرسله التطبيق:
// • مسار الـ dispatch/FCM يرسل التوكن سليماً (نص صريح من add_ride.php).
// • مسار "سوق الرحلات" يرسله كما خرج من getRideWaiting.php أي **مشفّراً**
// (جدول tokens يخزّنه مشفّراً — انظر ride/firebase/addToken.php)، فكان
// FCM يرفضه بـ 400 ولا يصل الراكب أي إشعار قبول.
// القيمة القادمة من العميل تبقى fallback أخيراً فقط.
$passengerTokenFromClient = $passengerToken;
$passengerToken = '';
if ($passengerIdValue) {
try {
$stmtTok = $con->prepare("SELECT token FROM tokens WHERE passengerID = ? LIMIT 1");
$stmtTok->execute([$passengerIdValue]);
$rawPassengerToken = $stmtTok->fetchColumn();
if (!empty($rawPassengerToken)) {
$decryptedToken = false;
try {
$decryptedToken = $encryptionHelper->decryptData($rawPassengerToken);
} catch (\Throwable $eTok) {
$decryptedToken = false;
}
$passengerToken = ($decryptedToken !== false && $decryptedToken !== null && $decryptedToken !== '')
? trim($decryptedToken)
: $rawPassengerToken;
}
} catch (PDOException $eTok) {
error_log("[accept_ride] passenger token lookup WARNING: " . $eTok->getMessage());
}
}
if (empty($passengerToken)) {
$passengerToken = $passengerTokenFromClient;
}
if ($passengerIdValue) {
// Socket — real-time update على خريطة الراكب
if (function_exists('notifyPassengerOnRideServer')) {
notifyPassengerOnRideServer($passengerIdValue, [
'status' => 'accepted',
'ride_id' => $rideId,
'driver_id' => $driverId,
'driver_info' => $driverInfo,
]);
}
// FCM — push notification صامت
if (!empty($passengerToken)) {
sendFCM_Internal(
$passengerToken,
"", // تفريغ العنوان للإرسال الصامت
"", // تفريغ المحتوى للإرسال الصامت
['ride_id' => (string) $rideId, 'driver_info' => $driverInfo, 'status' => 'accepted'],
"Accepted Ride",
false
);
}
}
// ═══════════════════════════════════════════════════════════
// STEP F — تنظيف السوق + Cache ride state (أبلغ location server)
// ═══════════════════════════════════════════════════════════
sendToLocationServer('ride_taken_event', [
'ride_id' => $rideId,
'taken_by_driver_id' => $driverId,
]);
// 🆕 Cache ride state in Redis
sendToLocationServer('update_ride_state', [
'ride_id' => $rideId,
'status' => $status,
'driver_id' => $driverId,
'passenger_id' => $passengerIdValue ?? '',
]);
error_log("[accept_ride] SUCCESS. RideID=$rideId accepted by DriverID=$driverId");
// ═══════════════════════════════════════════════════════════
// STEP G — رد النجاح للسائق (نفس بنية الرد القديمة)
// ═══════════════════════════════════════════════════════════
echo json_encode([
"status" => "success",
"message" => "Ride Accepted",
"data" => $driverInfo,
]);
} catch (PDOException $e) {
error_log("[accept_ride] CRITICAL: " . $e->getMessage());
printFailure("Server error");
}