การแบ่งหน้าและการซิงก์
เส้นแบบลิสต์คืนผลลัพธ์ทีละหน้า แล้วส่ง cursor ของหน้าถัดไปมาให้ หน้านี้ครอบคลุมสองครึ่งของงานนี้ คือการอ่านผลลัพธ์ชุดหนึ่งให้ครบ และการรักษาข้อมูลฝั่งคุณให้ตรงกันต่อไปโดยไม่ต้องดาวน์โหลดใหม่ทั้งหมด
การแบ่งหน้าแบบ cursor
หัวข้อที่มีชื่อว่า “การแบ่งหน้าแบบ cursor”| ช่อง | ทิศทาง | คืออะไร |
|---|---|---|
limit |
คำขอ | จำนวนแถวต่อหน้า ค่าตั้งต้น 100 สูงสุด 200 |
cursor |
คำขอ | ค่า nextCursor ของหน้าก่อน หน้าแรกไม่ต้องส่ง |
nextCursor |
ผลลัพธ์ | cursor ของหน้าถัดไป หรือ null เมื่อได้ครบแล้ว |
กติกาที่ตามมาจากตารางนี้
limitถูกแก้ให้ ไม่ถูกปฏิเสธ ค่าที่เกิน 200 จะถูกลดเหลือ 200 และค่าที่ไม่ใช่จำนวนบวกจะตกกลับไปเป็น 100 คุณจะไม่ได้400จากเรื่องนี้ จึงควรตรวจว่าได้มากี่แถวจริง- cursor เป็นค่าทึบ เก็บเป็นสตริงแล้วส่งกลับไปเหมือนเดิม อย่าแกะและอย่าสร้างเอง
ในเส้นฝั่งสมาชิก cursor คือ token รูป
m1_…การส่งรหัสดิบไปจะได้400 bad_cursor - “หน้าเต็ม” แปลว่า “ถามต่อ” ระบบจะยื่น cursor ถัดไปให้ทุกครั้งที่หน้านั้นเต็มพอดี
ถ้าหน้าสุดท้ายบังเอิญเต็มพอดี คุณจะได้อีกหนึ่งคำขอที่คืน
"data": []พร้อม"nextCursor": nullซึ่งเป็นเรื่องปกติ ไม่ใช่ข้อผิดพลาด - ไม่มียอดรวม ไม่มี
hasMoreไม่มีจำนวนแถวทั้งหมด ไม่มีpageและไม่มีoffsetเงื่อนไขหยุดมีอย่างเดียวคือnextCursor === null GET /v1/shopsไม่แบ่งหน้าเลย คืนทุกสาขาที่กุญแจอ่านได้ในคำตอบเดียว
ไล่อ่านให้ครบทุกหน้า
หัวข้อที่มีชื่อว่า “ไล่อ่านให้ครบทุกหน้า”ลูปเดียวกันใช้ได้ทั้งฝั่งบิลและฝั่งสมาชิก ต่างกันแค่พารามิเตอร์
# หน้าแรกcurl -s -G https://api.scanfood.co/ext/v1/transactions \ -H "Authorization: Bearer $SCANFOOD_API_KEY" \ --data-urlencode "shopId=aK3mP9xQ2bR7cT4dV6wY" \ --data-urlencode "from=20260801" --data-urlencode "to=20260831" \ --data-urlencode "limit=200"
# หน้าถัดไป ส่ง nextCursor ที่เพิ่งได้กลับไปcurl -s -G https://api.scanfood.co/ext/v1/transactions \ -H "Authorization: Bearer $SCANFOOD_API_KEY" \ --data-urlencode "shopId=aK3mP9xQ2bR7cT4dV6wY" \ --data-urlencode "from=20260801" --data-urlencode "to=20260831" \ --data-urlencode "limit=200" \ --data-urlencode "cursor=dN6pS2aT5eU0fW7gY9zB"const BASE = 'https://api.scanfood.co/ext/v1';
async function* pages(path, query) { let cursor = null; do { const params = new URLSearchParams({ ...query, limit: '200' }); if (cursor) params.set('cursor', cursor);
const res = await fetch(`${BASE}${path}?${params}`, { headers: { Authorization: `Bearer ${process.env.SCANFOOD_API_KEY}` }, }); const body = await res.json(); if (!body.ok) throw new Error(`${res.status} ${body.error}`);
yield body.data; cursor = body.nextCursor; } while (cursor);}
for await (const rows of pages('/transactions', { shopId: 'aK3mP9xQ2bR7cT4dV6wY', from: '20260801', to: '20260831',})) { await save(rows);}BASE = "https://api.scanfood.co/ext/v1"
def pages(path, query): cursor = None while True: params = {**query, "limit": 200} if cursor: params["cursor"] = cursor
r = requests.get(BASE + path, headers=HEADERS, params=params, timeout=30) r.raise_for_status() body = r.json()
yield body["data"] cursor = body.get("nextCursor") if not cursor: return
for rows in pages("/transactions", { "shopId": "aK3mP9xQ2bR7cT4dV6wY", "from": "20260801", "to": "20260831",}): save(rows)ให้พารามิเตอร์ทุกตัวเหมือนเดิมตลอดการไล่หน้าของผลลัพธ์ชุดหนึ่ง
ถ้าเปลี่ยนช่วงวันหรือเปลี่ยน limit กลางทาง cursor ที่ถืออยู่จะกลายเป็นของคำขอคนละชุด
ซิงก์แบบเพิ่มทีละรอบ
หัวข้อที่มีชื่อว่า “ซิงก์แบบเพิ่มทีละรอบ”เมื่อมีสำเนาครบชุดแล้ว ให้ถามเฉพาะสิ่งที่เปลี่ยน
ฝั่งบิล รับ updatedSince ซึ่งเป็นเวลาแบบ ISO-8601 แถวจะเรียงตามเวลาที่ถูกแก้ล่าสุด
ดังนั้นใบกำกับภาษีที่ออกใหม่ บิลที่ถูกยกเลิก และช่องที่ถูกแก้ จะกลับมาให้เห็นทั้งหมด
curl -s -G https://api.scanfood.co/ext/v1/transactions \ -H "Authorization: Bearer $SCANFOOD_API_KEY" \ --data-urlencode "shopId=aK3mP9xQ2bR7cT4dV6wY" \ --data-urlencode "updatedSince=2026-09-09T04:00:00.000Z" \ --data-urlencode "limit=200"ใช้ค่า serverTime ของรอบที่สำเร็จครั้งล่าสุดเป็น updatedSince ของรอบถัดไป
และเลื่อนค่านี้ต่อเมื่อเก็บข้อมูลครบทุกหน้าของรอบนั้นแล้ว การเผื่อย้อนหลังสักหนึ่งนาที
ไม่มีต้นทุนอะไร เพราะทุกแถวมี id ที่เสถียร การอ่านซ้ำจึงเป็นการอัปเดต ไม่ใช่ข้อมูลซ้ำ
from/to กับ updatedSince เป็นสองโหมดของเส้นเดียวกัน และต้องเลือกโหมดเดียวต่อคำขอ
ส่งมาทั้งคู่หรือไม่ส่งเลย จะได้ 400 bad_query
ดึงข้อมูลย้อนหลัง
หัวข้อที่มีชื่อว่า “ดึงข้อมูลย้อนหลัง”ทำครั้งเดียวต่อร้าน แล้วสลับไปโหมดเพิ่มทีละรอบ
- ดูรายชื่อสาขา
GET /v1/shopsให้shopIdทุกตัวที่กุญแจอ่านได้ - ไล่ปฏิทินเป็นช่วงละไม่เกิน 31 วัน ช่วงที่กว้างกว่านั้นจะถูกปฏิเสธด้วย
400 bad_query(range too wide) ช่วงละหนึ่งเดือนคิดตามได้ง่ายที่สุด - ไล่หน้าของแต่ละช่วงให้ครบ ด้วย
limit=200แล้วตามnextCursorไปเรื่อย ๆ - บันทึกค่า
serverTimeของช่วงสุดท้ายที่สำเร็จ เวลานั้นคือจุดเริ่มต้นของรอบเพิ่มทีละรอบครั้งแรก - ฝั่งสมาชิกทำแยก ไล่หน้า
GET /v1/membersทีละproviderIdหนึ่งแบรนด์อาจถูกใช้ร่วมกันหลายสาขา จึงควรตัดรหัสซ้ำจากขั้นที่ 1 ออกก่อนเริ่ม
กันข้อมูลซ้ำฝั่งคุณ
หัวข้อที่มีชื่อว่า “กันข้อมูลซ้ำฝั่งคุณ”API รับประกันว่าการอ่านไม่เปลี่ยนอะไร แต่ที่เก็บข้อมูลฝั่งคุณต้องทนได้เมื่อแถวเดิมมาถึงสองครั้ง
- บันทึกแบบ upsert ด้วย
idสำหรับบิล และด้วยmemberIdสำหรับสมาชิก ทั้งคู่เสถียรและไม่ถูกนำกลับมาใช้ซ้ำ - ถือว่า
status: "void"คือการอัปเดต ไม่ใช่การลบ บิลที่คุณเก็บไว้แล้วอาจกลับมาเป็นบิลที่ถูกยกเลิก ให้เก็บแถวนั้นไว้แล้วทำเครื่องหมาย ยอดของคุณกับยอดของร้านจะได้ตรงกัน - เก็บ
updatedAtไว้ข้างบิลด้วย ถ้ามีแถวมาถึงพร้อมupdatedAtที่เก่ากว่าที่คุณถืออยู่ แปลว่าเป็นข้อมูลซ้ำ ให้ข้ามไป - เลื่อนหมุดเวลาต่อเมื่อรอบนั้นสำเร็จ การเก็บแถวและค่า
updatedSinceใหม่ ในธุรกรรมเดียวกัน ช่วยไม่ให้เกิดช่องโหว่เมื่องานตายกลางคัน - เตรียมรับค่า
nullไม่ใช่ช่องที่หายไป ช่องที่ไม่เกี่ยวกับบิลนั้นจะมาเป็นnullคอลัมน์ที่อยู่ ๆ ว่างจึงเป็นข้อมูล ไม่ใช่การเปลี่ยนโครงสร้าง