Đồ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, và cùng gặp một vấn đề. Đây là cách tôi tách phần tích hợp ra thành một service Node.js riêng.
Ba website bán lẻ tôi phát triển — 1Pro, MacOne và 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 được, cho tới khi không chạy được nữa.
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. Nhờ vậy mỗi site 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
Đây là bài học đắt nhất của dự án.
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. Nếu tin webhook là kênh duy nhất, 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, còn một cron quét toàn bộ danh mục mỗi đêm lo phần đúng. Mọi handler phải idempotent — 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 sẽ bị bên gửi coi là timeout và 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ạ và chuyện hai tiến trình cùng ghi
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 xử lý bằng cách đối chiếu hai danh sách và đá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 là loại tính năng chỉ cần sai một lần là mất nhiều ngày khôi phục.
Thứ khó nhất hoá ra không phải code
Sau vài tháng vận hành, phần tốn công nhất không phải hiệu năng hay kiến trúc, mà là trả lời được 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 không phải là log đẹp mà 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.
Nếu bạn định làm một tích hợp tương tự, hãy dựng endpoint đó ngay từ đầu — trước cả khi tối ưu tốc độ.
Bạn đang dùng KiotViet và muốn website hiển thị đúng giá, đúng tồn kho? Xem các dự án tôi đã làm hoặc gửi yêu cầu tư vấn.
Bài viết liên quan