Get a quote

بناء أنظمة نقاط بيع تعمل بدون إنترنت باستخدام Go وSQLite

معظم أنظمة نقاط البيع في لبنان والمنطقة تفشل بالضبط عندما يحتاجها المشغل: أثناء انقطاع الإنترنت. هذه هي البنية التي نستخدمها لبناء أنظمة نقاط بيع تعمل بدون إنترنت في Go مع SQLite، بما في ذلك المزامنة وحل التعارضات وتحديثات الكتالوج.

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

لماذا يجب أن يعمل نظام نقاط البيع بدون إنترنت؟

في المطاعم والمتاجر التي تعمل خلال ساعات الذروة، لا مجال لتوقف النظام. الكاشير يحتاج إلى تسجيل المبيعات، طباعة الفواتير، وإغلاق الجلسة اليومية بغض النظر عن حالة الشبكة.

النهج الصحيح هو تصميم النظام بحيث تكون الشبكة اختيارية لجميع العمليات الأساسية: تسجيل المبيعات، خصم المخزون، طباعة الإيصالات، والتقارير اليومية. المزامنة مع الخادم المركزي تحدث عندما تتوفر الاتصالية، ليس كشرط لإتمام العملية.

قاعدة البيانات المحلية: SQLite مع وضع WAL

كل جهاز نقطة بيع يُشغّل قاعدة بيانات SQLite محلية مدمجة في تطبيق Go. SQLite هي الخيار الصحيح لأسباب تشغيلية محددة: تعمل داخل العملية بدون خادم قاعدة بيانات منفصل، تخزن البيانات في ملف واحد قابل للنقل، وتتحمل انقطاع الكهرباء المفاجئ بدون فساد في البيانات عند تفعيل وضع WAL.

db, err := sql.Open("sqlite3", "./pos_local.db?_journal_mode=WAL&_synchronous=NORMAL")

كل جدول يحتاج إلى مزامنة يمتلك عمود local_id (UUID يُولَّد محلياً عند الإدراج) وعمود synced_at (يظل NULL حتى يُؤكد الخادم المركزي استلام السجل).

CREATE TABLE IF NOT EXISTS sales (
  local_id    TEXT PRIMARY KEY,
  remote_id   TEXT,
  tenant_id   TEXT NOT NULL,
  terminal_id TEXT NOT NULL,
  total_amount INTEGER NOT NULL,
  created_at  TEXT NOT NULL,
  synced_at   TEXT
);

لا تستخدم أرقاماً تسلسلية تلقائية كمفاتيح رئيسية في قاعدة بيانات محلية. جهازان ينشئان سجلات بنفس الأرقام التسلسلية ستتعارض عند المزامنة مع قاعدة البيانات المركزية.

حلقة المزامنة

المزامنة تعمل كـ goroutine في الخلفية داخل تطبيق نقطة البيع. تستيقظ كل 30 ثانية عند توفر الاتصال، وفوراً عند عودة الشبكة بعد انقطاع.

خطوات المزامنة:

  1. استعلام عن كل السجلات ذات synced_at IS NULL في الجداول القابلة للمزامنة.
  2. تجميعها في دفعات بحد أقصى 100 سجل لكل طلب.
  3. إرسال كل دفعة إلى نقطة نهاية المزامنة في الخادم المركزي.
  4. الخادم يُرجع تعيين local_id إلى remote_id لكل سجل مقبول.
  5. تحديث السجل المحلي بـ remote_id وتعيين synced_at = NOW().
  6. عند فشل الشبكة أو استجابة 5xx، تسجيل المحاولة وإعادة المحاولة في الدورة التالية.

نقطة نهاية API المركزية تستخدم local_id كمفتاح idempotency. إذا وصل نفس السجل مرتين (بسبب إعادة المحاولة بعد timeout)، يُرجع الخادم نجاحاً مع remote_id الموجود بدون إدراج سجل مكرر.

كيف نتعامل مع التعارضات

التعارض الأكثر شيوعاً في المطاعم متعددة الأجهزة: نفس المنتج يُباع على جهازين في نفس اللحظة. الجهازان يُعيّنان local_id مختلفين، لذا لا يوجد تعارض على مستوى مفتاح قاعدة البيانات. كلا السجلين تتم مزامنتهما بشكل مستقل وصحيح.

التعارض الأهم تشغيلياً: تحديث الأسعار يُرسَل من النظام المركزي بينما الجهاز لديه مبيعات غير متزامنة بالأسعار القديمة. قاعدتنا: السجلات غير المتزامنة لا تُعاد تسعيرها بأثر رجعي. تتزامن بالسعر الذي سُجّلت به. هذا هو السلوك الصحيح محاسبياً.

الدروس الأساسية من الإنتاج

استخدم SQLite مع وضع WAL والمعاملات الصريحة لكل كتابة محلية. استخدم UUID v4 كمفتاح رئيسي في جميع الجداول المحلية. ابن حلقة المزامنة لتتبع الحالة لكل سجل وليس لكل دفعة. طبّق idempotency في الخادم باستخدام local_id الذي يُولّده الجهاز. حدّد قواعد حل التعارضات كقرارات عمل صريحة قبل كتابة أي كود مزامنة.

هل تحتاج إلى مساعدة في بناء نظام نقاط بيع يعمل بدون إنترنت؟

Voxire تبني أنظمة نقاط البيع والمخزون للمطاعم وسلاسل التجزئة وشركات التوزيع في لبنان ومنطقة الشرق الأوسط. إذا كنت تبني نظاماً يحتاج إلى الصمود أمام انقطاعات الاتصال، تواصل معنا.

https://voxire.com/get-a-quote/

العودة إلى المدونة
Chat on WhatsApp