Files
travelplanning/ARCHITECTURE.md
T

19 KiB

Kiến Trúc Hệ Thống: Tính Năng Lập Kế Hoạch Du Lịch (Travel Planning Feature)

Tài liệu này mô tả kiến trúc tổng thể, mô hình dữ liệu và các giải pháp kỹ thuật cho tính năng lập kế hoạch du lịch đa người dùng thuộc nền tảng Travel.


1. Kiến Trúc Tổng Quan (System Overview)

Hệ thống được thiết kế theo mô hình Client-Server kết hợp với kiến trúc Modular Monolith hoặc Microservices (tùy quy mô triển khai), chia làm 3 tầng chính:

  • Frontend (Presentation Layer): Đảm nhiệm việc tương tác trực tiếp với người dùng, hiển thị bản đồ trực quan, quản lý luồng tạo tour/chặng và xử lý tải ảnh/hiển thị album.
  • Backend (Business Logic Layer): Xử lý xác thực bảo mật, tính toán chi phí (quy tắc phân chia tiền), định tuyến đường đi ngắn nhất, lập lịch kiểm tra task (Cron job/Event-driven) và kiểm tra quyền truy cập (RBAC).
  • Database & Storage (Data Layer): Lưu trữ dữ liệu quan hệ (PostgreSQL với extension PostGIS để xử lý tọa độ địa lý) và lưu trữ tệp tin hình ảnh riêng biệt (Object Storage như AWS S3 hoặc MinIO).

2. Công Nghệ Đề Xuất (Tech Stack)

Thành phần Công nghệ đề xuất Lý do lựa chọn
Frontend React / Next.js + TailwindCSS Tối ưu cho ứng dụng Single Page (SPA), quản lý trạng thái tốt, hỗ trợ SSR cho SEO nếu cần.
State Management Zustand / Redux Toolkit Quản lý state phức tạp của Tour, Chặng (Leg) và Chi phí.
Map Engine Mapbox GL JS / Google Maps API / Leaflet Hỗ trợ hiển thị bản đồ trực quan, marker kéo thả và tính toán định tuyến (Routing).
Backend Node.js (NestJS hoặc Express) Xử lý bất đồng bộ tốt, phù hợp hệ thống nhiều I/O và tích hợp mDNS/IoT nếu mở rộng sau này.
Database PostgreSQL + PostGIS Hệ quản trị cơ sở dữ liệu mạnh mẽ, PostGIS giúp tính toán khoảng cách và tọa độ địa lý chính xác.
File Storage AWS S3 / MinIO Lưu trữ ảnh tách biệt hoàn toàn khỏi Database để tối ưu hiệu năng.

3. Mô Hình Dữ Liệu (Database Schema)

Dưới đây là các thực thể cốt lõi phục vụ tính năng:

3.1. User (Người dùng)

Lưu trữ thông tin định danh và trạng thái quản trị. Toàn bộ ID sử dụng định dạng UUID.

  • User: id, email, passwordHash, name, avatar, isAdmin, isBlocked, createdAt.

3.2. Tour & Phân quyền (RBAC)

Hệ thống sử dụng bảng trung gian để quản lý thành viên cho từng chuyến đi.

  • Tour: id, title, startDate, endDate, totalCost (Decimal), createdById (FK -> User).
  • TourParticipant: Quản lý vai trò thành viên trong Tour qua enum ParticipantRole:
    • OWNER: Toàn quyền quản lý.
    • MANAGER: Quản lý nội dung và thành viên.
    • MEMBER: Thành viên chính thức (Xem được chi phí).
    • MEMBER_NO_FINANCE: Thành viên không được xem thông tin tài chính.
    • VIEWER_ONLY: Chỉ xem lộ trình và ảnh.

3.3. Lộ trình (Leg & Location)

Một Tour được chia thành nhiều chặng di chuyển (Leg), mỗi chặng chứa danh sách các điểm đến (Location).

  • Leg: id, tourId (FK), sequence (Thứ tự chặng), note.
  • Location: Tích hợp tính năng theo dõi tiến độ (Thay thế khái niệm Task cũ).
    • Tọa độ: latitude, longitude.
    • Thời gian: plannedStart, plannedEnd, actualStart, actualEnd.
    • Trạng thái (LocationStatus): PENDING (Chờ), COMPLETED (Hoàn thành).
    • Loại địa điểm (LocationType): MOVE, VISIT, REST, EAT.

3.4. Chi phí (Expense)

Quản lý tài chính cho từng chặng hoặc gắn trực tiếp vào một địa điểm cụ thể.

  • Expense: id (UUID, PK), leg_id (UUID, FK), location_id (UUID, FK, Nullable), category (Enum: ACCOMMODATION, FOOD, TRANSPORT, TICKET, OTHER), amount (Decimal), description (Text).

3.5. Album Ảnh (Photo)

Lưu trữ tài nguyên đa phương tiện gắn với bối cảnh chuyến đi.

  • Photo: id, tourId (FK), locationId (FK, nullable), uploaderId (FK), imageUrl, privacy.
    • Cấp độ bảo mật (PrivacyLevel): PUBLIC, TOUR_ONLY, PRIVATE.

4. Giải Pháp Kiến Trúc Cho Các Tính Năng Core

4.1. Thuật Toán Tìm Đường Ngắn Nhất (Shortest Path Routing)

  • Cơ chế: Khi người dùng thêm các địa điểm vào một chặng, Frontend sẽ gửi danh sách tọa độ hình học (Lat, Lng) về Backend.
  • Xử lý: Backend tích hợp với Mapbox Direction API hoặc chạy một instance OSRM (Open Source Routing Machine) cục bộ để giải bài toán Người đi giao hàng (TSP - Travelling Salesman Problem). Hệ thống sẽ sắp xếp lại thứ tự sequence_in_leg của các places để tối ưu tổng quãng đường hoặc thời gian di chuyển, sau đó trả về mảng tọa độ đường đi (Polyline) để Frontend vẽ lên bản đồ.

4.2. Engine Tính Toán Chi Phí & Chia Tiền (Expense Splitting Engine)

  • Công thức tự động: * Tổng chi phí Tour (T) = \sum \text{Expenses} của tất cả các chặng.
    • Hệ thống cho phép cấu hình số lượng Người lớn (N_{adult}) và Trẻ em (N_{child}), kèm phần trăm giảm giá của trẻ em (P_{child\_discount}).
    • Gọi X là chi phí của 1 người lớn: T = N_{adult} \times X + N_{child} \times \left( X \times (1 - P_{child\_discount}) \right)
    • Từ đó Backend tự động giải phương trình để tìm ra chi phí trung bình của người lớn (X) và trẻ em, hiển thị báo cáo chi tiết ở cuối mỗi chặng và kết thúc tour.

4.3. Theo Dõi Tiến Độ & Tính Toán Độ Lệch Thời Gian (Time Variance Tracking)

  • Tự động (Auto-trigger): Một Worker/Cron-job chạy ngầm mỗi phút quét các tasks có cài đặt AUTO_BY_TIME. Nếu current_time >= planned_timestamp và chưa hoàn thành, hệ thống tự động chuyển is_completed = true và ghi nhận completed_at = planned_timestamp.
  • Thủ công (Manual-trigger): Khi user bấm tích chọn hoàn thành, hệ thống ghi nhận ngay completed_at = current_time.
  • Báo cáo thời gian (Time Summary): Backend tính toán hiệu số: \Delta t = \text{completed\_at} - \text{planned\_timestamp} Đồng thời group dữ liệu theo loại hoạt động để xuất ra tổng thời gian: Di chuyển (dựa trên khoảng cách bản đồ), Tham quan (thời gian ở các địa điểm giải trí), Nghỉ ngơi (thời gian ở chỗ nghỉ).

4.4. Bảo Mật & Tách Biệt Album Ảnh (Photo Album Isolation)

  • Tách biệt logic: Cơ sở dữ liệu ảnh liên kết qua mã tour_idplace_id nhưng API endpoint kiểm tra quyền (/api/v1/tours/:id/photos) hoạt động độc lập với endpoint kế hoạch (/api/v1/tours/:id/plans).
  • Bảo mật phân quyền (Privacy Middleware): Trước khi trả về dữ liệu hình ảnh hoặc kế hoạch, hệ thống sẽ chạy qua một tầng Middleware để check quyền trong bảng tour_members:
    // Giả lập Logic kiểm tra quyền truy cập thông tin tài chính/kế hoạch
    if (member.role === 'MEMBER_PHOTO_ONLY' && request.path.includes('/plans')) {
        throw new ForbiddenException("Bạn không có quyền xem kế hoạch và chi phí của tour này.");
    }
    

5. Thiết Kế Luồng Đi (Workflow Diagrams)

Luồng tạo chặng và tính toán đường đi (Frontend <=> Backend)

  1. User tìm kiếm/chọn địa điểm trên bản đồ -> Frontend cập nhật state cục bộ.
  2. User nhấn "Tối ưu đường đi" -> Frontend gửi danh sách coords[] lên /api/v1/routing/optimize.
  3. Backend gọi Mapbox/OSRM API -> Sắp xếp lại thứ tự tối ưu -> Lưu vào DB -> Trả về JSON chứa optimized_placespolyline_geometry.
  4. Frontend render lại danh sách địa điểm theo thứ tự mới và vẽ nét đứt/đường đi trên Map.

Luồng chia sẻ hình ảnh và kiểm tra bảo mật

  1. User tải ảnh lên tại một địa điểm trong Tour.
  2. Frontend gửi file đến dịch vụ lưu trữ (S3) nhận về image_url, sau đó lưu thông tin vào DB kèm metadata (ngày giờ, địa điểm).
  3. Khi một User khác truy cập link Tour:
    • Hệ thống check tour_members.
    • Nếu thuộc diện External Viewer hoặc Photo Only: Ẩn hoàn toàn tab "Kế hoạch chặng hành trình" và "Tổng số tiền", chỉ hiển thị giao diện Grid hình ảnh (album).

6. Thiết kế UI Layout: Màn hình Tổng quan Chuyến đi (Tour Dashboard)

Đây là màn hình Hub chính sau khi người dùng truy cập vào một Tour cụ thể. Giao diện được thiết kế để tối ưu hóa trải nghiệm trên thiết bị di động và kiểm soát hiển thị nội dung động dựa trên vai trò của thành viên (ParticipantRole).

6.1. Thành phần Giao diện chính

  • Top Banner (Khu vực tiêu đề):

    • Cover Image: Ảnh bìa của tour (lấy từ ảnh đầu tiên trong Album hoặc ảnh phong cảnh mặc định).
    • Thông tin Tour: Hiển thị tên Tour và khoảng thời gian diễn ra (startDate -> endDate).
    • Member Avatars: Danh sách avatar các thành viên tham gia (TourParticipant). Cho phép nhấn vào để xem chi tiết danh sách hoặc mời thêm người (nếu có quyền).
  • Financial Quick-View Widget (Khối tài chính nhanh):

    • Hiển thị totalCost hiện tại của toàn bộ chuyến đi.
    • Logic Ẩn/Hiện động (RBAC):
      • Hiển thị: Với các role OWNER, MANAGER, MEMBER.
      • Ẩn: Với các role MEMBER_NO_FINANCE hoặc VIEWER_ONLY. Thay thế khối này bằng một câu quote truyền cảm hứng du lịch (Ví dụ: "Đừng nghe họ nói, hãy tự mình đi xem").
  • Bottom Navigation Bar hoặc Tab View: Hệ thống chia làm 4 tab chính để tránh nhồi nhét dữ liệu:

    1. Lộ trình (Itinerary): Hiển thị danh sách Leg & Location. Đây là tab mặc định khi vào Tour.
    2. Chi phí (Finance): Quản lý các Expense. Chỉ hiển thị cho các role có quyền truy cập tài chính.
    3. Album ảnh (Gallery): Hiển thị lưới hình ảnh Photo được chia sẻ trong chuyến đi.
    4. Thành viên & Cài đặt (Settings): Khu vực dành riêng cho OWNER hoặc MANAGER để quản lý thành viên, phân quyền và cấu hình tour.

6.2. Sơ đồ bố cục (Wireframe mô phỏng)

 +---------------------------------------+
 | [ < ]       Tên Chuyến Đi        [ . . ] |  <-- Header bar
 +---------------------------------------+
 |                                       |
 |            IMAGE COVER PHOTO          |  <-- Top Banner
 |                                       |
 |  [Avatar][Avatar][Avatar] +3          |
 +---------------------------------------+
 |                                       |
 |  { TỔNG CHI PHÍ: 2.500.000 VND }      |  <-- Finance Widget (Hoặc Quote)
 |                                       |
 +---------------------------------------+
 | [ Lộ trình ] [ Chi phí ] [ Ảnh ] [S]  |  <-- Tab Navigation
 +---------------------------------------+
 |                                       |
 |         (Nội dung tab hiện tại)       |
 |                                       |
 +---------------------------------------+

+---------------------------------------+

6.3. Tab Lộ Trình: Cấu trúc Cây Phân Cấp (Timeline View)

Đây là phần phức tạp nhất vì dữ liệu có cấu trúc lồng nhau: Tour ➔ Leg (Chặng) ➔ Location (Địa điểm). Giao diện sử dụng mô hình Timeline đứng (Vertical Timeline) để hiển thị lịch trình di chuyển một cách chuẩn mực.

A. Tầng 1 - Chọn Chặng (Leg Picker)

  • UI Component: Sử dụng Horizontal Tab Scroller đặt ở phía trên cùng của tab.
  • Chức năng: Thay vì kéo một danh sách dài vô tận, người dùng có thể chọn nhanh các Chặng (ví dụ: Chặng 1, Chặng 2, Chặng 3 hoặc Ngày 1, Ngày 2...).
  • Logic: Khi người dùng nhấn vào một Leg, danh sách các Location tương ứng sẽ được hiển thị phía dưới dựa vào trường sequence.

B. Tầng 2 - Danh sách Địa điểm (Location Cards)

Mỗi địa điểm là một thẻ (card) nằm trên đường timeline đứng, bao gồm các đặc điểm nhận diện:

  • Icon nhận diện theo loại (LocationType):

    • MOVE: Icon Phương tiện (Xe bus, Máy bay).
    • VISIT: Icon Tham quan (Kính thiên văn, Lá cờ).
    • REST: Icon Nghỉ ngơi (Giường ngủ).
    • EAT: Icon Ẩm thực (Dao dĩa).
  • Trạng thái & Tiến độ (LocationStatus):

    • Trạng thái PENDING:
      • UI: Card có viền nét đứt (dashed), màu xám nhạt. Hiển thị giờ dự kiến (plannedStart).
      • Hành động: Nút "Bắt đầu" (Chỉ hiển thị cho vai trò OWNER hoặc MANAGER).
    • Trạng thái Đang diễn ra (Active):
      • UI: Card được làm sáng (highlight), hiển thị giờ bắt đầu thực tế (actualStart).
      • Hành động: Nút "Hoàn thành".
    • Trạng thái COMPLETED:
      • UI: Card có dấu tích xanh, màu nền chuyển sang tone dịu (ví dụ: xanh lá nhạt), hiển thị giờ kết thúc thực tế (actualEnd).

6.4. Tab Chi Phí (Expense Management)

Cấu trúc dữ liệu của hệ thống cho phép gắn Expense (Chi phí) vào một Leg (Chặng) hoặc gắn trực tiếp vào một Location (Địa điểm) cụ thể.

A. Phân tích trực quan

  • Biểu đồ tròn (Donut Chart): Hiển thị ở đầu tab để phân tích chi phí theo danh mục (category: FOOD, TRANSPORT, ACCOMMODATION, TICKET...). Giúp người dùng nhìn trực quan ngân sách đang đổ vào đâu nhiều nhất.

B. Danh sách hiển thị (List View)

  • Hiển thị dòng tiền: Liệt kê các khoản chi tiêu kèm mô tả (description) và số tiền chính xác (amount).
  • Nhãn bối cảnh (Context Tag): Mỗi dòng chi phí có một nhãn nhỏ đi kèm giúp người dùng xác định bối cảnh chi tiêu:
    • Nếu location_id có dữ liệu: Hiển thị nhãn “Tại: [Tên địa điểm]”.
    • Nếu chỉ có leg_id: Hiển thị nhãn “Thuộc: [Tên Chặng]”.
    • Điều này giúp người dùng nắm bắt nhanh khoản tiền này được tiêu dùng tại vị trí nào trong hành trình.

C. Nút thêm nhanh (Floating Action Button)

  • Nút +: Nằm ở góc dưới màn hình để thêm mới chi phí.
  • Logic thông minh: Khi nhấn vào, hệ thống sẽ tự động chọn sẵn Leg hoặc Location dựa trên ngữ cảnh người dùng đang xem để giảm thiểu các bước nhập liệu thủ công.

Bảng Photo có trường locationId (nullable) và privacy. Giao diện cần tận dụng tốt hai trường này để tối ưu hóa việc lưu giữ và quản lý kỷ niệm chuyến đi.

A. Chế độ xem thông minh (Smart View)

Cung cấp 2 chế độ lọc thông qua nút gạt (Toggle Switch):

  • Xem theo dòng thời gian (Timeline Photo): Ảnh được gom nhóm (group by) theo từng Location. Đi tới địa điểm nào, ảnh chụp tại đó sẽ hiện ngay bên dưới địa điểm đó hoặc gom thành một cụm để dễ nhớ kỷ niệm gắn liền với lộ trình di chuyển.
  • Xem dạng lưới (Grid View): Hiển thị dạng lưới ảnh tiêu chuẩn, tương tự các ứng dụng quản lý ảnh (Photos) thông thường trên điện thoại.

B. Bộ lọc Quyền riêng tư (PrivacyLevel)

Logic hiển thị tuân thủ nghiêm ngặt theo phân quyền:

  • Các ảnh có tag PRIVATE chỉ hiển thị duy nhất với chính người tải lên (uploaderId).
  • Ảnh TOUR_ONLY hiển thị cho mọi thành viên có trong Tour.

C. Chỉ báo UI (Privacy Indicators)

Trên góc mỗi ảnh hiển thị một icon nhỏ giúp người dùng dễ dàng kiểm soát trạng thái chia sẻ của mình:

  • Hình ổ khóa: Dành cho ảnh ở chế độ PRIVATE.
  • Hình con mắt: Dành cho ảnh ở chế độ TOUR_ONLY hoặc PUBLIC.

7. Tối ưu hóa State Management & Hiệu năng Frontend (Crucial)

Vì cấu trúc dữ liệu của dự án sử dụng rất nhiều ID nối nhau (Foreign Keys), để frontend chạy mượt và không bị giật lag khi render, các kỹ thuật sau được ưu tiên áp dụng:

7.1. Chuẩn hóa dữ liệu ở Frontend (Data Normalization)

Khi gọi API lấy chi tiết Tour, cấu trúc cây sẽ được biến đổi thành cấu trúc phẳng (Flatten State) bằng cách lưu dữ liệu dưới dạng Object Key-Value (Dùng ID làm Key).

// Ví dụ cấu trúc State tối ưu ở Frontend
const locationsState = {
  "uuid-location-1": { latitude: 16.0, longitude: 108.0, status: "PENDING", ... },
  "uuid-location-2": { ... }
};

Khi một Location thay đổi trạng thái từ PENDING sang COMPLETED, hệ thống chỉ cần cập nhật đúng object đó thông qua ID, giúp các thành phần khác của UI không bị re-render vô ích, tối ưu hóa hiệu năng render của React.

7.2. Xử lý Real-time và Optimistic Updates (Cập nhật lạc quan)

  • Đồng bộ Real-time: Sử dụng Websocket để đồng bộ trạng thái giữa các thành viên trong Tour. Khi một người bấm "Hoàn thành" địa điểm hoặc "Thêm chi phí", những người khác sẽ nhận được cập nhật ngay lập tức.
  • Optimistic Update: Khi một thành viên thêm một Expense, Frontend sẽ lập tức cộng số tiền đó vào totalCost hiển thị trên màn hình trước khi nhận phản hồi từ server. Nếu API trả về lỗi, hệ thống sẽ thực hiện roll-back trạng thái dữ liệu. Điều này tạo cảm giác ứng dụng phản hồi tức thì.

7.3. Phân quyền UI động (Dynamic UI Rendering based on Role)

Sử dụng các hàm Helper tại Frontend để kiểm tra quyền hạn trước khi render các thành phần tương tác, đảm bảo tính bảo mật và trải nghiệm người dùng:

const canEdit = ['OWNER', 'MANAGER'].includes(userRole);

// Trên UI Component:
{canEdit && <Button onClick={handleEditLocation}>Chỉnh sửa lộ trình</Button>}