Bỏ qua

Kế hoạch Chi tiết — 3 Feature Ưu tiên

Phạm vi: Thống kê hệ thống · Admin Dashboard · Realtime Availability Grid
Ngày: 2026-05-24
Nguyên tắc: Plan only — không đi vào code implementation


Feature 1 — Thống kê & Báo cáo Hệ thống

1.1 Bài toán & Mục tiêu

Admin cần nhìn thấy sức khỏe toàn hệ thống theo thời gian: tăng trưởng người dùng, hoạt động booking, doanh thu trên toàn platform, phân bố sân theo trạng thái. Data phải hỗ trợ filter theo khoảng thời gian (date range) và hiển thị xu hướng theo ngày.


1.2 Quyết định Kiến trúc

Vấn đề: Data stats nằm phân tán ở 3 service khác nhau (user, field, booking). Cần quyết định ai là người aggregate.

Lựa chọn Mô tả Trade-off
A — Gateway aggregation Gateway gọi song song 3 service, merge kết quả Đơn giản, tái dùng code đã có, không cần infra mới
B — Dedicated analytics service Tạo service riêng thu thập data từ Kafka Scalable hơn nhưng over-engineering ở quy mô hiện tại
C — Booking-service làm hub Booking-service tổng hợp toàn bộ stats Phá vỡ domain boundary, coupling cao

Quyết định: Chọn A — Gateway aggregation

Lý do: Pattern này đã được dùng trong GET /bookings/owner/revenue (gateway gọi field-service lấy fieldIds rồi query booking-service). Áp dụng nhất quán. Mỗi service chỉ expose stats thuộc domain của mình.


1.3 Các API cần tạo mới

Layer 1: Internal stats endpoints (trong từng service)

Cả 3 endpoint này là internal — chỉ được gọi từ gateway qua proxy headers, không expose ra ngoài trực tiếp.


[user-service] — Thêm endpoint mới

GET /users/admin/stats?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD

Data cần trả về:

  • Tổng số users
  • Phân bố theo role: CUSTOMER, FIELD_OWNER, ADMIN
  • Phân bố theo status: ACTIVE, BANNED, SUSPENDED
  • Số users mới đăng ký trong date range (group by ngày)

Nơi thêm: apps/user-service/src/users/ — thêm method vào UsersService + endpoint vào UsersController


[field-service] — Thêm endpoint mới

GET /fields/admin/stats?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD

Data cần trả về:

  • Tổng số fields
  • Phân bố theo status: ACTIVE, PENDING_APPROVAL, CLOSED, REJECTED
  • Số fields mới tạo trong date range (group by ngày)

Nơi thêm: apps/field-service/src/fields/services/fields.service.ts + apps/field-service/src/fields/controllers/


[booking-service] — Thêm endpoint mới

GET /bookings/admin/stats?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD

Data cần trả về:

  • Tổng số bookings trong date range, phân bố theo status
  • Tổng doanh thu (chỉ đếm CONFIRMED + COMPLETED)
  • Booking trend: group by ngày (count + revenue/ngày)
  • Cancellation rate

Nơi thêm: apps/booking-service/src/bookings/bookings.service.ts — có thể mở rộng từ getOwnerRevenueStats đã có, nhưng scope toàn bộ hệ thống thay vì filter theo fieldIds


Layer 2: Gateway aggregate endpoint (public-facing)

[api-gateway] — Tạo controller mới

GET /admin/stats?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD
Authorization: Bearer {token}   (Role: ADMIN)

Nơi tạo: apps/api-gateway/src/admin/admin-stats.controller.ts (tạo mới)

Luồng xử lý trong gateway:

1. Validate request (ADMIN role check, date params)
2. Gọi Promise.all([
     userProxy.get('/users/admin/stats?...'),
     fieldProxy.get('/fields/admin/stats?...'),
     bookingProxy.get('/bookings/admin/stats?...')
   ])
3. Merge 3 kết quả thành 1 response duy nhất
4. Cache kết quả tại gateway (TTL: 5 phút)

1.4 Caching Strategy

Stats là data "chấp nhận được nếu hơi cũ" — không cần realtime. Cache là bắt buộc để tránh query nặng trên DB.

┌─────────────────────────────────────────────┐
│ Cache key: admin:stats:{startDate}:{endDate} │
│ TTL: 5 phút tại gateway                     │
│ TTL: 10 phút tại mỗi service                │
│                                             │
│ Invalidation: không cần tự động            │
│ (user tự refresh bằng cách thay date range) │
└─────────────────────────────────────────────┘

1.5 Checklist triển khai (theo thứ tự)

Step 1 — user-service
  □ Thêm method getAdminStats(startDate, endDate) vào UsersService
  □ Thêm endpoint GET /users/admin/stats vào UsersController
  □ Thêm Redis cache cho endpoint này (TTL: 10 phút)

Step 2 — field-service
  □ Thêm method getAdminStats(startDate, endDate) vào FieldsService
  □ Thêm endpoint GET /fields/admin/stats vào AdminFieldsController (hoặc controller mới)
  □ Thêm Redis cache

Step 3 — booking-service
  □ Thêm method getSystemStats(startDate, endDate) vào BookingsService
  □ Mở rộng từ getOwnerRevenueStats nhưng không filter fieldIds
  □ Thêm endpoint GET /bookings/admin/stats vào BookingsController
  □ Thêm Redis cache

Step 4 — api-gateway
  □ Tạo apps/api-gateway/src/admin/ folder
  □ Tạo AdminStatsController với GET /admin/stats
  □ Implement Promise.all aggregation
  □ Thêm gateway-level cache (TTL: 5 phút)
  □ Register vào app.module.ts

Feature 2 — Tổng quan Admin Dashboard

2.1 Bài toán & Mục tiêu

Màn hình đầu tiên admin thấy sau khi đăng nhập. Mục tiêu: thông báo ngay những việc cần xử lý (pending actions) và cung cấp snapshot nhanh hôm nay + tháng này. Response phải nhanh (< 500ms) vì đây là trang landing.


2.2 Quyết định Kiến trúc

Vấn đề: Dashboard cần data từ nhiều sources, nhưng cần fast. Conflict giữa completeness và latency.

Lựa chọn Latency Độ phức tạp
A — Gateway gọi parallel, cache ngắn ~200-300ms (parallel) Thấp
B — Pre-computed background job ~10ms (read cache) Cao (cần scheduler)
C — Separate dashboard service ~50ms Rất cao

Quyết định: Chọn A + cache ngắn (TTL: 2 phút)

Lý do: Dashboard không cần accurate đến từng giây. Cache 2 phút là chấp nhận được — admin refresh mỗi lần vào trang sẽ thấy data mới nhất trong 2 phút trước.

Insight quan trọng: Không cần tạo endpoint mới ở downstream services. Gateway có thể tái sử dụng các endpoint đã có:

  • Count pending owner applications → GET /owner-applications → đọc total từ response (hoặc thêm count=true param để lightweight hơn)
  • Count pending fields → GET /admin/fields/pending?page=1&limit=1 → đọc pagination.total
  • Today's bookings/revenue → GET /bookings/admin/stats?startDate=today&endDate=today
  • User count → có thể lấy từ GET /admin/stats (đã có ở Feature 1) hoặc call riêng

2.3 API cần tạo mới

Chỉ cần 1 endpoint duy nhất ở gateway — không cần thêm gì ở downstream:

[api-gateway] — Thêm vào AdminStatsController đã tạo ở Feature 1

GET /admin/dashboard
Authorization: Bearer {token}   (Role: ADMIN)

Luồng xử lý trong gateway:

1. Xác định ngày hôm nay + đầu tháng này
2. Gọi Promise.all([
     userProxy.get('/owner-applications'),         → đếm pending
     fieldProxy.get('/admin/fields/pending?limit=1'), → đếm pending fields
     bookingProxy.get('/bookings/admin/stats?today'), → booking hôm nay
     userProxy.get('/users/admin/stats?thisMonth'),   → user growth
   ])
3. Extract và aggregate thành dashboard response
4. Cache tại gateway (TTL: 2 phút)

Response schema:

{
  generatedAt: timestamp,

  pendingActions: {
    ownerApplications: number,     // Số đơn đăng ký chờ duyệt
    fieldsAwaitingApproval: number, // Số sân chờ phê duyệt
    totalActionRequired: number    // Tổng
  },

  today: {
    date: YYYY-MM-DD,
    newUsers: number,
    newBookings: number,
    revenue: number,        // VNĐ
    newFields: number
  },

  thisMonth: {
    totalBookings: number,
    totalRevenue: number,
    newUsers: number,
    cancellationRate: number  // 0.0 - 1.0
  },

  systemHealth: {
    totalActiveUsers: number,
    totalActiveFields: number,
    totalConfirmedBookings: number  // all-time
  }
}

2.4 Tối ưu Latency

Vì đây là trang landing, latency ảnh hưởng trực tiếp đến UX. Kế hoạch tối ưu:

┌─ Gateway cache (TTL: 2 phút) ──────────────────────┐
│  Cache key: admin:dashboard                        │
│  Hit rate cao vì nhiều admin cùng xem              │
│  Cache miss → gọi parallel → kết quả trong ~300ms │
└────────────────────────────────────────────────────┘

Nếu cache miss:
  Promise.all([4 calls]) thay vì sequential
  → Total latency = max(single call latency) ≈ 150-200ms
  → KHÔNG phải sum(4 calls) = 600-800ms

2.5 Checklist triển khai (theo thứ tự)

Step 1 — Phụ thuộc vào Feature 1
  □ Phải có GET /bookings/admin/stats trước (để lấy today stats)
  □ Phải có GET /users/admin/stats trước (để lấy user growth)

Step 2 — api-gateway
  □ Thêm GET /admin/dashboard vào AdminStatsController (đã tạo từ Feature 1)
  □ Implement Promise.all với 4 parallel calls
  □ Extract pendingActions từ pagination.total của existing endpoints
  □ Thêm gateway-level cache (TTL: 2 phút, key: admin:dashboard)

Dependency: Feature 2 phụ thuộc Feature 1 (cần booking/user stats endpoints).
Nên implement Feature 1 trước, Feature 2 sau.


Feature 3 — Realtime Availability Grid

3.1 Bài toán & Mục tiêu

Khi nhiều khách hàng cùng xem lịch trống của một sân phổ biến vào giờ cao điểm, slot có thể bị đặt trong vài giây. FE cần hiển thị lịch luôn phản ánh trạng thái mới nhất — tránh tình huống user thấy slot "AVAILABLE" nhưng khi submit lại bị lỗi 409 Conflict.


3.2 Quyết định Kiến trúc — Polling vs SSE vs WebSocket

Đây là quyết định kỹ thuật quan trọng nhất của feature này. Cần phân tích kỹ:

So sánh 3 phương án

Tiêu chí Client Polling SSE (Server-Sent Events) WebSocket
Độ phức tạp backend Thấp (không thay đổi) Trung bình Cao
Độ phức tạp frontend Thấp (setInterval) Thấp (EventSource API) Trung bình
Latency cập nhật Bằng interval (15-30s) Gần realtime (< 1s) Gần realtime (< 1s)
Server load Cao (mọi client đều poll) Thấp (push khi có event) Thấp
Infra hiện có Không cần gì thêm Redis Pub/Sub (đã có) Cần thêm WS layer
Horizontal scale Đơn giản Cần Redis Pub/Sub (đã có) Cần sticky session hoặc Redis
HTTP Proxy friendly ✅ (HTTP chunked) ⚠️ Cần upgrade connection
Phù hợp use case này Chấp nhận được ✅ Phù hợp nhất Over-engineering

Quyết định: SSE + Redis Pub/Sub

Lý do chọn SSE:

  1. Booking là event-driven — slot thay đổi khi có booking created/confirmed/cancelled. Đây chính xác là use case SSE được thiết kế cho: server push khi có event, không push khi không có gì thay đổi.

  2. Redis đã có trong infralibs/cache/ đã dùng Redis. Pub/Sub là tính năng native của Redis, không cần thêm infrastructure.

  3. Kafka events đã tồn tạiBOOKING_CREATED, BOOKING_CONFIRMED, BOOKING_CANCELLED, BOOKING_EXPIRED đã được publish. Chỉ cần consumer mới để chuyển Kafka event → Redis Pub/Sub → SSE client.

  4. Tránh poll storm — nếu trang detail sân phổ biến có 100 user cùng xem, polling 15s = 6-7 requests/phút/user × 100 users = 600-700 requests/phút trên 1 sân. SSE: 100 connections dài hạn nhưng server chỉ push khi có thay đổi.


3.3 Kiến trúc Event Flow

┌──────────────┐     booking      ┌─────────────────┐
│   Customer   │ ─── created ───► │ booking-service  │
│  (Bên A)    │                  └────────┬────────┘
└──────────────┘                          │ Kafka publish
                                          │ booking.created
                                          ▼
                               ┌─────────────────────┐
                               │    Kafka Broker      │
                               └──────────┬──────────┘
                                          │ consume
                                          ▼
                              ┌───────────────────────┐
                              │ api-gateway            │
                              │ AvailabilityConsumer   │◄── NEW
                              │ (Kafka consumer)       │
                              └──────────┬────────────┘
                                         │ Redis PUBLISH
                                         │ field:avail:{fieldId}:{date}
                                         ▼
                              ┌───────────────────────┐
                              │       Redis            │
                              │     Pub/Sub            │
                              └──────────┬────────────┘
                                         │ subscribe
                                         ▼
                              ┌───────────────────────┐
                              │ api-gateway            │
                              │ SSE Controller         │◄── NEW
                              │ GET /bookings/fields/  │
                              │ :fieldId/stream        │
                              └──────────┬────────────┘
                                         │ SSE event push
                                         ▼
                              ┌───────────────────────┐
                              │   Customer Browser     │
                              │   (Bên B, C, D...)    │
                              │   EventSource API      │
                              └───────────────────────┘

3.4 Redis Pub/Sub Channel Design

Channel naming convention:

field:availability:{fieldId}:{date}

Ví dụ: field:availability:b2c3d4e5-...:2026-06-10

Payload khi publish lên channel:

{
  "event": "SLOT_UPDATED",
  "fieldId": "b2c3d4e5-...",
  "date": "2026-06-10",
  "trigger": "BOOKING_CREATED",
  "bookingId": "a1b2c3d4-..."
}

Payload chỉ là notification (tín hiệu), không chứa toàn bộ grid data. FE nhận event → tự fetch lại GET /bookings/fields/:fieldId/schedule-grid?date=... để lấy grid mới nhất. Cách này tránh vấn đề stale data trong SSE payload.


3.5 API cần tạo mới

[api-gateway] — Tạo SSE endpoint mới

GET /bookings/fields/:fieldId/availability/stream?date=YYYY-MM-DD
Authorization: Không bắt buộc (public field availability)
Accept: text/event-stream

Response headers:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

SSE event format:

event: slot_updated
data: {"fieldId":"b2c3d4e5-...","date":"2026-06-10","trigger":"BOOKING_CREATED"}

event: heartbeat
data: {"timestamp":"2026-06-10T08:00:30.000Z"}

Events phát ra:

  • slot_updated — khi có booking thay đổi trên field+date này
  • heartbeat — mỗi 30 giây để giữ connection alive (tránh proxy timeout)

[api-gateway] — Thêm Kafka consumer mới (internal)

Không phải HTTP endpoint, là Kafka consumer chạy trong api-gateway:

Consumer group: api-gateway-availability
Topics subscribe:
  - booking.created
  - booking.confirmed
  - booking.cancelled
  - booking.expired

Khi nhận event:
  1. Extract fieldId + startTime từ event
  2. Tính date = startTime.toISOString().slice(0, 10)
  3. Redis PUBLISH field:availability:{fieldId}:{date} {payload}

3.6 Handling Edge Cases

Tình huống Cách xử lý
Client mất kết nối EventSource tự reconnect (browser-native behavior)
Redis Pub/Sub node restart SSE stream đứt → EventSource reconnect → resubscribe
api-gateway có nhiều instance (scale out) Redis Pub/Sub broadcast tới tất cả instances → mỗi instance push tới subscribers của mình
Client xem nhiều ngày (date picker) Chỉ subscribe 1 stream cho ngày đang xem, khi đổi ngày → close stream cũ, mở stream mới
Không có ai xem sân này Redis channel không có subscriber → Redis tự cleanup, không tốn resource
SSE không được support (proxy cũ) Fallback về polling (FE detect EventSource support, nếu không có → dùng polling)

3.7 Phân chia Phase triển khai

Feature này có thể chia làm 2 phase để giảm rủi ro:

Phase 1: Efficient Polling (deploy nhanh, low risk)

Thay đổi backend: Thêm ETag header vào GET /bookings/fields/:fieldId/schedule-grid

  • Tính ETag = hash của danh sách bookingIds trong grid
  • Nếu client gửi If-None-Match: {etag} và grid không đổi → trả 304 Not Modified (không body)
  • Giảm bandwidth ~90% khi không có thay đổi

FE polling logic:

FE poll GET /schedule-grid mỗi 15 giây
  → Nếu 304 → không update UI
  → Nếu 200 → update grid

Ưu điểm Phase 1: Zero infrastructure change, deploy ngay.

Phase 2: SSE + Redis Pub/Sub (thực sự realtime)

Implement theo kiến trúc ở mục 3.3:

  • Tạo SSE endpoint
  • Tạo Kafka consumer trong api-gateway
  • FE chuyển từ polling sang EventSource

3.8 Checklist triển khai

Phase 1 — Efficient Polling (ưu tiên trước)
  □ [booking-service] Thêm ETag header vào GET /bookings/fields/:fieldId/schedule-grid
    → ETag = hash(sorted bookingIds in grid)
    → Hỗ trợ If-None-Match → 304 Not Modified
  □ [FE] Implement polling với If-None-Match header, interval 15 giây
  □ [FE] Stop polling khi user rời khỏi trang / tab mất focus

Phase 2 — SSE Realtime (sau Phase 1)
  □ [api-gateway] Tạo AvailabilityKafkaConsumer
    → Subscribe: booking.created, booking.confirmed, booking.cancelled, booking.expired
    → Action: Redis PUBLISH field:availability:{fieldId}:{date}
  □ [api-gateway] Tạo AvailabilityStreamController
    → GET /bookings/fields/:fieldId/availability/stream?date=YYYY-MM-DD
    → Subscribe Redis channel, pipe thành SSE stream
    → Heartbeat mỗi 30s
    → Cleanup subscription khi client disconnect
  □ [api-gateway] Register consumer + controller vào module
  □ [FE] Thay polling bằng EventSource API
    → Trên event 'slot_updated': gọi lại GET /schedule-grid để lấy data mới
    → Heartbeat timeout: nếu không nhận heartbeat trong 60s → reconnect
    → Fallback: nếu EventSource không support → dùng polling Phase 1

Tóm tắt Dependency Graph

Feature 1 (Stats)
    ├── user-service: GET /users/admin/stats          [độc lập]
    ├── field-service: GET /fields/admin/stats        [độc lập]
    ├── booking-service: GET /bookings/admin/stats    [độc lập]
    └── gateway: GET /admin/stats                     [phụ thuộc 3 endpoint trên]
          │
Feature 2 (Dashboard)
    └── gateway: GET /admin/dashboard                 [phụ thuộc Feature 1]

Feature 3 (Realtime Grid)
    Phase 1: ETag polling                             [độc lập — có thể làm song song]
    Phase 2: SSE                                      [phụ thuộc Phase 1 done]

Thứ tự implement khuyến nghị:

1. Feature 3 Phase 1 (ETag) — 1 ngày — low risk, deploy ngay
2. Feature 1 Stats — 3-4 ngày — 3 services + 1 gateway controller
3. Feature 2 Dashboard — 1 ngày — tái dùng Feature 1
4. Feature 3 Phase 2 (SSE) — 2-3 ngày — Kafka consumer + SSE endpoint

Phụ lục: Các file cần tạo/chỉnh sửa

Feature 1 — Stats

Action File
Tạo mới apps/user-service/src/users/users-stats.service.ts
Chỉnh sửa apps/user-service/src/users/users.controller.ts (thêm endpoint)
Tạo mới apps/field-service/src/fields/services/fields-stats.service.ts
Chỉnh sửa apps/field-service/src/fields/controllers/admin-fields.controller.ts
Chỉnh sửa apps/booking-service/src/bookings/bookings.service.ts
Chỉnh sửa apps/booking-service/src/bookings/bookings.controller.ts
Tạo mới apps/api-gateway/src/admin/admin-stats.controller.ts
Chỉnh sửa apps/api-gateway/src/app.module.ts

Feature 2 — Dashboard

Action File
Chỉnh sửa apps/api-gateway/src/admin/admin-stats.controller.ts (thêm method)

Feature 3 — Realtime

Action File
Chỉnh sửa apps/booking-service/src/bookings/ (thêm ETag logic vào schedule-grid)
Tạo mới apps/api-gateway/src/availability/availability-kafka.consumer.ts
Tạo mới apps/api-gateway/src/availability/availability-stream.controller.ts
Tạo mới apps/api-gateway/src/availability/availability.module.ts
Chỉnh sửa apps/api-gateway/src/app.module.ts