Kiến trúc hệ thống
Bốn ứng dụng, một API và một cơ sở dữ liệu PostgreSQL.
Sơ đồ tổng thể
┌─────────────────────────────┐
Khách hàng ───▶ │ apps/citizen (web PWA) │ ─┐
│ apps/citizen-mobile (Expo) │ │
└─────────────────────────────┘ │
│ HTTPS, cookie phiên
┌─────────────────────────────┐ │
Nhân viên ───▶ │ apps/admin (web, /admin) │ ─┤
└─────────────────────────────┘ │
▼
┌─────────────────────────────┐
│ apps/api (Fastify 5) │
│ /api/v1/app/* (khách) │
│ /api/v1/admin/* (nhân viên)│
└──────────────┬──────────────┘
│ Drizzle ORM
▼
┌─────────────────────────────┐
│ PostgreSQL │
└─────────────────────────────┘Bốn ứng dụng
| App | Công nghệ | Cổng khi chạy local | Ai dùng |
|---|---|---|---|
apps/citizen | React 19 + Vite, PWA | 5173 | Khách hàng |
apps/citizen-mobile | Expo 57 / React Native 0.86 | — | Khách hàng |
apps/admin | React 19 + Vite, base /admin/ | 5176 | Nhân viên |
apps/api | Fastify 5 + TypeScript | 3001 | Chạy nền |
Bốn package dùng chung
| Package | Vai trò |
|---|---|
packages/contracts | Schema Zod và kiểu dữ liệu dùng chung cho mọi app. Là nguồn sự thật cho tên trạng thái và luật kiểm tra. |
packages/client | Thư viện gọi API, gồm cả polling. |
packages/ui | Component, token thiết kế và font. |
packages/citizen-domain | Logic hiển thị thuần, dùng chung cho bản web và bản di động của app khách. |
Một origin công khai, hai app web
Gateway scripts/serve-apps.mjs phục vụ cả hai app web trên một cổng công khai:
/và các đường dẫn khác → app người dân./admin/*→ trang quản trị./api/*,/healthz,/readyz→ chuyển tiếp sang API.
Nhờ vậy phiên đăng nhập và cookie hoạt động thống nhất trên cùng một tên miền, không vướng vấn đề CORS giữa hai app.
Hai app web dùng chung một cơ sở dữ liệu
/api/v1/app/* và /api/v1/admin/* là hai cây route tách biệt trong cùng một
tiến trình API, nhưng dùng cùng một cơ sở dữ liệu và một số bảng chung như
orders, sessions, photos. Vì vậy đơn của khách và màn Đặt lịch của nhân
viên luôn nhìn thấy cùng một bản ghi.
Luồng dữ liệu của một đơn
| Bước | Ghi vào đâu | Ai đọc |
|---|---|---|
| Khách đặt lịch | orders với status = pending | Điều phối thấy trong màn Đặt lịch |
| Điều phối duyệt và chọn xe | orders.status = confirmed, ghi vehicleTypeId | Khách thấy trạng thái mới |
| Tạo phiên thu gom | collection_sessions + gán sessionId, status = scheduled | Người thu gom và điều phối |
| Bắt đầu phiên | status = collecting | Khách thấy "Đã bắt đầu thu gom" |
| Nhập phiếu | collection_session_entries theo từng loại | Phiên kiểm tra đọc lại |
| Khách xác nhận | cust_ok_at trên đơn | Mở khoá bước nhập phiếu |
| Chuyển kiểm tra | collection_inspections | Nhân viên kiểm tra |
| Hoàn tất kiểm tra | inspection_invoices, inspection_payouts, sổ giá | Màn Hoá đơn và Tài chính |
| Chi trả | invoice_payout_logs, status = paid | Khách nhận thông báo |