Flow từ Sheet sản phẩm vào database
Tài liệu này mô tả quy trình đưa dữ liệu sản phẩm và biến thể sản phẩm từ Google Sheet vào PostgreSQL của Supabase. Dữ liệu nguồn không được import thẳng vào database mà phải đi qua script Node.js để chuẩn hóa trước.
Mục tiêu
- Cho phép người nhập liệu làm việc bằng tên cột dễ đọc trên Sheet.
- Chuẩn hóa SKU, slug, giá, category, brand và attributes tại một nơi.
- Phát hiện lỗi trước khi SQL ghi dữ liệu vào database.
- Tạo CSV normalized có cấu trúc cố định cho các file SQL import.
- Import lại nhiều lần mà không tạo bản ghi trùng.
Sơ đồ tổng thể
flowchart TD
A[Google Sheet sản phẩm] --> B[Export CSV UTF-8]
B --> C[normalize-products-csv.mjs]
K[product-attribute-config.json] --> C
C --> D[products.normalized.csv]
D --> E[import_products.sql]
E --> F[(public.products)]
G[Google Sheet biến thể] --> H[Export CSV UTF-8]
H --> I[normalize-product-variant.js]
D --> I
K --> I
I --> J[product_variants.normalized.csv]
J --> L[import_product_variants.sql]
F --> L
L --> M[(public.product_variants)]
Thứ tự bắt buộc:
- Chuẩn hóa sản phẩm.
- Import sản phẩm vào
public.products. - Chuẩn hóa biến thể dựa trên danh sách sản phẩm đã chuẩn hóa.
- Import biến thể vào
public.product_variants.
Không được import biến thể trước sản phẩm vì product_sku phải tìm thấy sản
phẩm cha.
Trách nhiệm của từng tầng
| Tầng | Trách nhiệm |
|---|---|
| Google Sheet | Chứa dữ liệu gốc, tên cột thân thiện với người nhập liệu và hai dòng header |
| Node.js | Đọc CSV nguồn, chuẩn hóa giá trị, kiểm tra lỗi và tạo JSON attributes |
| CSV normalized | Là giao diện dữ liệu cố định giữa Node.js và SQL |
| SQL import | Kiểm tra khóa ngoại, kiểu dữ liệu, dữ liệu trùng và upsert vào PostgreSQL |
| Supabase/PostgreSQL | Lưu sản phẩm, biến thể và cung cấp dữ liệu cho các API |
Node.js không ghi trực tiếp vào database. SQL import không chịu trách nhiệm đọc cấu trúc Sheet phức tạp hoặc sửa dữ liệu nguồn.
1. Chuẩn bị Sheet sản phẩm
Sheet sản phẩm sử dụng hai dòng header:
Dòng 1: name | sku | brand | category | ... | attributes | [trống] | [trống] | origin | ...
Dòng 2: | | | | ... | Kích thước (size) | Số lớp bố (pr) | Mã gai (pattern_code) | | ...
Quy tắc:
- Chỉ có đúng một main header tên
attributes. - Các ô main header tiếp theo thuộc vùng attributes phải để trống.
- Vùng attributes kết thúc khi gặp main header tiếp theo, ví dụ
origin. - Dòng header thứ hai phải để trống ngoài vùng attributes.
- Mỗi cột con của attributes dùng dạng
Tên tiếng Việt (json_name). json_namephải là tên tiếng Anh dạngsnake_case, không dấu và không có khoảng trắng.- Ví dụ
Kích thước (size)được lưu thành key JSONsize, không dùng phầnKích thướclàm key. - Sheet dùng cột
category; Node.js tạocategory_slugđể SQL tra cứucategories.id. - Sheet không cần cột
slug; Node.js tự tạo slug sản phẩm từname. - Khi tạo slug, dấu
+được chuyển thànhplusvà dấu/được chuyển thành dấu-. - Ví dụ
Lốp Michelin ENERGY XM2+tạo sluglop-michelin-energy-xm2-plus. - Ví dụ
Lốp Michelin Pilot Sport 4 225/45R17tạo sluglop-michelin-pilot-sport-4-225-45r17. summaryphải dưới 256 ký tự.cover_image_urlkhông bắt buộc.- Nội dung
descriptionphải là Markdown. Script hiện đọc nội dung Markdown từ ôdescription; nó không tự mở một đường dẫn file.mdđược ghi trong ô.
2. Chuẩn bị Sheet biến thể
Sheet biến thể cũng sử dụng hai dòng header:
Dòng 1: product_sku | name | variant_sku | price | attributes | [trống] | [trống] | ...
Dòng 2: | | | | Kích thước (size) | Số lớp bố (pr) | Mã gai (pattern_code) | ...
Quy tắc:
- Có
product_skuđể liên kết với sản phẩm cha. - Có
variant_skuduy nhất để định danh biến thể. - Không dùng cột
quantitytrong file biến thể. - Nhóm
attributesvà các cột con tuân theo cùng pattern của Sheet sản phẩm. - Giá có thể được nhập như
4,089,600,4.089.600hoặc4.089.600₫; Node.js chuyển thành4089600. - Khoảng trắng trong SKU được đổi thành dấu
-; ký tự/trong SKU được giữ nguyên.
3. Node.js chuẩn hóa dữ liệu
Chuẩn hóa sản phẩm
Script:
scripts/normalize-products-csv.mjs
Đầu ra mặc định:
supabase/imports/products.normalized.csv
Các nhiệm vụ chính:
- Đọc CSV có hai dòng header.
- Xác định các main field bằng tên header.
- Tạo
slugtừname, trong đó+thànhplusvà/thành-. - Tạo
category_slugtừcategory. - Tạo
brand_slugtừbrandkhi Sheet không cung cấp slug. - Chuẩn hóa SKU.
- Chuyển
C+trong tên thànhC Plustrước khi tạo slug. - Chuyển giá trị xuất xứ dạng
NHẬT BẢNthànhNhật Bản. - Gom các cột con thuộc
attributesthànhattributes_json. - Kiểm tra attributes theo cấu hình của category tại
supabase/imports/product-attribute-config.json. - Phát hiện field bắt buộc bị thiếu và SKU/slug bị trùng trong CSV.
Chạy kiểm tra trước, chưa ghi file:
pnpm products:normalize -- \
--input "supabase/imports/<sheet-san-pham>.csv" \
--mode strict \
--dry-run
Khi không còn lỗi, tạo file normalized:
pnpm products:normalize -- \
--input "supabase/imports/<sheet-san-pham>.csv" \
--mode strict \
--force
Chuẩn hóa biến thể
Script:
scripts/normalize-product-variant.js
Đầu ra mặc định:
supabase/imports/product_variants.normalized.csv
Script dùng thêm products.normalized.csv để kiểm tra mỗi product_sku có sản
phẩm cha tương ứng.
Chạy kiểm tra trước:
pnpm product-variants:normalize -- \
--input "supabase/imports/<sheet-bien-the>.csv" \
--mode strict \
--dry-run
Khi không còn lỗi, tạo file normalized:
pnpm product-variants:normalize -- \
--input "supabase/imports/<sheet-bien-the>.csv" \
--mode strict \
--force
draft và strict
| Mode | Cách dùng |
|---|---|
draft | Dùng trong lúc đang hoàn thiện Sheet; một số vấn đề chỉ được cảnh báo |
strict | Dùng trước khi import; lỗi header, attribute hoặc mapping phải được sửa |
Chỉ dùng kết quả strict để import vào database.
4. Kiểm tra CSV normalized
Không chỉnh sửa thủ công file normalized nếu có thể. Nếu kết quả sai, sửa Sheet hoặc logic Node.js rồi chạy chuẩn hóa lại để nguồn dữ liệu và kết quả không lệch nhau.
Hai file cần có:
supabase/imports/products.normalized.csv
supabase/imports/product_variants.normalized.csv
Kiểm tra tối thiểu:
- Số dòng normalized khớp số dòng dữ liệu hợp lệ trên Sheet.
skusản phẩm không trùng.variant_skukhông trùng.- Mọi
product_skucủa biến thể đều tồn tại trongproducts.normalized.csv. attributes_jsonlà JSON hợp lệ và dùng đúngjson_name.- Giá biến thể chỉ còn chữ số.
5. Import vào Supabase local
Đảm bảo Supabase local đang chạy:
supabase status
Import sản phẩm trước:
psql \
'postgresql://postgres:postgres@127.0.0.1:54322/postgres' \
-v ON_ERROR_STOP=1 \
-f supabase/imports/import_products.sql
Sau khi sản phẩm thành công, import biến thể:
psql \
'postgresql://postgres:postgres@127.0.0.1:54322/postgres' \
-v ON_ERROR_STOP=1 \
-f supabase/imports/import_product_variants.sql
ON_ERROR_STOP=1 làm psql dừng ngay khi gặp lỗi. Mỗi file import chạy trong
transaction nên lỗi validation sẽ rollback toàn bộ file đó.
Không thay URL local bằng URL staging hoặc production nếu chưa xác nhận đúng môi trường và có bản sao lưu.
6. SQL import thực hiện gì?
import_products.sql
- Copy
products.normalized.csvvào temporary staging table. - Kiểm tra field bắt buộc, SKU trùng và slug trùng.
- Tìm
category_idbằngcategory_slug. - Tìm
brand_idbằngbrand_slug. - Kiểm tra
spec_typehợp lệ. - Chuyển
attributes_jsonthànhjsonb. - Insert sản phẩm mới hoặc update sản phẩm có cùng slug.
- Giữ ảnh cover hiện tại nếu CSV không cung cấp
cover_image_url.
import_product_variants.sql
- Copy
product_variants.normalized.csvvào temporary staging table. - Kiểm tra
product_sku,variant_skuvà giá. - Kiểm tra
variant_skutrùng trong CSV. - Tìm
product_idtừproducts.sku = product_sku. - Chuyển
attributes_jsonthànhjsonb. - Insert biến thể mới hoặc update biến thể có cùng
variant_sku.
7. Kiểm tra sau import
Chạy trong DBeaver hoặc Supabase SQL Editor của môi trường local:
SELECT COUNT(*) AS product_count
FROM public.products;
SELECT COUNT(*) AS variant_count
FROM public.product_variants;
SELECT
p.sku AS product_sku,
pv.sku AS variant_sku,
pv.price,
pv.attributes
FROM public.product_variants pv
JOIN public.products p ON p.id = pv.product_id
ORDER BY p.sku, pv.sku
LIMIT 100;
Nếu DBeaver có dữ liệu nhưng API không trả dữ liệu, kiểm tra tiếp URL Supabase, API key, trạng thái sản phẩm và Row Level Security của đúng môi trường. Đây là bước sau import, không phải nhiệm vụ của script normalize.
8. Xử lý khi có lỗi
| Lỗi | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Thiếu main header | Sheet sai hoặc thiếu tên cột | Sửa dòng header thứ nhất rồi export lại |
UNKNOWN_ATTRIBUTE | json_name chưa có trong config category | Sửa chính tả hoặc cập nhật config |
| Duplicate SKU | Nhiều dòng dùng cùng SKU | Sửa SKU trên Sheet |
| Category/brand không tồn tại | Slug tạo ra không khớp database | Chuẩn hóa tên hoặc seed category/brand trước |
product_sku not found | Chưa import sản phẩm hoặc mapping sai | Import sản phẩm trước và sửa product_sku |
| Giá không hợp lệ | Giá chứa ký tự không được hỗ trợ | Sửa giá nguồn rồi normalize lại |
Lưu ý triển khai về header attributes
Flow chuẩn yêu cầu Node.js tách json_name nằm trong ngoặc đơn:
Kích thước (size) -> size
Loại hoa lốp (pattern_type) -> pattern_type
Nếu script đang tạo key như kích_thước_(size) thay vì size, nghĩa là bước
tách json_name chưa được triển khai trong normalizer. Phải cập nhật cả script
sản phẩm và script biến thể trước khi chạy strict; không nên sửa key thủ công
trong file normalized.