Thiết kế Sổ cái Bất biến (Durable Ledger) và Đối soát Doanh thu thực tế
Bài học xương máu trong thiết kế cơ sở dữ liệu: Tại sao không bao giờ được dùng bảng nghiệp vụ có chính sách dọn dẹp (Retention) để tính tiền, và cách thiết kế bảng sổ cái bất biến.
1. Sai lầm kinh điển: Tính tiền dựa trên Bảng nghiệp vụ có Retention
Trong các ứng dụng cung cấp dịch vụ AI (như tạo ảnh, render video tại IMG Studio), việc lưu trữ các file media tốn rất nhiều dung lượng ổ cứng. Vì vậy, hệ thống bắt buộc phải áp dụng Chính sách dọn dẹp định kỳ (Retention Policy):
- File ảnh và bản ghi trong bảng
Imagesẽ tự động bị xóa sau 24 giờ hoặc 7 ngày. - Người dùng cũng có quyền bấm nút "Xóa ảnh vĩnh viễn" trong bộ sưu tập (Gallery).
Bi kịch đối soát tài chính:
Ban đầu, đội ngũ thực hiện thống kê doanh thu và sản lượng theo công thức:
-- SAI LẦM NGUY HIỂM:
SELECT COUNT(*), SUM(cost)
FROM "Image"
WHERE "createdAt" >= '2026-08-01' AND "userId" = 'user_123';
Hậu quả:
- Vào cuối tháng, khi người dùng hoặc cron job dọn dẹp xóa các bức ảnh cũ, số lượng ảnh và tổng số tiền trên bảng thống kê bị tụt dần theo thời gian!
- Báo cáo tài chính cho thấy tổng tiền người dùng đã nạp qua PayOS không khớp với tổng tiền dịch vụ đã tiêu thụ, gây hoang mang và không thể đối soát sổ sách.
2. Nguyên lý cốt lõi: Sổ cái Bất biến (Durable Ledger Pattern)
Trong kiến trúc FinTech và cơ sở dữ liệu giao dịch:
Bản ghi tiền tệ và sản lượng một khi đã ghi nhận thì KHÔNG BAO GIỜ ĐƯỢC PHÉP BỊ THAY ĐỔI HOẶC XÓA BỎ (Immutable / Append-only).
Ngay cả khi file ảnh vật lý trên đĩa bị xóa, hay bản ghi hiển thị trên giao diện người dùng bị ẩn đi, thì dấu vết giao dịch (Audit Trail) vẫn phải tồn tại vĩnh viễn.
3. Kiến trúc 2 tầng dữ liệu (Dual-Table Architecture)
Hệ thống được tái cấu trúc thành 2 bảng riêng biệt:
[User tạo ảnh thành công]
│
├───> [Bảng 1: Image] (Tầng hiển thị / Transient)
│ - id, url, prompt, userId
│ - Có nút Xóa, có Cron dọn sau 24h
│
└───> [Bảng 2: ImageUsage / RequestLog] (Tầng Sổ cái / Durable)
- id, userId, model, resolution, costVnd, timestamp
- CHỈ CHO PHÉP INSERT, CẤM UPDATE, CẤM DELETE
Định nghĩa Schema Prisma chuẩn:
model ImageUsage {
id String @id @default(cuid())
userId String
model String // Ví dụ: 'imagen-3', 'gemini-3-pro'
resolution String // '1K', '2K', '4K'
costVnd Int // Số tiền trừ ví (VND)
status String // 'SUCCESS', 'FAILED', 'REFUNDED'
createdAt DateTime @default(now())
// KHÔNG tạo quan hệ CASCADE DELETE với bảng Image!
user User @relation(fields: [userId], references: [id])
@@index([userId, createdAt])
@@index([createdAt])
}
4. Quy trình đối soát tiền 3 bên chuẩn xác
Khi cần đối soát dòng tiền của bất kỳ người dùng nào:
[Tổng tiền nạp qua PayOS]
= [Số dư ví hiện tại]
+ [Tổng chi phí trên bảng ImageUsage (SUCCESS)]
- [Tổng tiền đã Hoàn (REFUNDED)]
Dù người dùng có xóa sạch hàng nghìn bức ảnh trong Gallery của họ, công thức trên vẫn luôn luôn cân bằng đến từng đồng lẻ!
5. Các nguyên tắc vàng cần ghi nhớ
- Tách biệt hiển thị và thanh toán: Dữ liệu phục vụ UI người dùng (ảnh, bài post tạm) có thể xóa, nhưng dữ liệu ví tiền và sản lượng phải là Append-only.
- Cấm Hard-delete bảng tài chính: Không bao giờ viết lệnh
DELETE FROM "WalletTransaction"hayDELETE FROM "ImageUsage". Nếu có sai sót, chỉ được ghi thêm một bản ghi điều chỉnh (Compensation Transaction). - Index theo thời gian: Bảng sổ cái sẽ phình to rất nhanh theo năm tháng, luôn đánh Index trên cột
createdAtvàuserIdđể câu lệnh đối soát luôn chạy dưới 50ms.