Sửa trang
Thời gian render trang: 02/08/2026 00:50:09.572
Thiết Kế Website Bán Hàng: Bí Quyết Tăng Tỷ Lệ Chuyển Đổi Hiệu Quả

Cách thiết kế cơ sở dữ liệu cho website bán hàng từ cơ bản đến nâng cao

5/5 - (0 Bình chọn )
8/1/2026 2:12:00 PM

Thiết kế cơ sở dữ liệu cho website bán hàng nên bắt đầu từ luồng giao dịch thực tế, không chỉ từ danh sách bảng cần có. Hệ thống cần mô hình hóa đầy đủ quá trình khách xem sản phẩm, thêm giỏ, chọn địa chỉ, tính phí vận chuyển, thanh toán, giao hàng và xử lý sau bán. Ở mức cơ bản, các nhóm dữ liệu quan trọng gồm sản phẩm, danh mục, khách hàng, địa chỉ, giỏ hàng, đơn hàng, chi tiết đơn và thanh toán; mỗi nhóm cần khóa chính, khóa ngoại, chỉ mục và ràng buộc rõ ràng để dữ liệu nhất quán.

Sơ đồ thiết kế cơ sở dữ liệu website bán hàng theo luồng giao dịch từ xem sản phẩm đến giao hàng

Khi số lượng SKU, đơn hàng hoặc kênh bán tăng lên, mô hình cần mở rộng theo hướng tách sản phẩm cha và biến thể, quản lý tồn kho theo từng kho, lưu lịch sử nhập – xuất, áp dụng khuyến mãi, vận chuyển, hoàn tiền, đánh giá và báo cáo. Giá, tên sản phẩm, SKU, thuộc tính, thuế, phí ship và mức giảm giá tại thời điểm mua phải được lưu thành snapshot trong đơn hàng, tránh sai lệch khi dữ liệu sản phẩm gốc thay đổi về sau.

Phần tồn kho cần xử lý bằng transaction, cập nhật có điều kiện và cơ chế giữ hàng có thời hạn để hạn chế bán vượt số lượng. Thanh toán và vận chuyển nên được tách thành các bảng riêng, có lịch sử trạng thái, mã giao dịch hoặc mã vận đơn để dễ đối soát. Khi dữ liệu lớn, có thể kết hợp chuẩn hóa ở tầng giao dịch với bảng tổng hợp phục vụ báo cáo nhanh. Cách tổ chức này giúp website ổn định, dễ tích hợp CRM, ERP, cổng thanh toán và mở rộng đa kho hoặc đa kênh bán hàng.

Cơ sở dữ liệu website bán hàng cần bắt đầu từ sản phẩm, khách hàng, đơn hàng và luồng giao dịch

Cơ sở dữ liệu cho website bán hàng nên được thiết kế xoay quanh sản phẩm, khách hàng, đơn hàng và luồng giao dịch, bảo đảm phản ánh đầy đủ hành trình từ lúc xem sản phẩm đến sau bán. Ở tầng hành vi, mỗi giai đoạn như khám phá, thêm giỏ, checkout, thanh toán và hậu kỳ cần được ánh xạ thành các bảng, quan hệ và event rõ ràng, cho phép replay toàn bộ lịch sử một đơn hàng để phục vụ vận hành, phân tích và kiểm soát. Dữ liệu sản phẩm nên tách riêng thông tin cơ bản, danh mục, thương hiệu, thuộc tính, biến thể, hình ảnh, giá bán và tồn kho. Cách tổ chức này giúp quá trình thiết kế website hỗ trợ hiệu quả sản phẩm nhiều màu sắc, kích thước, mức giá hoặc trạng thái kinh doanh khác nhau.

Sơ đồ quy trình mua sắm online từ khám phá sản phẩm đến thanh toán và giao hàng cho khách hàng

Mô hình dữ liệu cơ bản phải có nhóm sản phẩm, người dùng, giỏ hàng, đơn hàng và thanh toán với khóa ngoại, chỉ mục, ràng buộc toàn vẹn chặt chẽ. Khi mở rộng, cần bổ sung biến thể, tồn kho, khuyến mãi, vận chuyển, đánh giá và báo cáo, cân bằng giữa chuẩn hóa và hiệu năng để tránh sai tồn kho, mất đơn, khó tích hợp.

Database ecommerce phải phản ánh đúng hành trình xem sản phẩm, thêm giỏ, đặt hàng và thanh toán

Cơ sở dữ liệu cho website bán hàng phải được thiết kế như một mô hình hóa chi tiết của hành trình giao dịch end-to-end, chứ không chỉ là tập hợp các bảng lưu dữ liệu tĩnh. Mỗi bước trong hành trình người dùng – từ lúc impression đầu tiên đến khi hoàn tất thanh toán và hậu kỳ sau bán – cần được ánh xạ rõ ràng thành các thực thể (entities), quan hệ (relationships) và các sự kiện (events) trong database. Nền tảng của cách tiếp cận này được hình thành từ mô hình thực thể – liên kết, trong đó cơ sở dữ liệu phải thể hiện được ý nghĩa của đối tượng, thuộc tính và mối quan hệ trong thế giới nghiệp vụ trước khi chuyển thành bảng vật lý. Theo Chen, mô hình khái niệm giúp thống nhất cách nhìn dữ liệu giữa người phân tích nghiệp vụ, người thiết kế hệ thống và lập trình viên. Vì vậy, Product, Cart, Order, Payment hay Shipment không nên được tạo theo từng màn hình giao diện, mà phải phản ánh những thực thể có danh tính và vòng đời riêng. Cách mô hình hóa này giảm nguy cơ bỏ sót quan hệ, trạng thái và ràng buộc quan trọng trong toàn bộ hành trình giao dịch (Chen, 1976).

Sơ đồ database ecommerce mô tả hành trình khách hàng từ xem sản phẩm, thêm giỏ hàng, đặt hàng đến thanh toán

Ở tầng hành vi, có thể chia hành trình thành các giai đoạn chính: khám phá (discovery), cân nhắc (consideration), ý định mua (intent), đặt hàng (checkout), thanh toán (payment) và hậu kỳ (post-purchase). Mỗi giai đoạn tương ứng với các nhóm bảng và luồng dữ liệu:

  • Khám phá & xem sản phẩm: người dùng truy cập trang danh mục, trang tìm kiếm, trang chi tiết sản phẩm. Database cần:
    • Bảng products, categories, productimages, productattributes để mô tả đầy đủ thông tin sản phẩm.
    • Bảng hoặc cơ chế log sự kiện như trackingevents (pageview, productview, search, filterapply) để phân tích hành vi và tối ưu funnel.
  • Thêm vào giỏ & cập nhật giỏ: khi người dùng thêm sản phẩm, thay đổi số lượng, xóa item, database cần:
    • carts để đại diện cho giỏ hàng gắn với user hoặc session ẩn danh.
    • cartitems để lưu từng dòng sản phẩm, số lượng, giá tại thời điểm thêm, các thuộc tính biến thể.
    • Các trường audit như createdat, updatedat, source (web, app, campaign) để phân tích hành vi.
  • Nhập thông tin giao hàng & chọn phương thức: giai đoạn checkout cần:
    • addresses để lưu địa chỉ giao hàng, địa chỉ thanh toán, có thể tách customeraddressesorderaddresses nếu cần snapshot.
    • shipments hoặc shippingquotes để lưu lựa chọn phương thức vận chuyển, phí ship, thời gian dự kiến.
    • payments hoặc paymentintents để lưu lựa chọn phương thức thanh toán, trạng thái chuẩn bị thanh toán.
  • Xác nhận đơn & tạo order: khi người dùng xác nhận, hệ thống cần chuyển trạng thái từ giỏ sang đơn:
    • orders lưu thông tin tổng quan: mã đơn, khách hàng, tổng tiền, trạng thái, kênh bán, mã chiến dịch.
    • orderitems lưu chi tiết từng dòng sản phẩm, đơn giá, thuế, giảm giá, snapshot thuộc tính tại thời điểm mua.
    • Các trường liên kết đến shipments, payments, coupons để tái hiện đầy đủ luồng giao dịch.
  • Thanh toán & hậu kỳ: sau khi tạo order, luồng thanh toán và giao hàng tiếp tục:
    • payments lưu trạng thái (pending, authorized, captured, failed, refunded), mã giao dịch từ cổng thanh toán, log callback.
    • shipments lưu trạng thái giao hàng (readytoship, shipped, delivered, failed), mã vận đơn, đơn vị vận chuyển.
    • Các bảng hậu kỳ như invoices, refunds, returns để quản lý hóa đơn, hoàn tiền, trả hàng.

Một thiết kế tốt phải cho phép replay lại toàn bộ luồng giao dịch của một đơn hàng: từ sự kiện xem sản phẩm, thêm giỏ, áp mã giảm giá, tính phí ship, đến trạng thái thanh toán và giao hàng cuối cùng. Điều này phục vụ đồng thời cho:

  • Vận hành: xử lý khiếu nại, tra soát đơn, đối soát với cổng thanh toán và đơn vị vận chuyển, kiểm tra sai lệch tồn kho.
  • Phân tích: đo lường tỷ lệ chuyển đổi theo từng bước (view → addtocart → checkout → paid), phân tích nguyên nhân rơi rụng, đánh giá hiệu quả chiến dịch marketing.
  • Kiểm soát & audit: truy vết thay đổi, phát hiện gian lận, kiểm tra thao tác nội bộ.

Hành trình chi tiết thường bao gồm các bước tuần tự sau, mỗi bước nên được ghi nhận bằng event hoặc trạng thái rõ ràng trong database:

  • Người dùng truy cập trang danh mục (category listing), áp filter, sort, pagination.
  • Người dùng tìm kiếm (search query, search filters) – nên log để tối ưu search relevance.
  • Xem chi tiết sản phẩm (productdetailview), có thể kèm theo tracking nguồn traffic, campaign, referrer.
  • Thêm sản phẩm vào giỏ (addtocart), cập nhật số lượng (updatecartitem), xóa khỏi giỏ (removefromcart).
  • Nhập thông tin giao hàng, chọn địa chỉ có sẵn hoặc tạo địa chỉ mới (addresscreate, addressselect).
  • Chọn phương thức vận chuyển (shippingmethodselect), tính phí ship, thời gian giao dự kiến.
  • Chọn phương thức thanh toán (paymentmethodselect), khởi tạo giao dịch với cổng thanh toán.
  • Xác nhận đơn hàng (placeorder), tạo bản ghi ordersorderitems trong transaction an toàn.
  • Thanh toán thành công hoặc thất bại (paymentsuccess, paymentfailed), cập nhật trạng thái order tương ứng.

Database vì vậy phải xoay quanh luồng nghiệp vụ, nghĩa là mỗi bảng, mỗi quan hệ đều có lý do tồn tại gắn với một bước cụ thể trong quy trình kinh doanh. Việc chỉ nhìn database như cấu trúc dữ liệu tĩnh (product, user, order) mà không gắn với luồng nghiệp vụ thường dẫn đến thiếu các trường quan trọng để phân tích funnel, không log đủ trạng thái trung gian, khó debug khi có sự cố.

Mô hình dữ liệu cơ bản cần đủ sản phẩm, danh mục, người dùng, giỏ hàng, đơn hàng và thanh toán

Ở mức cơ bản, một website bán hàng cần tối thiểu các nhóm bảng sau để vận hành trơn tru, đảm bảo tính toàn vẹn dữ liệu và khả năng mở rộng ban đầu. Mỗi nhóm nên được thiết kế với khóa chính, khóa ngoại, chỉ mục và các ràng buộc toàn vẹn (constraints) rõ ràng. Mô hình quan hệ của Codd xác lập nguyên tắc tổ chức dữ liệu thành các quan hệ có cấu trúc, trong đó mỗi hàng biểu diễn một bộ dữ liệu và các liên kết được xác định bằng giá trị khóa thay vì phụ thuộc vào vị trí lưu trữ vật lý. Với website bán hàng, nguyên tắc này cho phép Customer, Product, Order và Payment phát triển độc lập nhưng vẫn liên kết chính xác qua khóa ngoại. Việc tách đúng thực thể giúp hạn chế bất thường khi thêm, sửa hoặc xóa dữ liệu, chẳng hạn không phải lặp lại toàn bộ thông tin khách hàng trong từng dòng sản phẩm của đơn. Đây cũng là nền tảng để ứng dụng thay đổi cấu trúc lưu trữ mà không phá vỡ ý nghĩa nghiệp vụ của dữ liệu (Codd, 1970).

Sơ đồ mô hình dữ liệu cơ bản cho website bán hàng với nhóm sản phẩm, người dùng, giỏ hàng, đơn hàng và thanh toán

  • Nhóm sản phẩm: chịu trách nhiệm mô tả catalog bán hàng.
    • products: lưu thông tin cốt lõi như tên, mô tả, SKU, giá niêm yết, trạng thái hiển thị, brand, categoryid.
    • categories: cấu trúc phân cấp danh mục (parentid), slug SEO, thứ tự hiển thị.
    • productimages: nhiều ảnh cho một sản phẩm, phân loại (thumbnail, gallery), thứ tự sắp xếp.
    • productattributes: thuộc tính động như chất liệu, xuất xứ, thông số kỹ thuật; có thể chuẩn hóa thành bảng attributesattributevalues.

    Thiết kế nhóm sản phẩm cần chú ý đến việc snapshot giá và thuộc tính tại thời điểm mua, vì sản phẩm có thể thay đổi theo thời gian. Do đó, orderitems thường lưu lại tên sản phẩm, SKU, giá bán, thay vì chỉ tham chiếu productid.

  • Nhóm người dùng: quản lý khách hàng và thông tin nhận hàng.
    • users/customers: thông tin tài khoản, email, số điện thoại, trạng thái, phân loại khách hàng (B2C, B2B), metadata.
    • addresses: địa chỉ giao hàng và thanh toán, tách rõ các trường tỉnh/thành, quận/huyện, phường/xã, mã bưu chính, để dễ tích hợp với hệ thống vận chuyển.

    Nên phân biệt giữa địa chỉ gắn với tài khoản (có thể chỉnh sửa) và địa chỉ snapshot gắn với đơn hàng (không thay đổi sau khi đặt), tránh làm sai lịch sử đơn.

  • Nhóm giỏ hàng: lưu trạng thái trước khi tạo đơn, hỗ trợ cả user đăng nhập và khách vãng lai.
    • carts: gắn với userid hoặc sessionid, lưu currency, tổng tạm tính, trạng thái (active, converted, abandoned).
    • cartitems: mỗi dòng sản phẩm trong giỏ, tham chiếu product hoặc productvariant, số lượng, giá tại thời điểm thêm, thông tin khuyến mãi áp dụng.

    Giỏ hàng là nơi diễn ra nhiều thao tác cập nhật, vì vậy cần index hợp lý (theo userid, sessionid) và có chiến lược dọn dẹp (cleanup) giỏ cũ để tránh phình dữ liệu.

  • Nhóm đơn hàng: phản ánh giao dịch đã được khách xác nhận.
    • orders: mã đơn (ordernumber), customerid, tổng tiền hàng, tổng giảm giá, phí ship, thuế, trạng thái (new, confirmed, shipped, completed, cancelled), kênh bán.
    • orderitems: chi tiết từng dòng, số lượng, đơn giá, tổng dòng, thuế, giảm giá theo dòng, tham chiếu đến SKU hoặc variantid.

    Nhóm đơn hàng cần hỗ trợ nhiều trạng thái và luồng xử lý (workflow), vì vậy nên có trường status, substatus, statushistory (có thể là bảng riêng) để audit.

  • Nhóm thanh toán: quản lý trạng thái thanh toán và tương tác với cổng thanh toán.
    • payments: tham chiếu orderid, amount, currency, paymentmethod, trạng thái (initiated, authorized, captured, failed, refunded), transactionid từ cổng.

    Thiết kế nhóm thanh toán cần chú ý đến tính idempotent khi nhận callback từ cổng, lưu đầy đủ raw payload để phục vụ tra soát và đối soát.

Mô hình cơ bản này cho phép xử lý đầy đủ vòng đời của một đơn hàng từ lúc khách chọn sản phẩm đến khi thanh toán xong. Việc tách bạch các bảng theo đúng vai trò giúp:

  • Dễ mở rộng khi thêm tính năng như khuyến mãi, loyalty, nhiều kho, nhiều kênh bán.
  • Giảm rủi ro phải refactor lớn khi hệ thống đã có nhiều dữ liệu.
  • Tối ưu phân quyền truy cập (ví dụ: bộ phận CS chỉ cần xem orders, shipments; bộ phận kế toán cần payments, invoices).

Mô hình nâng cao cần hỗ trợ biến thể, tồn kho, khuyến mãi, vận chuyển, đánh giá và báo cáo

Khi website phát triển, số lượng SKU, đơn hàng và nghiệp vụ tăng lên, mô hình dữ liệu cần được mở rộng có kiểm soát, đảm bảo vừa chuẩn hóa hợp lý vừa đáp ứng hiệu năng truy vấn. Các nhóm bảng nâng cao thường được bổ sung để tách biệt các mảng nghiệp vụ chuyên sâu.

Mô hình dữ liệu thương mại điện tử nâng cao với biến thể, tồn kho, khuyến mãi, vận chuyển, đánh giá và báo cáo

  • Biến thể sản phẩm: phục vụ các sản phẩm có nhiều lựa chọn như màu, size, dung lượng.
    • productvariants: mỗi bản ghi là một SKU cụ thể (ví dụ: áo thun, màu đỏ, size M), có giá, mã vạch, trạng thái riêng.
    • variantattributes: ánh xạ variant với các thuộc tính (color, size, material), giúp hiển thị và lọc chính xác.

    Việc tách productproductvariant giúp quản lý tồn kho, giá, khuyến mãi theo từng SKU chi tiết, thay vì chỉ ở cấp sản phẩm chung.

  • Tồn kho: đảm bảo số lượng chính xác theo từng kho, từng SKU.
    • inventoryitems: số lượng tồn hiện tại cho mỗi variant tại mỗi warehouse, trạng thái (available, reserved, damaged).
    • warehouses: thông tin kho, địa chỉ, loại kho (fulfillment center, store, drop-shipper).
    • inventorytransactions: log mọi biến động tồn kho (import, export, adjustment, reservation, release), kèm reference đến order hoặc shipment.

    Mô hình tồn kho tốt phải hỗ trợ transaction khi trừ kho, cơ chế reservation khi đặt hàng, tránh oversell và âm kho, đồng thời phục vụ đối soát với hệ thống ERP.

  • Khuyến mãi: linh hoạt trong việc áp dụng và ghi nhận giảm giá.
    • coupons: mã giảm giá, loại giảm (theo %, theo số tiền), điều kiện áp dụng, giới hạn sử dụng.
    • promotionrules: các rule phức tạp như mua X tặng Y, giảm giá theo category, theo kênh, theo nhóm khách hàng.
    • orderdiscounts: ghi nhận chi tiết giảm giá áp dụng cho từng order hoặc từng orderitem, phục vụ báo cáo và đối soát.

    Khuyến mãi thường là nguồn gây phức tạp lớn trong logic tính tiền, vì vậy cần lưu rõ cách tínhkết quả tính ở cấp order và orderitem để có thể giải thích cho khách và kiểm tra sai lệch.

  • Vận chuyển: quản lý luồng giao hàng và phí ship.
    • shipments: mỗi bản ghi đại diện cho một lần giao hàng (có thể một order có nhiều shipment), lưu carrier, trackingnumber, trạng thái, ngày gửi, ngày giao.
    • shippingmethods: cấu hình các phương thức vận chuyển (standard, express, same-day), bảng giá, vùng phục vụ.

    Việc tách shipments khỏi orders cho phép xử lý các trường hợp chia đơn, giao nhiều lần, đổi địa chỉ giao sau khi đặt, và tích hợp với nhiều đơn vị vận chuyển.

  • Đánh giá: thu thập và quản lý feedback khách hàng.
    • productreviews: rating, nội dung review, userid, productid, trạng thái duyệt (pending, approved, rejected).
    • reviewphotos: ảnh đính kèm review, giúp tăng độ tin cậy và nội dung UGC.

    Đánh giá nên liên kết với order hoặc orderitem để xác minh chỉ khách đã mua mới được đánh giá (verified purchase), đồng thời hỗ trợ cơ chế moderation.

  • Báo cáo: tối ưu truy vấn phân tích khi dữ liệu lớn.
    • dailysalessummary: tổng hợp doanh thu, số đơn, số khách, theo ngày, theo kênh, theo phương thức thanh toán.
    • productperformance: số lượng bán theo sản phẩm/SKU, tỷ lệ hoàn trả, biên lợi nhuận ước tính.

    Các bảng tổng hợp này thường được cập nhật bằng batch jobs hoặc ETL, giúp giảm tải cho các bảng giao dịch khi chạy báo cáo nặng, đồng thời là nền tảng cho hệ thống BI.

Bảng báo cáo cần có quy tắc xác định chỉ số rõ ràng và khả năng truy ngược về dữ liệu giao dịch. Doanh thu không nên chỉ được tính từ mọi Order đã tạo, mà phải quy định trạng thái hợp lệ, cách xử lý hoàn tiền, đơn hủy, thuế, phí giao hàng và múi giờ chốt ngày. Dữ liệu tổng hợp có thể được xây dựng theo batch, dòng sự kiện hoặc materialized view, nhưng phải lưu thời điểm cập nhật và phiên bản logic tính toán. Bảng summary không được trở thành nguồn duy nhất không thể kiểm chứng; mỗi con số cần đối chiếu được với Order, OrderItem, Payment và Refund để phát hiện sai lệch khi quy tắc nghiệp vụ thay đổi hoặc tiến trình ETL bị gián đoạn.

Mô hình nâng cao cần cân bằng giữa chuẩn hóa và hiệu năng. Chuẩn hóa giúp tránh trùng lặp dữ liệu, dễ bảo trì, nhưng có thể làm truy vấn phức tạp hơn. Trong thực tế, thường kết hợp chuẩn hóa ở tầng giao dịch với một số bảng denormalized hoặc materialized view cho mục đích đọc nhanh (read-optimized), đặc biệt trong các hệ thống có lưu lượng truy cập lớn. Dữ liệu giao dịch và dữ liệu phân tích có đặc điểm truy cập khác nhau nên không nhất thiết phải dùng cùng một cấu trúc vật lý. Các bảng Order, Payment và Inventory cần ưu tiên cập nhật chính xác từng bản ghi, trong khi báo cáo doanh thu thường quét nhiều dòng nhưng chỉ đọc một số cột như ngày, kênh, doanh thu và lợi nhuận. Nghiên cứu về hệ quản trị hướng cột cho thấy cách lưu theo cột phù hợp với truy vấn phân tích nhờ giảm lượng dữ liệu phải đọc và hỗ trợ nén hiệu quả. Do đó, bảng tổng hợp hoặc kho dữ liệu báo cáo nên được tách khỏi nguồn giao dịch, thay vì denormalize tùy tiện ngay trong các bảng lõi đang phục vụ đặt hàng (Stonebraker et al., 2005).

Thiết kế sai database từ đầu dễ gây lỗi mở rộng, sai tồn kho, mất đơn và khó tích hợp hệ thống

Thiết kế cơ sở dữ liệu sai ngay từ đầu thường không bộc lộ vấn đề khi hệ thống còn nhỏ, nhưng sẽ gây hậu quả nghiêm trọng khi lưu lượng và dữ liệu tăng. Các lỗi thiết kế phổ biến thường liên quan đến việc không hiểu rõ luồng nghiệp vụ, không tách bạch trách nhiệm của từng bảng, và bỏ qua các ràng buộc toàn vẹn.

Thiết kế sai cơ sở dữ liệu gây lỗi mở rộng, sai tồn kho, khó tích hợp và mất đơn hàng trong hệ thống quản lý

  • Sai tồn kho:
    • Không tách bảng tồn kho riêng, lưu số lượng tồn trực tiếp trong bảng products hoặc productvariants, dẫn đến khó quản lý nhiều kho, nhiều trạng thái tồn.
    • Không sử dụng transaction hoặc cơ chế lock phù hợp khi trừ kho, dẫn đến race condition khi nhiều đơn cùng đặt một SKU.
    • Không log inventory_transactions, khiến việc điều tra sai lệch tồn kho gần như không thể.
  • Mất đơn hoặc trùng đơn:
    • Xử lý tạo đơn và thanh toán không nguyên tử, không có cơ chế idempotent key cho mỗi request thanh toán, dẫn đến tạo nhiều đơn cho cùng một giao dịch.
    • Không lưu đầy đủ trạng thái trung gian (pending, authorized, captured), chỉ lưu “đã thanh toán” hoặc “chưa thanh toán”, khiến khó xử lý các case ngoại lệ.
    • Không log đầy đủ callback từ cổng thanh toán, không lưu raw response, gây khó khăn khi đối soát và xử lý tranh chấp.
  • Khó mở rộng:
    • Gộp quá nhiều thông tin vào một bảng “tất cả trong một” (ví dụ: order chứa cả địa chỉ, item, thanh toán, shipment), làm bảng phình to, khó index, khó thay đổi.
    • Không tách biến thể sản phẩm, lưu size, màu trong một chuỗi text, khiến việc lọc, tìm kiếm, báo cáo theo thuộc tính gần như bất khả thi.
    • Không chuẩn hóa thuộc tính sản phẩm, dẫn đến trùng lặp dữ liệu, khó đồng bộ với các kênh bán khác (marketplace, POS).
  • Khó tích hợp:
    • Không có khóa ngoại rõ ràng, không định nghĩa quan hệ một cách tường minh, khiến việc xây dựng API hoặc ETL sang hệ thống khác phức tạp và dễ sai.
    • Không có các trường mã chuẩn như SKU, mã kho, mã khách hàng, dẫn đến mỗi hệ thống dùng một kiểu mã khác nhau, khó mapping.
    • Không lưu timezone, currency, hoặc không chuẩn hóa format dữ liệu (ví dụ: số điện thoại, địa chỉ), gây lỗi khi tích hợp đa quốc gia.

Để tránh các vấn đề này, cần xuất phát từ mô hình nghiệp vụ đã được phân tích kỹ: xác định rõ các thực thể, luồng trạng thái, các sự kiện chính, rồi mới ánh xạ sang mô hình dữ liệu. Áp dụng các nguyên tắc chuẩn hóa (tối thiểu đến 3NF cho dữ liệu giao dịch), ràng buộc toàn vẹn (foreign key, unique, check), và thiết kế với tư duy audit-first (luôn có khả năng truy vết) và integration-ready (dễ dàng kết nối với CRM, ERP, BI, hệ thống vận chuyển, cổng thanh toán).

Thực thể dữ liệu cốt lõi trong website bán hàng cơ bản

Hệ thống bán hàng cơ bản xoay quanh một số thực thể dữ liệu cốt lõi, mỗi thực thể đảm nhận một vai trò rõ ràng trong luồng nghiệp vụ. Products lưu trữ thông tin sản phẩm, giá và trường SEO, là nguồn dữ liệu trung tâm cho hiển thị và tìm kiếm. Categories tổ chức sản phẩm theo cây danh mục, hỗ trợ điều hướng, lọc và tối ưu SEO. Users/Customers quản lý tài khoản, thông tin nhận diện và các chỉ số hành vi mua sắm. Ordersorder_items ghi nhận giao dịch, giá và thuộc tính sản phẩm tại thời điểm mua, đảm bảo tính bất biến và khả năng kiểm toán. Các bảng này liên kết với nhau qua khóa ngoại, tạo nên mô hình dữ liệu nhất quán, dễ mở rộng cho khuyến mãi, đa ngôn ngữ và đa kênh bán.

Infographic thực thể dữ liệu cốt lõi cho website bán hàng gồm sản phẩm, danh mục, khách hàng, đơn hàng

Bảng products lưu tên sản phẩm, slug, mô tả, giá, trạng thái và thông tin SEO

Bảng products là trung tâm của hệ thống bán hàng, đóng vai trò như “single source of truth” cho mọi thông tin liên quan đến sản phẩm. Khi thiết kế, cần chú ý đến khả năng mở rộng (thêm thuộc tính mới, hỗ trợ nhiều kênh bán), hiệu năng truy vấn (listing, search, filter) và tính toàn vẹn dữ liệu (ràng buộc, khóa, chỉ mục).

Tên trường Kiểu dữ liệu Mô tả
id BIGINT / UUID Khóa chính sản phẩm
name VARCHAR Tên sản phẩm hiển thị
slug VARCHAR (unique) Đường dẫn SEO thân thiện
sku VARCHAR (unique) Mã sản phẩm chuẩn, dùng cho tồn kho và tích hợp
description TEXT Mô tả chi tiết sản phẩm
shortdescription TEXT Mô tả ngắn cho listing
price DECIMAL Giá niêm yết hiện tại
status ENUM / SMALLINT Trạng thái: active, draft, archived, outofstock
seotitle VARCHAR Tiêu đề SEO
seometadescription VARCHAR Meta description cho SEO
canonicalurl VARCHAR URL canonical nếu cần
createdat / updatedat TIMESTAMP Thời gian tạo và cập nhật

Một số điểm thiết kế chuyên sâu cho bảng products:

  • id (BIGINT / UUID): – BIGINT auto-increment phù hợp với hệ thống đơn node, dễ debug, index nhỏ, query nhanh. – UUID phù hợp hệ thống phân tán, microservices, đồng bộ dữ liệu giữa nhiều hệ thống; nên dùng dạng UUID v4/v7 và có thể lưu dạng BINARY(16) để tối ưu index.
  • name, slug: – name nên được index dạng full-text (tùy RDBMS) để hỗ trợ search. – slug unique, có thể thêm index composite (slug, status) để tối ưu truy vấn chỉ lấy sản phẩm active. – Cần cơ chế xử lý trùng slug (thêm hậu tố -1, -2) và không thay đổi slug tùy tiện để tránh lỗi SEO.
  • sku: – Là khóa nghiệp vụ quan trọng, thường dùng trong tích hợp ERP, WMS, POS. – Nên unique toàn hệ thống; nếu có biến thể (size, color) thì sku có thể chuyển xuống bảng productvariants (trong thiết kế mở rộng), còn bảng products giữ sku “cha”.
  • description, shortdescription: – description có thể lưu HTML đã được sanitize. – shortdescription dùng cho listing, email, feed; nên giới hạn độ dài (ví dụ 255–500 ký tự) ở tầng ứng dụng.
  • price (DECIMAL): – Nên dùng DECIMAL(10,2) hoặc lớn hơn tùy domain; tránh dùng FLOAT/DOUBLE để không bị sai số tiền tệ. – price là giá niêm yết; giá khuyến mãi, giá theo nhóm khách hàng, theo kênh bán nên tách sang bảng khác (ví dụ productprices) để tránh làm bảng products phình to và logic giá trở nên phức tạp.
  • status: – Dùng ENUM hoặc SMALLINT map sang các trạng thái: 0 = draft, 1 = active, 2 = archived, 3 = outofstock. – outofstock nên được xem là trạng thái hiển thị (vẫn có thể xem sản phẩm) chứ không xóa khỏi hệ thống, giúp giữ lịch sử SEO và analytics.
  • Trường SEO (seotitle, seometadescription, canonicalurl): – Đặt trực tiếp trong bảng products giúp render trang chi tiết chỉ cần 1 query, giảm join. – Có thể thêm logic fallback: nếu seotitle rỗng thì dùng name; nếu seometadescription rỗng thì sinh tự động từ shortdescription.
  • createdat, updatedat: – Nên có index trên createdat để phục vụ sort theo “sản phẩm mới nhất”. – updatedat hỗ trợ cache invalidation, đồng bộ sang search engine (Elasticsearch, OpenSearch) hoặc hệ thống recommendation.

Trong hệ thống đa ngôn ngữ, nên tách bảng producttranslations chứa các trường phụ thuộc ngôn ngữ như name, slug, description, shortdescription, seotitle, seometadescription, trong khi bảng products chỉ giữ các trường ngôn ngữ độc lập như sku, price, status. Cách này giúp:

  • Giảm trùng lặp dữ liệu khi có nhiều locale.
  • Cho phép thêm ngôn ngữ mới mà không ảnh hưởng logic cốt lõi.
  • Tối ưu index cho từng ngôn ngữ (full-text theo locale).

Bảng categories quản lý danh mục cha con và cấu trúc điều hướng sản phẩm

Bảng categories dùng để tổ chức sản phẩm theo cấu trúc phân cấp, hỗ trợ điều hướng, SEO, phân quyền merchandising và các tính năng lọc theo nhóm. Thiết kế cần đảm bảo:

  • Hỗ trợ cây danh mục nhiều cấp (2–5 cấp hoặc hơn).
  • Truy vấn nhanh breadcrumb, danh mục con, danh mục cha.
  • Ổn định URL khi cấu trúc danh mục thay đổi.
Tên trường Kiểu dữ liệu Mô tả
id BIGINT / UUID Khóa chính danh mục
name VARCHAR Tên danh mục
slug VARCHAR (unique) Đường dẫn SEO của danh mục
parentid BIGINT (nullable) Tham chiếu danh mục cha, hỗ trợ cây danh mục
position INT Thứ tự hiển thị trong menu
seotitle VARCHAR Tiêu đề SEO danh mục
seometadescription VARCHAR Meta description danh mục
isactive BOOLEAN Danh mục có hiển thị hay không

Một số lưu ý chuyên sâu cho categories:

  • parentid: – Mô hình adjacency list (parentid) đơn giản, dễ hiểu, phù hợp phần lớn website bán hàng. – Để tối ưu truy vấn cây sâu (lấy toàn bộ subtree), có thể bổ sung thêm cấu trúc hỗ trợ như bảng categoryclosure hoặc trường path (lưu chuỗi id cha) ở tầng ứng dụng, nhưng không thay đổi số lượng heading.
  • slug: – Unique toàn bảng giúp URL dạng /category/slug. – Nếu muốn URL phản ánh cấu trúc cây (ví dụ /electronics/phones), có thể build ở tầng ứng dụng bằng cách kết hợp slug của cha và con, trong khi DB vẫn chỉ lưu slug đơn.
  • position: – Dùng để sắp xếp danh mục trong menu, sidebar. – Có thể kết hợp với createdat để sort ổn định khi position trùng.
  • isactive: – Cho phép ẩn danh mục khỏi frontend mà không xóa dữ liệu. – Khi isactive = false, cần quyết định rõ: sản phẩm trong danh mục đó có còn hiển thị qua các danh mục khác không; logic này nên xử lý ở tầng ứng dụng.

Quan hệ giữa sản phẩm và danh mục là nhiều-nhiều, nên sử dụng bảng trung gian productcategories với các trường productid, categoryid, isprimary. Thiết kế này cho phép:

  • Một sản phẩm thuộc nhiều danh mục (ví dụ “Điện thoại” và “Flash sale”).
  • Xác định danh mục chính (isprimary = true) để dùng cho breadcrumb, URL canonical và báo cáo.
  • Tối ưu filter theo danh mục bằng index composite (categoryid, productid).

Bảng users hoặc customers lưu tài khoản, thông tin liên hệ và lịch sử mua hàng

Bảng users/customers lưu thông tin nhận diện khách hàng và tài khoản đăng nhập. Tùy kiến trúc:

  • Hệ thống đơn giản: gộp chung thành một bảng users hoặc customers, mỗi bản ghi vừa là tài khoản, vừa là khách hàng.
  • Hệ thống phức tạp: tách users (tài khoản hệ thống, có thể là admin, staff, customer) và customers (thực thể khách hàng gắn với đơn hàng, CRM). Bảng customers tham chiếu đến users qua userid.
Thông tin xác thực phải được tách khỏi dữ liệu hồ sơ và không được lưu mật khẩu nguyên văn trong bất kỳ trường hợp nào. Argon2 được phát triển như một hàm băm mật khẩu có độ khó về bộ nhớ, làm tăng chi phí của việc dò mật khẩu hàng loạt bằng phần cứng chuyên dụng. Mỗi mật khẩu cần có salt ngẫu nhiên riêng, đồng thời hệ thống nên lưu tham số thời gian, bộ nhớ, mức song song và phiên bản thuật toán cùng kết quả băm. Trường passwordhash không nên được dùng chung với token đặt lại mật khẩu, mã OTP hoặc dữ liệu phiên đăng nhập. Việc lưu version còn giúp nâng cấp cấu hình băm dần khi người dùng đăng nhập thành công (Biryukov et al., 2016). 
Mô hình hồ sơ khách hàng với bốn phần tài khoản đăng nhập, thông tin cá nhân, dữ liệu hệ thống, tổng hợp mua hàng

Các nhóm trường quan trọng:

  • Thông tin đăng nhập: – email (unique): dùng làm username chính, nên index unique để login nhanh. – passwordhash: lưu hash mật khẩu với thuật toán mạnh (bcrypt, Argon2), không lưu plaintext. – phone (có thể unique): dùng cho OTP, xác thực hai lớp, hỗ trợ login bằng số điện thoại. – status: active, blocked, pendingverification; dùng để khóa tài khoản khi có dấu hiệu gian lận.
  • Thông tin cá nhân: – fullname, gender, dateofbirth phục vụ cá nhân hóa, marketing, phân khúc khách hàng. – Có thể chuẩn hóa thêm bảng addresses riêng cho địa chỉ giao hàng, thanh toán, nhưng không bắt buộc ở mức cơ bản.
  • Thông tin hệ thống: – lastloginat: hỗ trợ phân tích khách hàng ngủ quên, chiến dịch re-engagement. – createdat, updatedat: phục vụ audit, báo cáo tăng trưởng user. – isverified: đánh dấu đã xác thực email/phone, ảnh hưởng đến hạn mức mua, quyền sử dụng voucher.

Lịch sử mua hàng không lưu trực tiếp trong bảng customers mà được suy ra từ bảng orders. Tuy nhiên, để tối ưu hiệu năng, có thể thêm các trường tổng hợp:

  • totalorders: tổng số đơn hàng đã đặt (trạng thái completed hoặc paid tùy định nghĩa).
  • totalspent: tổng số tiền đã chi (tính từ grandtotal các đơn hợp lệ).
  • lastorderat: thời điểm đơn hàng gần nhất.

Các trường này nên được cập nhật thông qua background job hoặc trigger để tránh làm chậm luồng đặt hàng, đồng thời đảm bảo tính nhất quán khi đơn bị hủy, hoàn tiền.

Bảng orders và orderitems lưu đơn hàng, sản phẩm mua, số lượng, giá tại thời điểm mua

Bảng orders là trung tâm của luồng giao dịch, còn orderitems lưu chi tiết từng dòng sản phẩm. Thiết kế phải đảm bảo tính bất biến của dữ liệu giao dịch: thông tin đơn hàng không thay đổi theo thời gian, ngay cả khi sản phẩm đổi giá, đổi tên hoặc bị xóa.

Bảng Trường chính Mô tả
orders id Khóa chính đơn hàng
orders customerid Tham chiếu khách hàng
orders ordernumber Mã đơn hiển thị cho khách, unique
orders status Trạng thái đơn (pending, paid, shipped, completed, cancelled, refunded)
orders subtotal Tổng tiền hàng trước giảm giá và phí
orders discounttotal Tổng giảm giá áp dụng
orders shippingfee Phí vận chuyển
orders taxtotal Thuế (nếu có)
orders grandtotal Tổng thanh toán cuối cùng
orderitems orderid Tham chiếu đơn hàng
orderitems productid Tham chiếu sản phẩm gốc (nếu còn tồn tại)
orderitems productname Tên sản phẩm tại thời điểm mua
orderitems sku SKU tại thời điểm mua
orderitems unitprice Giá đơn vị tại thời điểm mua
orderitems quantity Số lượng mua
orderitems linetotal Thành tiền dòng (unitprice quantity - discountline)

Các điểm thiết kế chuyên sâu cho orders:

  • ordernumber: – Nên tách khỏi id để có thể định dạng thân thiện (ví dụ: YYYYMMDD-XXXX, hoặc prefix theo kênh bán). – Unique và có index để truy vấn nhanh theo mã đơn mà khách cung cấp.
  • status: – Chuỗi trạng thái nên được chuẩn hóa theo luồng nghiệp vụ: pending → paid → shipped → completed; nhánh cancelled, refunded. – Có thể lưu thêm paymentstatus, fulfillmentstatus ở mức thiết kế mở rộng, nhưng logic cốt lõi vẫn xoay quanh status chính.
  • subtotal, discounttotal, shippingfee, taxtotal, grandtotal: – Tất cả đều là DECIMAL, tính toán tại thời điểm đặt hàng và lưu cố định. – subtotal = tổng unitprice quantity của tất cả orderitems trước giảm giá. – grandtotal = subtotal - discounttotal + shippingfee + taxtotal; nên đảm bảo tính nhất quán bằng logic ở tầng service.

Đối với orderitems, việc lưu productname, sku, unitprice là bắt buộc để đảm bảo:

  • Lịch sử đơn hàng không bị ảnh hưởng khi sản phẩm đổi tên, đổi SKU hoặc đổi giá.
  • Đối soát doanh thu, chiết khấu, hoa hồng theo thời gian chính xác.
  • Xử lý khiếu nại, bảo hành dựa trên thông tin tại thời điểm mua, không phụ thuộc vào trạng thái hiện tại của bảng products.

linetotal nên được tính và lưu sẵn (unitprice * quantity - discountline nếu có) để:

  • Giảm chi phí tính toán khi render lịch sử đơn hàng, báo cáo.
  • Tránh sai lệch do thay đổi quy tắc làm tròn về sau.

Khóa ngoại product_id trong orderitems có thể nullable hoặc không bắt buộc cascade delete; khi sản phẩm bị xóa hoặc ẩn, dữ liệu đơn hàng vẫn phải giữ nguyên. Thông thường, nên tránh xóa vật lý sản phẩm đã từng được bán, hoặc dùng soft delete để bảo toàn tham chiếu.

Việc lưu snapshot thông tin sản phẩm trong orderitems là nền tảng cho một hệ thống bán hàng ổn định, đảm bảo tính pháp lý và khả năng kiểm toán dữ liệu giao dịch.

Thiết kế bảng sản phẩm, danh mục và thuộc tính sản phẩm

Thiết kế bảng sản phẩm cần phân tách rõ giữa sản phẩm chabiến thể để tối ưu quản lý SKU, giá, tồn kho và thuộc tính. Với sản phẩm đơn giản, có thể lưu trực tiếp các trường sku, price, stockquantity trong bảng products, giúp cấu trúc gọn, truy vấn nhanh, ít join và dễ index. Khi xuất hiện biến thể theo màu, size, dung lượng, nên bổ sung bảng productvariants để mỗi biến thể có SKU, giá, tồn kho và bộ thuộc tính riêng, tránh over-selling và hỗ trợ hiển thị chính xác trạng thái còn hàng. Song song, hệ thống thuộc tính cần linh hoạt, kết hợp giữa mô hình EAV, cột cố định và JSON để vừa hỗ trợ lọc, so sánh, SEO danh mục, vừa đảm bảo hiệu năng và khả năng mở rộng.

Infographic cấu trúc database sản phẩm và thuộc tính, quản lý SKU, biến thể, hình ảnh và SEO danh mục

Sản phẩm đơn giản dùng một SKU, một giá và một tồn kho

Đối với website chỉ bán các sản phẩm đơn giản (không có biến thể màu, size, dung lượng), có thể thiết kế mỗi bản ghi trong bảng products tương ứng với một SKU duy nhất. Khi đó, các trường price, stockquantity, sku nằm trực tiếp trong bảng products. Ở mức thiết kế cơ bản, bảng có thể bao gồm các nhóm trường sau:

  • Thông tin nhận diện: id (PK), sku (unique), slug, name, shortdescription, description.
  • Thông tin thương mại: price, compareatprice (giá gốc để hiển thị khuyến mãi), currency, taxclass.
  • Thông tin tồn kho: stockquantity, stockstatus (instock, outofstock, backorder), managestock (boolean).
  • Thông tin hiển thị: status (draft, published), visibility (catalog, search, both), createdat, updatedat.
Mô hình sản phẩm đơn giản 1 SKU 1 giá 1 tồn kho cho khóa học ebook dịch vụ trong quản lý bán hàng

Mô hình này phù hợp với:

  • Các cửa hàng bán dịch vụ, khóa học, ebook, license, subscription đơn giản (mỗi sản phẩm chỉ có một gói).
  • Các sản phẩm vật lý không phân loại theo thuộc tính như size, màu, dung lượng.
  • Các catalog nhỏ, ít thay đổi, không cần cấu trúc thuộc tính phức tạp.

Ưu điểm là đơn giản, dễ truy vấn, ít join, dễ index trực tiếp trên các cột sku, price, stockquantity. Các truy vấn phổ biến như:

  • Lấy danh sách sản phẩm còn hàng: WHERE stockstatus = 'instock'.
  • Lọc theo khoảng giá: WHERE price BETWEEN x AND y.
  • Tìm theo SKU: WHERE sku = ?.

Tuy nhiên, khi bắt đầu xuất hiện nhu cầu bán cùng một sản phẩm với nhiều biến thể, việc giữ tồn kho và giá trong bảng products sẽ trở nên hạn chế. Một số vấn đề thường gặp:

  • Không thể quản lý tồn kho riêng cho từng size/màu, dẫn đến over-selling hoặc under-selling.
  • Không thể đặt giá khác nhau cho từng biến thể (ví dụ size XL đắt hơn size S).
  • Khó hiển thị chính xác trạng thái còn hàng theo từng lựa chọn của khách.

Lúc này cần chuyển sang mô hình có bảng productvariants để tránh trùng lặp và hỗ trợ linh hoạt hơn. Thực tế triển khai thường giữ bảng products làm “sản phẩm cha” (SPU – Standard Product Unit), còn bảng productvariants là các SKU con cụ thể.

Sản phẩm có biến thể cần tách productvariants theo màu, size, dung lượng hoặc cấu hình

Khi sản phẩm có nhiều biến thể (ví dụ: áo thun nhiều size và màu, điện thoại nhiều dung lượng), cần tách bảng productvariants để mỗi biến thể có SKU, giá và tồn kho riêng. Cấu trúc thường gồm:

Tên trường Bảng Mô tả
id productvariants Khóa chính biến thể
productid productvariants Tham chiếu sản phẩm cha
sku productvariants SKU riêng cho biến thể
price productvariants Giá bán của biến thể
stockquantity productvariants Tồn kho của biến thể (nếu không tách bảng tồn kho riêng)
attributesjson productvariants Lưu tổ hợp thuộc tính (màu, size, dung lượng) dạng JSON hoặc tham chiếu bảng khác

Ở mức chuyên sâu hơn, có thể bổ sung thêm các trường như barcode, weight, dimensions, costprice, status cho từng biến thể. Tuy nhiên, các trường này nên được cân nhắc kỹ để tránh trùng lặp với bảng products

Minh họa phân tách biến thể sản phẩm áo thun và điện thoại theo màu sắc kích thước giá và tồn kho

Quy tắc thường dùng:

  • Thuộc tính chung cho toàn bộ biến thể (brand, model, category, description) đặt ở bảng products.
  • Thuộc tính khác nhau giữa các biến thể (màu, size, dung lượng, giá, tồn kho, barcode) đặt ở bảng productvariants.

Trường attributesjson có thể lưu dạng JSON như:

  • {"color": "red", "size": "M"}
  • {"capacity": "128GB", "color": "black"}

Cách này đơn giản, dễ serialize/deserialze ở tầng ứng dụng, nhưng khó đảm bảo tính toàn vẹn dữ liệu (ví dụ: trùng biến thể, giá trị thuộc tính sai chính tả) và khó tối ưu truy vấn lọc theo thuộc tính. Để quản lý thuộc tính biến thể một cách chuẩn hóa, có thể dùng các bảng:

  • attributes: định nghĩa loại thuộc tính (color, size, capacity), có thể có các trường như id, code, name, type (select, multiselect, text, number), isfilterable, isvariantattribute.
  • attributevalues: giá trị cụ thể (Red, Blue, S, M, 64GB, 128GB), thường gồm id, attributeid, value, sortorder, có thể thêm slug để hỗ trợ SEO.
  • productvariantattributevalues: bảng nối giữa biến thể và giá trị thuộc tính, gồm variantid, attributeid, attributevalueid, tạo unique index trên (variantid, attributeid) hoặc trên (productid, attributevalueid) tùy chiến lược.

Cách này giúp đảm bảo mỗi tổ hợp thuộc tính là duy nhất, tránh tạo trùng biến thể, đồng thời hỗ trợ lọc sản phẩm theo thuộc tính trên trang danh mục. Một số kỹ thuật chuyên môn thường dùng:

  • Tạo unique constraint trên tập thuộc tính biến thể cho mỗi sản phẩm, ví dụ: UNIQUE(productid, attributevalueidcolor, attributevalueidsize) (có thể hiện thực bằng bảng trung gian hoặc bằng một cột hash).
  • Index trên attributeid, attributevalueid để tối ưu truy vấn lọc (filter by color, size).
  • Cache tổ hợp thuộc tính phổ biến (ví dụ ma trận size–color) để render nhanh UI chọn biến thể.

Trong các hệ thống lớn, tồn kho có thể được tách ra bảng riêng (ví dụ inventoryitems, stockledger) để hỗ trợ nhiều kho (multi-warehouse), tracking lịch sử nhập/xuất, và đồng bộ với hệ thống ERP. Khi đó, trường stockquantity trong productvariants chỉ là cache hoặc bị loại bỏ hoàn toàn.

Thuộc tính sản phẩm cần thiết kế linh hoạt để hỗ trợ lọc, so sánh và SEO danh mục

Thuộc tính sản phẩm không chỉ phục vụ hiển thị thông tin mà còn là nền tảng cho lọc sản phẩm, so sánhSEO danh mục. Việc thiết kế sai có thể dẫn đến:

  • Truy vấn chậm khi số lượng sản phẩm lớn.
  • Khó mở rộng khi thêm ngành hàng mới với bộ thuộc tính khác biệt.
  • Khó tạo landing page SEO theo thuộc tính (ví dụ “điện thoại 128GB màu đen”).
Mô hình Entity–Attribute–Value phù hợp khi tập thuộc tính thay đổi mạnh giữa các nhóm sản phẩm và phần lớn thuộc tính chỉ xuất hiện ở một số ít bản ghi. Nghiên cứu của Nadkarni và cộng sự cho thấy EAV có thể quản lý dữ liệu không đồng nhất bằng một schema vật lý tương đối ổn định, nhưng việc truy vấn và trình bày dữ liệu trở nên phức tạp hơn vì ứng dụng thường cần chuyển nhiều dòng thuộc tính thành dạng nhiều cột. Vì vậy, EAV không nên áp dụng cho mọi trường sản phẩm. SKU, giá, trạng thái, thương hiệu và các thuộc tính thường xuyên lọc nên có cấu trúc rõ ràng; EAV hoặc JSON phù hợp hơn với thông số thưa, biến động và ít tham gia truy vấn hiệu năng cao (Nadkarni et al., 1999).

Minh họa thuộc tính sản phẩm linh hoạt, lọc và so sánh sản phẩm, tối ưu SEO danh mục và mô hình dữ liệu EAV JSON

Có hai hướng thiết kế phổ biến:

  • Mô hình EAV (Entity-Attribute-Value): dùng các bảng attributes, attributevalues, productattributevalues. Mỗi thuộc tính của sản phẩm là một dòng trong bảng EAV. Ưu điểm:
    • Linh hoạt, dễ thêm thuộc tính mới mà không cần sửa schema hay migrate database.
    • Hỗ trợ nhiều loại dữ liệu (text, number, boolean, option) thông qua các cột generic như valuetext, valuedecimal, valueint.
    • Phù hợp với hệ thống multi-category, mỗi ngành hàng có bộ thuộc tính riêng.
    Nhược điểm:
    • Truy vấn phức tạp, cần nhiều join hoặc subquery để lấy đầy đủ thuộc tính.
    • Khó tối ưu hiệu năng khi cần lọc theo nhiều thuộc tính cùng lúc.
    • Khó index hiệu quả vì dữ liệu bị “dàn” ra nhiều dòng.
  • Mô hình cột cố định: thêm trực tiếp các cột như color, size, brand, material vào bảng products hoặc productvariants. Ưu điểm:
    • Truy vấn nhanh, đơn giản, dễ index trên từng cột.
    • Thân thiện với ORM, dễ mapping sang model trong code.
    • Phù hợp với catalog ổn định, ít thay đổi bộ thuộc tính.
    Nhược điểm:
    • Kém linh hoạt khi cần thêm nhiều thuộc tính khác nhau cho từng ngành hàng.
    • Dễ dẫn đến bảng “rộng” với rất nhiều cột, nhiều cột null.
    • Mỗi lần thêm thuộc tính mới phải migrate schema, tiềm ẩn rủi ro downtime.

Trong thực tế, nhiều hệ thống dùng mô hình lai: các thuộc tính phổ biến và quan trọng cho SEO, lọc (brand, pricerange, maincolor) được đưa thành cột riêng để index, còn các thuộc tính ít dùng hơn được lưu theo dạng EAV hoặc JSON. Một số chiến lược tối ưu:

  • Đánh dấu thuộc tính nào isfilterable, issearchable, iscomparable trong bảng attributes để quyết định cách lưu trữ và index.
  • Đối với thuộc tính numeric (RAM, dung lượng, kích thước màn hình), nên lưu dạng số để hỗ trợ lọc theo khoảng và sort chính xác.
  • Đối với SEO danh mục, có thể tạo bảng mapping giữa combination thuộc tính và URL (ví dụ: category + brand + capacity) để sinh landing page tĩnh, đồng thời lấy dữ liệu từ EAV.

Trong một số trường hợp, thuộc tính chỉ phục vụ hiển thị (ví dụ: “hướng dẫn sử dụng”, “thành phần chi tiết”) có thể lưu dạng JSON hoặc text dài trong bảng products để giảm số lượng bản ghi EAV, tránh phình to bảng và giảm chi phí join.

Ảnh sản phẩm nên tách bảng riêng để quản lý gallery, ảnh đại diện, alt text và thứ tự hiển thị

Ảnh sản phẩm là phần không thể thiếu trong website bán hàng. Thay vì lưu danh sách URL ảnh trong một trường JSON của bảng products, nên tách bảng productimages để quản lý chi tiết:

Tên trường Mô tả
id Khóa chính ảnh
productid Tham chiếu sản phẩm
variantid (nullable) Tham chiếu biến thể nếu ảnh chỉ áp dụng cho một biến thể
url Đường dẫn ảnh gốc hoặc đã resize
alttext Văn bản thay thế cho SEO và accessibility
isprimary Đánh dấu ảnh đại diện
position Thứ tự hiển thị trong gallery

Thiết kế này hỗ trợ các trường hợp như: mỗi biến thể màu có ảnh riêng, thay đổi thứ tự ảnh dễ dàng, tối ưu alt text cho SEO, và quản lý nhiều kích thước ảnh nếu cần (có thể thêm bảng hoặc trường cho thumbnail, medium, large). Một số lưu ý chuyên sâu:

  • variantid nullable cho phép dùng chung ảnh giữa các biến thể (ảnh gắn với product) và ảnh riêng cho từng biến thể (ảnh gắn với variant). Khi render UI, có thể ưu tiên ảnh theo variantid, nếu không có thì fallback sang ảnh theo productid.
  • isprimary nên được đảm bảo chỉ có một ảnh primary cho mỗi scope (mỗi product hoặc mỗi variant) bằng constraint hoặc logic ứng dụng.
  • position hỗ trợ sort ảnh trong gallery mà không cần hard-code thứ tự ở frontend; có thể dùng kiểu số nguyên và cho phép “chèn” ảnh mới bằng cách dùng khoảng cách (10, 20, 30...).

Sơ đồ cấu trúc bảng sản phẩm và bảng PRODUCT_IMAGES quản lý ảnh sản phẩm và alt text SEO

Về mặt lưu trữ, url thường là đường dẫn đến object storage (S3, GCS, local storage) thay vì lưu binary trong database. Có thể kết hợp với một service xử lý ảnh để sinh nhiều kích thước khác nhau (thumbnail, medium, large) dựa trên cùng một file gốc. Trong trường hợp cần quản lý nhiều phiên bản ảnh, có thể:

  • Lưu thêm các cột urlthumbnail, urlmedium, urllarge trong cùng bảng.
  • Hoặc tách bảng phụ (ví dụ productimagevariants) tham chiếu đến productimages để lưu từng kích thước.

alttext đóng vai trò quan trọng cho SEO và accessibility: nên chứa từ khóa liên quan đến sản phẩm, nhưng không nhồi nhét; đồng thời mô tả đúng nội dung ảnh để hỗ trợ screen reader. Trong các hệ thống đa ngôn ngữ, có thể cần bảng hoặc cột riêng cho alttext theo từng locale.

Việc tách bảng product_images cũng giúp dễ dàng thống kê và tối ưu: đếm số ảnh trên mỗi sản phẩm, tìm sản phẩm thiếu ảnh, kiểm tra ảnh không còn được tham chiếu để dọn dẹp storage, và áp dụng các chính sách CDN, cache một cách nhất quán.

Thiết kế giỏ hàng, checkout và đơn hàng không mất dữ liệu giao dịch

Thiết kế giỏ hàng và đơn hàng cần tách rõ hai lớp dữ liệu: trạng thái tạmsnapshot bất biến. Lớp tạm gồm carts, cartitems lưu mọi thao tác thêm/xóa/sửa, hỗ trợ cả khách đăng nhập lẫn guest qua session hoặc device, đồng thời cho phép persistent cart và phân tích hành vi (abandoned cart, remarketing, tối ưu UX). Giá trong giỏ nên được lưu dưới dạng các trường snapshot (list price, unit price, discount, tax) và được recalculate khi checkout.

Thiết kế quy trình giỏ hàng và đơn hàng đảm bảo toàn vẹn dữ liệu cho website thương mại điện tử

Khi khách xác nhận, hệ thống tạo order snapshot qua các bảng orders, orderitems, order_discounts, lưu đầy đủ giá, thuế, phí, khuyến mãi, subtotal, grand total và tax breakdown. Trạng thái đơn và trạng thái thanh toán được tách riêng, có lịch sử thay đổi và có thể dùng state machine để kiểm soát luồng. Lịch sử đơn hàng luôn hiển thị từ dữ liệu snapshot (tên, SKU, giá, ảnh, thuộc tính), không phụ thuộc hoàn toàn vào bảng sản phẩm hiện tại, đảm bảo toàn vẹn lịch sử giao dịch và hỗ trợ kế toán, đối soát, khiếu nại, kiểm toán.

Cart và cartitems lưu trạng thái giỏ hàng trước khi khách xác nhận mua

Bảng cartscartitems đóng vai trò như lớp trạng thái tạm thời, tách biệt hoàn toàn với đơn hàng chính thức. Thiết kế tốt cần đảm bảo:

  • Không làm mất dữ liệu hành vi người dùng (thêm/xóa/sửa sản phẩm trong giỏ).
  • Hỗ trợ cả khách đăng nhập và khách vãng lai (guest) thông qua session hoặc device fingerprint.
  • Có khả năng khôi phục giỏ hàng khi người dùng quay lại (persistent cart).

Giỏ hàng tạm thời màu vàng minh họa lưu trạng thái và chi tiết sản phẩm trong quá trình mua sắm online

Thiết kế cơ bản có thể mở rộng như sau:

  • carts: id, customerid (nullable), sessionid, deviceid (nullable), status (active, converted, abandoned, merged), currency, channel (web, app, pos), createdat, updatedat, expiredat.
  • cartitems: id, cartid, productid (nullable nếu sản phẩm bị xóa), variantid (nullable), quantity, unitpricesnapshot, listpricesnapshot, discountamountsnapshot, taxratesnapshot, addedat, updatedat, metadata (JSON cho các thuộc tính tùy biến như màu, size, personalization).

unitpricesnapshot trong cartitems nên là giá đã qua bước tính toán cơ bản (đã áp dụng giá theo kênh, theo nhóm khách hàng, theo khu vực), nhưng trước khi áp dụng các khuyến mãi động như voucher, coupon, campaign. Trong nhiều hệ thống, cần phân tách:

  • listpricesnapshot: giá niêm yết tại thời điểm thêm vào giỏ.
  • unitpricesnapshot: giá sau khi áp dụng chính sách giá (pricing rules) nhưng chưa áp dụng khuyến mãi đơn hàng.
  • discountamountsnapshot: tổng giảm giá áp dụng riêng cho dòng sản phẩm (nếu có).

Khi checkout, hệ thống thường cần recalculate toàn bộ giỏ hàng dựa trên:

  • Giá hiện tại của sản phẩm và biến thể (nếu chính sách yêu cầu dùng giá mới).
  • Các chương trình khuyến mãi đang hiệu lực (voucher, coupon, promotion engine).
  • Điều kiện vận chuyển (địa chỉ, phương thức ship, trọng lượng, thể tích).
  • Thuế theo khu vực, loại sản phẩm, loại khách hàng (B2B/B2C).

Để tránh mất dữ liệu giao dịch, cần lưu lại cả kết quả tính toán cuối cùng trong snapshot đơn hàng (phần bên dưới), đồng thời vẫn giữ lịch sử giỏ hàng cho mục đích phân tích. Giỏ hàng là nguồn dữ liệu quan trọng cho:

  • Phân tích abandoned cart: xác định tỷ lệ bỏ giỏ, sản phẩm hay bị bỏ, bước funnel bị rơi.
  • Remarketing: gửi email, push, SMS nhắc nhở; tạo audience cho quảng cáo.
  • Tối ưu UX: xem các thao tác chỉnh sửa giỏ, số lần thay đổi số lượng, thay đổi biến thể.

Để phục vụ phân tích sâu, có thể bổ sung bảng log chi tiết cho giỏ hàng (không thay đổi số lượng bảng hiện tại, chỉ là gợi ý khái niệm), ví dụ lưu từng sự kiện: thêm sản phẩm, xóa sản phẩm, đổi số lượng, đổi địa chỉ ship, đổi phương thức thanh toán. Các sự kiện này giúp hiểu rõ hơn hành vi trước khi đơn hàng được tạo.

Order snapshot cần lưu giá, tên sản phẩm, SKU, thuế, phí và khuyến mãi tại thời điểm đặt hàng

Khi khách xác nhận đặt hàng, hệ thống cần tạo một snapshot bất biến, phản ánh chính xác trạng thái giao dịch tại thời điểm đó. Snapshot này không chỉ là bản sao của giỏ hàng, mà còn là kết quả của:

  • Engine tính giá (pricing engine).
  • Engine khuyến mãi (promotion/discount engine).
  • Engine vận chuyển (shipping rate calculator).
  • Engine thuế (tax engine).
Dữ liệu đơn hàng là dữ liệu có yếu tố thời gian: tên sản phẩm, giá, thuế, địa chỉ và chính sách khuyến mãi đúng ở thời điểm giao dịch nhưng có thể thay đổi sau đó. Lý thuyết cơ sở dữ liệu thời gian phân biệt thời gian dữ liệu có hiệu lực trong nghiệp vụ với thời gian bản ghi được lưu hoặc thay đổi trong hệ thống. Vì vậy, OrderItem cần giữ snapshot riêng và có các mốc như createdat, paidat, cancelledat hoặc validfrom–validto khi nghiệp vụ yêu cầu. Chỉ lưu productid rồi truy vấn ngược dữ liệu hiện tại sẽ làm mất trạng thái lịch sử, khiến hóa đơn, đối soát và xử lý khiếu nại không còn phản ánh đúng giao dịch ban đầu (Snodgrass, 1999). 

Sơ đồ hệ thống xử lý đơn hàng thương mại điện tử với tính giá, khuyến mãi, vận chuyển, thuế và cấu trúc tổng tiền

Các bảng snapshot chính:

  • orders: tổng tiền, phí ship, thuế, giảm giá, địa chỉ giao hàng, phương thức thanh toán, phương thức vận chuyển, currency, exchangerate (nếu có đa tiền tệ), customergroup, channel, ordernumber (mã đơn hiển thị), internalid (khóa kỹ thuật), notes (ghi chú khách), internalnotes (ghi chú nội bộ).
  • orderitems: tên sản phẩm, SKU, giá đơn vị, số lượng, giảm giá dòng, thuế dòng, unitpriceincltax, unitpriceexcltax, totallineamount, totaltaxamount, productid (nullable), variantid (nullable), productnamesnapshot, skusnapshot, thumbnailurlsnapshot, attributessnapshot (JSON: màu, size, cấu hình tùy chọn).
  • orderdiscounts: chi tiết các khuyến mãi, mã giảm giá đã áp dụng, bao gồm loại giảm (theo phần trăm, theo số tiền, miễn phí ship), phạm vi áp dụng (toàn đơn, theo dòng), nguồn (voucher code, auto promotion, loyalty points), giá trị trước và sau giảm.

Để đảm bảo tính toàn vẹn dữ liệu giao dịch, snapshot nên lưu rõ các thành phần cấu thành tổng tiền:

  • subtotalexcltax: tổng tiền hàng trước thuế, trước giảm giá đơn hàng.
  • subtotalincltax: tổng tiền hàng đã bao gồm thuế trên từng dòng.
  • shippingfeeexcltax, shippingfeeincltax.
  • discounttotal: tổng giảm giá (bao gồm giảm giá dòng và giảm giá đơn hàng).
  • taxtotal: tổng thuế (có thể tách theo loại thuế nếu cần: VAT, eco tax,...).
  • grandtotal: số tiền cuối cùng khách phải trả.

Đối với hệ thống có đa loại thuế hoặc đa khu vực, nên lưu thêm bảng chi tiết thuế theo loại thuế (tax breakdown) gắn với đơn hàng hoặc từng dòng, nhưng thông tin cốt lõi vẫn phải được phản ánh trong orderitemsorders để không phụ thuộc vào cấu hình thuế hiện tại.

Snapshot còn quan trọng cho:

  • Kế toán: đối chiếu doanh thu, ghi nhận doanh thu theo chuẩn kế toán (ví dụ: tách doanh thu hàng hóa, phí ship, thuế).
  • Đối soát với cổng thanh toán: so sánh số tiền yêu cầu thanh toán và số tiền thực nhận.
  • Xử lý khiếu nại: chứng minh giá, khuyến mãi, phí ship tại thời điểm khách đặt.
  • Kiểm toán: đảm bảo dữ liệu lịch sử không bị thay đổi khi cấu hình hệ thống thay đổi.

Một điểm chuyên môn quan trọng là idempotency trong quá trình tạo đơn: khi nhận callback từ cổng thanh toán hoặc khi người dùng bấm thanh toán nhiều lần, hệ thống phải đảm bảo không tạo nhiều snapshot đơn hàng trùng lặp. Thường sử dụng idempotency key gắn với phiên checkout hoặc payment intent để đảm bảo mỗi giao dịch chỉ tạo một đơn hàng duy nhất.

Trạng thái đơn hàng cần rõ: chờ xác nhận, đã thanh toán, đang giao, hoàn tất, hủy, hoàn tiền

Trạng thái đơn hàng là trục chính để vận hành fulfillment, chăm sóc khách hàng và báo cáo. Thiết kế trạng thái cần:

  • Đủ chi tiết để phản ánh vòng đời thực tế của đơn.
  • Đủ chặt chẽ để không gây mơ hồ trong báo cáo và xử lý nghiệp vụ.
  • Hỗ trợ phân tách giữa trạng thái đơn và trạng thái thanh toán.
Trạng thái không nên chỉ là một chuỗi có thể cập nhật tự do, mà cần được quản lý như một máy trạng thái với các phép chuyển hợp lệ. Chẳng hạn, đơn có thể chuyển từ pending sang paid, nhưng không được chuyển trực tiếp từ cancelled sang shipped nếu chưa có nghiệp vụ khôi phục và phê duyệt. Mỗi lần chuyển cần lưu trạng thái cũ, trạng thái mới, thời gian, tác nhân và lý do. OrderStatus, PaymentStatus và FulfillmentStatus phải được tách biệt, bởi đơn đã hủy vẫn có thể đang chờ hoàn tiền, còn thanh toán thành công chưa đồng nghĩa hàng đã giao. Cấu trúc này bảo toàn lịch sử và ngăn một giá trị status duy nhất phải biểu diễn nhiều chiều nghiệp vụ mâu thuẫn (Snodgrass, 1999). 

Các trạng thái đơn hàng: chờ xác nhận, đã thanh toán, đang giao, hoàn tất, hủy, hoàn tiền

Một số trạng thái phổ biến cho orderstatus:

  • pending: khách đã đặt nhưng chưa thanh toán (online payment) hoặc chờ xác nhận (COD). Ở trạng thái này, hàng có thể được giữ tồn kho tạm thời (stock reservation) hoặc chưa, tùy chính sách.
  • paid: thanh toán thành công, chờ xử lý giao hàng. Thường là điểm kích hoạt quy trình chuẩn bị hàng (picking, packing).
  • processing: đang chuẩn bị hàng, đóng gói, có thể đã in phiếu xuất kho, phiếu giao hàng.
  • shipped: đã bàn giao cho đơn vị vận chuyển, có mã vận đơn (tracking number), carrier, service level.
  • completed: giao thành công, không có khiếu nại trong khoảng thời gian cho phép, doanh thu được ghi nhận chính thức.
  • cancelled: đơn bị hủy trước khi giao (hoặc trước khi hoàn tất), cần quy định rõ điều kiện hủy (trước khi đóng gói, trước khi giao cho carrier,...).
  • refunded: đã hoàn tiền toàn bộ hoặc một phần; trong thực tế có thể cần thêm thông tin chi tiết về mức độ hoàn tiền.

Để quản lý rõ hơn, nên tách orderstatuspaymentstatus. Một số trạng thái cho paymentstatus có thể gồm:

  • unpaid: chưa thanh toán.
  • pending: đang chờ xác nhận từ cổng thanh toán.
  • paid: đã thanh toán đủ.
  • partiallypaid: đã thanh toán một phần (áp dụng cho đặt cọc, trả góp, B2B).
  • refunded: đã hoàn tiền đủ.
  • partiallyrefunded: hoàn tiền một phần.
  • failed: thanh toán thất bại.

Ví dụ: orderstatus = cancelled nhưng paymentstatus = refunded để thể hiện rằng đơn đã bị hủy và tiền đã được hoàn. Ngược lại, có thể có trường hợp orderstatus = cancelled nhưng paymentstatus = paid trong thời gian chờ xử lý hoàn tiền, điều này cần được thể hiện rõ trong UI và báo cáo.

Bảng orderstatushistory giúp lưu lại lịch sử thay đổi trạng thái, người thao tác và thời gian, phục vụ audit và chăm sóc khách hàng. Các trường nên có:

  • orderid
  • oldstatus
  • newstatus
  • changedby (userid hoặc system)
  • changedat
  • reason (tùy chọn, ghi chú lý do thay đổi)

Đối với hệ thống phức tạp, có thể áp dụng mô hình state machine để kiểm soát luồng chuyển trạng thái hợp lệ, tránh các chuyển đổi không hợp lệ (ví dụ: từ completed quay lại processing mà không có quy trình đặc biệt).

Không phụ thuộc hoàn toàn vào dữ liệu sản phẩm hiện tại khi hiển thị lịch sử đơn hàng

Khi hiển thị lịch sử đơn hàng cho khách hoặc cho nội bộ, việc join trực tiếp với bảng products để lấy tên, giá, ảnh hiện tại là một rủi ro lớn về tính toàn vẹn lịch sử. Các vấn đề thường gặp:

  • Sản phẩm đổi tên, đổi mô tả marketing, dẫn đến khách không nhận ra sản phẩm mình đã mua.
  • Giá hiện tại khác xa giá đã mua, gây hiểu nhầm về việc tính sai tiền.
  • Sản phẩm hoặc biến thể bị ẩn, bị xóa, hoặc được gộp vào SKU khác, làm mất liên kết.

Infographic bảo vệ tính toàn vẹn lịch sử đơn hàng bằng dữ liệu snapshot sản phẩm trong order items

Thay vào đó, cần sử dụng các trường snapshot trong orderitems như productname, sku, unitprice, thumbnailurlsnapshot, attributessnapshot. Các trường này phải được coi là nguồn sự thật (source of truth) cho lịch sử giao dịch. Khi render lịch sử đơn hàng:

  • Luôn hiển thị tên, SKU, giá, ảnh từ snapshot.
  • Nếu cần hiển thị thêm thông tin mới (ví dụ: link đến sản phẩm hiện tại, trạng thái còn bán hay ngừng bán), có thể join thêm với bảng products nhưng chỉ để bổ sung, không thay thế thông tin snapshot.
  • Trong trường hợp sản phẩm đã bị xóa, vẫn phải hiển thị đầy đủ thông tin từ snapshot, tránh để đơn hàng bị “trống” dòng.

Cách tiếp cận này đảm bảo tính toàn vẹn lịch sử giao dịch, đồng thời giảm rủi ro khi thay đổi cấu trúc dữ liệu sản phẩm (ví dụ: refactor catalog, đổi hệ thống PIM). Về mặt kỹ thuật, có thể áp dụng một số nguyên tắc:

  • Không dùng ON DELETE CASCADE giữa productsorderitems; nếu cần xóa sản phẩm, nên dùng cờ isdeleted hoặc isarchived.
  • Giữ productidvariantid trong orderitems ở dạng nullable để không phá vỡ dữ liệu khi sản phẩm bị xóa vật lý.
  • Thiết kế API/Service layer sao cho endpoint lịch sử đơn hàng luôn ưu tiên dữ liệu snapshot, chỉ gọi sang service sản phẩm để lấy thông tin bổ sung (như link, trạng thái còn bán).

Về trải nghiệm người dùng, việc hiển thị đúng thông tin tại thời điểm mua giúp:

  • Khách hàng dễ đối chiếu với email xác nhận, hóa đơn VAT, chứng từ kế toán.
  • Giảm tranh chấp về giá, khuyến mãi, đặc biệt trong các chiến dịch flash sale, khuyến mãi giới hạn thời gian.
  • Hỗ trợ chăm sóc khách hàng tra cứu lịch sử mua hàng chính xác, ngay cả khi danh mục sản phẩm đã thay đổi mạnh sau nhiều năm.

Thiết kế tồn kho, kho hàng và quản lý số lượng sản phẩm

Thiết kế tồn kho và kho hàng cần xuất phát từ mô hình kinh doanh và mức độ phức tạp vận hành. Với hệ thống nhỏ, có thể lưu trực tiếp số lượng trong bảng sản phẩm hoặc biến thể, xoay quanh một trường duy nhất như stockquantity, kết hợp thêm các ngưỡng cảnh báo cơ bản. Tuy nhiên, khi xuất hiện nhiều kho, nhiều kênh bán và các trạng thái hàng hóa khác nhau, cần tách thành các bảng chuyên biệt như warehouses, inventoryitems, inventory_transactions để quản lý tồn theo từng kho, từng SKU, đồng thời có lịch sử nhập xuất rõ ràng. Mọi thao tác trừ kho, giữ hàng, giải phóng hàng phải được xử lý trong transaction, sử dụng cập nhật có điều kiện và cơ chế reserve/release có thời hạn, nhằm tránh overselling và tối ưu vòng quay tồn kho.

Infographic quy trình quản lý tồn kho và kho hàng với mô hình đơn giản và mô hình phức tạp nhiều kho

Tồn kho đơn giản có thể lưu trực tiếp theo SKU hoặc biến thể sản phẩm

Với mô hình đơn giản, tồn kho có thể lưu trực tiếp trong bảng products hoặc productvariants thông qua trường stockquantity. Mỗi bản ghi tương ứng với một SKU hoặc một biến thể (màu, size, chất liệu…), và trường stockquantity thể hiện tổng số lượng khả dụng để bán. Khi có đơn hàng, hệ thống trừ số lượng trực tiếp trên trường này. Cách này phù hợp khi:

  • Chỉ có một kho duy nhất, toàn bộ hàng hóa được xem như nằm trong một vị trí lưu trữ logic.
  • Không cần theo dõi lịch sử nhập xuất chi tiết theo từng lần nhập, từng phiếu xuất, hoặc từng lô hàng.
  • Không có quy trình phức tạp như đặt trước, giữ hàng, chuyển kho nội bộ, hoặc phân bổ theo kênh bán.

Minh họa quy trình quản lý tồn kho đơn giản theo SKU và biến thể với trường stockquantity và cảnh báo nhập thêm

Trong mô hình này, có thể bổ sung thêm một số trường đơn giản như reorderlevel (ngưỡng cảnh báo cần nhập thêm), maxstock (giới hạn tối đa mong muốn) để phục vụ báo cáo cơ bản. Tuy nhiên, toàn bộ logic vẫn xoay quanh một con số duy nhất là stockquantity.

Dù đơn giản, vẫn cần đảm bảo việc cập nhật tồn kho được thực hiện trong transaction để tránh race condition khi nhiều khách đặt cùng lúc. Ở tầng cơ sở dữ liệu, nên:

  • Đảm bảo mỗi lần trừ tồn kho đều nằm trong một transaction bao trùm cả việc tạo order item và cập nhật stockquantity.
  • Có thể sử dụng cơ chế row-level lock (ví dụ: SELECT ... FOR UPDATE) trên bản ghi sản phẩm hoặc biến thể trước khi trừ tồn.
  • Thiết kế API cập nhật tồn kho theo kiểu “atomic” (một lệnh cập nhật duy nhất) để giảm khả năng sai lệch do nhiều tiến trình cùng truy cập.

Trong các hệ thống nhỏ, mô hình này giúp giảm độ phức tạp, dễ triển khai, dễ bảo trì. Tuy nhiên, khi số lượng kênh bán, kho hàng, hoặc quy trình vận hành tăng lên, mô hình này nhanh chóng bộc lộ hạn chế: khó phân biệt tồn kho theo vị trí, khó audit, khó xử lý các trạng thái như hàng đang chuyển, hàng đang giữ cho đơn chờ thanh toán.

Website nhiều kho cần bảng warehouses, inventoryitems và lịch sử nhập xuất tồn

Khi có nhiều kho hoặc cần quản lý tồn kho chi tiết, nên tách thành các bảng chuyên biệt để phản ánh đúng mô hình nghiệp vụ. Cấu trúc cơ bản:

Bảng Mục đích
warehouses Lưu thông tin từng kho: tên, địa chỉ, loại kho (online, cửa hàng), trạng thái
inventoryitems Lưu số lượng tồn theo kho và SKU/variant: warehouseid, variantid, quantityavailable, quantityreserved
inventorytransactions Lưu lịch sử nhập xuất: type (import, export, adjust, reserve, release), quantity, reference (orderid, purchaseorderid)

Số lượng ghi trong hệ thống không phải lúc nào cũng trùng với lượng hàng vật lý. Nghiên cứu thực nghiệm trên gần 370.000 hồ sơ tồn kho của 37 cửa hàng cho thấy 65% hồ sơ được khảo sát có sai lệch, đồng thời mức độ sai lệch thay đổi đáng kể theo nhóm sản phẩm và môi trường cửa hàng. Nguyên nhân có thể đến từ thất thoát, quét nhầm, hàng hỏng, hàng đặt sai vị trí hoặc giao dịch chưa được ghi nhận đúng. Vì vậy, inventoryitems chỉ phản ánh số dư hiện tại, còn inventorytransactions và kiểm kê vật lý mới tạo khả năng giải thích số dư đó. Hệ thống cần lưu thời điểm kiểm kê, số lượng thực tế, mức chênh lệch và người phê duyệt điều chỉnh (DeHoratius & Raman, 2008). 

Sơ đồ giải pháp phần mềm quản lý kho đa chi nhánh multi warehouse WMS với các bảng tồn kho và giao dịch

Trong bảng warehouses, ngoài các trường cơ bản như tên, địa chỉ, có thể bổ sung:

  • type: phân loại kho (fulfillment center, cửa hàng bán lẻ, kho trung chuyển, kho ký gửi…).
  • priority hoặc fulfillmentstrategy: phục vụ logic chọn kho xuất hàng (ưu tiên kho gần khách, kho còn nhiều hàng, hoặc kho chi phí thấp).
  • isactive: trạng thái kho (đang hoạt động, tạm dừng, đóng cửa).

Bảng inventoryitems là trung tâm của mô hình tồn kho nhiều kho. Mỗi bản ghi thể hiện mối quan hệ giữa một warehouseid và một variantid. Hai trường quan trọng:

  • quantityavailable: số lượng thực tế có thể bán, chưa bị giữ cho đơn nào.
  • quantityreserved: số lượng đã được giữ cho các đơn pending, chưa xuất kho nhưng không còn khả dụng cho khách khác.

Có thể bổ sung thêm các trường như quantityintransit (đang chuyển giữa các kho), quantitydamaged (hàng hỏng), nhưng vẫn giữ nguyên cấu trúc bảng như đã mô tả. Việc mở rộng này giúp phản ánh chính xác hơn vòng đời của hàng hóa trong chuỗi cung ứng.

Bảng inventorytransactions đóng vai trò audit trail, cho phép truy vết mọi thay đổi tồn kho. Mỗi transaction thường bao gồm:

  • type: import (nhập hàng), export (xuất bán hoặc chuyển kho), adjust (điều chỉnh do kiểm kê), reserve, release.
  • quantity: số lượng thay đổi, có thể là số dương hoặc âm tùy loại nghiệp vụ.
  • warehouseid, variantid: xác định vị trí và sản phẩm bị ảnh hưởng.
  • reference: liên kết đến orderid, purchaseorderid, hoặc mã phiếu kiểm kê để dễ đối chiếu.

Thiết kế này cho phép:

  • Quản lý tồn kho theo từng kho, từng kênh bán, hỗ trợ mô hình omnichannel (website, cửa hàng, marketplace) với cùng một nguồn dữ liệu.
  • Theo dõi lịch sử nhập hàng, điều chỉnh, chuyển kho, từ đó phân tích hiệu suất từng kho, từng SKU, và phát hiện sai lệch tồn kho.
  • Phân bổ đơn hàng theo kho gần khách nhất hoặc kho còn hàng, tối ưu chi phí vận chuyển và thời gian giao hàng.

Khi triển khai, tầng ứng dụng thường cần một lớp inventory service để:

  • Đọc tổng tồn khả dụng của một SKU trên nhiều kho (tổng quantityavailable).
  • Áp dụng chiến lược chọn kho (ví dụ: ưu tiên kho nội vùng, sau đó mới đến kho liên vùng).
  • Ghi nhận các inventorytransactions tương ứng với từng thay đổi, đảm bảo tính toàn vẹn dữ liệu.

Trừ kho cần xử lý transaction để tránh bán vượt số lượng khi nhiều khách đặt cùng lúc

Vấn đề overselling thường xảy ra khi nhiều khách đặt cùng một sản phẩm trong khoảng thời gian ngắn, trong khi hệ thống không khóa hoặc không cập nhật tồn kho một cách nguyên tử. Để tránh, cần:

  • Sử dụng transaction ở mức isolation phù hợp (ví dụ: READ COMMITTED hoặc REPEATABLE READ) khi cập nhật tồn kho, tùy vào đặc điểm hệ quản trị cơ sở dữ liệu.
  • Dùng câu lệnh cập nhật dạng: UPDATE inventoryitems SET quantityavailable = quantityavailable - :qty WHERE id = :id AND quantityavailable >= :qty và kiểm tra số dòng bị ảnh hưởng.
  • Nếu số dòng cập nhật = 0, nghĩa là không đủ tồn kho, cần báo lỗi cho người dùng hoặc yêu cầu họ giảm số lượng.

Kiểm soát đồng thời phải bảo đảm kết quả của nhiều giao dịch chạy song song tương đương với một thứ tự thực thi tuần tự hợp lệ. Nếu hai request cùng đọc tồn kho bằng 1 rồi đều ghi lại 0, hệ thống có thể chấp nhận hai đơn cho một sản phẩm. Các cơ chế khóa, timestamp hoặc kiểm soát đa phiên bản được phát triển để ngăn xung đột đọc–ghi và bảo đảm tính serializable khi cần. Câu lệnh cập nhật có điều kiện kết hợp kiểm tra số dòng ảnh hưởng thường an toàn hơn quy trình đọc số lượng rồi cập nhật ở hai bước riêng biệt. Việc chọn isolation level phải dựa trên loại xung đột thực tế, không chỉ dựa vào cấu hình mặc định của hệ quản trị (Bernstein et al., 1987). 

Quy trình trừ kho an toàn trong bán hàng online với các bước đặt hàng, tạm giữ, xuất kho và ghi nhận nhật ký giao dịch

Cách cập nhật “có điều kiện” này giúp đảm bảo tính atomic: hoặc toàn bộ phép trừ tồn thành công khi đủ hàng, hoặc không có thay đổi nào nếu thiếu hàng. Điều này đặc biệt quan trọng khi nhiều tiến trình song song cùng cố gắng trừ tồn trên cùng một bản ghi.

Có thể kết hợp với lock hàng (reserve) khi đơn ở trạng thái chờ thanh toán, sau đó giải phóng (release) nếu thanh toán thất bại hoặc đơn bị hủy. Quy trình điển hình:

  • Khi khách bấm “đặt hàng” nhưng chưa thanh toán, hệ thống tạo order ở trạng thái pending và thực hiện thao tác reserve: trừ quantityavailable, tăng quantityreserved.
  • Khi thanh toán thành công, hệ thống chuyển từ reserved sang trạng thái “đã xuất” bằng cách giảm quantityreserved và ghi nhận transaction export.
  • Nếu thanh toán thất bại hoặc đơn bị hủy, hệ thống thực hiện release: giảm quantityreserved, tăng lại quantityavailable.

Tất cả các thao tác này nên được ghi lại trong bảng inventorytransactions để dễ audit, phân tích nguyên nhân chênh lệch tồn kho, và hỗ trợ kiểm toán nội bộ. Một số hệ thống còn lưu thêm metadata (user thao tác, lý do điều chỉnh, ghi chú) để tăng tính minh bạch.

Dự phòng tồn kho cho đơn chưa thanh toán cần có thời hạn giữ hàng rõ ràng

Khi cho phép khách giữ hàng trong một khoảng thời gian (ví dụ: 15 phút để thanh toán), hệ thống cần cơ chế reserve inventory được thiết kế chặt chẽ để không làm “đóng băng” tồn kho quá lâu. Các bước chính:

  • Khi tạo đơn ở trạng thái pending, trừ số lượng từ quantityavailable sang quantityreserved cho từng inventoryitem tương ứng.
  • Lưu thời gian hết hạn giữ hàng (reservationexpiresat) trong đơn hoặc bảng riêng, gắn với từng dòng order item nếu cần chi tiết hơn.
  • Job nền định kỳ kiểm tra các đơn quá hạn, tự động hủy hoặc giải phóng tồn kho (chuyển quantityreserved về quantity_available), đồng thời cập nhật trạng thái đơn.

Tồn kho trên hệ thống nên được xem là một ước lượng nghiệp vụ cần được cập nhật liên tục từ bán hàng, nhập hàng, giải phóng giữ chỗ và kiểm kê, thay vì một con số chắc chắn tuyệt đối. Nghiên cứu về quản trị tồn kho khi hồ sơ không chính xác cho thấy việc sử dụng thêm quan sát và kiểm kê có thể cải thiện quyết định bổ sung hàng so với việc tin hoàn toàn vào số dư ghi nhận. Trong website bán hàng, mỗi reservation cần có mã tham chiếu, thời điểm bắt đầu, thời điểm hết hạn và trạng thái giải phóng. Job hết hạn phải có tính idempotent, để chạy lại nhiều lần không cộng trả tồn kho lặp lại hoặc giải phóng phần hàng đã được thanh toán thành công (DeHoratius et al., 2008). 

Minh họa quy trình dự phòng tồn kho cho đơn chưa thanh toán với thời hạn giữ hàng và cân bằng bán hàng

Cơ chế này giúp cân bằng giữa trải nghiệm khách hàng (không bị mất hàng trong lúc thanh toán) và tối ưu tồn kho (không bị giữ hàng quá lâu cho các đơn không hoàn tất). Một số điểm cần lưu ý khi thiết kế:

  • Thời gian giữ hàng nên linh hoạt theo loại sản phẩm hoặc kênh bán; hàng khan hiếm có thể cần thời gian giữ ngắn hơn.
  • Job nền cần chạy đủ thường xuyên để giải phóng tồn kịp thời, tránh tình trạng tồn kho ảo do đơn pending quá hạn nhưng chưa được xử lý.
  • Khi khách quay lại thanh toán sau khi hết hạn giữ hàng, hệ thống phải kiểm tra lại tồn kho thực tế trước khi cho phép hoàn tất thanh toán.

Ở tầng kỹ thuật, việc xử lý hết hạn giữ hàng cũng nên nằm trong transaction để đảm bảo khi job nền giải phóng tồn, không xảy ra xung đột với các thao tác đặt hàng mới. Kết hợp với bảng inventorytransactions, có thể phân tích được tỷ lệ đơn bị hủy do hết hạn giữ hàng, từ đó tối ưu thời lượng reservation và chính sách bán hàng.

Thiết kế thanh toán, vận chuyển và hóa đơn trong database bán hàng

Thiết kế nhóm bảng thanh toán, vận chuyển và hóa đơn cần xoay quanh đơn hàng nhưng vẫn tách bạch từng luồng nghiệp vụ. Payments tập trung vào dòng tiền: phương thức, trạng thái, đối soát cổng thanh toán, hỗ trợ thanh toán nhiều lần, hoàn tiền và tranh chấp. Shipments mô tả luồng vận chuyển vật lý, cho phép chia nhiều kiện, nhiều kho, nhiều hãng vận chuyển và đồng bộ trạng thái qua webhook. Addresses vừa là sổ địa chỉ khách hàng, vừa cần snapshot trong đơn để không sai lệch lịch sử. Các bảng invoices, refunds, returns tách riêng để phản ánh chính xác nghiệp vụ kế toán và đổi trả, giúp báo cáo doanh thu, kiểm toán và phân quyền vận hành rõ ràng, nhất quán.

Sơ đồ quy trình dữ liệu bán hàng cho đơn hàng online gồm thanh toán, vận chuyển, hóa đơn, hoàn tiền, đổi trả

Payments lưu phương thức, số tiền, trạng thái, mã giao dịch và phản hồi từ cổng thanh toán

Bảng payments là trung tâm của luồng tiền trong hệ thống bán hàng, đóng vai trò ghi nhận mọi giao dịch tài chính gắn với một đơn hàng. Thiết kế tốt phải hỗ trợ: thanh toán nhiều lần cho một đơn, hoàn tiền một phần, đối soát với cổng thanh toán, và truy vết khi có tranh chấp.

Sơ đồ hệ thống payments ghi nhận và quản lý giao dịch tài chính với phương thức, số tiền, mã giao dịch, trạng thái, phản hồi cổng

Một số trường quan trọng và cách thiết kế chi tiết:

  • id (khóa chính): thường dùng kiểu BIGINT hoặc UUID để đảm bảo mở rộng lâu dài.
  • orderid: khóa ngoại tham chiếu bảng orders, nên có index để truy vấn nhanh lịch sử thanh toán của một đơn.
  • amount: số tiền thanh toán, nên dùng kiểu DECIMAL(18,2) hoặc tương đương để tránh lỗi làm tròn của kiểu float.
  • currency: mã tiền tệ (VD: VND, USD), nên dùng chuẩn ISO 4217, có thể lưu dạng CHAR(3).
  • method: phương thức thanh toán, có thể chuẩn hóa bằng enum hoặc bảng lookup (COD, creditcard, ewallet, banktransfer, voucher, points).
  • status: các trạng thái chi tiết như pending, authorized, captured, failed, refunded, chargeback. Nên quy ước rõ:
    • pending: đã tạo yêu cầu nhưng chưa có kết quả từ cổng.
    • authorized: đã giữ tiền nhưng chưa trừ (thường dùng với thẻ tín dụng).
    • captured: đã trừ tiền thành công.
    • failed: thanh toán thất bại, có thể thử lại.
    • refunded: đã hoàn tiền (toàn phần hoặc một phần, tùy cách thiết kế).
    • chargeback: bị ngân hàng/cổng thanh toán thu hồi do tranh chấp.
  • transactionid: mã giao dịch từ cổng thanh toán, rất quan trọng cho đối soát. Nên unique theo từng cổng để tránh trùng.
  • provider: tên cổng thanh toán (VD: VNPay, MoMo, ZaloPay, Stripe, PayPal) để phân biệt logic xử lý.
  • providerresponse: lưu JSON phản hồi từ cổng (mã lỗi, message, raw data, requestid, signature, v.v.). Nên dùng kiểu JSON/JSONB (PostgreSQL) hoặc TEXT.
  • paidat: thời điểm thanh toán thành công (khi status chuyển sang captured).
  • createdat, updatedat: phục vụ audit và debug.
  • failurereason: mô tả ngắn lý do thất bại (mapping từ providerresponse để dễ đọc).
  • ispartial: cờ đánh dấu đây là thanh toán một phần của tổng giá trị đơn.

Về mặt nghiệp vụ, một đơn hàng có thể có nhiều bản ghi trong payments:

  • Khách thanh toán trước một phần, phần còn lại COD.
  • Thanh toán thất bại nhiều lần, chỉ một bản ghi thành công.
  • Hoàn tiền nhiều lần (mỗi lần hoàn là một payment âm hoặc một bản ghi trong bảng refunds liên kết với payment gốc).

Để hỗ trợ hoàn tiền chi tiết, có thể thiết kế thêm quan hệ giữa paymentsrefunds theo kiểu 1-n, trong đó mỗi refund tham chiếu đến payment gốc. Khi đó, trường amount trong payments luôn là số tiền thu vào, còn số tiền hoàn được quản lý riêng, giúp báo cáo doanh thu và hoàn tiền rõ ràng hơn.

Về bảo mật, cần tuân thủ các nguyên tắc:

  • Không lưu số thẻ đầy đủ (PAN), chỉ lưu 4 số cuối và brand (Visa, MasterCard) nếu cần hiển thị cho người dùng.
  • Không lưu CVV, không lưu dữ liệu nhạy cảm chưa mã hóa.
  • Nếu lưu token từ cổng thanh toán (cardtoken, customertoken), cần mã hóa hoặc bảo vệ bằng cơ chế quyền truy cập chặt chẽ.
  • Log providerresponse nhưng có thể mask các trường nhạy cảm trước khi ghi vào DB.

Về hiệu năng, nên tạo index trên orderid, transactionid, status, và có thể trên paidat để phục vụ báo cáo theo thời gian. Với hệ thống lớn, có thể tách bảng payments thành bảng onlinepayments và offlinepayments hoặc dùng partition theo thời gian để tối ưu truy vấn.

Shipments lưu đơn vị vận chuyển, phí ship, mã vận đơn, trạng thái giao và thời gian cập nhật

Bảng shipments phản ánh luồng vận chuyển vật lý của hàng hóa. Thiết kế cần hỗ trợ: giao nhiều lần cho một đơn, giao từ nhiều kho, đồng bộ trạng thái với hãng vận chuyển, và xử lý các trường hợp thất bại/hoàn hàng.

Các trường thường có và nên làm rõ:

  • id: khóa chính.
  • orderid: tham chiếu đơn hàng, nên index.
  • warehouseid: nếu có nhiều kho, trường này cho biết shipment xuất phát từ kho nào.
  • carrier: đơn vị vận chuyển (GHTK, GHN, VNPost, J&T, v.v.), có thể chuẩn hóa bằng bảng carriers để lưu thêm SLA, cấu hình webhook.
  • trackingnumber: mã vận đơn, dùng để tra cứu trạng thái với hãng vận chuyển.
  • shippingfee: phí ship thực tế mà hệ thống phải trả cho hãng, có thể khác với phí thu của khách.
  • customershippingfee: phí ship thu khách (nếu cần phân tích lãi/lỗ trên phí vận chuyển).
  • status: các trạng thái chi tiết như:
    • pendingpickup: đã tạo đơn vận chuyển nhưng chưa được lấy hàng.
    • pickedup: đã được shipper lấy.
    • intransit: đang trên đường giao.
    • outfordelivery: đang giao cho khách.
    • delivered: giao thành công.
    • failed: giao thất bại (khách không nghe máy, sai địa chỉ, v.v.).
    • returned: hàng đã hoàn về kho.
    • cancelled: hủy vận đơn trước khi giao.
  • shippedat: thời điểm hàng rời kho hoặc được hãng xác nhận nhận hàng.
  • deliveredat: thời điểm giao thành công.
  • laststatusupdateat: lần cuối cùng cập nhật trạng thái (thường từ webhook).
  • rawwebhookdata: lưu dữ liệu webhook từ hãng vận chuyển (JSON), giúp debug khi trạng thái hiển thị không khớp.
  • note: ghi chú nội bộ (yêu cầu giao giờ hành chính, gọi trước khi giao, v.v.).

Minh họa bảng giao hàng quản lý trạng thái, thời gian, phí ship, hãng vận chuyển và mã vận đơn

Để hỗ trợ giao tách đơn hoặc giao từ nhiều kho, có thể thêm bảng shipmentitems với các trường:

  • shipmentid: tham chiếu shipments.
  • orderitemid: tham chiếu dòng sản phẩm trong đơn.
  • quantity: số lượng sản phẩm nằm trong shipment này.
  • weight, volume: nếu cần tính phí ship chi tiết theo kiện.

Cách thiết kế này cho phép:

  • Một đơn hàng có thể chia thành nhiều shipment, mỗi shipment chứa một tập con sản phẩm.
  • Mỗi shipment có thể gắn với một carrier khác nhau (VD: hàng cồng kềnh dùng hãng A, hàng nhỏ dùng hãng B).
  • Theo dõi chính xác sản phẩm nào đã giao, sản phẩm nào còn chờ giao hoặc đã trả về.

Về đồng bộ trạng thái, nên có cơ chế:

  • Webhook từ carrier cập nhật trực tiếp vào bảng shipments, lưu toàn bộ payload vào rawwebhookdata để truy vết.
  • Mapping trạng thái từ carrier sang trạng thái chuẩn nội bộ (VD: nhiều hãng có bộ status khác nhau).
  • Trigger hoặc job nền để cập nhật trạng thái tổng của đơn hàng dựa trên trạng thái các shipment (VD: đơn coi là delivered khi tất cả shipment delivered).

Addresses lưu địa chỉ giao hàng, thanh toán và bản sao địa chỉ trong đơn hàng

Bảng addresses thường được dùng như sổ địa chỉ của khách hàng, cho phép lưu nhiều địa chỉ và phân loại theo mục đích sử dụng. Thiết kế tốt cần đảm bảo vừa linh hoạt cho người dùng, vừa không làm sai lệch dữ liệu lịch sử đơn hàng.

Minh họa tính năng quản lý sổ địa chỉ và bản sao địa chỉ trong đơn hàng của phần mềm bán hàng

Các trường cơ bản trong bảng addresses:

  • id: khóa chính.
  • customerid: tham chiếu khách hàng sở hữu địa chỉ.
  • fullname: tên người nhận hoặc người liên hệ.
  • phone: số điện thoại liên hệ.
  • line1: địa chỉ chi tiết (số nhà, tên đường).
  • line2: thông tin bổ sung (tòa nhà, tầng, phòng, v.v.).
  • ward, district, province: các cấp địa giới hành chính, nên chuẩn hóa theo mã để dễ lọc và báo cáo.
  • country: quốc gia, dùng mã ISO 3166-1 alpha-2 hoặc alpha-3.
  • postalcode: mã bưu chính, quan trọng với đơn quốc tế.
  • type: shipping, billing, hoặc both.
  • isdefault: đánh dấu địa chỉ mặc định của khách.

Vấn đề quan trọng là bản sao địa chỉ trong đơn hàng. Nếu chỉ lưu addressid tham chiếu đến bảng addresses, khi khách sửa hoặc xóa địa chỉ, lịch sử đơn sẽ hiển thị sai thông tin tại thời điểm đặt hàng. Vì vậy, cần lưu snapshot địa chỉ trong đơn.

Có hai cách tiếp cận:

  • Lưu trực tiếp các trường địa chỉ trong bảng orders:
    • shippingname, shippingphone, shippingaddressline1, shippingaddressline2, shippingward, shippingdistrict, shippingprovince, shippingcountry, shippingpostalcode.
    • billingname, billingphone, billingaddressline1, v.v. (nếu cần tách riêng).

    Cách này đơn giản, dễ truy vấn, phù hợp đa số hệ thống B2C. Chỉ cần đảm bảo khi hiển thị lịch sử đơn, luôn dùng các trường snapshot này, không phụ thuộc bảng addresses.

  • Dùng bảng orderaddresses:
    • orderid: tham chiếu orders.
    • type: shipping hoặc billing.
    • Các trường địa chỉ giống bảng addresses nhưng là bản sao tại thời điểm đặt hàng.

    Cách này linh hoạt hơn khi một đơn có nhiều địa chỉ giao (VD: chia shipment đến nhiều người nhận), hoặc khi cần lưu lịch sử thay đổi địa chỉ trong quá trình xử lý đơn.

Trong cả hai cách, có thể lưu thêm addressreferenceid (tham chiếu đến addresses nếu có) để biết snapshot này được tạo từ địa chỉ nào trong sổ địa chỉ, nhưng không phụ thuộc vào nó khi hiển thị.

Invoices, refunds và returns cần tách riêng khi website có hoàn tiền, đổi trả hoặc xuất hóa đơn

Khi hệ thống hỗ trợ xuất hóa đơn, hoàn tiền, đổi trả, việc tách bảng là cần thiết để phản ánh đúng quy trình kế toán và chăm sóc khách hàng. Mỗi loại nghiệp vụ có vòng đời, trạng thái, và dữ liệu riêng, nếu gộp vào orders sẽ khó quản lý và khó kiểm toán.

Quy trình quản lý tách biệt xuất hóa đơn, hoàn tiền và trả hàng trong hệ thống bán hàng

invoices: lưu thông tin hóa đơn VAT và các chứng từ tương đương.

  • id, orderid: khóa chính và tham chiếu đơn hàng.
  • invoicenumber: số hóa đơn, thường phải unique theo series.
  • issuedate: ngày xuất hóa đơn.
  • companyname, taxcode, companyaddress: thông tin bên mua trên hóa đơn.
  • subtotal, taxamount, totalamount: chi tiết tiền hàng, thuế, tổng cộng.
  • status: draft, issued, cancelled, adjusted.
  • pdfurl hoặc đường dẫn lưu file hóa đơn điện tử (nếu có).

Việc tách invoices giúp:

  • Quản lý nhiều hóa đơn cho một đơn hàng (VD: xuất tách theo phòng ban).
  • Hỗ trợ điều chỉnh, hủy hóa đơn mà không làm thay đổi bản ghi orders.
  • Đáp ứng yêu cầu lưu trữ và truy xuất của bộ phận kế toán, kiểm toán.

refunds: lưu yêu cầu và giao dịch hoàn tiền.

  • id, orderid, paymentid: tham chiếu đơn và payment gốc.
  • amount: số tiền hoàn, có thể nhỏ hơn số tiền thanh toán (hoàn một phần).
  • reason: lý do hoàn (hàng lỗi, giao sai, khách hủy, v.v.).
  • status: requested, approved, rejected, processing, completed, failed.
  • requestedby, approvedby: ai tạo và ai duyệt (nếu có workflow).
  • createdat, processedat: thời điểm tạo và xử lý.
  • providerresponse: nếu hoàn qua cổng thanh toán, lưu phản hồi tương tự payments.

Thiết kế tách refunds giúp:

  • Theo dõi lịch sử hoàn tiền độc lập với trạng thái đơn hàng.
  • Hỗ trợ nhiều lần hoàn cho cùng một đơn, mỗi lần có lý do và trạng thái riêng.
  • Đối soát với cổng thanh toán và kế toán (báo cáo doanh thu ròng sau hoàn tiền).

returns: lưu yêu cầu trả hàng và kết quả xử lý.

  • id, orderid, có thể thêm shipmentid nếu trả theo kiện.
  • status: requested, received, inspected, approved, rejected, completed.
  • reason: không ưng, lỗi kỹ thuật, hư hỏng khi vận chuyển, v.v.
  • resolution: exchange (đổi hàng), refund (hoàn tiền), repair (sửa chữa), noaction.
  • createdat, resolvedat: thời điểm yêu cầu và thời điểm kết thúc xử lý.

Thường cần thêm bảng returnitems để chi tiết sản phẩm trả:

  • returnid: tham chiếu returns.
  • orderitemid: dòng sản phẩm trong đơn.
  • quantity: số lượng trả.
  • condition: tình trạng hàng khi nhận lại (mới, đã dùng, hư hỏng, thiếu phụ kiện).

Quan hệ giữa returnsrefunds có thể là 1-n hoặc n-1 tùy quy trình: một yêu cầu trả hàng có thể dẫn đến một hoặc nhiều lần hoàn tiền, hoặc một lần hoàn tiền có thể gộp cho nhiều yêu cầu trả. Tách riêng hai bảng cho phép:

  • Quản lý quy trình vật lý (hàng hóa) tách biệt với quy trình tài chính (tiền).
  • Áp dụng chính sách khác nhau: có thể chấp nhận trả hàng nhưng chỉ đổi hàng, không hoàn tiền.
  • Phân quyền rõ ràng: bộ phận kho xử lý returns, bộ phận kế toán xử lý refunds.

Toàn bộ thiết kế tách biệt payments, shipments, addresses, invoices, refunds, returns giúp hệ thống bán hàng mở rộng dễ dàng, hỗ trợ nhiều luồng nghiệp vụ phức tạp mà vẫn giữ được tính nhất quán, khả năng truy vết và minh bạch khi kiểm toán.

Thiết kế khuyến mãi, mã giảm giá và chương trình bán hàng

Hệ thống khuyến mãi hiệu quả cần kết hợp chặt chẽ giữa coupons, promotion rules và bảng orderdiscounts để vừa linh hoạt cho marketing, vừa an toàn cho dữ liệu doanh thu. Coupons đóng vai trò như “tài sản marketing” có vòng đời, điều kiện áp dụng, giới hạn sử dụng và phạm vi ảnh hưởng rõ ràng, được theo dõi chi tiết qua bảng couponusages. Song song, promotion rules cho phép tự động hóa các chương trình giảm giá theo sản phẩm, danh mục, đơn hàng, khách hàng hoặc thời điểm, với cấu trúc điều kiện và hành động có thể mở rộng. Cuối cùng, mọi khoản giảm thực tế phải được ghi nhận, “đóng băng” trong order_discounts, tránh tính lại theo rule hiện tại để đảm bảo đối soát, báo cáo và truy vết minh bạch.

Thiết kế khuyến mãi và bán hàng với mã giảm giá, quy tắc khuyến mãi và giảm giá đơn hàng minh bạch doanh thu

Coupons lưu mã, loại giảm giá, thời hạn, giới hạn lượt dùng và điều kiện áp dụng

Bảng coupons không chỉ lưu mã giảm giá để khách nhập trong quá trình checkout, mà còn là nền tảng cho việc kiểm soát ngân sách khuyến mãi, phân khúc khách hàng và đo lường hiệu quả chiến dịch. Khi thiết kế, nên coi mỗi coupon như một “tài sản marketing” có vòng đời, phạm vi áp dụng và chi phí rõ ràng.

Infographic hướng dẫn cách xây dựng và quản lý coupon giảm giá với 5 bước chiến lược chi tiết

Các trường quan trọng và cách sử dụng chuyên sâu:

  • code: mã giảm giá, unique, thường viết hoa, không dấu.
    • Nên index theo code để tra cứu nhanh trong quá trình checkout.
    • Có thể thêm trường normalizedcode (không khoảng trắng, không ký tự đặc biệt) để xử lý trường hợp người dùng nhập sai định dạng (ví dụ: có dấu cách).
    • Nên có cơ chế sinh mã tự động theo chiến dịch, ví dụ: SUMMER202610P, để dễ phân tích theo prefix.
  • discounttype: percentage, fixedamount, freeshipping.
    • percentage: giảm theo % trên giá trị áp dụng. Cần quy định rõ giảm trên subtotal, subtotal sau khi trừ khuyến mãi khác hay subtotal trước thuế/phí vận chuyển.
    • fixedamount: giảm số tiền cố định. Nên có logic chặn giảm vượt quá giá trị áp dụng (không để số âm).
    • freeshipping: miễn phí vận chuyển. Cần xác định rõ áp dụng cho:
      • Tất cả phương thức vận chuyển hay chỉ một số carrier/service nhất định.
      • Chỉ phí vận chuyển cơ bản hay bao gồm phụ phí (COD, phí vùng xa,...).
  • discountvalue: giá trị giảm (phần trăm hoặc số tiền).
    • Với percentage, nên giới hạn tối đa (ví dụ <= 80%) để tránh cấu hình sai.
    • Với fixedamount, nên lưu cùng đơn vị tiền tệ của hệ thống; nếu hệ thống đa tiền tệ, cần thêm cơ chế quy đổi hoặc ràng buộc theo currency.
  • startat, endat: thời gian hiệu lực.
    • Nên lưu theo UTC và convert theo timezone khi hiển thị, tránh sai lệch khi đổi múi giờ.
    • Cần index theo startat, endat để lọc nhanh các coupon đang hoạt động.
    • Có thể thêm logic “grace period” nội bộ (không lưu DB) cho phép CSKH vẫn áp dụng trong một số trường hợp ngoại lệ.
  • usagelimit: tổng số lần được dùng.
    • Dùng để kiểm soát ngân sách chiến dịch (ví dụ: chỉ cho phép 1000 lượt dùng).
    • Cần cơ chế tăng số lượt dùng một cách atomic khi tạo đơn để tránh race condition (hai đơn cùng dùng mã ở ngưỡng giới hạn).
  • usagelimitpercustomer: số lần mỗi khách được dùng.
    • Thường dùng cho mã “khách mới”, “sinh nhật”, “ưu đãi thành viên”.
    • Cần xác định rõ “customer” là theo customerid đăng nhập hay theo email/phone đối với khách vãng lai.
  • minordervalue: giá trị đơn tối thiểu.
    • Cần định nghĩa rõ tính trên subtotal (tổng tiền hàng) hay grand total (bao gồm phí ship, thuế).
    • Nên tính sau khi trừ các khuyến mãi tự động khác hay trước? Quy tắc này phải nhất quán để tránh tranh chấp.
  • applicableto: toàn bộ đơn, theo sản phẩm, theo danh mục (có thể tham chiếu bảng khác hoặc lưu JSON điều kiện).
    • Nếu tham chiếu bảng khác:
      • Có thể có bảng couponproducts (couponid, productid) và couponcategories (couponid, categoryid) để linh hoạt.
      • Ưu điểm: dễ join, dễ query phân tích; nhược điểm: phức tạp hơn khi cấu hình.
    • Nếu lưu JSON điều kiện:
      • Ví dụ: { "type": "product", "includeskus": ["SKU1","SKU2"], "excludecategories": [10,11] }.
      • Cần chuẩn hóa schema JSON và versioning để tránh lỗi khi thay đổi cấu trúc.

Cần thêm bảng couponusages để ghi lại mỗi lần sử dụng: couponid, orderid, customerid, amountdiscounted, usedat. Bảng này là nguồn dữ liệu chính cho:

  • Kiểm soát usagelimitusagelimitpercustomer một cách chính xác.
  • Phân tích hiệu quả chiến dịch theo:
    • Doanh thu tạo ra từ mỗi coupon.
    • Tỷ lệ chuyển đổi khi có mã so với không có mã.
    • Hành vi lạm dụng mã (một khách dùng nhiều tài khoản,...).
  • Hỗ trợ CSKH tra cứu lịch sử sử dụng mã của từng khách.

Promotion rules cần hỗ trợ giảm theo sản phẩm, danh mục, đơn hàng, khách hàng hoặc thời điểm

Khuyến mãi dạng rule là các quy tắc tự động áp dụng mà khách không cần nhập mã. Hệ thống rule càng linh hoạt thì đội marketing càng dễ triển khai các chiến dịch phức tạp như mua 2 tặng 1, combo giảm giá, flash sale theo khung giờ, ưu đãi khách hàng thân thiết.

Sơ đồ hệ thống quy tắc khuyến mãi với các loại giảm giá đơn hàng, sản phẩm, mua X tặng Y và freeship

Bảng promotionrules có thể thiết kế:

  • name, description: tên và mô tả chương trình.
    • name dùng cho nội bộ và có thể hiển thị cho khách (ví dụ: “Giảm 10% đơn trên 1 triệu”).
    • description nên mô tả rõ điều kiện và phạm vi áp dụng để đội vận hành, CSKH dễ hiểu.
  • type: orderdiscount, productdiscount, buyxgety, freeshipping.
    • orderdiscount: giảm trên tổng đơn, thường dùng cho chiến dịch toàn sàn.
    • productdiscount: giảm trên một số sản phẩm/danh mục cụ thể (flash sale, giảm giá theo brand,...).
    • buyxgety: mua X sản phẩm (hoặc đạt X giá trị) tặng Y sản phẩm/giảm giá Y%. Cần mô tả rõ:
      • Cách chọn sản phẩm được tặng (rẻ nhất, cùng danh mục, SKU cụ thể,...).
      • Giới hạn số lần áp dụng trên một đơn (ví dụ: tối đa 3 lần “mua 2 tặng 1”).
    • freeshipping: miễn phí vận chuyển theo điều kiện (đơn tối thiểu, khu vực, nhóm khách hàng,...).
  • conditionsjson: điều kiện áp dụng (sản phẩm, danh mục, giá trị đơn, nhóm khách hàng, thời gian).
    • Nên thiết kế theo dạng rule engine cơ bản:
      • Cấu trúc AND/OR: ví dụ { "all": [cond1, cond2], "any": [cond3, cond4] }.
      • Các loại điều kiện phổ biến:
        • Theo sản phẩm: SKU, productid, brand.
        • Theo danh mục: categoryid, tree path.
        • Theo đơn hàng: subtotal, quantity, paymentmethod, shippingmethod.
        • Theo khách hàng: customergroup, firstorder, registrationdate, loyaltytier.
        • Theo thời gian: khung giờ trong ngày, ngày trong tuần, ngày lễ.
    • Cần có cơ chế validate JSON khi lưu để tránh rule lỗi gây crash engine.
  • actionsjson: hành động giảm giá (giảm bao nhiêu, áp dụng cho dòng nào).
    • Ví dụ cho orderdiscount: { "action": "percent", "value": 10, "applyto": "ordersubtotal" }.
    • Ví dụ cho productdiscount: { "action": "fixed", "value": 20000, "applyto": "matchingitems" }.
    • Ví dụ cho buyxgety:
      • { "action": "buyxgety", "buyqty": 2, "getqty": 1, "applyto": "cheapestinset" }.
    • Cần quy định rõ:
      • Áp dụng trước hay sau các rule khác (liên quan đến priority).
      • Có giới hạn số tiền giảm tối đa trên đơn hay không.
  • priority: độ ưu tiên khi nhiều rule cùng áp dụng.
    • Engine nên sort rule theo priority (tăng dần hoặc giảm dần tùy quy ước) trước khi áp dụng.
    • Có thể dùng để:
      • Ưu tiên rule “chiến lược” hơn rule “thường xuyên”.
      • Giải quyết xung đột khi hai rule cùng giảm trên một dòng sản phẩm.
  • isstackable: có cho phép cộng dồn với khuyến mãi khác không.
    • Nếu false, khi rule này áp dụng thì các rule khác (hoặc coupon) có thể bị chặn.
    • Cần định nghĩa rõ:
      • Không cộng dồn với tất cả hay chỉ với một số loại (ví dụ: không cộng với coupon, nhưng vẫn cộng với freeshipping).
      • Chiến lược ưu tiên: chọn rule giảm nhiều nhất cho khách, hay theo priority do business quyết định.

Engine tính khuyến mãi sẽ đọc các rule này, áp dụng cho giỏ hàng và ghi kết quả vào orderdiscounts khi đơn được tạo. Một số lưu ý kỹ thuật:

  • Nên tách bước tính toán (simulation) và ghi nhận (persist) để có thể hiển thị preview khuyến mãi trên giỏ hàng mà chưa tạo đơn.
  • Cần đảm bảo tính deterministic: cùng một giỏ hàng, cùng tập rule, luôn cho ra kết quả giống nhau.
  • Nên log thêm thông tin debug (không lưu DB, chỉ log) về rule nào được evaluate, rule nào bị skip để dễ kiểm tra khi có lỗi.

Order discounts cần lưu lại giá trị giảm thực tế để đối soát doanh thu sau bán

Bảng orderdiscounts là nơi lưu chi tiết các khoản giảm giá đã áp dụng cho đơn, đóng vai trò như “sổ cái khuyến mãi” phục vụ kế toán, phân tích và CSKH. Mỗi bản ghi thể hiện một nguồn giảm giá cụ thể.

Sơ đồ sổ cái khuyến mãi với các trường orderid description amount sourcetype và đối soát doanh thu

  • orderid: đơn hàng liên quan.
    • Index theo orderid để truy vấn nhanh toàn bộ giảm giá của một đơn.
    • Có thể dùng để tổng hợp số tiền giảm theo ngày, theo kênh bán, theo khu vực.
  • sourcetype: coupon, promotionrule, manualadjustment.
    • coupon: giảm giá đến từ mã khách nhập.
    • promotionrule: giảm giá tự động từ engine rule.
    • manualadjustment: điều chỉnh thủ công (CSKH bù thêm giảm, xử lý khiếu nại,...).
  • sourceid: tham chiếu couponid hoặc promotionruleid.
    • Với manualadjustment, có thể để null hoặc tham chiếu đến bảng log nội bộ.
    • Giúp phân tích hiệu quả từng coupon/rule cụ thể theo doanh thu, số đơn, số tiền giảm.
  • amount: số tiền giảm.
    • Nên lưu theo đơn vị tiền tệ của đơn hàng (nếu hệ thống đa tiền tệ, mỗi order có currency riêng).
    • Cần đảm bảo tổng amount của tất cả bản ghi trong orderdiscounts khớp với trường tổng giảm giá trong bảng orders.
  • description: mô tả hiển thị cho khách và nội bộ.
    • Có thể lưu dạng: “Giảm 10% với mã SUMMER10”, “Khuyến mãi mua 2 tặng 1”, “Điều chỉnh thủ công bởi CSKH”.
    • Giúp:
      • Khách hiểu rõ vì sao được giảm số tiền đó.
      • CSKH dễ giải thích khi có thắc mắc.

Việc lưu chi tiết từng khoản giảm giúp:

  • Đối soát doanh thu thực tế sau khi trừ khuyến mãi:
    • Kế toán có thể tách phần “chi phí khuyến mãi” theo từng loại (coupon, rule, manual).
    • Dễ dàng lập báo cáo P&L theo chiến dịch marketing.
  • Đánh giá hiệu quả từng chương trình khuyến mãi:
    • Tính ROI cho từng coupon/rule: doanh thu tăng thêm so với chi phí giảm giá.
    • Phát hiện chương trình gây “đốt tiền” nhưng không tăng trưởng khách hàng mới hay tần suất mua.
  • Hạn chế tranh chấp khi khách thắc mắc về số tiền giảm:
    • Có thể hiển thị chi tiết trên trang lịch sử đơn hàng hoặc gửi trong email hóa đơn.
    • CSKH có căn cứ rõ ràng để giải thích từng khoản giảm.

Không tính lại khuyến mãi từ rule hiện tại cho đơn hàng đã hoàn tất

Một sai lầm phổ biến là khi hiển thị lịch sử đơn hàng, hệ thống tính lại khuyến mãi dựa trên rule hiện tại, dẫn đến số tiền giảm khác với lúc khách mua. Điều này gây lệch dữ liệu doanh thu, khó đối soát và dễ tạo tranh chấp với khách hàng.

Infographic bảo vệ kết quả khuyến mãi đơn hoàn tất với snapshot dữ liệu, không chạy lại engine, điều chỉnh thủ công, minh bạch doanh thu

  • Chỉ sử dụng dữ liệu snapshot trong orderdiscounts và các trường tổng trong orders.
    • Khi đơn được tạo, toàn bộ kết quả tính khuyến mãi (tổng giảm, chi tiết từng nguồn) phải được “đóng băng” trong DB.
    • Mọi màn hình hiển thị lịch sử, báo cáo, export cho kế toán đều đọc từ snapshot này, không chạy lại engine.
  • Không chạy lại engine khuyến mãi cho đơn đã completed hoặc cancelled.
    • Engine chỉ nên chạy ở các trạng thái như cart, pending, draft (tùy thiết kế), trước khi đơn được “chốt”.
    • Sau khi đơn chuyển sang trạng thái hoàn tất hoặc hủy, mọi thay đổi rule/coupon mới không được phép ảnh hưởng ngược lại.
  • Nếu cần thay đổi khuyến mãi sau bán (ví dụ: bù thêm giảm giá), nên tạo bản ghi manualadjustment mới trong orderdiscounts thay vì sửa dữ liệu cũ.
    • Giữ nguyên các bản ghi gốc từ coupon/rule để bảo toàn lịch sử.
    • Thêm một bản ghi mới:
      • sourcetype = manualadjustment.
      • amount có thể là số âm (tăng giá) hoặc dương (giảm thêm), tùy chính sách.
      • description ghi rõ lý do điều chỉnh (ví dụ: “Bù giảm giá do lỗi hệ thống”).
    • Cập nhật lại trường tổng giảm giá và tổng tiền phải trả trong bảng orders dựa trên tổng các bản ghi order_discounts.

Cách này đảm bảo tính nhất quán và minh bạch của dữ liệu doanh thu, đồng thời cho phép truy vết đầy đủ mọi thay đổi liên quan đến khuyến mãi trên từng đơn hàng.

Thiết kế dữ liệu khách hàng, phân quyền và bảo mật thông tin

Thiết kế dữ liệu khách hàng cần tách rõ các nhóm thông tin: nhận diện, đăng nhập, địa chỉ, lịch sử mua và trạng thái tài khoản để vừa dễ mở rộng, vừa tuân thủ nguyên tắc data minimization và bảo vệ dữ liệu cá nhân. Mỗi nhóm nên được lưu ở bảng riêng, liên kết bằng khóa ngoại, cho phép ẩn danh hoặc xóa chọn lọc mà vẫn giữ được dữ liệu giao dịch phục vụ kế toán, phân tích. Hệ thống phân quyền nên áp dụng RBAC, tách vai trò admin, kho, CSKH, marketing với tập quyền chi tiết, kết hợp audit và nguyên tắc least privilege. Dữ liệu nhạy cảm như mật khẩu, token, thông tin thanh toán phải được hash/mã hóa, hạn chế lưu trữ và không xuất hiện trong log. Audit logs ghi lại thao tác quan trọng, lưu before/after, IP, user agent và được bảo vệ ở chế độ append-only.

Thiết kế hệ thống dữ liệu với phân tách dữ liệu, phân quyền RBAC, bảo mật thông tin nhạy cảm và truy vết nhật ký

Customers cần tách thông tin cá nhân, địa chỉ, lịch sử mua và trạng thái tài khoản

Thiết kế dữ liệu khách hàng trong hệ thống thương mại điện tử hoặc CRM nên tuân thủ nguyên tắc phân tách trách nhiệm (separation of concerns)tối thiểu hóa dữ liệu (data minimization). Việc tách bạch các nhóm thông tin không chỉ giúp mô hình dữ liệu rõ ràng, dễ mở rộng, mà còn hỗ trợ tuân thủ các quy định về bảo vệ dữ liệu cá nhân, đáp ứng yêu cầu xóa, ẩn danh hoặc hạn chế xử lý dữ liệu khi cần.

Sơ đồ phân tách dữ liệu khách hàng và phân quyền RBAC cho quản lý vai trò và quyền truy cập hệ thống

Cấu trúc bảng customers có thể tập trung vào các nhóm thông tin chính:

  • Thông tin nhận diện:
    • email: định danh đăng nhập chính, cần unique, có thể thêm cờ isverified để đánh dấu đã xác thực.
    • phone: có thể dùng cho OTP, chăm sóc khách hàng; nên chuẩn hóa theo chuẩn E.164.
    • fullname: có thể tách thành firstname, lastname để dễ tìm kiếm, cá nhân hóa.
    • dateofbirth, gender: dùng cho phân tích, cá nhân hóa; nên đánh dấu là dữ liệu nhạy cảm và hạn chế truy cập.

    Những trường này nên được index phù hợp (ví dụ: email, phone) để tối ưu tra cứu, nhưng tránh index trên các trường quá nhạy cảm nếu không cần thiết để giảm nguy cơ lộ lọt qua log hoặc snapshot.

  • Thông tin đăng nhập:
    • passwordhash: lưu hash mật khẩu với thuật toán mạnh (bcrypt, Argon2) kèm salt riêng cho từng user; có thể thêm trường passwordupdatedat để áp chính sách đổi mật khẩu định kỳ.
    • lastloginat: timestamp lần đăng nhập gần nhất, phục vụ bảo mật và phân tích hành vi.
    • failedloginattempts: đếm số lần đăng nhập sai liên tiếp, dùng để kích hoạt cơ chế khóa tạm thời.
    • lockeduntil: thời điểm tài khoản được mở khóa tự động; nếu giá trị lớn hơn thời gian hiện tại thì từ chối đăng nhập.

    Nhóm thông tin này nên được tách logic xử lý sang module authentication, hạn chế truy cập trực tiếp từ các module khác (marketing, CSKH) để giảm bề mặt tấn công.

  • Địa chỉ:

    Địa chỉ nên lưu trong bảng addresses riêng, liên kết với customers qua khóa ngoại customerid. Mỗi khách hàng có thể có nhiều địa chỉ (shipping, billing, office...). Một số trường điển hình:

    • id, customerid
    • label (Home, Office, Default shipping...)
    • fullname, phone cho người nhận
    • addressline1, addressline2, ward, district, city, country, postalcode
    • isdefaultshipping, isdefaultbilling

    Cách tách này giúp:

    • Không làm bảng customers phình to khi khách hàng có nhiều địa chỉ.
    • Dễ ẩn danh hoặc xóa địa chỉ riêng lẻ khi khách yêu cầu, mà vẫn giữ được dữ liệu đơn hàng đã hoàn tất (có thể thay bằng địa chỉ đã được mask).
  • Lịch sử mua:

    Lịch sử mua không nên lưu trực tiếp trong bảng customers mà được suy ra từ bảng orders và các bảng liên quan (orderitems, payments). Tuy nhiên, để tối ưu truy vấn, có thể bổ sung các trường thống kê đã được tổng hợp (denormalized) trong customers như:

    • totalorders: tổng số đơn hàng đã đặt (trạng thái completed/paid).
    • totalspent: tổng giá trị đã chi tiêu.
    • lastorderat: thời điểm đơn hàng gần nhất.
    • firstorderat: thời điểm đơn hàng đầu tiên.

    Các trường này nên được cập nhật thông qua event-driven (ví dụ: khi đơn hàng chuyển sang trạng thái completed thì phát event để service khách hàng cập nhật thống kê), tránh cập nhật trực tiếp từ nhiều nơi gây race condition.

  • Trạng thái tài khoản:

    Trạng thái tài khoản có thể được biểu diễn bằng trường status với các giá trị:

    • active: tài khoản hoạt động bình thường.
    • inactive: tài khoản chưa kích hoạt hoặc đã bị vô hiệu hóa tạm thời (ví dụ: người dùng yêu cầu đóng tài khoản).
    • banned: bị khóa do vi phạm điều khoản, gian lận, chargeback...
    • pendingverification: đã đăng ký nhưng chưa xác thực email/phone.

    Có thể bổ sung các trường như bannedreason, bannedat, deactivatedat để phục vụ audit và hỗ trợ khách hàng. Khi thực hiện ẩn danh (anonymization), có thể giữ lại một số trường không nhận diện trực tiếp (ví dụ: thống kê) và xóa/mã hóa các trường nhận diện (email, phone, địa chỉ).

Việc phân tách các nhóm dữ liệu trên giúp dễ áp dụng các chính sách vòng đời dữ liệu (data retention), ví dụ: xóa hoặc ẩn danh thông tin nhận diện sau X năm không hoạt động, nhưng vẫn giữ dữ liệu đơn hàng đã được yêu cầu bởi quy định kế toán hoặc thuế.

Roles và permissions quản lý quyền admin, nhân viên kho, chăm sóc khách hàng và marketing

Mô hình Role-Based Access Control (RBAC) cho phép quản lý quyền truy cập một cách có cấu trúc, giảm rủi ro lạm dụng quyền và sai sót thao tác. Thay vì gán quyền trực tiếp cho từng user, hệ thống gán quyền cho role, sau đó gán role cho user. 

Sơ đồ mô hình RBAC phân quyền người dùng theo vai trò admin, nhân viên kho, CS, marketing và các quyền quản lý

Cấu trúc bảng điển hình:

  • roles:

    Bảng định nghĩa các vai trò trong hệ thống, ví dụ:

    • admin: có quyền quản trị cao nhất, nhưng vẫn nên tách biệt quyền cực kỳ nhạy cảm (như thay đổi cấu hình bảo mật) sang role riêng để giảm số người có quyền này.
    • warehousestaff: nhân viên kho, tập trung vào quản lý tồn kho, xuất nhập hàng, in phiếu giao.
    • customersupport: chăm sóc khách hàng, xem và chỉnh sửa một số thông tin đơn hàng, hỗ trợ đổi trả.
    • marketing: quản lý chiến dịch khuyến mãi, voucher, nội dung hiển thị, nhưng không được truy cập thông tin thanh toán chi tiết.

    Các trường cơ bản: id, name, description, createdat, updatedat.

  • permissions:

    Bảng định nghĩa các quyền chi tiết, càng cụ thể càng dễ kiểm soát. Ví dụ:

    • vieworders, editorders, cancelorders
    • manageinventory, adjuststock, viewstockcost
    • managepromotions, viewcustomerbasic, viewcustomersensitive
    • manageroles, managepermissions, viewauditlogs

    Quyền nên được đặt tên theo chuẩn nhất quán (ví dụ: resource:action như order:view, order:update) để dễ quản lý và kiểm tra.

  • rolepermissions:

    Bảng nối nhiều-nhiều giữa rolespermissions, gồm các trường: roleid, permissionid, grantedby, grantedat. Có thể thêm cờ isallowed để hỗ trợ mô hình cho phép/không cho phép (allow/deny) chi tiết hơn.

    Việc thay đổi quyền của một role sẽ tự động áp dụng cho tất cả user thuộc role đó, giúp quản trị tập trung và giảm lỗi cấu hình.

  • userroles:

    Bảng gán vai trò cho tài khoản nội bộ (staff, admin), gồm: userid, roleid, assignedby, assignedat. Một user có thể có nhiều role (ví dụ: vừa là warehousestaff vừa là customersupport), khi đó tập quyền hiệu lực là hợp (union) của tất cả quyền từ các role.

    Nên kết hợp RBAC với các ràng buộc bổ sung như attribute-based access control (ABAC) ở tầng ứng dụng, ví dụ: nhân viên kho chỉ xem được đơn hàng thuộc kho của mình, dù có quyền vieworders.

Thiết kế RBAC cần đi kèm cơ chế audit cho các thao tác thay đổi role và permission, vì đây là điểm nhạy cảm có thể bị lợi dụng để leo thang đặc quyền. Ngoài ra, nên áp dụng nguyên tắc least privilege: mỗi role chỉ được cấp quyền tối thiểu cần thiết để hoàn thành công việc.

Dữ liệu nhạy cảm như mật khẩu, token, thanh toán cần mã hóa hoặc không lưu khi không cần thiết

Dữ liệu nhạy cảm cần được xử lý theo các nguyên tắc bảo mật chặt chẽ, kết hợp giữa hashing, encryptiondata minimization. Mục tiêu là giảm thiểu tác hại nếu hệ thống bị xâm nhập hoặc rò rỉ dữ liệu.

Infographic bảo mật dữ liệu nhạy cảm và hệ thống audit logs với mã hóa, hash mật khẩu, token, cảnh báo gian lận

  • Mật khẩu:
    • Chỉ lưu dạng hash với thuật toán mạnh như bcrypt hoặc Argon2, cấu hình cost phù hợp để chống tấn công brute-force.
    • Mỗi mật khẩu cần salt riêng, thường được lưu kèm trong hash theo format của thư viện.
    • Không bao giờ lưu plaintext hoặc có cơ chế hiển thị lại mật khẩu cho người dùng.
    • Nên có cơ chế password history (lưu hash của một số mật khẩu cũ) nếu muốn ngăn người dùng đặt lại mật khẩu trùng lặp, nhưng vẫn chỉ lưu dạng hash.
  • Token đăng nhập:
    • Với token dạng JWT không lưu state, có thể không cần lưu trong DB, nhưng phải đặt exp ngắn và ký bằng khóa bí mật an toàn.
    • Nếu cần khả năng revoke (đặc biệt với refresh token), nên lưu token ở dạng hash trong bảng như usertokens với các trường: userid, tokenhash, type, expiresat, revokedat, ipaddress, useragent.
    • Không lưu token ở dạng plaintext để tránh lộ token nếu DB bị truy cập trái phép.
  • Thông tin thẻ thanh toán:
    • Không lưu số thẻ đầy đủ (PAN) và CVV trong hệ thống nếu không bắt buộc phải tuân thủ chuẩn PCI-DSS ở mức cao.
    • Chỉ lưu token hoặc payment method id do cổng thanh toán (payment gateway) cung cấp, cùng với một số thông tin không nhạy cảm như 4 số cuối, loại thẻ, tháng/năm hết hạn (nếu cần hiển thị cho người dùng).
    • Thông tin này nên được phân quyền chặt chẽ, chỉ cho phép một số service truy cập, và không hiển thị đầy đủ cho nhân viên nội bộ.
  • Dữ liệu cá nhân:

    Với các trường nhạy cảm như số CMND/CCCD, hộ chiếu, mã số thuế, hoặc thông tin tài chính, nên áp dụng field-level encryption:

    • Mã hóa từng trường bằng khóa riêng, có thể sử dụng KMS (Key Management Service) để quản lý vòng đời khóa.
    • Chỉ giải mã ở tầng ứng dụng khi thực sự cần, và chỉ cho phép một số role truy cập.
    • Hạn chế log giá trị plaintext của các trường này; nếu cần log, chỉ log dạng đã mask (ví dụ: chỉ hiển thị 3–4 ký tự cuối).

Bên cạnh đó, cần xây dựng chính sách backuprestore rõ ràng, đảm bảo backup cũng được mã hóa, lưu trữ tách biệt và kiểm soát truy cập. Hệ thống logging phải tránh ghi log dữ liệu nhạy cảm, đồng thời ghi nhận đầy đủ các sự kiện truy cập, thay đổi dữ liệu quan trọng để phục vụ điều tra khi có sự cố.

Audit logs ghi lại thao tác sửa đơn, sửa giá, đổi tồn kho và thay đổi quyền truy cập

Bảng auditlogs là thành phần cốt lõi trong chiến lược observabilitygovernance của hệ thống. Audit logs cho phép truy vết mọi thao tác quan trọng, hỗ trợ phát hiện gian lận nội bộ, phân tích sự cố và chứng minh tính minh bạch với đối tác hoặc cơ quan quản lý.

Giao diện tính năng audit logs ghi lại sửa đơn, sửa giá, đổi tồn kho, thay đổi quyền và truy vết phân tích hệ thống

  • userid:

    Định danh người thực hiện thao tác, thường là internaluserid (admin, nhân viên). Với thao tác tự động (job, service), có thể dùng một user kỹ thuật hoặc trường servicename riêng để phân biệt.

  • action:

    Loại hành động, nên chuẩn hóa theo danh sách cố định, ví dụ:

    • updateorder: sửa thông tin đơn hàng (địa chỉ, ghi chú, trạng thái).
    • changeinventory: điều chỉnh tồn kho (nhập, xuất, điều chỉnh chênh lệch).
    • changeprice: thay đổi giá sản phẩm, giá khuyến mãi.
    • changerole: gán, thu hồi role hoặc permission của user nội bộ.

    Việc chuẩn hóa giúp dễ lọc, thống kê và phát hiện mẫu hành vi bất thường (ví dụ: một user thực hiện quá nhiều changeprice trong thời gian ngắn).

  • entitytype, entityid:

    Xác định đối tượng bị tác động, ví dụ: entitytype = 'order', entityid = 12345; hoặc entitytype = 'product', entityid = 6789. Có thể dùng enum hoặc bảng riêng để quản lý các loại entity hợp lệ.

  • beforedata, afterdata:

    Snapshot dữ liệu trước và sau khi thay đổi, thường lưu dạng JSON. Một số lưu ý:

    • Chỉ lưu các trường liên quan đến thay đổi, tránh lưu toàn bộ record nếu không cần để giảm dung lượng và tránh lộ dữ liệu nhạy cảm.
    • Không lưu các trường cực kỳ nhạy cảm (mật khẩu, token, số thẻ) trong audit logs, hoặc nếu bắt buộc phải lưu thì phải được mask/mã hóa.
    • Có thể lưu thêm diff (chỉ các trường thay đổi) để dễ đọc và phân tích.
  • createdat, ipaddress, useragent:

    createdat ghi lại thời điểm thao tác; ipaddressuseragent giúp xác định nguồn truy cập, hỗ trợ phát hiện truy cập bất thường (ví dụ: admin đăng nhập từ IP lạ, thiết bị lạ).

Audit logs nên được ghi append-only, không cho phép chỉnh sửa hoặc xóa trực tiếp từ giao diện thông thường. Nếu cần xóa theo quy định pháp lý, nên có quy trình riêng, được log lại đầy đủ. Ngoài ra, nên tách cơ sở dữ liệu audit khỏi cơ sở dữ liệu giao dịch chính hoặc ít nhất là tách schema, phân quyền truy cập chặt chẽ để tránh việc người có quyền trên dữ liệu giao dịch cũng có thể xóa dấu vết audit.

Khi kết hợp với hệ thống cảnh báo (alerting), audit logs có thể được dùng để thiết lập các rule như: cảnh báo khi có nhiều thao tác changeinventory vượt ngưỡng trong ngày, hoặc khi một user marketing thực hiện hành động không thuộc phạm vi role (ví dụ: cố gắng update_order), từ đó tăng cường khả năng phát hiện sớm rủi ro và gian lận.

Thiết kế dữ liệu SEO, nội dung và marketing cho website bán hàng

Thiết kế dữ liệu SEO cho website bán hàng cần xem mỗi trang sản phẩm, danh mục, landing, blog, page và FAQ như một SEO unit độc lập, có đầy đủ title, meta description, slug, canonical, index status và khả năng mở rộng schema markup. Nên chuẩn hóa metadata vào bảng chung để quản lý tập trung, tái sử dụng logic và dễ mở rộng khi thêm entity mới, đồng thời cân nhắc hiệu năng JOIN bằng cache, denormalization hoặc materialized view. Hệ thống nội dung hỗ trợ (blog, hướng dẫn, FAQ) phải được liên kết chặt với sản phẩm/danh mục qua các bảng quan hệ để tăng topical authority và internal linking. Bên cạnh đó, cần lớp redirects để bảo toàn traffic khi đổi URL và lớp event tracking append-only để phân tích hành vi, hiệu quả marketing và tối ưu funnel.

Cấu trúc SEO và nội dung website e commerce với tối ưu onpage, chuyển hướng URL và theo dõi sự kiện chuyển đổi

Products, categories và landing pages cần trường title, meta description, slug, canonical và index status

Ở tầng dữ liệu, mỗi thực thể hiển thị ra giao diện (product, category, landing page) nên được thiết kế như một “SEO unit” độc lập, có đầy đủ metadata để công cụ tìm kiếm hiểu rõ nội dung và mối quan hệ của trang. Tối thiểu, mỗi bản ghi cần các trường sau:

  • title: tiêu đề trang, thường map với thẻ <title> trong HTML. Nên cho phép:
    • Độ dài linh hoạt (tối thiểu 255 ký tự để tránh cắt cụt khi sinh tự động).
    • Hỗ trợ đa ngôn ngữ (nếu site đa ngôn ngữ) bằng cách tách bảng hoặc thêm cột locale.
    • Override so với tên sản phẩm/danh mục mặc định (không bắt buộc giống name).
  • metadescription: mô tả meta, map với thẻ <meta name="description">:
    • Nên cho phép độ dài khoảng 320 ký tự để linh hoạt A/B test.
    • Có thể thêm cột autogenerated (boolean) để phân biệt mô tả do hệ thống sinh hay do SEO chỉnh tay.
  • slug: phần URL thân thiện, dùng cho routing:
    • Cần unique trong phạm vi từng entity type (product, category, page) hoặc unique toàn hệ thống tùy chiến lược URL.
    • Nên có cột slughistory hoặc bảng riêng để lưu các slug cũ phục vụ redirect tự động.
    • Hỗ trợ chuẩn hóa: lowercase, bỏ dấu, thay khoảng trắng bằng dấu gạch ngang.
  • canonicalurl: URL canonical khi có nhiều phiên bản nội dung:
    • Dùng cho các trường hợp filter, sort, phân trang, hoặc sản phẩm xuất hiện ở nhiều danh mục.
    • Có thể lưu dạng full URL (absolute) để dễ tích hợp nhiều domain/subdomain.
    • Nên có logic ưu tiên: nếu null thì sinh canonical từ slug chính.
  • indexstatus: điều khiển index/noindex, follow/nofollow:
    • Có thể thiết kế dạng enum: INDEXFOLLOW, NOINDEXFOLLOW, INDEXNOFOLLOW, NOINDEXNOFOLLOW.
    • Cho phép override theo từng bản ghi, nhưng vẫn có default theo loại entity (ví dụ: product mặc định index, trang filter mặc định noindex).
    • Có thể map trực tiếp sang thẻ <meta name="robots"> hoặc header HTTP.

Cấu trúc SEO dữ liệu tập trung với bảng seometadata liên kết product category landing page title slug canonical meta description index status

Một hướng thiết kế phổ biến là tách riêng bảng seometadata dùng chung cho nhiều entity (product, category, landing page, blog post, v.v.). Cấu trúc cơ bản:

  • entitytype: kiểu thực thể, ví dụ: product, category, landingpage, blogpost.
  • entityid: khóa ngoại trỏ đến bản ghi cụ thể trong bảng tương ứng.
  • title, metadescription, canonicalurl, indexstatus, slug.

Cách thiết kế này mang lại một số lợi ích chuyên môn:

  • Quản lý SEO tập trung: đội SEO có thể thao tác trên một bảng duy nhất, dễ xây dựng CMS nội bộ cho metadata.
  • Tái sử dụng logic: middleware, service SEO, hoặc API có thể dùng chung cho mọi entity mà không cần viết riêng từng loại.
  • Dễ mở rộng: khi thêm entity mới (brand, collection, tag), chỉ cần thêm giá trị mới cho entitytype mà không phải sửa schema SEO.

Tuy nhiên, nhược điểm là tăng số lượng JOIN khi truy vấn, đặc biệt ở trang listing nhiều sản phẩm. Một số điểm cần cân nhắc theo quy mô hệ thống:

  • Với hệ thống nhỏ hoặc vừa, JOIN thêm 1 bảng thường không phải vấn đề nếu có index tốt trên (entitytype, entityid).
  • Với hệ thống lớn, traffic cao, có thể:
    • Dùng denormalization: cache metadata vào search index (Elasticsearch, OpenSearch) hoặc vào bảng read-only tối ưu cho đọc.
    • Dùng cơ chế materialized view hoặc job đồng bộ để giảm JOIN runtime.
  • Cần index composite trên (entitytype, entityid) và có thể thêm index riêng cho slug để hỗ trợ routing nhanh.

Blog posts, pages và FAQ hỗ trợ nội dung SEO, hướng dẫn mua hàng và topical authority

Để xây dựng topical authority và hỗ trợ hành trình mua hàng, hệ thống nội dung cần được thiết kế như một graph liên kết giữa sản phẩm, danh mục và nội dung hỗ trợ. Ba nhóm bảng quan trọng:

  • blogposts: bài viết chia sẻ kiến thức, review, so sánh, hướng dẫn sử dụng:
    • Các trường cốt lõi:
      • id, title, slug, metadescription, canonicalurl, indexstatus.
      • contenthtml hoặc contentrichtext để lưu nội dung chi tiết.
      • excerpt (tóm tắt) phục vụ listing và thẻ meta og:description.
      • publishedat, updatedat, authorid, status (draft, scheduled, published).
    • Nên hỗ trợ schema markup (Article, BlogPosting) bằng một cột schemajson để dễ render JSON-LD.
Sơ đồ hệ thống nội dung hỗ trợ SEO với blog posts, product, pages, FAQ, category và phân tích hiệu suất
  • pages: trang tĩnh như giới thiệu, chính sách, hướng dẫn:
    • Thường có cấu trúc tương tự blogposts nhưng ít thay đổi hơn.
    • Có thể thêm cột templatekey để map với layout front-end (policy, about, landing, v.v.).
    • SEO metadata tương tự: title, slug, metadescription, canonicalurl, indexstatus.
  • faqs: câu hỏi thường gặp:
    • Có thể thiết kế:
      • Bảng faqcategories (nhóm chủ đề: thanh toán, vận chuyển, bảo hành).
      • Bảng faqs với các trường: question, answerhtml, slug, sortorder, isfeatured.
    • Để hỗ trợ SEO, có thể thêm cột schemajson cho FAQPage schema (JSON-LD) giúp tăng khả năng hiển thị rich result.

Các bảng này nên có cấu trúc hỗ trợ SEO tương tự products/categories (title, slug, meta, canonical, indexstatus) và có khả năng mở rộng cho các loại schema markup khác nhau (Article, FAQPage, HowTo, Product). Một cách linh hoạt là dùng cột schemajson kiểu JSON để lưu cấu trúc schema đã được chuẩn hóa.

Để tăng liên quan chủ đề (topical authority) và hỗ trợ internal linking, cần thiết kế các bảng quan hệ:

  • postproductrelations:
    • postid: khóa ngoại tới blogposts.
    • productid: khóa ngoại tới products.
    • relationtype (optional): ví dụ review, comparison, guide, giúp phân tích sâu hơn.
    • sortorder: thứ tự hiển thị sản phẩm liên quan trong bài viết.
  • postcategoryrelations:
    • Cho phép gắn bài viết với một hoặc nhiều danh mục sản phẩm.
    • Hỗ trợ xây dựng hub nội dung theo chủ đề (category page <-> blog cluster).
  • faqproductrelations hoặc faqcategoryrelations:
    • Gắn FAQ với sản phẩm/danh mục cụ thể, giúp hiển thị Q&A ngay trên trang sản phẩm.
    • Có thể dùng để sinh structured data Q&A cho từng product page.

Về mặt triển khai, nên chuẩn hóa các quan hệ này ở tầng database để:

  • Dễ query cho SEO audit (ví dụ: sản phẩm nào chưa có bài review, danh mục nào thiếu bài hướng dẫn).
  • Dễ xây dựng recommendation nội dung (“bài viết liên quan”, “hướng dẫn sử dụng cho sản phẩm này”).
  • Hỗ trợ phân tích hiệu quả nội dung: bài viết nào đóng góp nhiều view product, add to cart, purchase.

Redirects lưu URL cũ, URL mới và mã chuyển hướng khi đổi slug hoặc migration

Khi thay đổi cấu trúc URL (đổi slug, đổi pattern URL, migration sang nền tảng mới), việc mất redirect sẽ gây lỗi 404, mất backlink và giảm mạnh organic traffic. Vì vậy, cần một bảng redirects được thiết kế đủ linh hoạt để xử lý nhiều kịch bản.

Sơ đồ quản lý redirect URL với bảng redirect, mã status 301 302, tránh lỗi 404 và chuỗi chuyển hướng

  • oldurl: URL cũ:
    • Nên lưu dạng path (ví dụ: /dien-thoai/iphone-13) thay vì full domain để dễ dùng cho nhiều môi trường (dev, staging, production).
    • Cần index unique trên oldurl để tra cứu nhanh trong routing layer.
  • newurl: URL mới:
    • Cũng nên lưu dạng path, trừ khi có nhu cầu redirect cross-domain.
    • Có thể cho phép null trong trường hợp muốn trả về 410 (Gone) thay vì redirect.
  • statuscode: mã chuyển hướng:
    • 301 (permanent): dùng khi thay đổi URL vĩnh viễn, quan trọng cho SEO.
    • 302 (temporary): dùng cho test, chiến dịch tạm thời, hoặc khi chưa chắc chắn cấu trúc mới.
    • Có thể mở rộng thêm 307/308 nếu cần, nhưng 301/302 là phổ biến nhất.
  • createdat, expiresat:
    • createdat: phục vụ audit, rollback, và dọn dẹp redirect cũ.
    • expiresat: nếu có, hệ thống routing có thể tự động vô hiệu hóa redirect sau một thời gian.

Luồng xử lý trong hệ thống routing thường như sau:

  • Request đến một URL, router cố gắng match với route hiện tại (product, category, page, blog, v.v.).
  • Nếu không tìm thấy, hệ thống query bảng redirects theo oldurl:
    • Nếu có bản ghi và expiresat chưa hết hạn (hoặc null), trả về redirect với statuscode tương ứng và header Location = newurl.
    • Nếu không có, trả về 404.

Một số điểm chuyên sâu cần lưu ý:

  • Nên có cơ chế bulk generation redirect khi migration (ví dụ: map slug cũ sang slug mới theo rule) và lưu vào bảng redirects.
  • Tránh tạo redirect chain (A → B → C). Khi thay đổi lần nữa, nên cập nhật redirect cũ để trỏ thẳng A → C.
  • Có thể thêm cột sourcetype (manual, autoslugchange, migrationscript) để phân loại nguồn gốc redirect, hỗ trợ quản trị.

Tracking events lưu lượt xem sản phẩm, thêm giỏ, checkout, mua hàng và nguồn chiến dịch

Để phân tích hành vi người dùng, tối ưu funnel và đo lường hiệu quả marketing, hệ thống cần một lớp event tracking độc lập với dữ liệu giao dịch. Bảng events là trung tâm, nhưng nên được thiết kế theo hướng append-only và tối ưu cho phân tích.

Sơ đồ tracking event thương mại điện tử từ xem sản phẩm đến hoàn tất mua hàng và đo lường marketing

  • id: khóa chính, có thể dùng UUID để dễ merge dữ liệu từ nhiều nguồn.
  • customerid (nullable):
    • Null với user chưa đăng nhập, có giá trị khi user đã login.
    • Giúp phân tích hành vi theo từng khách hàng, LTV, cohort.
  • sessionid:
    • Định danh phiên truy cập, thường sinh ở front-end và gửi kèm mọi event.
    • Cho phép phân tích hành vi trong một session: từ viewproduct → addtocart → purchase.
  • eventname:
    • Một số event cốt lõi:
      • viewproduct: xem sản phẩm.
      • addtocart: thêm vào giỏ.
      • begincheckout: bắt đầu checkout.
      • purchase: hoàn tất mua hàng.
    • Có thể mở rộng thêm: viewcategory, search, viewblogpost, clickbanner, v.v.
  • eventtime:
    • Lưu theo UTC để dễ phân tích đa múi giờ.
    • Nên index kết hợp với eventname để tối ưu query theo khoảng thời gian.
  • propertiesjson:
    • Lưu các thuộc tính linh hoạt dạng JSON, ví dụ:
      • productid, categoryid, quantity, price, currency.
      • campaign, medium, source, term, content (UTM parameters).
      • devicetype, browser, os, referrerurl, landingpage.
    • Cách này cho phép mở rộng schema event mà không phải thay đổi cấu trúc bảng.

Với lượng dữ liệu lớn, việc lưu toàn bộ events trong database giao dịch chính (OLTP) sẽ gây áp lực về dung lượng và hiệu năng. Một số chiến lược kiến trúc:

  • Logging system riêng:
    • Gửi event vào message queue (Kafka, RabbitMQ) hoặc log collector, sau đó đẩy vào data warehouse (BigQuery, Snowflake, Redshift).
    • Database chính chỉ lưu các event quan trọng hoặc đã được tổng hợp (aggregated metrics).
  • Data warehouse:
    • Lưu toàn bộ events dạng append-only, tối ưu cho truy vấn phân tích (OLAP).
    • Có thể xây dựng bảng factevents và các bảng dimension (dimcustomer, dimproduct, dimcampaign) để phục vụ BI.
  • Schema logic rõ ràng:
    • Dù dùng JSON linh hoạt, vẫn nên chuẩn hóa key cho từng loại event (event contract) để tránh dữ liệu bừa bộn.
    • Có thể duy trì tài liệu schema event (eventname → required/optional properties) cho team marketing và data.

Việc thiết kế tracking events tốt giúp:

  • Đo lường chính xác hiệu quả từng kênh marketing (source/medium/campaign) đến từng hành vi (view, addto_cart, purchase).
  • Phân tích funnel: tỷ lệ rơi rụng giữa các bước, tối ưu UX và nội dung.
  • Xây dựng mô hình gợi ý (recommendation) và cá nhân hóa dựa trên lịch sử hành vi.

Tối ưu hiệu năng database cho website bán hàng nhiều sản phẩm và nhiều đơn

Hệ thống bán hàng nhiều sản phẩm và đơn cần tối ưu database theo hướng thực dụng, bám sát pattern truy vấn thực tế thay vì chỉ dựa vào phỏng đoán. Trọng tâm là thiết kế index hợp lý cho các bảng chính như products, categories, customers, orders, orderitems và inventoryitems, ưu tiên các cột truy vấn thường xuyên: SKU, slug, categoryid, customerid, orderstatus, created_at. Song song, cần tối ưu truy vấn danh mục với phân trang (ưu tiên keyset pagination), lọc theo thuộc tính, sắp xếp gắn với index phù hợp và tách bớt logic sang bảng phụ, cache hoặc search engine. Cuối cùng, kết hợp nhiều lớp cache và kiến trúc phân tán (read replica, search engine, message queue) để đảm bảo hiệu năng khi traffic và số đơn tăng mạnh.

Infographic tối ưu hiệu năng database bán hàng với index hợp lý, cache dữ liệu và kiến trúc phân tán

Index cần đặt cho SKU, slug, categoryid, customerid, orderstatus và createdat

Index là yếu tố then chốt để đảm bảo hiệu năng truy vấn khi dữ liệu lớn, đặc biệt với website bán hàng có hàng trăm nghìn sản phẩm và hàng triệu đơn. Cần thiết kế index dựa trên pattern truy vấn thực tế, không chỉ dựa trên trực giác. Các nhóm bảng và cột nên được index và tối ưu như sau:

  • products:
    • sku (unique): thường dùng để tra cứu nhanh một sản phẩm cụ thể (từ ERP, kho, hoặc khi nhập đơn). Nên tạo unique index trên sku để vừa đảm bảo toàn vẹn dữ liệu, vừa tối ưu truy vấn dạng WHERE sku = ?.
    • slug (unique): dùng cho SEO URL, truy vấn dạng WHERE slug = ? cho trang chi tiết sản phẩm. Cũng nên là unique index để tránh trùng URL.
    • status: thường lọc theo trạng thái (active, draft, outofstock). Nếu chỉ lọc đơn giản theo status, có thể dùng index đơn cột; nhưng trong thực tế thường kết hợp với category, price, createdat, nên ưu tiên index tổng hợp.
    • categoryid (thông qua bảng productcategories): với mô hình nhiều-nhiều, bảng liên kết productcategories(productid, categoryid) nên có index:
      • (categoryid, productid) để phục vụ listing sản phẩm theo danh mục.
      • (productid, categoryid) để phục vụ các thao tác đồng bộ, xóa, cập nhật theo sản phẩm.
    • createdat: thường dùng để sắp xếp “mới nhất”, lọc theo khoảng thời gian. Nên index kết hợp với các cột lọc chính như (status, createdat DESC) hoặc (categoryid, createdat DESC) (thông qua bảng liên kết).

Minh họa các cột dữ liệu quan trọng cần index trong hệ thống bán hàng online như SKU, slug, createdat, categoryid

  • categories:
    • slug (unique): dùng cho URL danh mục, truy vấn dạng WHERE slug = ?. Unique index giúp đảm bảo mỗi slug ánh xạ một danh mục duy nhất.
    • parentid: phục vụ xây dựng cây danh mục, load danh mục con. Nên có index trên parentid để tối ưu truy vấn WHERE parentid = ?. Nếu thường xuyên sắp xếp theo sortorder, có thể dùng index tổng hợp (parentid, sortorder).
  • customers:
    • email (unique): dùng cho đăng nhập, tìm kiếm khách hàng, đồng bộ CRM. Unique index là bắt buộc để tránh trùng email.
    • phone: thường dùng để tìm nhanh khách hàng khi chăm sóc hoặc tạo đơn. Nên index nếu tần suất tìm theo số điện thoại cao; có thể cân nhắc partial index (nếu DB hỗ trợ) cho các bản ghi active.
    • createdat: dùng cho báo cáo tăng trưởng khách hàng, lọc theo thời gian đăng ký. Index trên createdat hoặc (createdat, status) nếu có trạng thái.
  • orders:
    • ordernumber (unique): mã đơn hiển thị cho khách và nội bộ. Unique index giúp truy vấn cực nhanh theo mã đơn.
    • customerid: dùng để lấy lịch sử đơn hàng của khách. Nên có index (customerid, createdat DESC) để vừa lọc theo khách, vừa sắp xếp đơn mới nhất.
    • status: thường dùng trong dashboard, báo cáo, xử lý workflow (pending, paid, shipped, cancelled...). Thực tế thường truy vấn theo status kết hợp createdat hoặc paymentstatus, nên ưu tiên index tổng hợp như (status, createdat).
    • createdat: dùng cho báo cáo theo ngày, tuần, tháng; phân trang đơn mới nhất. Index trên createdat hoặc kết hợp với status giúp tối ưu các truy vấn báo cáo.
  • orderitems:
    • orderid: bắt buộc index để load chi tiết sản phẩm trong đơn: WHERE orderid = ?. Thường là foreign key index.
    • productid, variantid: dùng cho báo cáo bán hàng theo sản phẩm/biến thể, đồng bộ tồn kho, phân tích top-selling. Nên có index:
      • (productid) hoặc (productid, variantid) nếu thường xuyên phân tích theo cả hai.
      • (variantid) nếu tồn kho và báo cáo chi tiết theo biến thể.
  • inventoryitems:
    • warehouseid, variantid: thường truy vấn tồn kho theo kho và biến thể. Nên có index tổng hợp (warehouseid, variantid) để tối ưu WHERE warehouseid = ? AND variantid IN (...).
    • Nếu thường xuyên tổng hợp tồn kho theo biến thể trên tất cả kho, có thể thêm index (variantid) để tối ưu các truy vấn báo cáo.

Cần tránh lạm dụng index trên quá nhiều cột vì mỗi index làm tăng chi phí ghi (INSERT/UPDATE/DELETE) và chi phí lưu trữ. Chiến lược tốt là:

  • Thu thập query log thực tế (slow query log, application log) để xác định các truy vấn chậm và tần suất cao.
  • Dùng EXPLAIN / EXPLAIN ANALYZE để xem kế hoạch thực thi, kiểm tra xem index có được sử dụng hay không.
  • Ưu tiên index cho:
    • Các truy vấn chạy nhiều lần mỗi request (ví dụ: load sản phẩm trong danh mục, load đơn của khách).
    • Các truy vấn báo cáo nặng nhưng chạy định kỳ có thể tối ưu bằng materialized view hoặc bảng tổng hợp thay vì thêm quá nhiều index.
  • Định kỳ review index, loại bỏ index trùng lặp hoặc ít được sử dụng (dựa trên thống kê của DBMS).

Truy vấn danh mục cần tối ưu phân trang, lọc, sắp xếp và tìm kiếm sản phẩm

Trang danh mục là nơi chịu tải lớn nhất trên website bán hàng, vì mỗi lần người dùng thay đổi bộ lọc hoặc trang, hệ thống phải truy vấn trên tập dữ liệu sản phẩm rất lớn. Cần tối ưu đồng thời các khía cạnh: phân trang, lọc, sắp xếp và tìm kiếm.

Minh họa tối ưu truy vấn danh mục sản phẩm với phân trang, bộ lọc, sắp xếp linh hoạt và tìm kiếm tức thì

  • Pagination:
    • LIMIT/OFFSET: phù hợp khi số trang không quá lớn. Tuy nhiên, với OFFSET lớn (ví dụ trang 1000), DB phải bỏ qua rất nhiều bản ghi, gây chậm.
    • Keyset pagination (cursor-based): dùng điều kiện như WHERE (price, id) > (?, ?) hoặc WHERE createdat < ? để phân trang dựa trên giá trị cột đã sắp xếp. Cách này hiệu quả hơn nhiều khi dữ liệu lớn vì tránh scan và skip số lượng lớn bản ghi.
    • Nên kết hợp với index phù hợp, ví dụ: (categoryid, createdat DESC, id) nếu phân trang theo “mới nhất” trong một danh mục.
  • Lọc theo thuộc tính:
    • Chuẩn hóa và index các trường dùng để lọc như brandid, price, maincolor. Với các thuộc tính dạng nhiều giá trị (size, color, tag), nên dùng bảng phụ (attribute tables) thay vì lưu JSON nếu cần lọc mạnh.
    • Với lọc theo khoảng giá, nên có index trên price hoặc index tổng hợp (categoryid, price) để tối ưu truy vấn WHERE categoryid = ? AND price BETWEEN ? AND ?.
    • Nếu dùng JSON/JSONB (PostgreSQL) cho thuộc tính linh hoạt, cần index GIN/GiST phù hợp cho các key thường xuyên lọc; tránh lọc tự do trên JSON không index.
  • Sắp xếp (ORDER BY):
    • Các kiểu sắp xếp phổ biến: theo giá tăng/giảm, theo sản phẩm mới nhất, theo bán chạy, theo độ ưu tiên (boost).
    • Để ORDER BY hiệu quả, index nên phù hợp với thứ tự sắp xếp. Ví dụ:
      • (categoryid, price ASC) cho sắp xếp theo giá trong danh mục.
      • (categoryid, createdat DESC) cho sắp xếp theo mới nhất.
    • Sắp xếp theo “bán chạy” thường dựa trên dữ liệu tổng hợp (salesrank, totalsold). Nên precompute các giá trị này vào bảng phụ hoặc cột riêng, sau đó index trên cột đó thay vì tính động mỗi lần.
  • Tối ưu join và cấu trúc dữ liệu:
    • Tránh join quá nhiều bảng trong một truy vấn danh mục (products + categories + brands + attributes + inventory + promotion...). Mỗi join làm tăng độ phức tạp và chi phí.
    • Có thể precompute một số dữ liệu vào bảng phụ, ví dụ:
      • Bảng productsearchindex chứa các trường đã denormalize: categoryids, brandid, price, maincolor, stockstatus, salesrank.
      • Các bảng tổng hợp cho filter (faceted search) như số lượng sản phẩm theo brand, khoảng giá.
    • Dùng cache (Redis) cho kết quả filter phổ biến (ví dụ: top category, brand phổ biến) để giảm truy vấn trực tiếp vào bảng sản phẩm.
  • Tìm kiếm full-text:
    • Đối với tìm kiếm full-text phức tạp (tìm theo tên, mô tả, thuộc tính, lỗi chính tả, gợi ý), nên dùng search engine chuyên dụng như Elasticsearch, OpenSearch, hoặc PostgreSQL full-text search thay vì chỉ dùng LIKE '%keyword%'.
    • Search engine cho phép:
      • Phân tích từ (tokenization), stemming, synonym.
      • Scoring theo độ liên quan, boost theo category/brand.
      • Faceted search (đếm số lượng theo filter) hiệu quả hơn rất nhiều so với SQL thuần.
    • Database giao dịch (OLTP) nên tập trung vào lưu trữ và truy vấn chính xác; phần tìm kiếm, gợi ý, ranking nên đẩy sang search engine để giảm tải.

Cache dữ liệu ít đổi như danh mục, cấu hình, menu và landing page để giảm tải database

Các dữ liệu ít thay đổi như danh mục, menu, cấu hình website, landing page thường được truy cập rất nhiều lần nhưng cập nhật ít. Tận dụng cache giúp giảm đáng kể số lượng truy vấn đến database và cải thiện thời gian phản hồi.

Sơ đồ tối ưu cơ sở dữ liệu bằng cache ứng dụng, cache HTTP CDN và sinh trang tĩnh để tăng hiệu năng hệ thống

  • Application cache:
    • Dùng Redis hoặc in-memory cache (Memcached, cache nội bộ framework) để lưu kết quả truy vấn thường xuyên lặp lại: cây danh mục, danh sách menu, cấu hình site, block nội dung landing.
    • Áp dụng mô hình cache-aside: ứng dụng đọc từ cache, nếu không có thì truy vấn DB, sau đó ghi vào cache.
    • Thiết lập TTL hợp lý (ví dụ 5–30 phút) cho dữ liệu ít đổi; với dữ liệu gần như tĩnh (menu, cấu hình), có thể dùng TTL dài và cơ chế cache invalidation khi có thay đổi.
  • HTTP cache:
    • Dùng CDN hoặc reverse proxy (Varnish, Nginx) để cache response HTML hoặc JSON cho các trang public như trang chủ, landing page, danh mục phổ biến.
    • Sử dụng header HTTP như Cache-Control, ETag, Last-Modified để trình duyệt và CDN có thể cache hiệu quả.
    • Với API JSON cho mobile/web, có thể cache các endpoint ít thay đổi (danh mục, cấu hình) ở layer CDN để giảm tải backend.
  • Static generation:
    • Với một số trang ít thay đổi (landing page, trang giới thiệu, bộ sưu tập tĩnh), có thể generate sẵn HTML tĩnh (static site generation) và phục vụ trực tiếp từ CDN hoặc web server.
    • Trong kiến trúc headless, có thể build trước các trang danh mục chính, trang sản phẩm hot, sau đó chỉ cập nhật lại khi có thay đổi lớn (rebuild theo schedule hoặc event-driven).

Chiến lược cache hiệu quả thường kết hợp nhiều lớp: cache trong ứng dụng, cache ở HTTP/CDN, và đôi khi cache ngay tại search engine. Cần cân bằng giữa độ tươi mới của dữ liệu và hiệu năng, đồng thời thiết kế cơ chế invalidation rõ ràng khi dữ liệu gốc thay đổi (ví dụ: cập nhật giá, ẩn/hiện sản phẩm).

Tách read replica, search engine hoặc queue khi traffic và đơn hàng tăng mạnh

Khi hệ thống phát triển, lượng truy vấn đọc/ghi tăng mạnh, cần chuyển từ kiến trúc đơn giản (single database) sang kiến trúc phân tán để đảm bảo khả năng mở rộng và độ ổn định.

Sơ đồ kiến trúc tách biệt khi tăng trưởng với read replica, search engine và message queue trong hệ thống backend

  • Read replica:
    • Tách database đọc (replica) và ghi (master) để giảm tải truy vấn đọc nặng như báo cáo, listing, thống kê. Ứng dụng sẽ ghi vào master, và đọc các truy vấn không yêu cầu tính nhất quán tức thời từ replica.
    • Cần lưu ý độ trễ replication: dữ liệu trên replica có thể chậm hơn master vài mili-giây đến vài giây. Không nên dùng replica cho các thao tác yêu cầu dữ liệu vừa ghi phải thấy ngay (ví dụ: hiển thị đơn vừa tạo cho khách).
    • Phân loại truy vấn trong code:
      • Truy vấn critical (sau khi thanh toán, cập nhật tồn kho) đọc từ master.
      • Truy vấn báo cáo, listing chung, trang public đọc từ replica.
  • Search engine:
    • Dùng Elasticsearch, Solr hoặc dịch vụ search để xử lý tìm kiếm và lọc phức tạp, giảm tải cho database giao dịch. Dữ liệu sản phẩm, thuộc tính, ranking được đồng bộ sang search index.
    • Thiết kế pipeline đồng bộ:
      • Đồng bộ gần real-time qua message queue khi sản phẩm thay đổi.
      • Định kỳ reindex toàn bộ hoặc từng phần để đảm bảo index sạch và tối ưu.
    • Database chính tập trung vào tính toàn vẹn và giao dịch; search engine tập trung vào truy vấn phức tạp, full-text, faceted search.
  • Message queue:
    • Dùng RabbitMQ, Kafka, SQS để xử lý bất đồng bộ các tác vụ như gửi email, cập nhật báo cáo, đồng bộ với hệ thống khác, cập nhật search index, tính điểm loyalty.
    • Thay vì thực hiện mọi thứ trong transaction của đơn hàng (gây chậm và dễ lỗi), chỉ ghi sự kiện (event) vào queue, sau đó worker xử lý nền.
    • Giảm tải database bằng cách gom các thao tác ghi nặng (ví dụ: ghi log chi tiết, thống kê) thành batch hoặc ghi vào kho dữ liệu riêng (data warehouse).

Thiết kế database ngay từ đầu nên tính đến khả năng tách này bằng cách giữ schema rõ ràng, khóa chính ổn định (thường là ID dạng số hoặc UUID), và tránh phụ thuộc chặt chẽ vào các tính năng đặc thù của một engine duy nhất nếu có kế hoạch mở rộng đa nền tảng. Việc chuẩn hóa event (ordercreated, productupdated, inventory_changed) giúp dễ dàng tích hợp với queue, search engine và các hệ thống vệ tinh khác.

Sai lầm thường gặp khi thiết kế database website bán hàng

Thiết kế database cho website bán hàng thường gặp nhiều sai lầm khiến hệ thống khó mở rộng, khó bảo trì và dễ sai lệch dữ liệu. Việc nhồi nhét sản phẩm, biến thể, ảnh, thuộc tính vào một bảng duy nhất làm truy vấn phức tạp, index kém hiệu quả và gần như không thể đảm bảo ràng buộc toàn vẹn. Bên cạnh đó, không lưu snapshot đơn hàng (giá, tên sản phẩm, khuyến mãi, phí vận chuyển) làm sai lịch sử giao dịch, gây rủi ro kế toán và pháp lý. Thiếu transaction và API idempotent trong luồng tạo đơn – thanh toán – trừ tồn kho dẫn đến oversell, thanh toán thành công nhưng không có đơn. Cuối cùng, bỏ qua foreign key, unique constraint và index khiến dữ liệu bẩn, truy vấn chậm, rất khó sửa khi hệ thống đã vận hành lớn.

Minh họa các sai lầm thiết kế database cho website bán hàng như gộp dữ liệu sản phẩm, thiếu snapshot đơn hàng

Gộp sản phẩm, biến thể, ảnh và thuộc tính vào một bảng khiến dữ liệu khó mở rộng

Một lỗi thiết kế khá “kinh điển” trong các hệ thống bán hàng là cố gắng nhét toàn bộ thông tin liên quan đến sản phẩm vào một bảng duy nhất, ví dụ: products chứa luôn biến thể, ảnh, thuộc tính, giá, tồn kho, thậm chí cả metadata dạng JSON. Cách làm này thường xuất phát từ mong muốn “làm nhanh”, nhưng về lâu dài sẽ gây ra nhiều vấn đề nghiêm trọng về hiệu năng, khả năng mở rộng và tính toàn vẹn dữ liệu.

So sánh thiết kế cơ sở dữ liệu sản phẩm gộp một bảng và tách riêng thực thể để tối ưu truy vấn, mở rộng dữ liệu

Một số biểu hiện thường gặp:

  • Bảng products có hàng trăm cột, rất nhiều cột nullable, hoặc một vài cột JSON chứa cấu trúc phức tạp (list biến thể, list thuộc tính, list ảnh).
  • Thông tin biến thể (size, color, material, SKU riêng, giá riêng) được lưu trong cùng một record với sản phẩm cha, hoặc trong một cột JSON duy nhất.
  • Ảnh sản phẩm được lưu trong các cột image1, image2, image3… hoặc một cột JSON chứa mảng URL ảnh.
  • Thuộc tính động (dynamic attributes) như “chất liệu”, “thương hiệu”, “dung tích” được lưu trong các cột mở rộng hoặc JSON mà không có cấu trúc chuẩn hóa.

Hệ quả kỹ thuật và nghiệp vụ:

  • Khó index hiệu quả cho các trường lọc quan trọng:
    • Với dữ liệu nằm trong JSON, việc tạo index phụ thuộc vào hỗ trợ của từng RDBMS (PostgreSQL, MySQL 8+…), nhưng vẫn khó tối ưu khi cần lọc theo nhiều thuộc tính kết hợp.
    • Các cột nullable hàng loạt khiến index kém hiệu quả, query planner khó tối ưu, đặc biệt khi số lượng bản ghi lớn (hàng trăm nghìn đến hàng triệu sản phẩm/biến thể).
  • Truy vấn phức tạp, khó bảo trì:
    • Logic nghiệp vụ như “tìm tất cả biến thể còn hàng của sản phẩm X với size M, màu đỏ” trở nên khó viết, phải parse JSON hoặc xử lý ở tầng ứng dụng.
    • Mỗi lần thay đổi yêu cầu nghiệp vụ (thêm thuộc tính mới, thêm loại biến thể mới) lại phải sửa cấu trúc JSON, migration dữ liệu, hoặc thêm cột mới, dễ gây lỗi.
  • Khó áp dụng ràng buộc toàn vẹn:
    • Không thể đặt UNIQUE constraint hiệu quả cho SKU của từng biến thể nếu SKU nằm trong JSON hoặc trong một cột không chuẩn hóa.
    • Không thể đảm bảo unique combination của thuộc tính (ví dụ: mỗi cặp (productid, size, color) chỉ tồn tại một biến thể) nếu không có bảng riêng cho biến thể và bảng mapping thuộc tính.
  • Khó mở rộng sang các kênh khác:
    • Khi cần đồng bộ sản phẩm sang marketplace, POS, app mobile, việc map dữ liệu từ một cấu trúc “tạp” sẽ phức tạp hơn nhiều so với dữ liệu đã chuẩn hóa.

Thiết kế hợp lý nên tách các thực thể cốt lõi thành các bảng riêng, với quan hệ rõ ràng:

  • products: thông tin chung của sản phẩm (tên, mô tả, brand, category, trạng thái hiển thị).
  • productvariants: từng biến thể cụ thể (SKU, giá, tồn kho, barcode, weight, dimension, status), liên kết với products qua productid.
  • productimages: danh sách ảnh, mỗi record một ảnh, có thể có trường position, isprimary, liên kết với productid hoặc variantid tùy thiết kế.
  • attributes: định nghĩa loại thuộc tính (size, color, material…), có thể có kiểu dữ liệu, phạm vi áp dụng.
  • attributevalues: các giá trị cụ thể (M, L, XL, Red, Blue…), liên kết với attributeid.
  • productattributevalues: bảng liên kết nhiều-nhiều giữa sản phẩm/biến thể và giá trị thuộc tính, cho phép filter linh hoạt.

Cách chuẩn hóa này giúp:

  • Tạo index tối ưu trên các trường thường dùng để lọc (category, brand, price range, attributevalueid…).
  • Đảm bảo ràng buộc toàn vẹn: UNIQUE (productid, attributevalueid...) cho từng biến thể.
  • Dễ mở rộng khi thêm thuộc tính mới, kênh bán mới, hoặc logic giá phức tạp (giá theo kênh, theo nhóm khách hàng).

Không lưu snapshot đơn hàng làm sai lịch sử giá, tên sản phẩm và khuyến mãi

Trong domain thương mại điện tử, đơn hàng là record lịch sử phản ánh chính xác những gì đã diễn ra tại thời điểm khách hàng đặt mua. Nếu database chỉ lưu productid, variantid trong orderitems và luôn join sang bảng products, productvariants để hiển thị, hệ thống sẽ gặp các vấn đề:

  • Khi sản phẩm đổi tên, lịch sử đơn hàng hiển thị tên mới, không còn khớp với hóa đơn gốc.
  • Khi giá sản phẩm thay đổi, tổng tiền hiển thị trong lịch sử có thể khác với số tiền khách đã thanh toán.
  • Khi SKU hoặc mã sản phẩm thay đổi, việc đối soát với hệ thống kho, kế toán, hoặc nhà vận chuyển trở nên khó khăn.
  • Khi rule khuyến mãi thay đổi (ví dụ: thay đổi phần trăm giảm, điều kiện áp dụng), nếu tính lại từ rule hiện tại, số tiền giảm giá trong đơn cũ sẽ sai.

Mô hình bảo mật hóa đơn immutable lưu snapshot sản phẩm, khuyến mãi, vận chuyển và cơ chế điều chỉnh đơn hàng

Về mặt kế toán và pháp lý, đơn hàng đã hoàn tất phải được xem như một snapshot bất biến của trạng thái hệ thống tại thời điểm giao dịch. Do đó, thiết kế đúng cần:

  • Lưu đầy đủ thông tin sản phẩm tại thời điểm mua trong orderitems:
    • productname, variantname (nếu có).
    • sku, barcode của biến thể.
    • unitprice (giá niêm yết), discountedprice (giá sau giảm), currency.
    • Các thuộc tính quan trọng có thể ảnh hưởng đến fulfillment (size, color, pack size…).
  • Lưu snapshot khuyến mãi và giảm giá:
    • Bảng orderdiscounts hoặc các field trong orders để lưu:
      • Mã khuyến mãi (coupon code), loại khuyến mãi (percentage, fixed amount, free shipping…).
      • Số tiền giảm cụ thể đã áp dụng cho đơn và cho từng dòng hàng (line item discount).
      • Thông tin rule tóm tắt (ví dụ: “Giảm 10% cho đơn từ 500k”).
    • Không tính lại discount dựa trên rule hiện tại khi hiển thị lịch sử; chỉ đọc từ snapshot đã lưu.
  • Lưu snapshot phí vận chuyển và phương thức vận chuyển:
    • shippingmethodname, shippingfee, carrier, servicecode tại thời điểm tạo đơn.
    • Các phụ phí (COD fee, insurance fee…) nếu có.
  • Thiết kế cơ chế immutable cho đơn hàng đã hoàn tất:
    • Sau khi đơn ở trạng thái “Completed” hoặc “Invoiced”, không được phép sửa trực tiếp các trường snapshot.
    • Nếu cần điều chỉnh (refund, partial refund, manual adjustment), phải:
      • Tạo record điều chỉnh riêng (ví dụ: orderadjustments, creditnotes).
      • Log đầy đủ: ai sửa, sửa lúc nào, lý do, giá trị trước/sau.

Cách tiếp cận này giúp:

  • Đảm bảo tính nhất quán giữa hệ thống bán hàng, kế toán, kho, và đối tác vận chuyển.
  • Hỗ trợ audit, đối soát, giải quyết khiếu nại khách hàng dựa trên dữ liệu lịch sử chính xác.
  • Đáp ứng yêu cầu pháp lý về lưu trữ hóa đơn và chứng từ giao dịch trong nhiều năm.

Không dùng transaction khi xử lý thanh toán, tồn kho và tạo đơn hàng

Luồng xử lý đặt hàng thường bao gồm nhiều bước phụ thuộc lẫn nhau: tạo record đơn hàng, trừ tồn kho, ghi nhận thanh toán, tạo shipment, gửi email xác nhận… Nếu các bước này được thực hiện rời rạc, không nằm trong một transaction hoặc không có cơ chế đảm bảo tính idempotent, hệ thống rất dễ rơi vào trạng thái không nhất quán. Transaction bảo vệ tính nguyên tử, nhất quán, cô lập và bền vững của nhóm thao tác dữ liệu. Tuy nhiên, không nên giữ transaction cơ sở dữ liệu mở trong lúc chờ phản hồi mạng từ cổng thanh toán hoặc đơn vị vận chuyển vì thời gian chờ có thể kéo dài, gây khóa và tăng nguy cơ deadlock. Thiết kế an toàn thường ghi Order và PaymentIntent trước, commit trạng thái pending, sau đó xử lý phản hồi bất đồng bộ bằng callback có idempotency key. Email, đồng bộ vận chuyển và tác vụ hậu kỳ nên được kích hoạt từ bản ghi sự kiện bền vững sau khi transaction chính thành công, tránh trường hợp database rollback nhưng thông báo xác nhận vẫn được gửi cho khách (Gray & Reuter, 1993).

Minh họa các rủi ro quy trình đặt hàng và toàn vẹn dữ liệu như lỗi tồn kho, thanh toán, khóa ngoại, unique và index

Một số kịch bản lỗi điển hình:

  • Đơn được tạo nhưng tồn kho không trừ:
    • Ứng dụng tạo record trong ordersorderitems, sau đó mới trừ tồn kho trong inventory.
    • Nếu lỗi xảy ra giữa chừng (timeout, crash, deadlock), đơn đã tồn tại nhưng tồn kho vẫn như cũ, dẫn đến oversell.
  • Thanh toán thành công nhưng đơn không được tạo:
    • Gateway thanh toán callback báo “success”, hệ thống ghi nhận payment nhưng lỗi khi insert đơn hàng.
    • Khách đã bị trừ tiền nhưng không thấy đơn trong lịch sử, gây khiếu nại và khó xử lý reconciliation.
  • Tồn kho bị trừ hai lần khi retry request:
    • Client hoặc gateway retry request do timeout, trong khi request trước đó thực ra đã xử lý thành công.
    • Nếu API không idempotent, mỗi lần retry sẽ trừ tồn kho thêm một lần, tạo nhiều đơn trùng.

Thiết kế đúng cần kết hợp:

  • Transaction ở mức database:
    • Gói các thao tác quan trọng trong một transaction duy nhất:
      • Tạo orders, orderitems.
      • Trừ tồn kho (update inventory hoặc insert inventorymovements).
      • Ghi nhận trạng thái thanh toán (pending/authorized/captured) nếu đã có kết quả.
    • Sử dụng mức isolation phù hợp (thường là READ COMMITTED hoặc REPEATABLE READ) và lock hợp lý để tránh race condition khi nhiều khách đặt cùng một sản phẩm.
  • API idempotent:
    • Sử dụng idempotency key cho các request quan trọng (tạo đơn, capture payment):
      • Mỗi request từ client/gateway gửi kèm một idempotencykey duy nhất.
      • Server lưu lại kết quả xử lý gắn với key này; nếu nhận lại request với cùng key, trả về kết quả cũ thay vì xử lý lại.
    • Thiết kế các endpoint theo hướng “upsert” hoặc “set state” thay vì “tăng/giảm” mù mờ, để khi retry không gây side-effect ngoài ý muốn.
  • Phân tách rõ luồng thanh toán và luồng đơn hàng:
    • Trong nhiều hệ thống, nên coi payment là một bounded context riêng:
      • payments lưu transaction với gateway, trạng thái (initiated, authorized, captured, failed, refunded…).
      • orders tham chiếu đến paymentid hoặc nhiều payment (split payment, partial payment).
    • Luồng xử lý:
      • Tạo đơn ở trạng thái pending + hold tồn kho.
      • Nhận callback thanh toán, trong transaction cập nhật trạng thái payment và order tương ứng.

Thiếu ràng buộc khóa ngoại, unique constraint và index khiến dữ liệu lỗi và truy vấn chậm

Trong thực tế, nhiều hệ thống bỏ qua hoặc sử dụng rất ít ràng buộc ở mức database, với lý do “logic đã kiểm tra ở tầng ứng dụng”. Điều này dẫn đến dữ liệu bẩn, khó sửa, và hiệu năng truy vấn kém khi dữ liệu tăng trưởng.

Minh họa các lỗi dữ liệu và truy vấn chậm trong database như trùng lặp, thiếu khóa ngoại, thiếu chỉ mục, hiệu năng giảm

Một số vấn đề thường gặp:

  • Thiếu foreign key:
    • Xuất hiện dữ liệu mồ côi (orphan records), ví dụ:
      • orderitems.orderid trỏ đến một orders.id không tồn tại do record order bị xóa tay hoặc xóa bằng script.
      • productvariants.productid trỏ đến sản phẩm đã bị xóa.
    • Khó đảm bảo tính toàn vẹn khi xóa hoặc cập nhật dữ liệu liên quan.
  • Thiếu unique constraint:
    • Email khách hàng bị trùng, gây lỗi khi đăng nhập hoặc gửi thông báo.
    • SKU trùng giữa các biến thể hoặc sản phẩm, làm hỏng quy trình nhập kho, xuất kho, đồng bộ với hệ thống khác.
    • ordernumber trùng, gây rối loạn khi đối soát với cổng thanh toán hoặc nhà vận chuyển.
  • Thiếu index trên các trường truy vấn thường xuyên:
    • Các màn hình list đơn hàng, list sản phẩm, báo cáo doanh thu chạy rất chậm khi dữ liệu lớn.
    • Query filter theo createdat, status, customerid, categoryid nhưng không có index tương ứng.

Thiết kế chuẩn cần chú trọng:

  • Đặt foreign key với hành vi ON DELETE/UPDATE phù hợp:
    • Ví dụ:
      • orderitems.orderidorders.id với ON DELETE CASCADE nếu cho phép xóa order và tự động xóa line items.
      • productvariants.productidproducts.id với ON DELETE RESTRICT để ngăn xóa sản phẩm khi còn biến thể.
    • Cân nhắc kỹ giữa CASCADE, SET NULL, RESTRICT để tránh mất dữ liệu ngoài ý muốn.
  • Đặt unique constraint cho các trường nhận diện duy nhất:
    • customers.email (có thể kết hợp với tenantid trong hệ thống multi-tenant).
    • productvariants.sku hoặc (tenantid, sku).
    • orders.order_number để đảm bảo mỗi đơn có mã duy nhất.
  • Phân tích truy vấn thực tế để thêm index cần thiết:
    • Sử dụng EXPLAIN / EXPLAIN ANALYZE để xem query plan, phát hiện full table scan không cần thiết.
    • Tạo index trên các cột thường xuyên xuất hiện trong:
      • WHERE (filter theo trạng thái, thời gian, khách hàng, category).
      • JOIN (các khóa ngoại).
      • ORDER BY (sắp xếp theo ngày tạo, theo giá).
    • Tránh lạm dụng index:
      • Mỗi index đều tốn dung lượng và làm chậm thao tác INSERT/UPDATE/DELETE.
      • Chỉ tạo index cho các pattern truy vấn thực sự tồn tại và có tần suất cao.

Câu hỏi thường gặp về thiết kế cơ sở dữ liệu website bán hàng

Phần FAQ tập trung giải đáp các băn khoăn khi thiết kế cơ sở dữ liệu cho website bán hàng, từ quy mô nhỏ đến khả năng mở rộng chuyên nghiệp. Nội dung làm rõ số lượng bảng hợp lý, cách tách các nhóm bảng sản phẩm, người dùng, đơn hàng, thanh toán, khuyến mãi, nội dung và báo cáo, nhấn mạnh việc tách đúng thực thể và thiết lập ràng buộc dữ liệu chặt chẽ. Bên cạnh đó là so sánh MySQL, PostgreSQL và MongoDB, gợi ý kiến trúc kết hợp RDBMS với NoSQL. Các câu hỏi chuyên sâu như thời điểm tách bảng product variants, kỹ thuật tránh bán vượt tồn kho, chiến lược lưu thông tin thanh toán an toàn, và chuẩn bị trường dữ liệu phục vụ SEO cũng được phân tích theo hướng thực tiễn, dễ áp dụng.

Các yếu tố quan trọng khi thiết kế cơ sở dữ liệu website bán hàng và tối ưu SEO

Website bán hàng nhỏ cần bao nhiêu bảng database là đủ?

Với một website bán hàng nhỏ nhưng muốn thiết kế bài bản, có khả năng mở rộng, số lượng bảng thường rơi vào khoảng 15–25 bảng. Con số này không phải là “chuẩn cứng”, mà là mức tham chiếu cho một mô hình đã tách tương đối rõ các thực thể và mối quan hệ. Một số nhóm bảng cốt lõi thường gặp:

  • Sản phẩm:
    • products: lưu thông tin sản phẩm ở mức “master” như tên, mô tả, thương hiệu, trạng thái hiển thị, SEO cơ bản.
    • categories: lưu danh mục, có thể hỗ trợ phân cấp bằng parentid hoặc nested set / adjacency list.
    • productcategories: bảng trung gian many-to-many, cho phép một sản phẩm thuộc nhiều danh mục, hỗ trợ SEO và điều hướng linh hoạt.
    • productimages: lưu nhiều ảnh cho một sản phẩm, có thể có thứ tự hiển thị, loại ảnh (thumbnail, gallery, banner).
  • Người dùng:
    • customers: chứa thông tin tài khoản, email, mật khẩu đã hash, trạng thái, ngày đăng ký, các cờ như verified, banned.
    • addresses: tách riêng để một khách có nhiều địa chỉ giao hàng / thanh toán, hỗ trợ chuẩn hóa địa chỉ, tỉnh/thành, quận/huyện.
  • Giỏ hàng và đơn hàng:
    • carts: đại diện cho giỏ hàng hiện tại (có thể gắn với customerid hoặc sessionid).
    • cartitems: từng dòng sản phẩm trong giỏ, liên kết tới product hoặc productvariant, lưu số lượng, giá tại thời điểm cho vào giỏ (nếu cần).
    • orders: đơn hàng đã chốt, chứa thông tin khách, địa chỉ snapshot, tổng tiền, trạng thái, phương thức thanh toán/vận chuyển.
    • orderitems: chi tiết từng dòng trong đơn, lưu giá, thuế, giảm giá tại thời điểm đặt để không bị ảnh hưởng khi giá sản phẩm thay đổi sau này.
  • Thanh toán và vận chuyển:
    • payments: lưu giao dịch thanh toán, trạng thái, mã giao dịch từ cổng thanh toán, loại phương thức.
    • shipments: lưu thông tin giao hàng, mã vận đơn, đơn vị vận chuyển, trạng thái giao, thời gian dự kiến.
  • Khuyến mãi cơ bản:
    • coupons: định nghĩa mã giảm giá, loại giảm (theo % hoặc số tiền), điều kiện áp dụng, thời gian hiệu lực, giới hạn số lần dùng.
    • couponusages: ghi nhận mỗi lần sử dụng coupon theo customerid và orderid để kiểm soát lạm dụng.
  • Nội dung và SEO:
    • blogposts: bài viết nội dung, tin tức, có thể gắn tag, category riêng.
    • pages (tùy nhu cầu): các trang tĩnh như giới thiệu, chính sách, điều khoản.

Điểm quan trọng không nằm ở số lượng bảng mà ở việc tách đúng thực thể và định nghĩa ràng buộc rõ ràng (khóa chính, khóa ngoại, unique, not null). Một số nguyên tắc chuyên môn nên cân nhắc:

  • Ưu tiên chuẩn hóa đến mức hợp lý (thường đến 3NF) để tránh trùng lặp dữ liệu khó kiểm soát.
  • Những dữ liệu snapshot (giá tại thời điểm đặt hàng, địa chỉ giao hàng của đơn) nên lưu trực tiếp trong bảng order/ordersitems thay vì chỉ tham chiếu, để đảm bảo tính lịch sử.
  • Khi hệ thống lớn dần, có thể bổ sung:
    • Bảng tồn kho đa kho (warehouses, inventory, inventorymovements).
    • Bảng khuyến mãi nâng cao (promotionrules, promotionconditions, promotionactions).
    • Bảng báo cáo, logging, audit (auditlogs, eventlogs) để truy vết thay đổi.

Nên dùng MySQL, PostgreSQL hay MongoDB cho website bán hàng?

Đa số website bán hàng nên ưu tiên cơ sở dữ liệu quan hệ như MySQL hoặc PostgreSQL vì mô hình dữ liệu giao dịch (đơn hàng, tồn kho, thanh toán) đòi hỏi tính toàn vẹn, nhất quán và hỗ trợ transaction mạnh. Một số khía cạnh chuyên sâu cần cân nhắc:

  • MySQL:
    • Phù hợp cho các hệ thống thương mại điện tử nhỏ đến trung bình, đặc biệt khi dùng chung với các CMS / framework PHP (WordPress, Laravel, Magento) hoặc stack LAMP.
    • Engine InnoDB hỗ trợ transaction, row-level locking, foreign key, rất phù hợp cho các thao tác cập nhật tồn kho, đơn hàng.
    • Hệ sinh thái hosting, tài liệu, công cụ quản trị phong phú, dễ tuyển dev có kinh nghiệm.
  • PostgreSQL:
    • Mạnh hơn về tính năng nâng cao: JSONB cho dữ liệu bán cấu trúc, full-text search tích hợp, window functions cho báo cáo phức tạp.
    • Phù hợp khi cần truy vấn phân tích, báo cáo, lọc sản phẩm phức tạp (nhiều điều kiện, sort, ranking).
    • Hỗ trợ tốt các ràng buộc toàn vẹn dữ liệu, constraint phức tạp, index đa dạng (GIN, GiST) giúp tối ưu hiệu năng truy vấn.
  • MongoDB và NoSQL:
    • Thích hợp cho các phần dữ liệu linh hoạt, ít ràng buộc như log, tracking events, hành vi người dùng, session, cấu hình động.
    • Không nên dùng làm database giao dịch chính cho đơn hàng, thanh toán nếu không có kinh nghiệm sâu về thiết kế nhất quán, xử lý transaction phân tán, và chiến lược chống mất dữ liệu.
    • Mô hình document có thể gây khó khăn khi cần join logic phức tạp giữa đơn hàng, khách hàng, tồn kho, khuyến mãi.

Một kiến trúc phổ biến là dùng MySQL/PostgreSQL cho dữ liệu giao dịch cốt lõi, kết hợp NoSQL cho logging, analytics, caching (Redis) để tối ưu hiệu năng và khả năng mở rộng.

Khi nào cần tách bảng productvariants khỏi products?

Bảng productvariants nên được tách riêng khi sản phẩm có nhiều biến thể khác nhau về thuộc tính và đặc biệt là về giá hoặc tồn kho. Một số dấu hiệu rõ ràng:

  • Sản phẩm có nhiều màu, size, dung lượng, cấu hình với giá hoặc tồn kho khác nhau cho từng tổ hợp thuộc tính.
  • Cần quản lý SKU riêng cho từng biến thể để phục vụ quản lý kho, quét mã vạch, tích hợp với hệ thống ERP.
  • Cần hiển thị lựa chọn thuộc tính trên trang sản phẩm (chọn màu, chọn size) và cần mapping chính xác tới từng biến thể.

Về mặt thiết kế, một mô hình chuyên nghiệp thường gồm:

  • products: mô tả sản phẩm chung (tên, mô tả, thương hiệu, nhóm thuộc tính áp dụng).
  • productvariants: mỗi dòng là một biến thể cụ thể, có SKU, giá, tồn kho, trạng thái riêng.
  • attributesattributevalues (nếu cần linh hoạt): định nghĩa các thuộc tính như color, size, capacity.
  • productvariantattributes: bảng trung gian gắn variant với các giá trị thuộc tính tương ứng.

Nếu cố gắng nhồi tất cả vào bảng products bằng cách thêm nhiều cột (color1, color2, size1, size2) hoặc lưu JSON, sẽ gặp các vấn đề:

  • Khó viết truy vấn lọc theo thuộc tính (ví dụ: tìm tất cả sản phẩm size M, màu đỏ).
  • Khó index hiệu quả, dẫn đến truy vấn chậm khi dữ liệu lớn.
  • Logic xử lý giá, tồn kho, khuyến mãi theo biến thể trở nên phức tạp, khó bảo trì.

Tách productvariants giúp mô hình rõ ràng, hỗ trợ tốt hơn cho tồn kho, khuyến mãi, báo cáo theo từng biến thể, đồng thời dễ tích hợp với hệ thống quản lý kho và vận hành thực tế.

Làm sao tránh bán vượt tồn kho khi nhiều người đặt cùng lúc?

Tránh bán vượt tồn kho là vấn đề concurrency control trong môi trường giao dịch cao. Cần kết hợp nhiều kỹ thuật ở cả mức database và ứng dụng:

  • Transaction + điều kiện cập nhật:
    • Sử dụng transaction bao quanh thao tác trừ tồn kho.
    • Dùng câu lệnh UPDATE có điều kiện, ví dụ: UPDATE inventory SET quantity = quantity - :requested WHERE productvariantid = :id AND quantity >= :requested;
    • Kiểm tra số dòng bị ảnh hưởng; nếu bằng 0 nghĩa là không đủ tồn kho, phải báo lỗi.
  • Row-level locking:
    • Dùng SELECT ... FOR UPDATE (tùy engine) để khóa bản ghi tồn kho trong transaction, tránh race condition khi nhiều request cùng trừ kho.
    • Cần thiết kế transaction ngắn gọn để tránh lock lâu gây nghẽn.
  • API idempotent:
    • Thiết kế endpoint thanh toán/đặt hàng sao cho cùng một request (cùng idempotency key) gọi lại nhiều lần chỉ được xử lý một lần.
    • Lưu trạng thái xử lý theo idempotency key để tránh trừ kho lặp khi client hoặc gateway retry.
  • Reserve inventory:
    • Khi đơn ở trạng thái pending (chờ thanh toán), có thể “giữ chỗ” tồn kho bằng cách tạo bản ghi reservation hoặc trừ kho tạm.
    • Nếu đơn hết hạn hoặc bị hủy, cần cơ chế tự động giải phóng tồn kho đã giữ.
    • Cần quy định rõ thời gian giữ chỗ để cân bằng giữa trải nghiệm khách hàng và tối ưu sử dụng tồn kho.

Quan trọng là mọi thao tác thay đổi tồn kho phải đi qua một lớp logic thống nhất (service hoặc stored procedure), không cho phép cập nhật trực tiếp từ nhiều nơi mà không kiểm soát, để đảm bảo tính nhất quán và dễ audit.

Có nên lưu thông tin thanh toán của khách hàng trong database không?

Không nên lưu thông tin thẻ thanh toán nhạy cảm (số thẻ đầy đủ, CVV, ngày hết hạn ở dạng đầy đủ) trong database của website bán hàng, trừ khi hệ thống đạt chuẩn bảo mật cao như PCI DSS và có đội ngũ chuyên trách. Thay vào đó, nên áp dụng các nguyên tắc:

  • Sử dụng cổng thanh toán (payment gateway):
    • Website chuyển hướng hoặc gọi API tới gateway, để gateway xử lý thông tin thẻ.
    • Hệ thống chỉ lưu token đại diện cho thẻ nếu cần thanh toán lại (recurring, one-click payment).
  • Lưu thông tin không nhạy cảm:
    • Loại thẻ (Visa, MasterCard, v.v.).
    • 4 số cuối của thẻ để hiển thị cho khách nhận diện.
    • Tên chủ thẻ (nếu cần cho hiển thị, đối soát).
  • Lưu thông tin giao dịch:
    • transactionid từ gateway, trạng thái (success, failed, pending), số tiền, loại tiền tệ.
    • providerresponse (đã được sanitize) để phục vụ đối soát, debug khi có tranh chấp.

Đối với ví điện tử hoặc chuyển khoản ngân hàng, chỉ lưu các thông tin cần thiết để đối chiếu như mã giao dịch, thời gian, số tiền, trạng thái; tuyệt đối không lưu thông tin đăng nhập ví hoặc mật khẩu tài khoản ngân hàng. Ngoài ra, cần mã hóa (encryption at rest) các trường nhạy cảm như email, số điện thoại nếu có yêu cầu tuân thủ bảo mật hoặc quy định pháp lý.

Database website bán hàng cần thiết kế sẵn cho SEO không?

Database nên được thiết kế ngay từ đầu để hỗ trợ SEO, vì việc bổ sung sau này thường tốn kém, dễ gây lỗi và ảnh hưởng đến dữ liệu đã có. Một số yếu tố quan trọng nên chuẩn bị:

  • Trường SEO cho từng thực thể:
    • slug: dùng tạo URL thân thiện cho sản phẩm, danh mục, bài viết.
    • title, metadescription: phục vụ thẻ <title> và meta description.
    • canonicalurl: xử lý trùng lặp nội dung, đặc biệt khi có nhiều đường dẫn tới cùng một nội dung.
  • Cấu trúc danh mục phân cấp:
    • Dùng parentid hoặc mô hình cây (nested set, materialized path) để thể hiện phân cấp.
    • Giúp tạo URL thân thiện (ví dụ: /dien-thoai/smartphone/iphone-15) và breadcrumb rõ ràng.
  • Bảng redirects:
    • Lưu mapping từ URL cũ sang URL mới khi đổi slug, đổi cấu trúc URL.
    • Giúp giữ thứ hạng SEO, tránh lỗi 404 khi tái cấu trúc site.
  • Trường indexstatus hoặc flags SEO:
    • Cho phép đánh dấu noindex, nofollow cho các trang không cần SEO (trang lọc quá sâu, trang test, trang nội bộ).
    • Giúp đội marketing kiểm soát index mà không phải chỉnh code backend liên tục.

Thiết kế tốt cho SEO trong database giúp đội marketing linh hoạt triển khai chiến lược nội dung, tối ưu cấu trúc site, A/B test URL, meta, mà vẫn đảm bảo dữ liệu có cấu trúc, dễ truy vấn và hạn chế phụ thuộc vào thay đổi code phức tạp sau này.

BÌNH LUẬN BÀI VIẾT
Nội dung *
Họ Tên
Email
GỬI BÌNH LUẬN
NỘI DUNG HAY
tác giả: HỒNG MINH (MINH HM)
CHUYÊN GIA HỒNG MINH
Hồng Minh, CEO LIGHT
Hơn 12 năm kinh nghiệm trong ngành Marketing Online bao gồm SEO, lập trình, thiết kế đồ họa, chạy quảng cáo, vv...
Trainning chuyên sâu về SEO, Google Ads, Quảng Cáo cho hơn 3000+ doanh nghiệp
20+ Khóa tư vấn đào tạo cho doanh nghiệp về Marketing Online
0942 890 168