Chuyển đến nội dung chính (Skip to content)
Tất cả dự án
FeaturedNestJSTypeScriptTypeORMMariaDBNext.jsReact

Napharco: NestJS, TypeORM và transaction trên MariaDB

Ảnh dự án Napharco: NestJS, TypeORM và transaction trên MariaDB

Kiến trúc Napharco với NestJS, TypeORM DataSource và MariaDB: transaction đơn hàng, khóa tồn kho, idempotency, version và hợp đồng API B2B.

11 phút đọc · Case study dự án

Napharco là hệ thống B2B có các phân hệ catalog, tài khoản khách hàng, giỏ hàng, đơn hàng, voucher và kho. Phần đáng phân tích nằm ở cách NestJS phối hợp các service và cách TypeORM quản lý kết nối, migration, transaction trên MariaDB. Nhiều nghiệp vụ dùng SQL tham số hóa qua DataSource hoặc EntityManager, nên gọi đây là một ứng dụng chỉ thao tác bằng entity repository sẽ không phản ánh đúng source.

NestJS và tổ chức các lớp trong hệ thống B2B

LớpThành phầnNhiệm vụ
Giao diện công khaiNext.js FECatalog, nội dung, giỏ hàng và tài khoản khách hàng
Quản trịNext.js CMSQuản lý sản phẩm, hồ sơ B2B, đơn và chứng từ kho
HTTP APINestJS controllersNhận request, xác định actor, validate và gọi nghiệp vụ
Nghiệp vụOrdersService, InventoryService, VoucherPricingServiceQuy tắc đơn hàng, stock ledger và phân bổ giảm giá
Data accessTypeORM DataSource / EntityManagerConnection pool, SQL, transaction và migration
Lưu trữMariaDB và media storageDữ liệu quan hệ tách khỏi binary tệp

Các nhóm source nằm trong src/orders, inventory, cart, vouchers, b2b, products và auth. Controller là điểm vào HTTP, service giữ nghiệp vụ. Tuy nhiên cấu hình AppModule hiện đăng ký tập trung nhiều controller và provider; thư mục theo domain chưa đồng nghĩa tất cả đã được tách thành bounded module hoàn chỉnh. Phân biệt này quan trọng khi đánh giá mức độ modular hóa thực tế.

Next.js FE / CMS
  → NestJS controller → actor + validation
  → service nghiệp vụ
  → TypeORM transaction / EntityManager
  → MariaDB: orders + payments + inventory + vouchers

Cùng một đơn có thể tác động nhiều bảng, vì vậy ranh giới service cần bám vào invariant của nghiệp vụ: không xuất nhiều hơn số lượng được phép, không dùng voucher sai điều kiện và không tạo hai đơn từ một lần gửi lặp. Chia file nhỏ chỉ hỗ trợ đọc code; tính đúng phụ thuộc cách các bước được phối hợp.

TypeORM DataSource, SQL trực tiếp và schema migration

Cấu hình TypeOrmModule đặt driver mariadb, timezone UTC và synchronize: false. Schema được quản lý bằng các migration đã đăng ký, không dựa vào tự đồng bộ entity mỗi lần khởi động. Cấu hình còn bật chạy migration khi bootstrap và không bao toàn bộ migration trong một transaction chung; vì vậy việc triển khai phải xem xét từng migration, đặc biệt với DDL và các thay đổi không tương thích.

Trong OrdersService, DataSource và EntityManager được dùng với tx.query(sql, params). TypeORM ở đây cung cấp vòng đời kết nối và transaction, còn truy vấn được viết rõ bằng SQL để kiểm soát join, khóa hàng và update có điều kiện. Cách dùng này khác với việc chỉ khai báo decorator trên entity rồi gọi repository.save. Vai trò connection/pool của DataSource có thể đối chiếu trong tài liệu TypeORM.

Tham số hóa bảo vệ các giá trị được bind, nhưng tên cột, hướng sort hoặc SQL dựng động vẫn cần allowlist. Đồng thời, một SQL query chạy qua DataSource bên ngoài callback transaction sẽ không tự thuộc transaction đang mở ở nơi khác. Bởi vậy helper nhận tx là chi tiết kiến trúc quan trọng: nó giữ các truy vấn liên quan trên cùng một transaction manager.

Đơn hàng, thanh toán và tồn kho là các mô hình liên kết

Nhóm dữ liệuVai trò kỹ thuậtĐiểm cần giữ nhất quán
orders và các dòng hàngTrạng thái, giá trị, khách hàng, kho, versionTổng tiền và lịch sử chuyển trạng thái
order_paymentsThu tiền, hoàn tiền và liên hệ khoản gốcPaid/refunded totals phải khớp giao dịch
inventory_documentsChứng từ nháp, duyệt, phiên bảnChứng từ chỉ tác động kho theo đúng bước nghiệp vụ
inventory_batchesTồn theo lôSố lượng sau cập nhật phải phù hợp chính sách bán thiếu
inventory_movementsDelta, before_quantity, after_quantityTruy vết mỗi thay đổi thay vì chỉ biết tồn hiện tại
vouchersĐiều kiện, quota và phạm vi áp dụngPreview và kết quả ghi cuối không mâu thuẫn

Stock ledger lưu ngữ cảnh của biến động. Nếu một lô từ 20 xuống 17, chỉ lưu số 17 không giải thích được thay đổi do bán hàng, điều chỉnh hay hoàn tác. Inventory movements ghi delta và lượng trước/sau cùng document_id, batch_id để có đường đối chiếu. Đây là thiết kế audit nghiệp vụ, không phải chứng cứ rằng tồn thực tế ngoài kho luôn bằng con số trong phần mềm.

Trạng thái đơn và trạng thái thanh toán không nên gộp thành một cờ. Một đơn có thể đi qua xác nhận, chuẩn bị, giao hàng, hoàn tất hoặc hủy, trong khi thu tiền và hoàn tiền có vòng đời riêng. Mô hình tách này giúp xử lý các trường hợp giao hàng và dòng tiền không diễn ra đồng thời mà không phải sửa ý nghĩa của một trạng thái dùng chung.

Luồng tạo đơn: idempotency và transaction READ COMMITTED

  1. Validate khách hàng, kho, requestToken và danh sách sản phẩm; loại productId trùng trong cùng payload.
  2. Tạo hash của request, mở transaction READ COMMITTED và tra request_token đã có.
  3. Nếu token đã tồn tại, đối chiếu hash và actor: cùng yêu cầu thì trả lại đơn cũ, khác nội dung hoặc người tạo thì báo conflict.
  4. Khóa hàng kho đang hoạt động bằng SELECT ... FOR UPDATE, load sản phẩm và điều kiện khách hàng.
  5. Tính voucher, phân bổ giảm giá, đối chiếu preview fingerprint khi luồng yêu cầu bước preview.
  6. Ghi dữ liệu đơn cùng các thay đổi liên quan; callback hoàn tất mới commit.
// Mã giả rút gọn từ luồng OrdersService
return db.transaction("READ COMMITTED", async (tx) => {
  const previous = await findRequest(tx, token)
  if (previous) return validateAndReuse(previous, hash, actor)
  await tx.query(
    "SELECT id FROM warehouses WHERE id=? AND active=TRUE FOR UPDATE",
    [warehouseId]
  )
  const prices = await calculateWithCurrentData(tx, input)
  return persistOrderAndRelatedData(tx, prices)
})

Request token không đơn thuần là một random ID hiển thị ở FE. Ghép token với request hash và người tạo ngăn việc một token được dùng lại cho payload khác. Cơ chế này xử lý retry của cùng thao tác; nó không thay thế unique constraint, khóa dữ liệu hay quy tắc chuyển trạng thái. Trường hợp lỗi timeout sau commit là ví dụ cần idempotency: client chưa nhận response nhưng đơn có thể đã tồn tại.

Khóa hàng, version và preview fingerprint bảo vệ điều gì?

Các luồng sửa đơn kiểm tra version rồi cập nhật có điều kiện và tăng phiên bản. Version hỗ trợ phát hiện người dùng đang ghi dựa trên bản cũ; row lock điều phối các transaction cùng chạm một đối tượng. Hai cơ chế giải quyết các thời điểm khác nhau. Một form mở lâu có thể đã stale trước khi request bắt đầu transaction, nên chỉ thêm FOR UPDATE không làm dữ liệu người dùng đang cầm trở nên mới.

Trong luồng cập nhật voucher, code lấy tập mã cần khóa, loại trùng và sắp xếp trước khi SELECT ... FOR UPDATE. Thứ tự khóa ổn định giảm cơ hội các request giành khóa theo hai thứ tự ngược nhau. Đây là biện pháp giảm rủi ro deadlock, không phải cam kết loại bỏ mọi deadlock. Về cơ chế, FOR UPDATE của MariaDB cần được hiểu trong ranh giới transaction và storage engine tương ứng.

Preview fingerprint gắn kết quả người dùng đã kiểm tra với dữ liệu tính giá tại thời điểm ghi. Nếu điều kiện voucher hoặc tổng tiền đổi, API có thể trả conflict thay vì lưu một tổng tiền khác với preview. Với kho, quá trình duyệt chứng từ khóa kho, chứng từ và dữ liệu sản phẩm cần thiết trước khi ghi batch/movement. Các khóa này có thể làm giảm mức song song, vì vậy cần đo thời gian giữ transaction trước khi tối ưu bằng cách nới khóa.

FE, CMS và hợp đồng lỗi nghiệp vụ

FE không được coi là nơi quyết định khách hàng có được mua hay không. API kiểm tra hồ sơ đã duyệt, trạng thái khóa và điều kiện nghiệp vụ trong luồng ghi. FE dùng phản hồi để bật/tắt chức năng, trình bày conflict và yêu cầu tải lại preview khi cần. Với service có transaction, gửi đúng hình dạng request chưa đủ; request vẫn có thể bị từ chối hợp lệ vì state đã thay đổi.

Catalog, nội dung page và shortcode là luồng đọc khác với đặt hàng. FE có thể render public page từ dữ liệu CMS, trong khi thao tác giỏ hàng và mua hàng phải tuân theo context tài khoản. Media cũng có ranh giới riêng: ảnh sản phẩm có thể công khai, nhưng giấy tờ hồ sơ B2B không nên được xử lý như một ảnh catalog có URL chia sẻ tự do.

Về tiền tệ và số lượng, nguồn dữ liệu, kiểu SQL, bước parse và quy tắc làm tròn phải được xem cùng nhau. Không nên suy ra đã có tính toán tiền tùy ý chính xác chỉ vì database có cột decimal, hoặc coi mọi số JavaScript đều an toàn với mọi giá trị. Các luồng cần test invariant tổng dòng hàng, phân bổ voucher và hoàn tiền thay vì chỉ snapshot giao diện.

Các tình huống kỹ thuật cần kiểm tra

  • Gửi cùng requestToken hai lần: cùng payload trả lại cùng đơn; payload khác phải conflict.
  • Hai form cùng sửa một version: form thứ hai phải nhận kết quả phát hiện xung đột.
  • Hai yêu cầu cùng tiêu thụ voucher hoặc lô hàng: kiểm tra khóa và invariant sau commit.
  • Lỗi giữa cập nhật chứng từ và movement: kiểm tra phạm vi rollback của cùng tx.
  • Migration trên database có dữ liệu và rollback ứng dụng: kiểm tra tương thích schema trước khi release.

Các script test trong repository thể hiện những nhóm kiểm thử nghiệp vụ riêng cho orders, inventory, vouchers và B2B. Bài viết không tuyên bố đã chạy toàn bộ bộ test hoặc chứng minh tải đồng thời trên production. Những tình huống trên là cách đọc đúng các cơ chế hiện có và xác định phép đo cần bổ sung.

Giao diện ứng dụng và dữ liệu minh họa

Các ảnh dưới đây được chụp trực tiếp từ FE hoặc trang quản trị của ứng dụng. Với các màn hình chạy trong phiên preview, dữ liệu mẫu được tách khỏi hệ thống vận hành; bố cục và thành phần giao diện do chính source của dự án render.

Catalog sản phẩm và bộ lọc

Catalog sản phẩm và bộ lọc
Ảnh chụp FE render trực tiếp từ source với catalog mẫu. Các sản phẩm và nhãn đều là dữ liệu preview.

Cổng đăng nhập khách hàng B2B

Cổng đăng nhập khách hàng B2B
Ảnh chụp form đăng nhập thật của FE trong phiên chạy cục bộ. Không nhập thông tin khách hàng.

Tài liệu kỹ thuật đối chiếu

Xem thêm các dự án khác hoặc trao đổi về kiến trúc và phạm vi triển khai.

Napharco: NestJS, TypeORM và transaction trên MariaDB | thanhnh.id.vn