prepare( "INSERT INTO transit_sessions (admin_id, org_id, token_hash, ip, user_agent, expires_at) VALUES (?,?,?,?,?,?)" )->execute([$adminId, $orgId, $hash, $ip, $ua, $expiresAt]); if ($redis) { $redis->setEx( "transit:session:{$hash}", 86400, json_encode(['admin_id' => $adminId, 'org_id' => $orgId]) ); } return $token; } function transitAuthAdmin(): array { global $redis; $header = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['HTTP_X_TRANSIT_TOKEN'] ?? ''; $token = str_replace('Bearer ', '', $header); if (!$token) jsonError('Missing admin session token', 401); $hash = hash('sha256', $token); if ($redis) { $val = $redis->get("transit:session:{$hash}"); if ($val) { $data = json_decode($val, true); if ($data) return $data; } jsonError('Session expired or invalid', 401); } // Fallback MySQL $con = Database::get('transit'); $st = $con->prepare( "SELECT admin_id, org_id FROM transit_sessions WHERE token_hash=? AND expires_at > NOW() LIMIT 1" ); $st->execute([$hash]); $row = $st->fetch(); if (!$row) jsonError('Session expired or invalid', 401); return $row; } // ── OTP مشرف الويب (يستخدم OtpService الموجود) ───────────── function transitSendAdminOtp(string $phone): void { global $redis; $otpSvc = new OtpService($redis); $otp = $otpSvc->generate($phone, 3); // 3 أرقام فقط (100–999) حسب سياسة مواصلاتي $message = "رمز تسجيل الدخول لمنصة مواصلاتي: {$otp}\nصالح لمدة 5 دقائق."; sendWhatsAppFromServer($phone, $message); } function transitVerifyAdminOtp(string $phone, string $otp): bool { global $redis; $otpSvc = new OtpService($redis); return $otpSvc->verify($phone, $otp); } function transitIsOtpLocked(string $phone): bool { global $redis; $otpSvc = new OtpService($redis); return $otpSvc->isLocked($phone); } // ── تشفير الرقم الجامعي / الوظيفي (يستخدم EncryptionHelper) ─ function transitEncryptStudentId(string $raw): string { global $encryptionHelper; return $encryptionHelper->encryptData($raw); } function transitDecryptStudentId(string $enc): string { global $encryptionHelper; return (string)$encryptionHelper->decryptData($enc); } // ── إشعارات FCM Topic للخطوط ──────────────────────────────── function transitRouteTopic(int $routeId): string { return 'transit_route_' . $routeId; } function transitOrgTopic(int $orgId): string { return 'transit_org_' . $orgId; } // إشعار مباشر لراكب واحد عبر موضوعه الشخصي passenger_{id} // (التطبيق يشترك بهذا الموضوع عند تسجيل الدخول — انظر firbase_messge.dart) function transitSendNotificationToPassenger(string $passengerId, string $title, string $body, array $data = []): void { if (!$passengerId) return; transitSendTopicNotification('passenger_' . $passengerId, $title, $body, $data); } // إشعار أولياء الأمور المرتبطين بعضوية طالب — يحترم أعمدة notify_* بالجدول // $event: depart | board | arrive | return function transitNotifyGuardians(int $enrollmentId, string $event, string $title, string $body): void { global $transit_con; if (!$transit_con) return; $column = 'notify_' . $event; if (!in_array($event, ['depart', 'board', 'arrive', 'return'])) return; $st = $transit_con->prepare( "SELECT guardian_passenger_id FROM transit_guardians WHERE enrollment_id=? AND {$column}=1" ); $st->execute([$enrollmentId]); foreach ($st->fetchAll(PDO::FETCH_COLUMN) as $guardianId) { transitSendNotificationToPassenger((string)$guardianId, $title, $body, ['type' => 'transit_guardian_' . $event]); } } function transitSendTopicNotification(string $topic, string $title, string $body, array $data = []): void { global $redis; $fcm = new FcmService($redis); $result = $fcm->sendToTopic($topic, $title, $body, $data); if ($result['status'] !== 'success') { appLog("[TRANSIT][FCM] Topic push failed on '{$topic}': " . json_encode($result), 'WARNING'); } } // ── موقع الباص في Redis سيرفر الموقع ($redisLocation) ──────── // موقع الباص يكتبه driver_socket على Redis سيرفر الموقع (بدون بادئة siro:) // تماماً مثل موقع السائق العادي — لذلك نقرؤه/نكتبه عبر $redisLocation // وليس $redis الرئيسي (الذي يضيف بادئة siro: ولا يراه السوكت). function transitUpdateBusPosition(int $tripId, float $lat, float $lng): void { global $redisLocation; if (!$redisLocation) return; $redisLocation->hMSet("transit:trip:{$tripId}:pos", ['lat' => $lat, 'lng' => $lng, 'ts' => time()]); $redisLocation->expire("transit:trip:{$tripId}:pos", 86400); } function transitGetBusPosition(int $tripId): ?array { global $redisLocation; if (!$redisLocation) return null; $pos = $redisLocation->hGetAll("transit:trip:{$tripId}:pos"); return ($pos && isset($pos['lat'])) ? $pos : null; } // ── ملكية الرحلة (Trip Ownership) ───────────────────────────── // تُكتب عند بدء الرحلة على Redis سيرفر الموقع، ليتحقق منها driver_socket // قبل قبول أي update_bus_location — يمنع أي سائق آخر من انتحال trip_id // أو البث لخط لا يملكه. main_driver_id هو نفسه sub في JWT (الحساب في // جدول driver الرئيسي)، وليس transit_drivers.id. function transitSetTripOwner(int $tripId, string $mainDriverId, int $routeId): void { global $redisLocation; if (!$redisLocation || !$mainDriverId) return; $key = "transit:trip:{$tripId}:owner"; $redisLocation->hMSet($key, ['driver_id' => $mainDriverId, 'route_id' => $routeId]); $redisLocation->expire($key, 86400); } function transitClearTripOwner(int $tripId): void { global $redisLocation; if (!$redisLocation) return; $redisLocation->del("transit:trip:{$tripId}:owner"); } // ── عضوية الراكب في Redis (location Redis — بدون بادئة siro:) ────── // يستخدمها passenger_socket.php للتحقق قبل قبول subscribe_transit_route. // تُكتب عند: تفعيل عضوية برقم الكشف، أو موافقة المشرف على الطلب اليدوي. // تُحذف عند: رفض أو تعليق العضوية. function transitCacheEnrollment(int $orgId, string $passengerId): void { global $redisLocation; if (!$redisLocation || !$passengerId) return; $key = "transit:org_members:{$orgId}"; $redisLocation->sAdd($key, $passengerId); $redisLocation->expire($key, 7 * 86400); // تجديد TTL سبعة أيام } function transitDropEnrollment(int $orgId, string $passengerId): void { global $redisLocation; if (!$redisLocation || !$passengerId) return; $redisLocation->sRem("transit:org_members:{$orgId}", $passengerId); } // ── كاش السائق في Redis ($redis الرئيسي — بادئة siro:) ────────── // جدول transit_drivers صغير، لكن endpoints الرحلات تستعلم عنه بكثرة // (today, start, end, delay, board_passenger, stop_passengers) — خصوصاً // board_passenger أثناء مسح تذاكر الصعود. نمط read-through: نقرأ من Redis // أولاً، وإن لم يوجد نستعلم القاعدة ونخزّن. المفتاح بـ main_driver_id // (المعرّف الآتي من JWT). نبطل الكاش عند أي تغيير حالة (تفعيل/تعليق). // // نخزّن فقط السائقين النشطين (status='active'). أي سجل غير نشط لا يُكاش // حتى لا نحتاج إبطالاً عند التفعيل — أول استعلام بعد التفعيل يبني الكاش. function transitResolveDriver(string $mainDriverId): ?array { global $redis, $transit_con; if (!$mainDriverId) return null; $key = "transit:driver:{$mainDriverId}"; if ($redis) { $cached = $redis->hGetAll($key); if (!empty($cached) && isset($cached['id'])) { return ['id' => (int)$cached['id'], 'org_id' => (int)$cached['org_id']]; } } if (!$transit_con) $transit_con = Database::get('transit'); $st = $transit_con->prepare( "SELECT id, org_id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" ); $st->execute([$mainDriverId]); $driver = $st->fetch(); if (!$driver) return null; $resolved = ['id' => (int)$driver['id'], 'org_id' => (int)$driver['org_id']]; if ($redis) { $redis->hMSet($key, $resolved); $redis->expire($key, 3600); // ساعة — طبقة حماية إن فات إبطالٌ ما } return $resolved; } function transitInvalidateDriverCache(string $mainDriverId): void { global $redis; if ($redis && $mainDriverId) $redis->del("transit:driver:{$mainDriverId}"); } // ── days_mask helpers ──────────────────────────────────────── // bit0=Sun … bit6=Sat | 62 = 0b0111110 = الأحد–الخميس function transitDayActive(int $mask): bool { return (bool)(($mask >> (int)date('w')) & 1); }