Update: 2026-08-07 15:50:04
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* sms/helpers.php — أدوات مشتركة لمسار الطلب بلا إنترنت
|
||||
* ─────────────────────────────────────────────────────────────
|
||||
* دوال صغيرة يستعملها الويبهوك والكرون و acceptRide معاً. وُضعت هنا
|
||||
* لا في connect.php لأن مسار SMS ليس جزءاً من المسار الحرج للتطبيقين،
|
||||
* فلا داعي لتحميله في كل طلب.
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// توحيد رقم الهاتف
|
||||
//
|
||||
// الرقم يصل بصيغ لا حصر لها: 0791234567، +962791234567،
|
||||
// 00962791234567، وبأرقام عربية-هندية أحياناً. بلا توحيد يصبح نفس
|
||||
// الراكب عدّة حسابات، ولن يجد الردّ طريقه إليه.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/** مقدمات الدول التي نعمل فيها، وطول الرقم المحلي بعد إسقاط الصفر. */
|
||||
const SMS_COUNTRY_CODES = [
|
||||
'Jordan' => '962',
|
||||
'Egypt' => '20',
|
||||
'Syria' => '963',
|
||||
];
|
||||
|
||||
function smsNormalizePhone(string $raw): string
|
||||
{
|
||||
// الأرقام العربية-الهندية (٠١٢…) تصل من بعض الهواتف كما هي.
|
||||
$raw = strtr($raw, [
|
||||
'٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4',
|
||||
'٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9',
|
||||
'۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4',
|
||||
'۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9',
|
||||
]);
|
||||
|
||||
$digits = preg_replace('/\D+/', '', $raw) ?? '';
|
||||
if ($digits === '') return '';
|
||||
|
||||
$cc = SMS_COUNTRY_CODES[getenv('APP_COUNTRY') ?: 'Jordan'] ?? '962';
|
||||
|
||||
// 00962… → 962…
|
||||
if (str_starts_with($digits, '00')) {
|
||||
$digits = substr($digits, 2);
|
||||
}
|
||||
|
||||
// محلي يبدأ بصفر (0791234567) → نستبدل الصفر بمقدمة الدولة.
|
||||
if (str_starts_with($digits, '0')) {
|
||||
$digits = $cc . ltrim($digits, '0');
|
||||
} elseif (!str_starts_with($digits, $cc)) {
|
||||
// بلا صفر وبلا مقدمة (791234567) → نفترض محلياً.
|
||||
$digits = $cc . $digits;
|
||||
}
|
||||
|
||||
return $digits;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// الصادر
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* يضع رسالة في الطابور. لا يرسل — الإرسال مسؤولية الهاتف الذي يسحب
|
||||
* من outbox_pull.php. الفصل مقصود: نداء شبكي فاشل داخل مسار قبول
|
||||
* الرحلة كان سيؤخّر السائق من أجل رسالة نصية.
|
||||
*/
|
||||
function smsEnqueue(PDO $con, string $recipient, string $body,
|
||||
string $context = '', ?string $refId = null): bool
|
||||
{
|
||||
$to = smsNormalizePhone($recipient);
|
||||
if ($to === '' || trim($body) === '') return false;
|
||||
|
||||
try {
|
||||
$con->prepare("
|
||||
INSERT INTO sms_outbox (recipient, body, context, ref_id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
")->execute([$to, trim($body), $context ?: null, $refId]);
|
||||
return true;
|
||||
} catch (PDOException $e) {
|
||||
error_log('[sms] تعذّر وضع رسالة في الطابور: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// مطابقة المعالم
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/** جدول المعالم حسب السوق — الأسماء تاريخية ولا تتبع نمطاً واحداً. */
|
||||
function smsPlacesTable(): string
|
||||
{
|
||||
return match (getenv('APP_COUNTRY') ?: 'Jordan') {
|
||||
'Egypt' => 'placesEgypt',
|
||||
'Syria' => 'palces11',
|
||||
default => 'places',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* يجرّد النص العربي مما يعيق المطابقة: التشكيل، التطويل، واختلاف
|
||||
* رسم الألف والياء والتاء المربوطة. «مستشفى البشير» و«مستشفي البشير»
|
||||
* يكتبهما الناس بالتبادل، ولا يجوز أن يفشل الطلب لهذا السبب.
|
||||
*/
|
||||
function smsNormalizeArabic(string $s): string
|
||||
{
|
||||
$s = trim($s);
|
||||
$s = preg_replace('/[\x{064B}-\x{0652}\x{0640}]/u', '', $s) ?? $s; // تشكيل + تطويل
|
||||
$s = strtr($s, ['أ'=>'ا','إ'=>'ا','آ'=>'ا','ى'=>'ي','ة'=>'ه','ؤ'=>'و','ئ'=>'ي']);
|
||||
$s = preg_replace('/\s+/u', ' ', $s) ?? $s;
|
||||
return trim($s);
|
||||
}
|
||||
|
||||
/**
|
||||
* نفس تجريد smsNormalizeArabic لكن كتعبير SQL على عمود.
|
||||
*
|
||||
* ضروري لا تجميلي: نحن نوحّد نص الراكب («المستشفى» ⇒ «المستشفي»)،
|
||||
* فلو قارنّاه بالعمود الخام لما طابق شيئاً أبداً — الجدول يحمل الرسم
|
||||
* الأصلي بالتاء المربوطة والألف المقصورة.
|
||||
*
|
||||
* نقبل ثمنه: التعبير يمنع استخدام الفهرس. الجدول عشرات الآلاف من
|
||||
* الصفوف والمسح كامله ميليثوانٍ، والقناة منخفضة التردد بطبعها.
|
||||
*/
|
||||
function smsSqlNormalize(string $column): string
|
||||
{
|
||||
$expr = "TRIM($column)";
|
||||
foreach ([['أ','ا'], ['إ','ا'], ['آ','ا'], ['ى','ي'],
|
||||
['ة','ه'], ['ؤ','و'], ['ئ','ي'], ['ـ','']] as [$from, $to]) {
|
||||
$expr = "REPLACE($expr, '$from', '$to')";
|
||||
}
|
||||
return $expr;
|
||||
}
|
||||
|
||||
/**
|
||||
* يبحث عن معلم بالاسم. ثلاث محاولات بترتيب الثقة: تطابق تام، ثم
|
||||
* «يبدأ بـ»، ثم «يحتوي». الترتيب مهم — البحث بـ LIKE '%…%' وحده
|
||||
* يجعل «الجامعة» تطابق أول صف عشوائي من عشرات الجامعات.
|
||||
*
|
||||
* @return array{lat:float,lng:float,name:string}|null
|
||||
*/
|
||||
function smsFindPlace(PDO $con, string $query): ?array
|
||||
{
|
||||
$q = smsNormalizeArabic($query);
|
||||
if (mb_strlen($q) < 2) return null;
|
||||
|
||||
$table = smsPlacesTable();
|
||||
|
||||
// نبحث في الأعمدة الثلاثة: الاسم الأساسي والعربي والإنجليزي.
|
||||
// ثلاث محاولات بترتيب تنازلي في الثقة، ونتوقف عند أول نجاح.
|
||||
$attempts = [
|
||||
['op' => '=', 'value' => $q],
|
||||
['op' => 'LIKE', 'value' => $q . '%'],
|
||||
['op' => 'LIKE', 'value' => '%' . $q . '%'],
|
||||
];
|
||||
|
||||
$nName = smsSqlNormalize('name');
|
||||
$nNameAr = smsSqlNormalize('name_ar');
|
||||
$nNameEn = smsSqlNormalize('name_en');
|
||||
|
||||
foreach ($attempts as $a) {
|
||||
try {
|
||||
$stmt = $con->prepare("
|
||||
SELECT latitude, longitude,
|
||||
COALESCE(NULLIF(name_ar,''), name) AS label
|
||||
FROM `$table`
|
||||
WHERE $nName {$a['op']} ? OR $nNameAr {$a['op']} ? OR $nNameEn {$a['op']} ?
|
||||
-- الأقصر أولاً: «مستشفى البشير» تسبق «موقف باص مستشفى
|
||||
-- البشير الخلفي» حين يكتب الراكب «البشير» فقط.
|
||||
ORDER BY CHAR_LENGTH(name) ASC
|
||||
LIMIT 1
|
||||
");
|
||||
// معلَّمات موضعية لا مسمّاة: إعادة استخدام اسم واحد ثلاث
|
||||
// مرات تفشل حين تكون المحاكاة معطّلة.
|
||||
$stmt->execute([$a['value'], $a['value'], $a['value']]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row && is_numeric($row['latitude']) && is_numeric($row['longitude'])) {
|
||||
return [
|
||||
'lat' => (float) $row['latitude'],
|
||||
'lng' => (float) $row['longitude'],
|
||||
'name' => (string) ($row['label'] ?: $query),
|
||||
];
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log('[sms] فشل البحث عن معلم: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// المسافة والزمن
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* مسافة هافرساين بالكيلومترات مضروبة بمعامل التواء الطرق.
|
||||
*
|
||||
* لا نستدعي خدمة توجيه هنا عمداً: نداء خارجي داخل مسار الويبهوك يعني
|
||||
* أن انقطاعه يوقف كل الطلبات، والتسعير النهائي يتم في add_ride على أي
|
||||
* حال. هذا تقدير كافٍ لبدء الرحلة.
|
||||
*/
|
||||
function smsHaversineKm(float $lat1, float $lng1, float $lat2, float $lng2): float
|
||||
{
|
||||
$R = 6371.0;
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLng = deg2rad($lng2 - $lng1);
|
||||
$a = sin($dLat / 2) ** 2
|
||||
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
|
||||
|
||||
// 1.3: الطريق ليس خطاً مستقيماً. المعامل تقريب متحفّظ شائع في
|
||||
// المدن العربية ذات الشبكة غير المنتظمة.
|
||||
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a)) * 1.3;
|
||||
}
|
||||
Reference in New Issue
Block a user