POS Sync API (/api/v1/pos/*)
ชั้น HTTP ที่ให้ SML Ai POS (Flutter ขายแบบออฟไลน์ มีฐาน ObjectBox ของตัวเอง) ดึงข้อมูลหลักแบบ delta จาก Account, ส่งบิลขึ้นมาให้ Go สร้างเอกสารบัญชี, อัปโหลดรูปสลิป และกู้เครื่องที่ล้างแล้ว; และให้หน้า Account (back office) ตั้งค่า/จับคู่เครื่อง. แพ็กเกจ http/pos เป็นแค่ตัวรับ-ส่ง HTTP — กฎเงิน/ภาษี/สต็อก/GL และ cursor อยู่ใน application/process (pos_sync.go, pos_bill.go, pos_media.go, pos_config.go) ซึ่งอธิบายใน ภาพรวมกลุ่ม process ของ Backend (Process overview).
ทำงานอย่างไร
- Route (
router.go:328-351, ลงทะเบียนเมื่อex.Pos != nil): ทั้งกลุ่มใช้ middlewareex.Pos.Authของตัวเอง แทนmiddleware.Auth+RequireRight(สิทธิ์เมนูและ middleware RequireRight (Menu rights / permission)); มี 2 กลุ่มย่อย:
| กลุ่ม | Method + path | หน้าที่ |
|---|---|---|
| (ทุกผู้เรียก) | POST /shift, GET /image |
เปิด/ปิดกะ ส่งเงิน ยกเลิกบิล; รูปย่อ JPEG |
DeviceOnly |
GET /sync , GET /restore (gzip), POST /bills, POST /media |
ฝั่งเครื่อง POS |
UserOnly |
GET /bootstrap, POST /config, GET /devices, POST /devices/{machine}/pair, POST /bills/reprocess |
ฝั่ง back office |
- ยืนยันตัวตน (
Auth,pos/sync.go:24-52): อ่านAuthorization: Bearer; ถ้าโทเค็นขึ้นต้นsmlpos1.(posTokenPrefix,process/pos_sync.go:228) →process.VerifyPosDevice(เทียบ SHA-256 ของส่วนลับกับpos_sync_machine.token_hashด้วย constant-time,pos_sync.go:310-333) แล้วสร้าง claimsUsername = "pos:<machine>"ผูก Provider + Company DB ของเครื่อง (ไม่มี level); ไม่ใช่ → ตรวจ JWT ผู้ใช้ตามปกติ (JWT session token และการเพิกถอน (JWT + revocation store)). ฐานข้อมูลมาจาก token เสมอ ไม่มี?db=(handler.go:46-51). - เครื่องจริง vs session เว็บ:
deviceFromContext(sync.go:54-69) ให้ session ผู้ใช้ที่มีDatabaseNameทำตัวเป็นเครื่องPOS-01ได้ (สำหรับ Browser POS ที่ฝังในเว็บ/pos), ดังนั้นผู้ใช้เว็บเรียก/sync,/bills,/restore,/mediaได้.UserOnly(:81-89) บล็อกเฉพาะ เครื่องจริงที่ใช้ device token → 403 (ไม่บล็อก session ผู้ใช้);DeviceOnlyตอบ 403 ถ้าไม่มี device/claims. - Pull (
GET /sync?cursor=&limit=): อ่าน headerX-Pos-Pending-Bills(จำนวนบิลค้างส่ง) และX-Pos-App-Version(ตัดเหลือ 40 ตัวอักษร) แล้วPosSyncPullคืน{"ok":true,"data":{changes,cursor,has_more}}; cursor ว่าง = ส่งทั้งหมด (รายละเอียด entityproduct|member|promotion|coupon|configอยู่ในdocs/POS_SYNC_API.md). cursor อาศัยpg_snapshot_xminจึงธุรกรรมที่เปิดค้างใน Postgres ทำให้ delta ของทุกเครื่องหยุดรอ (ตามเอกสาร). - ส่งบิล (
POST /bills) (sync.go:111-127): body จำกัด 1MB →ReceivePosBill(บันทึก JSON ดิบลงpos_bill_inboxก่อน แล้วสร้างic_trans/ic_trans_detailtrans_flag 44 พร้อม GL และคิวสต็อกpos_stock_queue). ส่งbill_guidซ้ำไม่สร้างขายซ้ำ. ตามสัญญา: 200 ใช้ได้ (statusdone|cancelled|error;errorก็เก็บไว้เพื่อแก้แล้ว reprocess), 409 ชน doc_no/bill_guid, 400 payload ผิด, 401 token ถูกเพิกถอน. เงินส่งเป็นสตริงทศนิยม (รูปแบบ error และ response ของ API (apperr + response helper) — ใช้apperrตามปกติ). - จับคู่เครื่อง (
POST /devices/{machine}/pair) (sync.go:129-138):PairPosDeviceสร้าง tokensmlpos1.…คืน{"data":{"token":...}}ครั้งเดียว (เก็บเฉพาะแฮชpos_sync.go:273-277); จับคู่ใหม่ = เพิกถอนของเดิมและล้างlast_cursor.GET /devicesแสดงสถานะ sync/บิลค้าง/บิล error/คิวสต็อกต่อเครื่อง;POST /bills/reprocess {"all":false}รันบิลจาก inbox ซ้ำหลังแก้บั๊ก และ log จำนวนที่ล้ม (sync.go:152-176). - ตั้งค่า / Bootstrap:
GET /bootstrap(handler.go:60) เรียกEnsurePosTablesแล้วยิง SELECT 11 คำสั่งผ่านBatchUseCase(Query Gateway: /query/list, limit, batch, exec, execBatch) ได้posconfig,pos_id(เครื่อง),erp_thai_qr_config,pos_point_condition,pos_promotions,coupon_list(100 แถว),pos_member_groups, ตัวอย่างสินค้า 60 รายการ,poscashiersettle(50), บิลล่าสุด trans_flag 44 (50),pos_temp_members(50) — เป็นข้อมูลหน้าตั้งค่า ไม่ใช่แคตตาล็อกเต็ม (แคตตาล็อกเต็มมาทาง/sync). ถ้าตารางว่างจะใส่ค่า default ของเงื่อนไขแต้มจากโค้ด (handler.go:175-186).POST /config→SavePosConfig(body ≤ 1MB,UseNumberกัน float) คืน{"ok":true,"section":...}. - กะ (
POST /shift) →PosShiftAction(เปิด/ปิดกะ ส่งเงินcancel_billสมาชิกชั่วคราว); ส่งdevMachineจาก device ที่ยืนยันแล้ว. เอกสารระบุว่าส่วนนี้ยังสร้าง SQL ใน handler/ใช้ float (รอย้ายเข้าprocess) — ขัดกับกฎเงิน (เครื่องมือกลาง platform: docno, sqllit, search, security (Platform utilities)). - รูป:
GET /image?e=product|member&k=&v=(media.go:25-59) คืน JPEG ย่อ ≤320px พร้อมCache-Control: private, max-age=31536000, immutable(URL มีเลขเวอร์ชันvจึงเปลี่ยนเมื่อรูปเปลี่ยน). ต้นทางเป็นไฟล์ใน MinIO (ผ่าน thumbnail) หรือรูป inline ในimages.image_file(ย่อทุกคำขอ); ถ้าไฟล์ใน MinIO ล้มหรือเป็นสินค้า seed จะ ใช้รูปสินค้าตัวอย่างจากแพ็กเกจmarketแทน (:33-44) (แทรกแถวพร้อมรูปภาพ: POST /blob/insert + MinIO (Blob insert)).POST /media?kind=slip&ref=<doc_no>(:61-101): body ≤ 5MB, ตรวจชนิดด้วยhttp.DetectContentType(รับเฉพาะ PNG/JPEG), key =<db>/pos-<kind>/<sha256 16 hex>.<ext>(retry ปลอดภัย), เก็บต้นฉบับ+รูปย่อลง MinIO แล้วRecordPosMediaลงsml_doc_images; บิลยังไม่มีบนเซิร์ฟเวอร์ = 409 (จากPosMediaImageID);kindรับแค่slip. - กู้เครื่อง:
GET /restore(media.go:103-112) คืนกะที่เปิดอยู่ (หรือวันนี้) + รายการส่งเงินสด + บิลพร้อม payload ดิบ (สูงสุด 5,000 บิลตามเอกสาร); เครื่องสร้างบิลกลับเป็น "ส่งแล้ว" และเลขบิลต่อจากconfig.last_doc_no.
ข้อควรระวัง
- POS ไม่ผ่าน
RequireRight: ผู้ใช้ที่ล็อกอินแล้วทุกคน (แม้สิทธิ์เมนูว่าง) เรียกกลุ่มUserOnlyได้ — จับคู่/เพิกถอน token เครื่อง, แก้posconfig, reprocess บิล (ตรวจจากโค้ดrouter.go:340-346ไม่มี guard เพิ่ม). การจำกัดสิทธิ์ถ้ามี น่าจะอยู่ที่ชั้นหน้าเว็บ (ยังไม่ยืนยัน). - session เว็บทำตัวเป็น
POS-01ได้ → ถ้าเครื่องจริงชื่อPOS-01ด้วย บิลจากสองทางใช้ชื่อเครื่องเดียวกัน (เลขบิลต่อเครื่องอ้างตามdoc_formatของpos_id). - เอกสาร
POS_SYNC_API.mdเขียน URL รูปว่า/api/pos/image(ผ่าน Next proxy) แต่ route จริงใน backend คือ/api/v1/pos/image; URL ที่เก็บของ/mediaขึ้นต้น/api/crm/imageส่วน route จริงคือ/api/v1/crm/image(router.go:262) — ต้องมี proxy คั่น (ยังไม่ยืนยัน). - GL ของบิล POS ลงบัญชีเงินสดทั้งหมด ยังไม่แยกตามวิธีจ่าย; แต้มสะสมเก็บที่
pos_bill_inbox.earned_pointsยังไม่ลงar_customer(ตามเอกสาร Limitation). - ตาราง POS (
pos_sync_change,pos_sync_machine,pos_bill_inbox,pos_stock_queue+ triggerpos_sync_log) ถูกสร้างอัตโนมัติ "ครั้งแรกที่ใช้ฐานบริษัท" ในชั้นprocessไม่ใช่ migration (ไฟล์ migration และการสร้างตารางตอนรัน (Migrations / runtime DDL)). - timeout รวมของ router คือ 60 วินาที (
router.go:64) —/syncที่ใช้ limit ใหญ่ต้องจบในเวลานี้.
ที่มาในโค้ด
backend/internal/adapter/inbound/http/router.go:328-351— route และ middleware ของ POS.backend/internal/adapter/inbound/http/pos/sync.go:20-176—Auth,DeviceOnly/UserOnly,Sync,PostBill,PairDevice,ListDevices,ReprocessBills.backend/internal/adapter/inbound/http/pos/handler.go:21-296—Bootstrap,SaveConfig,ShiftAction.backend/internal/adapter/inbound/http/pos/media.go:20-112—Image,PostMedia,Restore.backend/internal/application/process/pos_sync.go:228-333— prefix,PairPosDevice,VerifyPosDevice.backend/docs/POS_SYNC_API.md,backend/docs/POS_SYNC_KMS.md— สัญญา API และกติกาที่ตกลงไว้ (ตรงกับโค้ดที่อ่าน ยกเว้น path รูปข้างต้น).
ดูเพิ่ม
ภาพรวม Backend Go ของ SML Ai Account (Backend overview) · ภาพรวมกลุ่ม process ของ Backend (Process overview) · Router, middleware และแผนผัง route ทั้งหมดของ backend (chi router) · สิทธิ์เมนูและ middleware RequireRight (Menu rights / permission) · แทรกแถวพร้อมรูปภาพ: POST /blob/insert + MinIO (Blob insert) · Query Gateway: /query/list, limit, batch, exec, execBatch · สารบัญ ADR ของ backend (Architecture Decision Records)