Tài Liệu Kỹ Thuật: CSDL, ORM & Kiến Trúc Java
Đặc tả kỹ thuật toàn diện về mô hình CSDL quan hệ PostgreSQL, chuẩn mapping Hibernate/JPA chống N+1, kiến trúc Spring Boot phân tầng dùng chung Service giữa SSR và REST API, 4 mẫu thiết kế GoF và 5 luồng nghiệp vụ tài chính cốt lõi của AiChiaBill.
Sơ Đồ Thực Thể Quan Hệ (Entity-Relationship Diagram)
Sơ đồ CSDL PostgreSQL thực tế của hệ thống, chuẩn hóa qua 11 file migration Flyway (src/main/resources/db/migration/).
Chi Tiết Các Bảng Dữ Liệu Cốt Lõi
Đặc tả kiểu dữ liệu, khóa chính, ràng buộc toàn vẹn và chiến lược đánh chỉ mục (Index).
| Cột | Kiểu | Ràng buộc / Ghi chú |
|---|---|---|
| id | UUID | PK · Sinh tự động ở tầng Java |
| VARCHAR(120) | UNIQUE · Định danh đăng nhập | |
| password_hash | VARCHAR(255) | BCrypt hash (cost factor 10) |
| display_name | VARCHAR(100) | Tên hiển thị công khai |
| status | VARCHAR(30) | ACTIVE, PENDING_VERIFICATION, BANNED |
| bank_name | VARCHAR(100) | Ngân hàng thụ hưởng VietQR (V9) |
| bank_account_number | VARCHAR(50) | Số tài khoản nhận tiền (V9) |
| created_at | TIMESTAMPTZ | Thời điểm tạo tài khoản |
| Cột | Kiểu | Ràng buộc / Ghi chú |
|---|---|---|
| groups.id | UUID | PK nhóm chi tiêu |
| groups.invite_code | VARCHAR(20) | UNIQUE · Mã mời tham gia nhóm 6 ký tự |
| groups.created_by | UUID | FK -> users(id) · Người lập nhóm |
| group_members.id | UUID | PK quan hệ thành viên |
| group_id, user_id | UUID | FK -> groups(id), users(id) · UNIQUE đôi |
| role | VARCHAR(20) | OWNER, ADMIN, MEMBER |
| status | VARCHAR(20) | ACTIVE, INVITED, LEFT |
| Cột | Kiểu | Ràng buộc / Ghi chú |
|---|---|---|
| expenses.id | UUID | PK khoản chi |
| group_id | UUID | FK -> groups(id) |
| paid_by_user_id | UUID | FK -> users(id) · Người ứng tiền trước |
| amount | DECIMAL(19,2) | CHECK (amount > 0) |
| split_type | VARCHAR(20) | EQUAL, EXACT, PERCENTAGE, BY_ITEM |
| version | BIGINT | Optimistic locking chống ghi đè đồng thời |
| expense_shares.id | UUID | PK phần chia nợ của từng thành viên |
| share_amount | DECIMAL(19,2) | CHECK (share_amount >= 0) |
| Cột | Kiểu | Ràng buộc / Ghi chú |
|---|---|---|
| settlements.id | UUID | PK giao dịch quyết toán |
| debtor_id, creditor_id | UUID | FK -> users(id) · CHECK (debtor_id != creditor_id) |
| amount | DECIMAL(19,2) | CHECK (amount > 0) |
| status | VARCHAR(20) | PENDING, MARKED_PAID, CONFIRMED |
| version | BIGINT | Optimistic locking chống 2 người bấm cùng lúc |
| provider_reference | VARCHAR(80) | UNIQUE · Mã tham chiếu giao dịch cổng TT |
| payment_attempts.id | UUID | PK lưu vết từng lượt bấm thanh toán |
Chiến Lược Đánh Chỉ Mục (PostgreSQL Index Strategy)
Đảm bảo truy vấn thời gian thực dưới 15ms ngay cả khi nhóm có hàng ngàn khoản chi tiêu.
-- 1. Index cho dòng thời gian khoản chi (Group Timeline Query) CREATE INDEX idx_expenses_group_occurred ON expenses (group_id, occurred_at DESC); -- 2. Index cho tính toán số dư cá nhân trên Dashboard (Dashboard Aggregation) CREATE INDEX idx_expense_shares_user ON expense_shares (user_id, expense_id); -- 3. Index cho tìm kiếm & kiểm tra nợ chéo giữa 2 thành viên CREATE INDEX idx_settlements_debtor_creditor ON settlements (debtor_id, creditor_id, status); -- 4. Partial Index cho chuông thông báo chưa đọc (Unread Notifications) CREATE INDEX idx_notifications_recipient_unread ON app_notifications (recipient_id, created_at DESC) WHERE is_read = FALSE;
Quy Chuẩn Mapping JPA / Hibernate trong Java
Các quy ước mapping thực tế dùng trong package com.aichiabill.model, triệt tiêu lỗi N+1 và đảm bảo an toàn giao dịch đồng thời.
Expense và Settlement có trường @Version Long version. Tránh hoàn toàn việc hai người cùng xác nhận hoặc cập nhật nợ gây xung đột dữ liệu.@ManyToOne đều đặt fetch = FetchType.LAZY. Không một quan hệ nào dùng EAGER, triệt tiêu hoàn toàn nguy cơ nạp thừa dữ liệu.CascadeType.ALL hay orphanRemoval bừa bãi. Việc tạo/xóa đối tượng con được quản lý minh bạch qua Repository chuyên biệt.Minh Họa Code Thực Tế: Entity & Repository
@Entity @Table(name = "expenses") @Getter @Setter @NoArgsConstructor public class Expense { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "group_id", nullable = false) private ExpenseGroup group; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "paid_by_user_id", nullable = false) private User paidBy; @Column(nullable = false, precision = 19, scale = 2) private BigDecimal amount; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) private SplitType splitType; @Version private Long version = 0L; // Optimistic Locking @PrePersist protected void onCreate() { this.createdAt = Instant.now(); this.updatedAt = Instant.now(); } }
public interface SettlementRepository extends JpaRepository<Settlement, UUID> { /** * @EntityGraph chỉ thị cho Hibernate JOIN FETCH người nợ (debtor), người nhận (creditor) * và nhóm (group) chỉ trong 1 câu SQL duy nhất, loại bỏ hoàn toàn bài toán N+1 Query. */ @EntityGraph(attributePaths = {"debtor", "creditor", "group"}) Page<Settlement> findByGroupIdOrderByCreatedAtDesc(UUID groupId, Pageable pageable); @EntityGraph(attributePaths = {"debtor", "creditor"}) List<Settlement> findByGroupIdAndStatusIn(UUID groupId, Collection<SettlementStatus> statuses); }
Kiến Trúc Phân Tầng Spring Boot 3.4 (Java 21)
Cấu trúc 4 lớp kinh điển với nguyên lý cốt lõi: Giao diện SSR Thymeleaf và REST API dùng chung 100% lớp Service nghiệp vụ.
Thymeleaf SSR Templates"] REST["api/* (5 @RestControllers)
JSON ApiResponse / ProblemDetail"] end subgraph ServiceLayer ["2. Business Logic Layer"] Service["service/* (22 Services)
ExpenseService · SettlementService · OtpService"] Strategy["service/strategy/*
SplitStrategy Interface & Factory"] end subgraph PersistenceLayer ["3. Data Access Layer"] Repo["repository/* (10 Spring Data JPA Interfaces)
@EntityGraph · Batch Queries"] end subgraph DatabaseLayer ["4. Storage & Cache Layer"] Postgres[("PostgreSQL 15 Database")] Redis[("Redis 7 Cache")] end Client -->|HTTP GET/POST| SSR Client -->|REST JSON /api/v1| REST SSR -->|Gọi nghiệp vụ| Service REST -->|Gọi nghiệp vụ| Service Service --> Strategy Service --> Repo Service -->|Cache Aside| Redis Repo -->|JPA / SQL| Postgres
ExpenseController và ExpenseApiController gọi chung cùng instance ExpenseService. Mọi business rules, kiểm tra quyền hạn, xác thực chỉ viết một lần duy nhất, không có nguy cơ lệch pha giữa Web và API.dto/: Dùng cho SSR form binding (linh hoạt, pre-fill qua session).dto/api/: Immutable Java Record có kiểm tra chặt chẽ qua Jakarta Validation (@Valid, @NotBlank, @Positive).@RequiredArgsConstructor của Lombok cùng các trường private final. Tuyệt đối không dùng @Autowired trên field, đảm bảo code dễ viết Unit Test và bất biến.Mẫu Thiết Kế GoF & Thuật Toán Tối Ưu Nợ
Ứng dụng các mẫu thiết kế chuẩn công nghiệp để giữ cho hệ thống mở rộng linh hoạt và thuật toán kế toán chính xác tuyệt đối.
1. Strategy Pattern: 4 Chiến Lược Chia Tiền
Đóng gói thuật toán tính toán và phân bổ phần dư từng cent cho 4 chế độ chia tiền linh hoạt.
@Component public class EqualSplitStrategy implements SplitStrategy { @Override public List<ExpenseShare> calculateShares(Expense expense, ExpenseForm form, Map<UUID, User> members) { BigDecimal total = expense.getAmount(); int count = form.participantIds().size(); // 1. Chia đều cơ sở, làm tròn xuống (FLOOR) BigDecimal baseShare = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.FLOOR); // 2. Tính toán phần dư còn lại (VD: 100,000 / 3 = 33,333 -> dư 1 đồng) BigDecimal remainder = total.subtract(baseShare.multiply(BigDecimal.valueOf(count))); // 3. Rải từng cent/đồng cho các thành viên đầu tiên để tổng shares luôn = total 100% return CalculatorUtils.distributeRemainderCents(expense, form.participantIds(), baseShare, remainder); } }
2. Thuật Toán Tối Ưu Hóa Nợ (Greedy Debt Simplification Graph)
Giảm thiểu tối đa số lần chuyển khoản giữa N thành viên từ O(N²) xuống tối đa N - 1 giao dịch.
public class SettlementOptimizer { /** * Thuật toán: * 1. Tính Net Balance của mỗi thành viên: Balance = Total_Paid - Total_Owed * 2. Phân loại thành 2 nhóm: Debtor (Balance < 0) và Creditor (Balance > 0) * 3. Ghép cặp Greedy: Lấy người nợ nhiều nhất ghép với người được nhận nhiều nhất * -> Sau mỗi bước, ít nhất một bên được triệt tiêu công nợ hoàn toàn! */ public static List<SettlementDraft> optimize(Map<UUID, BigDecimal> netBalances) { PriorityQueue<BalanceEntry> debtors = new PriorityQueue<>((a, b) -> a.amount.compareTo(b.amount)); // Min-heap PriorityQueue<BalanceEntry> creditors = new PriorityQueue<>((a, b) -> b.amount.compareTo(a.amount)); // Max-heap // Đưa vào hàng đợi ưu tiên... List<SettlementDraft> results = new ArrayList<>(); while (!debtors.isEmpty() && !creditors.isEmpty()) { BalanceEntry debtor = debtors.poll(); BalanceEntry creditor = creditors.poll(); BigDecimal settleAmount = debtor.amount.abs().min(creditor.amount); results.add(new SettlementDraft(debtor.userId, creditor.userId, settleAmount)); // Cập nhật số dư còn lại sau khi thanh toán từng phần BigDecimal remainingDebtor = debtor.amount.add(settleAmount); BigDecimal remainingCreditor = creditor.amount.subtract(settleAmount); if (remainingDebtor.compareTo(BigDecimal.ZERO) < 0) debtors.add(new BalanceEntry(debtor.userId, remainingDebtor)); if (remainingCreditor.compareTo(BigDecimal.ZERO) > 0) creditors.add(new BalanceEntry(creditor.userId, remainingCreditor)); } return results; } }
3. Quy Trình Quyết Toán 2 Bước (Two-Phase Commit Protocol)
Đảm bảo chỉ khi chủ nợ bấm xác nhận nhận tiền thì số dư hiển thị mới được gạch nợ.
5 Luồng Nghiệp Vụ Cốt Lõi (Sequence & Workflows)
Trình tự tương tác end-to-end giữa trình duyệt, Controller, Service, Repository, Database và các bên thứ ba.
PENDING_VERIFICATION -> OtpService sinh mã ngẫu nhiên 6 số, băm SHA-256 lưu DB kèm thời hạn 10 phút -> Gửi plaintext qua Gmail SMTP. Khi người dùng nhập OTP hợp lệ -> Kích hoạt tài khoản thành ACTIVE.
ExpenseController.createExpense nhận dữ liệu form hoặc bản nháp quét hóa đơn AI OCR trong session -> Gọi ExpenseService kiểm tra quyền thành viên nhóm -> Lưu Expense -> Kích hoạt SplitStrategyFactory để chọn chiến lược chia tiền (EQUAL, EXACT, PERCENTAGE, BY_ITEM) -> Tính toán và lưu danh sách ExpenseShare -> Bắn thông báo real-time qua WebSocket.
SettlementService lấy tất cả khoản chi chưa quyết toán -> Tính mảng số dư ròng -> Gọi SettlementOptimizer.optimize khử nợ chéo -> Sinh bộ giao dịch tối ưu -> Người nợ bấm trả tiền (kèm mã VietQR tạo tức thì) -> Người nhận xác nhận -> Số dư nợ được thanh toán dứt điểm.
ExpenseDraft vào session để pre-fill màn hình tạo khoản chi.
NotificationService -> Lưu bản ghi vào bảng app_notifications -> Đẩy gói tin qua kênh STOMP cá nhân /topic/users/{id} (rung chuông và tăng badge counter trên thanh Topbar) và kênh nhóm /topic/groups/{groupId} để đồng bộ màn hình trực tiếp.
Bảng Màu Chủ Đạo (Color Palette Tokens)
Bấm vào bất kỳ ô màu nào để sao chép mã HEX hoặc biến CSS trực tiếp vào bộ nhớ tạm.
Thư Viện Linh Kiện Giao Diện (Component Library)
Các khối linh kiện tương tác cao cấp phục vụ trải nghiệm người dùng fintech mượt mà.
Thư Viện Màn Hình & Mockups Hệ Thống
Bấm vào bất kỳ thẻ màn hình nào để mở chế độ xem phóng to chi tiết (Lightbox).