يبدو منتج طلب السيارات بسيطًا من مقعد الراكب: اضغط، انتظر، اركب، ادفع. لكن خلف هذه البساطة يقف واحد من أكثر الأنظمة الفورية تطلّبًا يمكن لفريق صغير أن يبنيه. في هذا المقال نستعرض المعمارية التي صممناها لمنصة طلب سيارات تخدم السوق الفنلندي — أربعة تطبيقات، ونظام خلفي واحد، وكثير من القرارات حول ما يجب أن يكون لحظيًا وما يكفي أن يكون موثوقًا.
التطبيقات الأربعة
- تطبيق الراكب (Flutter) — الحجز، التتبع المباشر، الدفع، التقييم.
- تطبيق السائق (Flutter) — التوفر، عروض الرحلات، الملاحة، الأرباح.
- لوحة الإدارة (Angular) — خريطة الأسطول الحية، التعرفة، العروض، النزاعات، التقارير.
- واجهة النظام الخلفي (ASP.NET Core) — مصدر الحقيقة للرحلات والمستخدمين والتسعير والمدفوعات.
اختيار Flutter لتطبيقي الهاتف سمح لفريق واحد بمشاركة رموز التصميم وكود الشبكة والتحليلات بين الراكب والسائق، مع تسليم تطبيقات بإحساس أصلي على iOS وAndroid. أما Angular، فأنواعه القوية وبنيته المنظمة جعلته خيارًا طبيعيًا للوحة إدارة كثيفة البيانات يصونها مطورو .NET.
الفورية: SignalR أم Firebase؟
المساران الساخنان هما تحديثات موقع السائق (آلاف الكتابات الصغيرة في الدقيقة) وأحداث حالة الرحلة (قليلة لكنها حرجة). نستخدم التقنيتين معًا عن قصد:
| الجانب | SignalR (ASP.NET Core) | Firebase Realtime Database |
|---|---|---|
| زمن الاستجابة | منخفض جدًا، WebSocket دائم | منخفض، مُدار عالميًا |
| العمل دون اتصال / إعادة الاتصال | معالجة يدوية | تخزين محلي ومزامنة مدمجان |
| البث لعدد كبير من الركاب | مجموعات، يحتاج Redis عند التوسع | أصلي ويتوسع تلقائيًا |
| قواعد العمل | تعمل داخل كود المجال لديك | قواعد أمان فقط |
| نموذج التكلفة | حوسبة تشغّلها أصلًا | حسب الاتصالات / النطاق الترددي |
قاعدتنا: كل ما يغيّر المال أو حالة الرحلة يمر عبر الواجهة على SignalR؛ أما تدفق المواقع عالي التكرار ومنخفض القيمة فيُنسخ إلى Firebase ليشترك فيه تطبيق الراكب بتكلفة زهيدة ويواصل العمل في الأنفاق والمناطق ضعيفة التغطية.
// ASP.NET Core — driver location hub
public class TrackingHub : Hub
{
private readonly ITripService _trips;
public TrackingHub(ITripService trips) => _trips = trips;
public async Task UpdateLocation(Guid tripId, double lat, double lng, double heading)
{
await _trips.RecordLocationAsync(tripId, lat, lng, heading);
await Clients.Group($"trip:{tripId}")
.SendAsync("driverMoved", new { lat, lng, heading, at = DateTime.UtcNow });
}
public Task WatchTrip(Guid tripId) =>
Groups.AddToGroupAsync(Context.ConnectionId, $"trip:{tripId}");
}مطابقة السائقين مع الركاب
التوزيع استعلام جغرافي مكاني يتبعه سير عمل للعروض. نخزّن آخر موقع معروف للسائق في عمود مكاني ونستعلم عن أقرب السائقين المتاحين ضمن نصف قطر معين، ثم نرتبهم حسب وقت الوصول من Google Maps Distance Matrix API لا بالمسافة المستقيمة — فبحيرة أو خط سكة حديد يصنعان فرقًا كبيرًا في هلسنكي.
-- Nearest available drivers (PostGIS / SQL Server spatial)
SELECT TOP (10) d.Id, d.Location.STDistance(@pickup) AS Metres
FROM Drivers d
WHERE d.Status = 'Available'
AND d.Location.STDistance(@pickup) < 4000
ORDER BY Metres;العرض نفسه آلة حالة صغيرة: Offered → Accepted | Declined | TimedOut. مهمة مؤجلة في Hangfire تفرض المهلة، فلا يعطّل سائق فقد الاتصال طابور الانتظار أبدًا.
خريطة الراكب الحية
في Flutter، تشترك شاشة الراكب ببساطة في تدفق المواقع وتعيد رسم العلامة. ولأن التدفق يأتي من Firebase، فهو ينجو من انتقال التطبيق إلى الخلفية ومن الاتصالات المتقطعة دون كود إضافي.
// Flutter rider app — live driver marker
StreamBuilder<DriverPosition>(
stream: trackingService.positions(tripId),
builder: (context, snap) {
if (!snap.hasData) return const Center(child: CircularProgressIndicator());
final p = snap.data!;
return GoogleMap(
initialCameraPosition: CameraPosition(target: LatLng(p.lat, p.lng), zoom: 15),
markers: { Marker(markerId: const MarkerId('driver'), position: LatLng(p.lat, p.lng), rotation: p.heading) },
);
},
);المدفوعات والرسائل والمهام الخلفية
- Stripe يتولى البطاقات وApple Pay وGoogle Pay. نحجز مبلغًا تقديريًا عند الحجز ونقتطع الأجرة النهائية عند الوصول.
- Twilio يرسل رموز OTP للتحقق من الهاتف وإشعارات الرحلة حين لا تصل الإشعارات الفورية.
- Hangfire يشغّل المؤقتات والتحويلات الأسبوعية للسائقين ورسائل الإيصالات والتقارير الليلية — وكلها مرئية في لوحته.
مبنية لأوروبا
خدمة فنلندا تعني تسعيرًا باليورو مع تفصيل ضريبة القيمة المضافة في الإيصالات، وتعريبًا بالفنلندية والإنجليزية، ومعالجة صارمة وفق GDPR: يُخفى تاريخ المواقع بعد 30 يومًا، وكل تصدير أو حذف للبيانات الشخصية استدعاء واحد للواجهة. والاستضافة في منطقة أوروبية على Azure تبقي إقامة البيانات بسيطة.
الخلاصة
- افصل الحالة (الواجهة + SignalR) عن القياسات (Firebase).
- رتّب السائقين حسب وقت الوصول الحقيقي لا المسافة.
- اجعل كل انتقال idempotent ومحكومًا بمهلة.
- شارك الكود بين تطبيقي الراكب والسائق عبر حزم Flutter لا بالنسخ واللصق.
تخطط لمنتج طلب سيارات أو تاكسي أو إدارة أسطول؟ تحدث مع فريقنا — لقد حللنا معظم الأجزاء الصعبة بالفعل.