انتقل إلى المحتوى الرئيسي

دليل بوابة USSD

← العودة إلى الوثائق الرئيسية

يغطي هذا الدليل بوابة USSD من OmniSS7، التي تربط حوارات USSD عبر SS7/MAP مع ردود HTTP/JSON، مما يمكّن المطورين من الطرف الثالث من بناء تطبيقات USSD من خلال نقطة نهاية HTTP بسيطة.

جدول المحتويات

  1. نظرة عامة
  2. العمارة
  3. تمكين بوابة USSD
  4. التكوين
  5. بروتوكول ردود HTTP
  6. USSD المنشأ من الشبكة (Push API)
  7. دورة حياة الجلسة
  8. معالجة الأخطاء
  9. المقاييس والمراقبة
  10. خادم ردود المثال
  11. استكشاف الأخطاء وإصلاحها

نظرة عامة

تتعامل ب��ابة USSD مع اتجاهين من حركة مرور USSD:

  • المنشأ من الهاتف المحمول (Inbound) — يقوم المشترك بالاتصال برمز قصير (مثل *100#). تتلقى البوابة MAP processUnstructuredSS-Request (opcode 59)، وتقوم بإعادة توجيهه إلى رد HTTP الخاص بك، وتعيد إرسال ردك عبر SS7.
  • المنشأ من الشبكة (Outbound) — يقوم تطبيقك بدفع رسالة USSD إلى مشترك عبر واجهة برمجة التطبيقات REST. ترسل البوابة MAP unstructuredSS-Request (opcode 60) عبر SS7 وتوجه رد المشترك إلى ردك.

يدعم كلا الاتجاهين حوارات متعددة الأدوار — قوائم تفاعلية حيث يرد المشترك ويتلقى مطالبات متابعة.

الخصائص الرئيسية

الخاصيةالقيمة
النقلHTTP POST متزامن لكل دور
التشفيرالأبجدية الافتراضية GSM 7-bit (DCS 0x0F) وفقًا لـ 3GPP TS 23.038
الحد الأقصى لطول النص182 حرفًا (قابل للتكوين)
تتبع الجلساتUUID تم إنشاؤه بواسطة البوابة لكل حوار
المصادقة��ا شيء (يثق بشبكة SS7)
التوجيهمطابقة بادئة الرمز القصير مع عناوين ردود

مراجع 3GPP

المواصفةالصلة
3GPP TS 23.090USSD المرحلة 2 — العمارة والإجراءات
3GPP TS 24.090USSD المرحلة 3 — تفاصيل البروتوكول
3GPP TS 29.002بروتوكول MAP — USSD-Arg، USSD-Res، opcodes 59/60/61
3GPP TS 23.038الأبجدية الافتراضية GSM 7-bit ونظام ترميز البيانات

العمارة

تدفق المنشأ من الهاتف المحمول (Inbound)

تدفق المنشأ من الشبكة (Outbound Push)

نظرة عامة على المكونات


تمكين بوابة USSD

تتطلب بوابة USSD تمكين وضع عميل MAP، بالإضافة إلى علم الميزة الخاص بها.

config :omniss7,
map_client_enabled: true,
ussd_gateway_enabled: true

تتطلب البوابة أيضًا اتصال M3UA يعمل (انظر دليل عميل MAP لإعداد M3UA).


التكوين

معلمات بوابة USSD

config :omniss7,
ussd_gateway_enabled: true,
ussd_gateway: %{
# توجيه الرمز القصير — مطابقة أطول بادئة
routes: [
%{pattern: "*100", url: "http://balance-app:9000/ussd"},
%{pattern: "*200", url: "http://topup-app:9000/ussd"},
%{pattern: "*", url: "http://default-app:9000/ussd"}
],

# مهلات الجلسة
session_timeout_ms: 180_000, # إجمالي مدة الجلسة (3 دقائق)
turn_timeout_ms: 30_000, # الحد الأقصى للانتظار لرد المشترك لكل دور (30 ثانية)

# إعدادات ردود HTTP
http_timeout_ms: 5_000, # مهلة لـ HTTP POST إلى تطبيقك (5 ثواني)

# حدود النص
max_text_length: 182 # الحد الأقصى لـ GSM 7-bit (يتم اقتطاعه مع تحذير إذا تم تجاوزه)
}

مرجع المعلمات

المعلمةالنوعمطلوبالافتراضيالوصف
ussd_gateway_enabledBooleanنعمfalseمفتاح رئيسي لميزة بوابة USSD
ussd_gateway.routesقائمة من الخرائطنعم[]قواعد توجيه بادئة الرمز القصير. تحتوي كل إدخال على pattern (سلسلة بادئة) و url (عنوان رد). تفوز أطول بادئة.
ussd_gateway.session_timeout_msعدد صحيحلا180_000الحد الأقصى لمدة الجلسة الإجمالية بالميلي ثانية. يتم إنهاء الجلسة مع خطأ إذا تم تجاوزها.
ussd_gateway.turn_timeout_msعدد صحيحلا30_000الحد الأقصى للوقت للانتظار لرد المشترك في حوار متعدد الأدوار، بالميلي ثانية.
ussd_gateway.http_timeout_msعدد صحيحلا5_000مهلة طلب HTTP لردود تطبيقك، بالميلي ثانية. تغطي كل من وقت الاتصال ووقت الاستجابة.
ussd_gateway.max_text_lengthعدد صحيحلا182الحد الأقصى لعدد الأحرف في سلسلة نص USSD. يتم اقتطاع النصوص التي تتجاوز هذا الحد ويتم تسجيل تحذير.

معلمات التوجيه

كل إدخال في قائمة routes هو خريطة:

المعلمةالنوعمطلوبالوصف
patternسلسلةنعمبادئة الرمز القصير للمطابقة. استخدم "*" كخيار احتياطي شامل. تأخذ البادئات الأطول الأولوية.
urlسلسلةنعمعنوان URL لنقطة النهاية لاستقبال ردود POST للرموز القصيرة المطابقة.

مطابقة التوجيه

يتم مطا��قة التوجيهات بواسطة أطول بادئة أولاً. لسلسلة الاتصال *100#:

  1. "*100" تطابق (الطول 4) — مختارة
  2. "*10" تطابق (الطول 3) — تم تخطيها، أقصر
  3. "*" تطابق (الطول 1) — احتياطي

إذا لم تتطابق أي توجيه، تعيد البوابة خطأ MAP إلى الهاتف المحمول وتقوم بتسجيل تحذير.


بروتوكول ردود HTTP

يتلقى تطبيقك طلبات HTTP POST من البوابة ويستجيب بتعليمات JSON.

الطلب من البوابة إلى تطبيقك

Content-Type: application/json

الدور الأول (بدء الجلسة):

{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"msisdn": "+254712345678",
"type": "initiation",
"text": "*100#",
"turn": 1
}

الأدوار اللاحقة (رد المشترك):

{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"msisdn": "+254712345678",
"type": "response",
"text": "1",
"turn": 2
}

حقول الطلب

الحقلالنوعالوصف
session_idسلسلةUUID تم إنشاؤه بواسطة البوابة. فريد لكل حوار USSD. استخدم هذا لربط الأدوار.
msisdnسلسلةMSISDN المشترك (إذا كان متاحًا من رسالة MAP). قد يكون فارغًا لبعض الشبكات.
typeسلسلة"initiation" للدور الأول، "response" لردود المشترك اللاحقة.
textسلسلةسلسلة الاتصال (مثل *100#) عند البدء، أو إدخال المشترك (مثل 1) عند الرد.
turnعدد صحيحعداد الدور يبدأ من 1. يزداد مع كل تفاعل للمشترك.

الرد من تطبيقك

يجب على تطبيقك الرد بـ JSON يحتوي على action و text:

استمرار (عرض القائمة، الانتظار لرد المشترك):

{
"action": "continue",
"text": "1. Balance\n2. Top up\n3. Transfer"
}

إنهاء (عرض الرسالة النهائية، إغلاق الجلسة):

{
"action": "end",
"text": "Your balance is $5.00"
}

حقول الرد

الحقلالنوعمطلوبالوصف
actionسلسلةنعم"continue" للحفاظ على الجلسة مفتوحة والانتظار لرد المشترك، ��و "end" لعرض رسالة نهائية وإغلاق الجلسة.
textسلسلةنعمنص لعرضه على جهاز المشترك. الحد الأقصى للطول تحكمه max_text_length (الافتراضي 182). استخدم \n لفواصل الأسطر.

USSD المنشأ من الشبكة (Push API)

ادفع رسالة USSD إلى مشترك من تطبيقك.

نقطة النهاية

POST /api/ussd/send

الطلب

{
"msisdn": "+254712345678",
"text": "لديك فاتورة معلقة. رد 1 للدفع.",
"callback_url": "http://billing-app:9000/ussd"
}

حقول الطلب

الحقلالنوعمطلوبالوصف
msisdnسلسلةنعمMSISDN المشترك الوجهة بالتنسيق الدولي.
textسلسلةنعمنص USSD الأولي للعرض. مشفر كـ GSM 7-bit.
callback_urlسلسلةنعمعنوان URL لاستقبال رد المشترك عبر بروتوكول الرد القياسي.

الرد

نجاح (200 OK):

{
"session_id": "xyz-789-abc-123",
"status": "sent"
}

ردود الأخطاء:

حالة HTTPالجسمالسبب
400{"error": "invalid request", "required": ["msisdn", "text", "callback_url"]}الحقول المطلوبة مفقودة
400{"error": "gsm7_encode_failed", ...}النص يحتوي على أحرف غير موجودة في الأبجدية GSM 7-bit
500{"error": "send_failed", ...}فشل إرسال M3UA (تحقق من الاتصال)
503{"error": "USSD gateway not enabled"}ussd_gateway_enabled هو false

مثال cURL

curl -X POST http://localhost:8080/api/ussd/send \
-H "Content-Type: application/json" \
-d '{
"msisdn": "+254712345678",
"text": "لديك فاتورة معلقة. رد 1 للدفع.",
"callback_url": "http://billing-app:9000/ussd"
}'

دورة حياة الجلسة

يتم تتبع كل حوار USSD كـ جلسة مع session_id فريد.

حالات الجلسة

جلسة ذات دور واحد

إذا كان ردك يعود بـ "end" في الدور الأول، فلا يتم إنشاء جلسة دائمة. ترسل البوابة MAP End مع النتيجة وتعود على الفور.

جلسة متعددة الأدوار

إذا كان ردك يعود بـ "continue"، تقوم البوابة:

  1. بإنشاء خادم جلسة GenServer مسجل في UssdGateway.Registry
  2. ترسل MAP Continue مع opcode 60 (unstructuredSS-Request) إلى الهاتف المحمول
  3. تنتظر رد المشترك (حتى turn_timeout_ms)
  4. تعيد توجيه الرد إلى ردك
  5. تكرر حتى يعود ردك بـ "end" أو يحدث مهلة

سلوك المهلة

المهلةالافتراضيالتأثير
مهلة الدور30 ثانيةإذا لم يرد المشترك ضمن هذه النافذة، يتم إنهاء الجلسة مع خطأ MAP.
مهلة الجلسة3 دقائقإجمالي مدة الجلسة. تنهي الجلسة بغض النظر عن الن��اط.
مهلة ردود HTTP5 ثوانيإذا لم يستجب تطبيقك في الوقت المحدد، ترسل البوابة خطأ MAP إلى الهاتف المحمول وتنهي الجلسة.

معالجة الأخطاء

تتعامل البوابة مع الفشل بشكل سلس وتحاول دائمًا إرسال رد خطأ MAP إلى الهاتف المحمول حتى يرى المشترك رسالة ذات معنى بدلاً من مهلة الشبكة.

السيناريوإجراء البوابةرمز خطأ MAP
مهلة ردود HTTP أو 5xxإنهاء الجلسة، إرسال MAP End مع خطأ34 (systemFailure)
JSON غير صالح من الردإنهاء الجلسة، إرسال MAP End مع خطأ34 (systemFailure)
نص USSD يتجاوز max_text_lengthاقتطاع النص، تسجيل تحذير، الاستمرار بشكل طبيعيN/A (truncated, not an error)
مهلة المشترك (لا رد)إنهاء الجلسة، إرسال MAP End مع خطأ34 (systemFailure)
لا توجد توجيه تطابق الرمز القصيرإرسال MAP End مع خطأ، تسجيل تحذير34 (systemFailure)
تعطل خادم جلسة GenServerتموت الجلسة، يرى المشترك مهلة الشبكةN/A (process exit)
بوابة USSD غير مفعلةإرجاع Facility Not Supported21 (facilityNotSupported)

المقاييس والمراقبة

تقوم بوابة USSD بتعريض مقاييس Prometheus على نقطة النهاية القياسية /metrics (المنفذ 8080).

مقاييس USSD

المقياس: ussd_requests_total النوع: عداد الوصف: إجمالي طلبات USSD المعالجة التسميات:

  • direction"inbound" (المنشأ من الهاتف المحمول) أو "outbound" (المنشأ من الشبكة)

المقياس: ussd_active_sessions النوع: مقياس الوصف: عدد جلسات USSD النشطة حاليًا

المقياس: map_request_duration_milliseconds النوع: هيستوجرام الوصف: مدة عمليات إرسال USSD بالميلي ثانية التسميات:

  • operation"ussd_send" لطلبات الدفع الخارجية

استعلامات Prometheus مثال

# معدل طلبات USSD حسب الاتجاه
rate(ussd_requests_total[5m])

# الجلسات النشطة
ussd_active_sessions

# زمن الاستجابة USSD الخارجي (p95)
histogram_quantile(0.95, rate(map_request_duration_milliseconds_bucket{operation="ussd_send"}[5m]))

خادم ردود المثال

بايثون (Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)
sessions = {}

@app.route('/ussd', methods=['POST'])
def ussd():
data = request.json
session_id = data['session_id']
text = data['text']
turn = data['turn']

if data['type'] == 'initiation':
sessions[session_id] = {'state': 'main_menu'}
return jsonify({
'action': 'continue',
'text': 'مرحبًا!\n1. تحقق من الرصيد\n2. شراء رصيد\n3. تحويل'
})

state = sessions.get(session_id, {}).get('state')

if state == 'main_menu':
if text == '1':
del sessions[session_id]
return jsonify({
'action': 'end',
'text': 'رصيدك هو $5.00'
})
elif text == '2':
sessions[session_id]['state'] = 'buy_airtime'
return jsonify({
'action': 'continue',
'text': 'أدخل المبلغ:'
})
else:
del sessions[session_id]
return jsonify({
'action': 'end',
'text': 'خيار غير صالح. وداعً��.'
})

elif state == 'buy_airtime':
del sessions[session_id]
return jsonify({
'action': 'end',
'text': f'لقد اشتريت رصيد بقيمة ${text}. شكرًا لك!'
})

return jsonify({'action': 'end', 'text': 'انتهت الجلسة.'})

if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000)

Node.js (Express)

const express = require('express');
const app = express();
app.use(express.json());

const sessions = new Map();

app.post('/ussd', (req, res) => {
const { session_id, text, type } = req.body;

if (type === 'initiation') {
sessions.set(session_id, { state: 'main_menu' });
return res.json({
action: 'continue',
text: 'مرحبًا!\n1. تحقق من الرصيد\n2. شراء رصيد'
});
}

const session = sessions.get(session_id);
if (!session) {
return res.json({ action: 'end', text: 'انتهت الجلسة.' });
}

if (session.state === 'main_menu' && text === '1') {
sessions.delete(session_id);
return res.json({ action: 'end', text: 'رصيدك هو $5.00' });
}

sessions.delete(session_id);
return res.json({ action: 'end', text: 'وداعًا.' });
});

app.listen(9000, () => console.log('ردود USSD على المنف�� 9000'));

استكشاف الأخطاء وإصلاحها

الاتصال بـ USSD يعود "الخدمة غير متاحة"

الأعراض: يقوم المشترك بالاتصال برمز قصير ويواجه خطأ في الشبكة على الفور.

الأسباب المحتملة:

  • ussd_gateway_enabled هو false
  • لا يوجد توجيه يتطابق مع الرمز القصير المتصل
  • اتصال M3UA معطل

الحل:

  1. تحقق من ussd_gateway_enabled: true في التكوين
  2. تحقق من أن نمط التوجيه يتطابق مع الرمز القصير (تذكر تضمين * كخيار احتياطي)
  3. تحقق من حالة نظير M3UA في واجهة الويب (صفحة النظائر)

عدم تلقي الردود من التطبيق

الأعراض: تظهر سجلات البوابة بدء USSD لكن تطبيقك لا يتلقى أبدًا HTTP POST.

الأسباب المحتملة:

  • عنوان رد غير قابل للوصول من مضيف OmniSS7
  • جدار حماية يمنع HTTP الصادر من OmniSS7
  • تطبيق الرد غير قيد التشغيل

الحل:

  1. اختبار الاتصال: curl -v http://your-app:9000/ussd من مضيف OmniSS7
  2. تحقق من قواعد جدار الحماية ��ـ HTTP الصادر
  3. تحقق من أن تطبيق الرد الخاص بك يستمع على المنفذ المحدد

انتهاء الجلسات بشكل مبكر

الأعراض: تنتهي الجلسات متعددة الأدوار مع "systemFailure" قبل أن يتمكن المشترك من الرد.

الأسباب المحتملة:

  • turn_timeout_ms قصير جدًا لقاعدة المشتركين لديك
  • http_timeout_ms قصير جدًا لوقت معالجة تطبيقك
  • زمن الانتقال بين OmniSS7 وخادم الرد الخاص بك

الحل:

  1. زيادة turn_timeout_ms (الافتراضي 30 ثانية يجب أن يكون كافيًا لمعظم الحالات)
  2. زيادة http_timeout_ms إذا كان تطبيقك يحتاج إلى مزيد من وقت المعالجة
  3. نشر خادم الرد بالقرب من OmniSS7 لتقليل زمن الانتقال

أخطاء تشفير GSM 7-bit

الأعراض: أخطاء gsm7_encode_failed في السجلات أو ردود 400 من /api/ussd/send.

الأسباب المحتملة:

  • النص يحتوي على أحرف خارج الأبجدية الافتراضية GSM 7-bit (مثل الرموز التعبيرية، أحرف CJK)

الحل:

  • تقييد نص USSD إلى مجموعة الأحرف الأساسية GSM: أحرف ASCII، أرقام، علامات الترقيم الشائعة، وبعض أحرف اليونانية/النوردية
  • انظر 3GPP TS 23.038 القسم 6.2.1 للحصول على جدول الأحرف الكامل

الوثائق ذات الصلة