Files
Hamza-AyedandClaude Opus 5 4d8414c96b 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>
2026-07-27 05:14:13 +03:00

196 lines
8.9 KiB
PHP

<?php
/**
* scripts/migrate.php
*
* مُشغّل ترحيلات بسيط يعمل من سطر الأوامر داخل حاوية php.
* يستخدم نفس إعدادات الاتصال في core/Database — لا تحتاج phpMyAdmin ولا
* تمرير كلمة مرور القاعدة يدوياً.
*
* الاستخدام:
* php scripts/migrate.php --status # ماذا سيُطبَّق وماذا هو مطبَّق
* php scripts/migrate.php --dry-run # عرض العبارات دون تنفيذ
* php scripts/migrate.php # التطبيق
*
* كل خطوة idempotent: تُفحص حالة المخطط قبل التنفيذ، فإعادة التشغيل آمنة
* ولا تُنتج خطأ "Duplicate column".
*/
declare(strict_types=1);
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit("This script runs from the command line only.\n");
}
require_once __DIR__ . '/../core/bootstrap.php';
$options = getopt('', ['dry-run', 'status']);
$dryRun = isset($options['dry-run']);
$status = isset($options['status']);
$con = Database::get('main');
/** هل يوجد العمود؟ */
function hasColumn(PDO $con, string $table, string $column): bool
{
$st = $con->prepare(
"SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?"
);
$st->execute([$table, $column]);
return (int) $st->fetchColumn() > 0;
}
/** هل يوجد الفهرس؟ */
function hasIndex(PDO $con, string $table, string $index): bool
{
$st = $con->prepare(
"SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?"
);
$st->execute([$table, $index]);
return (int) $st->fetchColumn() > 0;
}
function tableExists(PDO $con, string $table): bool
{
$st = $con->prepare(
"SELECT COUNT(*) FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?"
);
$st->execute([$table]);
return (int) $st->fetchColumn() > 0;
}
// ── تعريف الترحيلات ──────────────────────────────────────────
// كلها إضافية: أعمدة جديدة تقبل NULL. لا تُعدَّل ولا تُحذف بيانات قائمة،
// ولا يقرأها أي كود قبل تعبئتها، فتطبيقها على قاعدة تعمل بلا أثر.
$columns = [
['driver', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالهاتف'"],
['driver', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالبريد'"],
['driver', 'name_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للاسم بعد التطبيع'"],
['passengers', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالهاتف'"],
['passengers', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالبريد'"],
['passengers', 'name_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للاسم بعد التطبيع'"],
['adminUser', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالهاتف'"],
['adminUser', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالبريد'"],
// نسخة مشفّرة قابلة للاسترجاع من رقم الهاتف في جداول التحقق.
// عمود phone_number صار يحمل مفتاح بحث أحادي الاتجاه (HMAC)، فلا يمكن
// استرجاع الرقم منه — وخدمة العملاء تحتاجه لمتابعة من طلب رمزاً ولم
// يُكمل تسجيله.
['phone_verification', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"],
['phone_verification_passenger', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"],
['phone_verification_service', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"],
['token_verification', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"],
['token_verification_driver', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"],
// مفتاح ربط جداول التحقق: نفس صيغة otpPhoneKey، حتى يمكن ربط
// phone_verification*.phone_number بصاحب الحساب دون مقارنة نصوص مشفّرة.
['driver', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق'"],
['passengers', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق'"],
// جداول ملاحظات خدمة العملاء تُربط بالسائق/الراكب عبر الهاتف المشفّر،
// وهو ربط يفشل لحظة أن يصبح التشفير عشوائياً.
['notesForDriverService', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط بالحساب'"],
['notesForPassengerService', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط بالحساب'"],
// بقية الجداول التي يُبحث فيها بحقل مشفّر
['users', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لبريد موظف الخدمة'"],
['users', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لهاتف موظف الخدمة'"],
['driver', 'national_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للرقم الوطني'"],
// أعمدة موافقات المشرفين المفقودة في هذا النشر
['adminUser', 'status', "VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active | pending | suspended | rejected'"],
['adminUser', 'approved_by', "VARCHAR(32) NULL DEFAULT NULL"],
['adminUser', 'approved_at', "TIMESTAMP NULL DEFAULT NULL"],
];
$indexes = [
['driver', 'idx_driver_phone_bidx', 'phone_bidx'],
['driver', 'idx_driver_email_bidx', 'email_bidx'],
['driver', 'idx_driver_name_bidx', 'name_bidx'],
['passengers', 'idx_passengers_phone_bidx', 'phone_bidx'],
['passengers', 'idx_passengers_email_bidx', 'email_bidx'],
['passengers', 'idx_passengers_name_bidx', 'name_bidx'],
['adminUser', 'idx_adminuser_phone_bidx', 'phone_bidx'],
['adminUser', 'idx_adminuser_email_bidx', 'email_bidx'],
['users', 'idx_users_email_bidx', 'email_bidx'],
['users', 'idx_users_phone_bidx', 'phone_bidx'],
['driver', 'idx_driver_national_bidx', 'national_bidx'],
['driver', 'idx_driver_phone_key', 'phone_key'],
['passengers', 'idx_passengers_phone_key', 'phone_key'],
['notesForDriverService', 'idx_notes_driver_phone_key', 'phone_key'],
['notesForPassengerService', 'idx_notes_passenger_phone_key', 'phone_key'],
];
$applied = 0;
$skipped = 0;
$missing = [];
echo "── Siro schema migration ──\n";
echo "database: " . $con->query('SELECT DATABASE()')->fetchColumn() . "\n";
if ($dryRun) echo "mode: DRY RUN (nothing will be executed)\n";
if ($status) echo "mode: STATUS ONLY\n";
echo "\n";
foreach ($columns as [$table, $column, $definition]) {
if (!tableExists($con, $table)) {
$missing[$table] = true;
continue;
}
if (hasColumn($con, $table, $column)) {
echo " = $table.$column already present\n";
$skipped++;
continue;
}
$sql = "ALTER TABLE `$table` ADD COLUMN `$column` $definition";
if ($status || $dryRun) {
echo " + would run: $sql\n";
continue;
}
try {
$con->exec($sql);
echo " + $table.$column added\n";
$applied++;
} catch (PDOException $e) {
echo " ✘ $table.$column failed: " . $e->getMessage() . "\n";
}
}
foreach ($indexes as [$table, $index, $column]) {
if (!tableExists($con, $table) || !hasColumn($con, $table, $column)) continue;
if (hasIndex($con, $table, $index)) {
echo " = $table.$index already present\n";
$skipped++;
continue;
}
$sql = "ALTER TABLE `$table` ADD INDEX `$index` (`$column`)";
if ($status || $dryRun) {
echo " + would run: $sql\n";
continue;
}
try {
$con->exec($sql);
echo " + $table.$index created\n";
$applied++;
} catch (PDOException $e) {
echo " ✘ $table.$index failed: " . $e->getMessage() . "\n";
}
}
foreach (array_keys($missing) as $table) {
echo " ! table `$table` does not exist in this database — skipped\n";
}
echo "\napplied: $applied · already in place: $skipped\n";
if (!$status && !$dryRun && $applied > 0) {
echo "\nNext: populate the indexes\n";
echo " php scripts/backfill_blind_index.php --dry-run\n";
echo " php scripts/backfill_blind_index.php\n";
}