Đồng bộ liên tục
Bệnh nhân, lịch hẹn, hóa đơn và liệu trình dùng chung một cách duyệt: lấy những gì đã thay đổi từ một thời điểm, theo từng trang bằng con trỏ (cách chia trang xem ở Phân trang).
Tham số truy vấn
| Tham số | Mô tả |
|---|---|
updatedSincestring · date-timetuỳ chọn |
Chỉ lấy bản ghi có updatedAt từ thời điểm này trở đi, gồm cả đúng thời điểm này. ISO 8601 đầy đủ có múi giờ, ví dụ 2026-10-01T00:00:00.000Z (viết dấu + của múi giờ thành %2B). Bỏ trống: lấy tất cả. |
cursorstringtuỳ chọn |
Lấy nguyên giá trị nextCursor của trang trước. Đừng tự dựng hay phân tích; chỉ dùng cho đúng đường dẫn đã cấp ra nó. |
limitintegertuỳ chọn |
Từ 1 đến 500. Mặc định 200. |
Phản hồi có dạng { requestId, <số nhiều>, nextCursor } (ví dụ patients). nextCursor là null ở trang cuối. Tham số không có trong bảng bị từ chối bằng 400 UNKNOWN_PARAMETER, để một lỗi gõ sai updatedSince không âm thầm biến thành tải lại toàn bộ.
Cách đồng bộ
- Lần đầu: gọi không có
updatedSince, đi theonextCursortới khinull. - Mỗi bản ghi: có
deletedAt→ xoá ở phía bạn theoid; không thì ghi đè theoid(đừng chèn mới). - Nhớ
updatedAtlớn nhất đã thấy, và chỉ lưu sau khi đã duyệt hết các trang. - Các lần sau:
updatedSince= giá trị đã nhớ trừ 5 phút, giữ nguyên ở mọi trang của lần đó.
Vì sao lùi 5 phút. Việc giao dữ liệu là ít nhất một lần: bản ghi có thể xuất hiện lại, và một bản ghi vừa ghi xong có thể mang giờ sớm hơn một bản ghi bạn đã đọc. Chồng lấn một chút kết hợp ghi đè theo
id làm cả hai điều đó vô hại.
Bản ghi đã xoá (tombstone)
Một bản ghi đã xoá hoặc đã gộp vẫn nằm trong danh sách, ở dạng rút gọn { id, deletedAt, updatedAt } (bệnh nhân có thêm mergedIntoId), không còn thông tin cá nhân. Gọi GET …/{id} của bản ghi như vậy vẫn trả 200 với tombstone. Một id không có thật, hoặc thuộc phòng khám khác, đều trả 404 NOT_FOUND giống hệt nhau.
curl -sS "https://iclinic.phongkhamhieploi.vn/api/integration/v1/patients?limit=2&updatedSince=2026-10-01T00:00:00.000Z" \
-H "Authorization: Bearer $ICLINIC_API_KEY"
# → 2 bản ghi, "nextCursor": "djF8cGF0aWVudHN8…"
curl -sS "https://iclinic.phongkhamhieploi.vn/api/integration/v1/patients?limit=2&updatedSince=2026-10-01T00:00:00.000Z&cursor=djF8cGF0aWVudHN8…" \
-H "Authorization: Bearer $ICLINIC_API_KEY"
# → 1 bản ghi, "nextCursor": null (hết)
const BASE = 'https://iclinic.phongkhamhieploi.vn/api/integration/v1';
const OVERLAP_MS = 5 * 60_000;
// Node 18+. `checkpoint` là giá trị trả về của lần chạy trước (null ở lần đầu).
export async function syncPatients({ checkpoint, upsert, remove }) {
const since = checkpoint
? new Date(Date.parse(checkpoint) - OVERLAP_MS).toISOString()
: null;
let newest = checkpoint;
let cursor = null;
do {
const url = new URL(`${BASE}/patients`);
url.searchParams.set('limit', '500');
if (since) url.searchParams.set('updatedSince', since); // giữ nguyên ở mọi trang
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.ICLINIC_API_KEY}` },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const page = await res.json();
for (const patient of page.patients) {
if (patient.deletedAt) await remove(patient.id); // tombstone
else await upsert(patient); // ghi đè theo id
if (!newest || patient.updatedAt > newest) newest = patient.updatedAt;
}
cursor = page.nextCursor;
} while (cursor);
return newest; // lưu làm checkpoint cho lần sau, chỉ khi đã duyệt hết
}
import os
from datetime import datetime, timedelta, timezone
import requests
BASE = "https://iclinic.phongkhamhieploi.vn/api/integration/v1"
OVERLAP = timedelta(minutes=5)
def sync_patients(checkpoint, upsert, remove):
"""checkpoint: chuỗi updatedAt của lần chạy trước (None ở lần đầu)."""
since = None
if checkpoint:
at = datetime.fromisoformat(checkpoint.replace("Z", "+00:00")) - OVERLAP
since = at.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
headers = {"Authorization": f"Bearer {os.environ['ICLINIC_API_KEY']}"}
newest, cursor = checkpoint, None
while True:
params = {"limit": 500}
if since:
params["updatedSince"] = since # giữ nguyên ở mọi trang
if cursor:
params["cursor"] = cursor
res = requests.get(f"{BASE}/patients", headers=headers, params=params, timeout=30)
res.raise_for_status()
page = res.json()
for patient in page["patients"]:
if patient["deletedAt"]:
remove(patient["id"]) # tombstone
else:
upsert(patient) # ghi đè theo id
if not newest or patient["updatedAt"] > newest:
newest = patient["updatedAt"]
cursor = page["nextCursor"]
if not cursor:
return newest # lưu làm checkpoint cho lần sau, chỉ khi đã duyệt hết