chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل
نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق». نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا `cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules · .dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore. هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ. ⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// transit/functions.php — دوال خاصة بمنصة مواصلاتي فقط
|
||||
//
|
||||
// ما لا يوجد هنا (يُستخدم مباشرة من النظام الأصلي بعد bootstrap):
|
||||
// • filterRequest() ← core/helpers.php
|
||||
// • jsonSuccess() / jsonError() ← core/helpers.php
|
||||
// • appLog() / securityLog() ← core/helpers.php
|
||||
// • normalizePhone($phone) ← core/helpers.php — يطبّع الهاتف دائماً قبل التشفير أو الاستعلام
|
||||
// • $encryptionHelper ← مهيَّأ في bootstrap.php
|
||||
// • $redis ← مهيَّأ في bootstrap.php
|
||||
// ============================================================
|
||||
|
||||
// Lazy-loaded services (bootstrap لا يحمّلها لأنها ليست مطلوبة لكل endpoint)
|
||||
require_once __DIR__ . '/../core/Services/OtpService.php';
|
||||
require_once __DIR__ . '/../core/Services/FcmService.php';
|
||||
// sendWhatsAppFromServer — الموجودة في backend/functions.php القديمة
|
||||
if (file_exists(__DIR__ . '/../functions.php')) {
|
||||
require_once __DIR__ . '/../functions.php';
|
||||
}
|
||||
|
||||
// ── حقول مطلوبة (غلاف رفيع يستخدم filterRequest + jsonError) ──
|
||||
function requireTransitFields(array $fields): void
|
||||
{
|
||||
foreach ($fields as $f) {
|
||||
if (filterRequest($f) === null) {
|
||||
jsonError("Missing required field: $f", 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Session مشرف الويب (OTP-based — مستقلة عن JWT) ──────────
|
||||
|
||||
function transitCreateSession(int $adminId, int $orgId): string
|
||||
{
|
||||
global $redis;
|
||||
|
||||
$token = bin2hex(random_bytes(32)); // 64 hex chars
|
||||
$hash = hash('sha256', $token);
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + 86400);
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
|
||||
$con = Database::get('transit');
|
||||
$con->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);
|
||||
}
|
||||
Reference in New Issue
Block a user