blog
Đồng bộ KiotViet với website WordPress — vì sao tôi bỏ cách gọi API trực tiếp
Ba website bán lẻ cùng gọi thẳng API KiotViet từ PHP, cùng chạm rate limit của một tài khoản. Cách tách phần tích hợp ra thành một service Node.js đứng giữa.
Ba website bán lẻ tôi phát triển (1Pro, MacOne, Nam Thành Công) đều dùng KiotViet làm hệ thống quản lý bán hàng tại cửa hàng. Ban đầu mỗi site tự gọi API KiotViet trực tiếp từ PHP. Cách đó chạy ổn với một site, và hỏng khi có ba.
Ba vấn đề của cách gọi API trực tiếp
Rate limit dùng chung nhưng tính riêng. Mỗi website chạy cron đồng bộ danh mục theo lịch của nó. Ba site cùng quét vài nghìn SKU vào giờ thấp điểm, request dồn lên cùng một tài khoản KiotViet, và bên nào chạm ngưỡng trước thì bên đó nhận lỗi. Website nào lỗi thì giá hiển thị sai cho tới lần đồng bộ sau.
Code tích hợp nhân bản ba lần. Mỗi lần KiotViet đổi cấu trúc response, tôi phải sửa cùng một logic ở ba theme WordPress khác nhau, mỗi theme lại có cách xử lý riêng do viết ở ba thời điểm khác nhau.
PHP-FPM không hợp với việc chờ. Một request đồng bộ chậm chiếm một worker. Khi API bên kia chậm, worker bị giữ, và người dùng thật đang duyệt web phải xếp hàng phía sau tác vụ nền.
Giải pháp là tách hẳn phần tích hợp ra khỏi website: một service Node.js đứng giữa, nhận webhook từ KiotViet, chuẩn hoá dữ liệu, rồi đẩy về từng site qua REST API nội bộ.
Kiến trúc: một service đứng giữa, website chỉ nhận
Luồng dữ liệu đi một chiều. KiotViet là nguồn sự thật cho giá và tồn kho. WordPress giữ quyền về nội dung: mô tả, ảnh, bài viết SEO. Hai bên không tranh nhau quyền ghi.
Service dùng Node.js với Express, MySQL để lưu bảng ánh xạ, một hàng đợi nội bộ để retry, JWT xác thực giữa service và các site, chạy trên VPS qua PM2 sau Nginx.
Điểm quan trọng về mặt thiết kế: website không bao giờ gọi ngược sang KiotViet. Nó chỉ mở một endpoint nhận cập nhật. Mỗi site vì vậy không cần biết gì về rate limit, token hay định dạng dữ liệu của KiotViet, trách nhiệm đó nằm gọn một chỗ.
Webhook để phản ứng nhanh, cron mới là nguồn sự thật
Webhook có thể đến trùng, đến muộn, hoặc không đến, nhất là khi service đang restart hoặc mạng chập chờn. Tin webhook là kênh duy nhất thì sớm muộn dữ liệu cũng lệch mà không ai biết.
Nguyên tắc cuối cùng: webhook lo phần nhanh, cron quét toàn bộ danh mục mỗi đêm lo phần đúng. Mọi handler phải idempotent, tức xử lý cùng một sự kiện hai lần vẫn cho kết quả như một lần.
// Webhook handler: idempotent theo (productId, modifiedDate)
app.post('/webhook/kiotviet', verifySignature, async (req, res) => {
// Trả 200 ngay — xử lý nặng đẩy vào hàng đợi, KiotViet không phải chờ
res.sendStatus(200)
for (const item of req.body.Notifications ?? []) {
const seen = await db.query(
'SELECT 1 FROM sync_log WHERE product_id = ? AND modified_at = ?',
[item.ProductId, item.ModifiedDate]
)
if (seen.length) continue // đã xử lý — bỏ qua bản trùng
queue.push({ type: 'product.update', payload: item })
}
})
Hai chi tiết đáng chú ý trong đoạn trên. Thứ nhất, res.sendStatus(200) gọi trước khi xử lý: KiotViet chỉ cần biết ta đã nhận, không cần chờ ta ghi xong database. Webhook chờ lâu bị bên gửi coi là timeout rồi gửi lại, tạo thêm bản trùng. Thứ hai, bảng sync_log chặn trùng bằng cặp (product_id, modified_at) thay vì chỉ ID sản phẩm, vì cùng một sản phẩm có thể đổi giá nhiều lần hợp lệ.
Bảng ánh xạ sản phẩm: khoá chính tổ hợp chống ghi trùng
Mỗi sản phẩm KiotViet tương ứng một bài viết WordPress trên mỗi site. Quan hệ đó phải lưu lại, và phải chịu được việc cron đêm chạy chồng lên một webhook đến cùng lúc.
CREATE TABLE product_map (
kiotviet_id BIGINT NOT NULL,
site VARCHAR(32) NOT NULL,
wp_post_id BIGINT NOT NULL,
synced_at DATETIME NOT NULL,
PRIMARY KEY (kiotviet_id, site)
);
Khoá chính tổ hợp (kiotviet_id, site) làm hết việc: hai tiến trình cùng ghi thì một cái thắng, cái còn lại nhận lỗi trùng khoá và bỏ qua thay vì tạo bản ghi thứ hai. Không cần lock ở tầng ứng dụng.
Trường hợp phiền nhất là sản phẩm bị xoá một phía: nhân viên xoá trên KiotViet nhưng bài viết vẫn còn trên web, hoặc ngược lại. Cron đêm đối chiếu hai danh sách rồi đánh dấu phần lệch để người thật quyết định, thay vì tự động xoá. Tự động xoá nội dung chỉ cần sai một lần là mất nhiều ngày khôi phục.
Endpoint chẩn đoán: vì sao sản phẩm này lệch giá
Sau vài tháng vận hành, phần tốn công nhất là trả lời câu hỏi "vì sao sản phẩm này lệch giá".
Trong hàng nghìn SKU, khi khách gọi báo một mã hiển thị sai, thứ cứu tôi là một endpoint chẩn đoán duy nhất: sản phẩm này đồng bộ lần cuối lúc nào, từ sự kiện nào, hàng đợi còn tồn gì, lỗi gần nhất là gì. Trước khi có nó, mỗi lần như vậy tốn nửa buổi lục log ba máy.
Định làm một tích hợp tương tự thì dựng endpoint đó ngay từ đầu, trước cả khi tối ưu tốc độ.
Website bán lẻ lấy giá và tồn kho từ KiotViet mà hiển thị sai thì gần như luôn lệch ở ba khâu trên. Xem các dự án đã làm hoặc gửi yêu cầu tư vấn.
Bài viết liên quan
tất cả bài viết →- Tự dựng dashboard giám sát uptime cho 30 website khách hàngKhách gọi báo web sập trước khi tôi biết. Cách theo dõi uptime, tốc độ phản hồi và hạn SSL của 30 site mà không sinh ra một núi cảnh báo rác.đọc tiếp →
- Claude Code + NotebookLM + Obsidian: dựng pipeline nghiên cứu, và nửa cầu nối không chính chủCách ghép Claude Code, NotebookLM và Obsidian thành pipeline nghiên cứu: skill nào có sẵn, vì sao NotebookLM không có API cho tài khoản cá nhân, và cái giá phải trả khi ghép ba công cụ này.đọc tiếp →
- Viết CLAUDE.md đúng chuẩn: bốn nhóm quy tắc và giới hạn thật của Claude CodeCLAUDE.md hoạt động tốt khi chia thành bốn nhóm rõ ràng: lệnh, quy tắc, kiến trúc theo path, và cách mở rộng khi dự án lớn lên. Hướng dẫn viết từng nhóm đúng giới hạn kỹ thuật thật của Claude Code, kèm ví dụ chạy được.đọc tiếp →
- Bỏ commit thư mục out/: dựng CI/CD build static ngay trên serverRepo này có 29 trên 51 commit đụng vào thư mục build output. Cách bỏ hẳn nó: self-hosted runner build ngay trên server, publish bằng một thao tác đổi symlink, purge Cloudflare, và ba cái bẫy bash chỉ lộ ra khi đem ra test.đọc tiếp →