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:

  1. Chuẩn hóa sản phẩm.
  2. Import sản phẩm vào public.products.
  3. Chuẩn hóa biến thể dựa trên danh sách sản phẩm đã chuẩn hóa.
  4. 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ầngTrách nhiệm
Google SheetChứ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 normalizedLà giao diện dữ liệu cố định giữa Node.js và SQL
SQL importKiểm tra khóa ngoại, kiểu dữ liệu, dữ liệu trùng và upsert vào PostgreSQL
Supabase/PostgreSQLLư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_name phải là tên tiếng Anh dạng snake_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 JSON size, không dùng phần Kích thước làm key.
  • Sheet dùng cột category; Node.js tạo category_slug để SQL tra cứu categories.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ành plus và dấu / được chuyển thành dấu -.
  • Ví dụ Lốp Michelin ENERGY XM2+ tạo slug lop-michelin-energy-xm2-plus.
  • Ví dụ Lốp Michelin Pilot Sport 4 225/45R17 tạo slug lop-michelin-pilot-sport-4-225-45r17.
  • summary phải dưới 256 ký tự.
  • cover_image_url không bắt buộc.
  • Nội dung description phả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_sku duy nhất để định danh biến thể.
  • Không dùng cột quantity trong file biến thể.
  • Nhóm attributes và 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.600 hoặc 4.089.600₫; Node.js chuyển thành 4089600.
  • 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 slug từ name, trong đó + thành plus và / thành -.
  • Tạo category_slug từ category.
  • Tạo brand_slug từ brand khi Sheet không cung cấp slug.
  • Chuẩn hóa SKU.
  • Chuyển C+ trong tên thành C Plus trước khi tạo slug.
  • Chuyển giá trị xuất xứ dạng NHẬT BẢN thành Nhật Bản.
  • Gom các cột con thuộc attributes thành attributes_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

ModeCách dùng
draftDùng trong lúc đang hoàn thiện Sheet; một số vấn đề chỉ được cảnh báo
strictDù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.
  • sku sản phẩm không trùng.
  • variant_sku không trùng.
  • Mọi product_sku của biến thể đều tồn tại trong products.normalized.csv.
  • attributes_json là JSON hợp lệ và dùng đúng json_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.csv vào temporary staging table.
  • Kiểm tra field bắt buộc, SKU trùng và slug trùng.
  • Tìm category_id bằng category_slug.
  • Tìm brand_id bằng brand_slug.
  • Kiểm tra spec_type hợp lệ.
  • Chuyển attributes_json thành jsonb.
  • 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.csv vào temporary staging table.
  • Kiểm tra product_sku, variant_sku và giá.
  • Kiểm tra variant_sku trùng trong CSV.
  • Tìm product_id từ products.sku = product_sku.
  • Chuyển attributes_json thành jsonb.
  • 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ỗiNguyên nhân thường gặpCách xử lý
Thiếu main headerSheet sai hoặc thiếu tên cộtSửa dòng header thứ nhất rồi export lại
UNKNOWN_ATTRIBUTEjson_name chưa có trong config categorySửa chính tả hoặc cập nhật config
Duplicate SKUNhiều dòng dùng cùng SKUSửa SKU trên Sheet
Category/brand không tồn tạiSlug tạo ra không khớp databaseChuẩn hóa tên hoặc seed category/brand trước
product_sku not foundChưa import sản phẩm hoặc mapping saiImport 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.