FE: module product — sản phẩm, biến thể, thuộc tính #
Phần chung (khởi tạo client, lỗi, danh sách, ngôn ngữ) ở tài liệu FE của Core:
docs/fe/common.md (bản đọc: xforge-documentation, ADR-0102 của xforge-core).
Sửa file này CÙNG PR khi đổi hành vi FE thấy được của module.
Mô hình #
sản phẩm (code, status, brand, category, đơn vị gốc, ảnh, bản dịch, version)
├─ đơn vị bán thêm uoms[]: { uomId, factor } (vd thùng = 24 lon)
└─ biến thể variants[] (≥ 1): sku, barcode, price, compareAtPrice, currency,
options[] (một giá trị mỗi thuộc tính: Màu = Đỏ, Size = M),
unitPrices[] (giá theo MỌI đơn vị bán, đơn vị gốc trước)
thuộc tính attributes (Màu, Size…) + values (Đỏ, Xanh… có thể có màu swatch #RRGGBB)
- Sản phẩm không có lựa chọn = MỘT biến thể
options: []. Mọi biến thể của một sản phẩm dùng CÙNG bộ thuộc tính, mỗi tổ hợp một lần. status:draft(soạn) →active(bán) →archived(ngừng). Lênactivecần ít nhất một bản dịch VÀ một biến thể đang bật (409product_not_publishable).unitPrices[].source:base(giá của biến thể),explicit(đặt tay cho đơn vị đó),converted(=price×factor). Tiền là chuỗi.- Field dịch được (
name,shortDescription,description,slug,metaTitle,metaDescription): phần chung §3.3.
CMS / admin (XforgeClient) #
Menu: key product.products (quyền product.read).
| Việc | Method | Quyền |
|---|---|---|
| Danh sách | listProducts({ query }) — không kèm biến thể: variantCount, minPrice / maxPrice | product.read |
| Picker sản phẩm | listProductOptions({ query }) | product.read |
| Picker biến thể (SKU) | listProductVariantOptions({ query }) — cho form nhập kho, đơn… | product.read |
| Chi tiết | getProduct({ params: { id } }) — kèm ảnh, đơn vị, MỌI biến thể | product.read |
| Tạo (kèm biến thể) | createProduct({ body }) | product.create |
| Sửa sản phẩm | updateProduct({ params: { id }, body: { version, … } }) | product.update |
| Thêm / sửa / xoá biến thể | addProductVariant / updateProductVariant / deleteProductVariant (query: { version }) | product.update |
| Xoá (thùng rác) / khôi phục | deleteProduct / listProductTrash / restoreProduct | product.delete / product.restore |
| Xoá vĩnh viễn | deleteProduct({ params: { id }, query: { force: "true" } }) | + product.hard_delete |
| Thuộc tính + giá trị | listProductAttributes, createProductAttribute, …AttributeValue | product.read / product.manage_attributes |
- Khoá phiên bản: mọi lần sửa sản phẩm HOẶC biến thể gửi
versioncủa sản phẩm đã đọc. Người khác lưu trước → 409version_conflict(details.currentVersion): tải lại, cho người dùng so rồi lưu lại. Mỗi thao tác biến thể trả LẠI cả sản phẩm (cóversionmới) — dùng nó cho lần sửa kế. - Danh sách: lọc
status,featured,brand_id,category_id(in_category= danh mục + mọi danh mục con),base_uom_id;qtìm mã và mọi tên đã dịch. translations,imageFileIds,uomsgửi = thay cả bộ. Bỏ một đơn vị bán thêm = bỏ luôn giá đặt tay theo đơn vị đó của các biến thể.listProductVariantOptionsmặc định KHÔNG lọc trạng thái — chỉ lấy hàng bán được:query: { status: "active", active: "true" }.optionsLabel(vdĐỏ / M) để hiện trong picker.- Ảnh: tải qua module
filesrồi gửiimageFileIds(thứ tự = thứ tự hiện). - Xoá sản phẩm / biến thể có thể bị module khác chặn khi còn tồn kho / đơn mở
(409 mã của module đó — hiện
error.message).
Xuất / nhập file (dataset product.products) #
Màn hình chung của Core (docs/fe/cms.md §7, listDatasets, exportDataset,
importDataset…). Xuất cần product.read. Nhập cần product.update; dòng tạo
SẢN PHẨM mới cần thêm product.create (không có → forbidden ở đúng dòng).
Mỗi dòng = một biến thể, khoá là SKU; các dòng cùng productCode là một sản
phẩm. Nhập lại cùng file không nhân đôi: dòng không đổi tính skipped, sản phẩm
không tăng version.
| Cột | Mức | Ghi chú |
|---|---|---|
productCode, sku, price | biến thể | bắt buộc. Không chuyển SKU sang sản phẩm khác |
barcode, compareAtPrice, weightGrams, sortOrder, active | biến thể | |
currency | biến thể | vắng = tiền của store. Biến thể đã có: không đổi qua file |
options | biến thể | theo MÃ: color=red\|size=m; thuộc tính + giá trị phải có sẵn |
name, shortDescription, slug, metaTitle, metaDescription | sản phẩm | bản dịch của lang |
lang | sản phẩm | vắng = ngôn ngữ mặc định của store; phải là ngôn ngữ store hỗ trợ |
brandCode, categoryCode, baseUomCode | sản phẩm | theo MÃ, trong store |
status, featured, productSortOrder | sản phẩm | featured: true / false; status áp ở dòng cuối của sản phẩm |
- Sản phẩm mới cần
name+baseUomCode(product_import_missing_fields). - Cột sản phẩm: chỉ cần điền ở một dòng; đã điền nhiều dòng thì phải giống nhau.
- Ô trống = giữ nguyên (sản phẩm mới: giá trị mặc định). Vì vậy file KHÔNG xoá được giá trị (bỏ thương hiệu, xoá mã vạch…) và không xoá biến thể / sản phẩm: dùng form.
- File xuất ghi bản dịch của ngôn ngữ MẶC ĐỊNH; bản dịch ngôn ngữ khác giữ nguyên. Sửa tên không đổi slug đã có.
- Tiền là ô chữ, đúng định dạng tiền của loại tiền:
150000hoặc150000.0000được;150.000của tiền VND bị từ chối (dấu chấm ngăn nghìn gõ nhầm). Khi nhập số lẻ trong Excel, để ô dạng Text (Excel có thể lưu19.99thành19.989999999999998, dòng đó bị từ chối). - Không có trong file, sửa ở form: ảnh,
description(mô tả dài, quá sức một ô file), đơn vị bán thêm + giá theo đơn vị, nhóm thuế (moduletax). Nhập KHÔNG đụng các phần này. File có cột khác danh sách trên (store_id,imageFileIds…) bị từ chốiunknown_column. - SKU của sản phẩm trong thùng rác →
product_sku_exists: khôi phục sản phẩm trước. dryRun=truechỉ kiểm định dạng ô. Mã thương hiệu sai, SKU trùng, thiếu quyền tạo… chỉ lộ khi nhập thật; dòng lỗi đầu tiên huỷ cả file.
Lỗi nhập nằm trong errors[] (HTTP vẫn 200, applied: false): line, code,
message. Hiện message: câu đã dịch, đã có sẵn mã / cột cụ thể (item không có
details).
code | Nghĩa |
|---|---|
product_import_missing_fields | sản phẩm mới còn thiếu cột |
product_import_row_conflict | hai dòng của cùng sản phẩm ghi khác nhau ở một cột |
product_import_sku_duplicate | SKU lặp trong file |
product_import_sku_other_product | SKU đang thuộc sản phẩm khác |
product_import_currency_change | đổi tiền tệ của biến thể: làm ở form |
product_import_price_format | số tiền lẻ hơn đơn vị của loại tiền / dấu ngăn nghìn |
product_brand_invalid / product_category_invalid | mã không có trong store |
product_uom_invalid / product_variant_options_invalid | mã không có trong store |
unsupported_language | lang không thuộc ngôn ngữ của store |
forbidden | thiếu product.create cho dòng tạo sản phẩm mới |
luật khác của form (product_not_publishable, product_slug_exists…) | như bảng lỗi bên dưới |
Storefront (StorefrontClient) #
Không cần đăng nhập. Chỉ sản phẩm active, có bản dịch và ít nhất một biến thể
đang bật; trả kèm các biến thể đang bật.
listPublicProducts({ query })— trang danh mục / tìm kiếm (≤ 50 / trang; không có mô tả dài / SEO). Lọcfeatured,brand_id,category_id,in_category;qtìm; sắpsort_order/code/created_at.getPublicProductBySlug({ params: { slug } })— trang sản phẩm, códescription,metaTitle,metaDescriptioncho SEO. Slug của ngôn ngữ nào cũng khớp (ngôn ngữ request trước).- Chọn biến thể: dựng bộ chọn từ
variants[].options(attributeInfo→ nhóm,valueInfo→ lựa chọn, có swatch). Hiệnprice/compareAtPricecủa biến thể đã chọn; bán theo đơn vị khác (thùng…) dùngunitPrices. Giá cuối (khuyến mãi, mã giảm) chỉ có ở giỏ (modulecart). - Ảnh:
images[].fileInfo.id→<img src="/api/v1/storefront/products/images/{fileId}">trên tên miền của store (route công khai, chỉ ảnh raster của sản phẩm đang bán, cache 5 phút,ETag). Dev không có tên miền store (chỉx-store): tải bằnggetPublicProductImagerồi tạo object URL. brandInfo/categoryInfo= null khi thương hiệu / danh mục đang ẩn khỏi storefront — đừng dựng link tới chúng.
Lỗi cần xử lý #
code | Status | FE nên |
|---|---|---|
product_not_found | 404 | trang 404 (storefront: không còn bán / slug đổi) |
version_conflict | 409 | người khác vừa lưu: tải lại, so, lưu lại |
product_code_exists / product_sku_exists / product_barcode_exists | 409 | báo ở đúng ô (mã / SKU / mã vạch của biến thể) |
product_slug_exists | 409 | báo ở ô slug của đúng ngôn ngữ |
product_not_publishable | 409 | cần bản dịch + biến thể đang bật trước khi đặt active |
product_variant_options_invalid | 400 | mỗi thuộc tính chọn đúng một giá trị |
product_variant_attributes_mismatch | 409 | biến thể phải dùng cùng bộ thuộc tính với các biến thể khác |
product_variant_combination_exists | 409 | tổ hợp lựa chọn đã có |
product_last_variant | 409 | không xoá được biến thể duy nhất |
product_variant_price_uom_invalid | 400 | giá đặt tay chỉ cho đơn vị bán thêm của sản phẩm |
product_brand_invalid / product_category_invalid / product_uom_invalid | 400 | tham chiếu không thuộc store → tải lại picker |
product_extra_uoms_invalid | 400 | đơn vị bán thêm phải khác đơn vị gốc |
product_image_not_found | 404 | ảnh không còn → ảnh giữ chỗ |
product_attribute_code_exists / product_attribute_value_code_exists | 409 | báo ở ô mã |
resource_in_use | 409 | thuộc tính / giá trị còn được biến thể dùng |
file_not_attachable | 400 | ảnh không phải file của store này |