0. Kết luận nhanh
| Hạng mục | Chọn | Một câu lý do |
|---|---|---|
| Ngôn ngữ toàn hệ thống | TypeScript trên Node.js 22 LTS | Zalo cá nhân bản web chỉ có thư viện Node ổn định; một ngôn ngữ cho api, worker, web, script; hiệu năng dư thừa cho tải thực tế |
| Khung máy chủ | NestJS 11 trên Fastify | Cấu trúc mô-đun, guard/interceptor, lịch, hàng đợi, OpenAPI có sẵn; Fastify nhanh gấp ~2 lần Express |
| CSDL | PostgreSQL 16 (apt Ubuntu 24.04, systemd) | Nhiều đại lý ghi đồng thời, khoá hàng, ràng buộc, JSONB, SQL báo cáo |
| Hàng đợi, cache, phiên | Redis 7 + BullMQ | Thử lại, job trễ, job lặp, giới hạn nhịp theo hàng đợi |
| Giao diện người dùng | React + Vite + Ant Design, đóng gói PWA | Một mã nguồn cho PC và mobile; cài lên màn hình điện thoại; đẩy thông báo; giai đoạn 2 bọc Capacitor để lên Google Play nếu cần |
| Zalo cá nhân | zca-js trong tiến trình worker-zalo riêng |
Thư viện không chính thức dựa trên Zalo Web; tách tiến trình để đứt phiên không kéo hệ thống |
| SMS SIM thường | Máy Android + SIM chạy SMS Gateway for Android, máy chủ gateway tự host | REST + webhook trạng thái; không phải viết app ngay |
| AI | Mô-đun ai-tro-ly dùng Claude API (TypeScript SDK), mô hình mặc định claude-opus-4-8 |
Đọc danh sách vé từ văn bản/ảnh, trợ lý đại lý, giải thích lệch đối soát; AI chỉ đề xuất, người xác nhận |
| Triển khai | aaPanel Website → Node project (kiểu PM2) cho 3 tiến trình, gắn domain riêng → aaPanel tự tạo reverse proxy nginx + SSL; PostgreSQL, Redis, Node, Google Drive cài từ App Store | Chỉ dùng tính năng có sẵn của aaPanel, thao tác tay tối thiểu (mục 5) |
| Sao lưu | Cron aaPanel: Shell Script pg_dump + gói cấu hình mã hoá → Backup Directory đẩy Google Drive giữ 30 bản → kiểm tra khôi phục hằng tuần |
Cron Backup Database của aaPanel chỉ hỗ trợ MySQL nên PostgreSQL đi đường Shell (mục 5.3) |
| Phiên bản, git | package.json là nguồn phiên bản; scripts/phien-ban.py, scripts/dong-bo-git.*; hook + CI kiểm tra tài liệu; GitHub riêng tư biencuong/CongGuiVe |
Đã cài trong repo, xem mục 9 |
Ghi chú về "gs" trong yêu cầu: hiểu là Go. Nếu ý là Google Apps Script thì không phù hợp: không chạy trên VPS riêng, không giữ tiến trình dài (Zalo, hàng đợi), giới hạn thời gian chạy và số lần gọi.
1. Ràng buộc từ hạ tầng thật và tải dự kiến
1.1 VPS Ubuntu + aaPanel
aaPanel cung cấp sẵn: nginx (site, reverse proxy, SSL Let's Encrypt tự gia hạn), tường lửa, cron, giám sát cơ bản, và qua App Store: PM2 Manager, Node.js version manager, Python manager, Docker manager, Redis, Supervisor. Kinh nghiệm từ Vé247 trên cùng loại VPS: aaPanel chỉ nên làm nginx + SSL, còn Node chạy bằng PM2 do script cài, không để aaPanel quản phiên bản Node (tránh lệch bản khi aaPanel cập nhật).
Hệ quả cho thiết kế:
- Mọi tiến trình ứng dụng lắng nghe 127.0.0.1 ở cổng nội bộ; chỉ nginx của aaPanel ra Internet.
- PostgreSQL 16 và Redis 7 cài từ
aptcủa Ubuntu 24.04 (không phụ thuộc plugin), chỉ nghe localhost/socket. - Cập nhật bằng
git pull+ di trú +pm2 reloadnhư Vé247, có nút Cập nhật trên trang quản trị.
1.2 Tải dự kiến (giả định, cần xác nhận)
| Đại lượng | Ước tính | Ý nghĩa kỹ thuật |
|---|---|---|
| Vé/ngày | 100–1.000 | ~1 vé/phút giờ cao điểm; CSDL nhỏ (vài trăm MB/năm) |
| Người dùng đồng thời | 10–50 đại lý + 2–3 điều hành | API vài chục yêu cầu/giây là dư |
| Tin gửi/ngày | 200–3.000 (SMS bị chặn nhịp 1 tin/5 giây ≈ 720 tin/giờ/SIM) | Nút thắt là SIM và Zalo, không phải CPU |
| Gọi VeXeRe/ngày | 500–5.000 | Nút thắt là độ trễ API ngoài (0,3–2 giây/lệnh) |
| Ảnh vé PNG/ngày | ≤ 3.000 | Dựng ảnh ~50–150 ms/ảnh; chạy ở worker |
Kết luận: hệ thống I/O-bound, chờ dịch vụ ngoài là chính. Ngôn ngữ nhanh hơn 3 lần về CPU không đổi trải nghiệm; kiến trúc bất đồng bộ, hàng đợi, cách ly lỗi mới quyết định tốc độ và độ bền.
2. So sánh ngôn ngữ theo từng chức năng của hệ thống
Thang 1–5 (5 tốt nhất). Trọng số theo mức ảnh hưởng đến dự án này.
| Chức năng / tiêu chí | Trọng số | Node.js (TS) | Python | PHP | Go |
|---|---|---|---|---|---|
| Zalo cá nhân bản web (thư viện, độ ổn định) | 5 | 5 — zca-js (đã dùng thật ở OSZalo) | 1 — thư viện cũ, bỏ bảo trì | 1 — không có | 1 — không có |
| API + SSE thời gian thực | 4 | 5 — Fastify, SSE gốc | 4 — FastAPI/uvicorn | 2 — PHP-FPM không giữ kết nối dài; cần Swoole/Reverb | 5 |
| Worker nền, hàng đợi có nhịp | 4 | 5 — BullMQ | 4 — Celery/RQ | 3 — Laravel Horizon | 4 — asynq/river |
| Tích hợp API ngoài (VeXeRe, SMS gateway, Zalo OA) | 3 | 5 | 5 | 4 | 5 |
| SDK AI chính thức (Anthropic) | 3 | 5 | 5 | 5 | 5 |
| Dựng ảnh vé PNG/PDF không cần trình duyệt | 2 | 4 — satori + resvg | 4 — Pillow/ReportLab | 3 — GD/Imagick | 4 |
| Excel, email, chuẩn hoá tiếng Việt | 2 | 5 | 5 | 5 | 4 |
| Một ngôn ngữ cho cả giao diện web/mobile | 4 | 5 — chia sẻ zod schema, kiểu dữ liệu | 2 | 2 | 2 |
| Hiệu năng CPU / độ trễ | 2 | 4 | 2 | 3 | 5 |
| Bộ nhớ trên VPS 4 GB | 2 | 4 (~150 MB/tiến trình) | 3 | 4 | 5 (~30 MB) |
| Chống chết: cô lập lỗi, giám sát, khởi động lại | 4 | 4 — PM2 cluster, cần kỷ luật bắt lỗi | 3 | 4 — FPM cô lập theo yêu cầu nhưng worker dài yếu | 5 — kiểu tĩnh, goroutine |
| Tương thích aaPanel/Ubuntu | 3 | 5 — PM2 (đã chạy Vé247) | 4 | 5 — mặc định aaPanel | 4 — nhị phân + Supervisor |
| Tốc độ phát triển với Claude Code, hệ sinh thái cho bài toán này | 4 | 5 — NestJS, Prisma, BullMQ, AntD | 4 — Django/FastAPI | 4 — Laravel/Filament | 3 |
| Bảo trì một người, tuyển thay thế tại Việt Nam | 3 | 5 | 4 | 5 | 3 |
| Điểm có trọng số (tối đa 225) | 211 | 158 | 150 | 172 |
2.1 Vì sao không chọn "lai" (Go hoặc PHP lõi + Node cho Zalo)
Kênh Zalo cá nhân bản web bắt buộc một tiến trình Node. Nếu lõi viết bằng Go/PHP/Python thì hệ thống có hai ngôn ngữ, hai bộ công cụ, hai cách gỡ lỗi cho một người bảo trì; mọi lợi ích hiệu năng của Go không đổi được trải nghiệm vì nút thắt nằm ở SIM, Zalo và API VeXeRe (mục 1.2). Node đủ nhanh: một tiến trình Fastify trên 1 vCPU phục vụ hàng nghìn yêu cầu/giây với route đơn giản, gấp nhiều chục lần nhu cầu.
2.2 Khi nào cân nhắc lại
- Tải vượt ~50.000 vé/ngày hoặc cần xử lý ảnh/AI tại chỗ nặng → tách dịch vụ đó sang Go/Python, giao tiếp qua Redis/HTTP; lõi + mô-đun không đổi nhờ hợp đồng (tài liệu 00, mục 4.9).
- Zalo đổi cơ chế khiến zca-js chết hẳn → mô-đun
zalo-ca-nhanthay bằng Zalo OA/ZNS hoặc điều khiển trình duyệt (Playwright), vẫn là mô-đunKenhGui.
2.3 Zalo cá nhân bản web: hiểu đúng rủi ro
- zca-js mô phỏng Zalo Web: đăng nhập bằng QR (hoặc cookie + IMEI + user-agent đã lưu), nhận sự kiện qua websocket, gửi tin/ảnh, tìm người theo số điện thoại. Không phải API chính thức; Zalo có thể đổi giao thức hoặc khoá tài khoản gửi hàng loạt.
- Thiết kế giảm rủi ro: tài khoản Zalo riêng cho nhà xe (không dùng số cá nhân chủ), gửi tin chỉ tới khách đã nhắn/kết bạn, nhịp gửi tự đặt (20/phút, 150/giờ, 600/ngày; chưa có con số chính thức), nội dung cá nhân hoá (mã vé, tên), không gửi liên kết lạ, giữ phiên (persist session) để không phải quét QR lại, giám sát "sống/chết" và đăng nhập lại bằng QR trên trang quản trị, luôn có SMS dự phòng.
- Khi khách chưa từng nhắn: tin rơi vào "Tin nhắn chờ" → coi là đã gửi nhưng chưa chắc đọc; nếu 10 phút không thấy đã xem thì gửi SMS.
3. Hiệu năng và tốc độ
3.1 Mục tiêu đo được
| Chỉ số | Mục tiêu | Cách đo |
|---|---|---|
| API nội bộ (tạo/sửa/đọc vé) p95 | < 150 ms | Nhật ký pino + Uptime Kuma |
| Xếp vé tự động (tìm chuyến → giữ chỗ → làm giàu) | < 5 giây (phụ thuộc VeXeRe) | Thời gian job BullMQ |
| Đại lý thấy trạng thái đổi | < 1 giây sau sự kiện | SSE |
| Dựng ảnh vé | < 200 ms | Job xuat-ve |
| Trang PWA mở lần 2 trên 4G, máy Android tầm trung | < 1,5 giây tương tác | Lighthouse mobile |
| Gói JS ban đầu | < 300 KB nén | Vite build report |
3.2 Kỹ thuật tối ưu từng lớp
- Máy chủ: NestJS trên Fastify; pool kết nối Prisma (10–20); chỉ mục CSDL theo tài liệu 00 mục 5; phân trang con trỏ; tránh N+1 (Prisma
includecó chọn lọc); nén brotli/gzip và HTTP/2 tại nginx; cache Redis cho danh mục (tuyến, điểm đón) và token VeXeRe. - Việc nặng ra khỏi API: dựng PNG/PDF, gọi VeXeRe, gửi tin đều là job ở
worker; API chỉ ghi CSDL và đẩy job. - Thời gian thực bằng SSE thay vì long-poll: một kết nối/người, nginx
proxy_buffering offcho đường/api/sse. - Giao diện: Vite + SWC, tách gói theo trang (lazy route), Ant Design nhập theo thành phần, service worker cache tài nguyên tĩnh, TanStack Query cache dữ liệu và làm mới theo SSE, ảnh vé tạo ở máy chủ (không dựng ở điện thoại yếu).
- Máy chủ Node:
--max-old-space-sizephù hợp (api 512 MB, worker 768 MB), PM2 cluster 2 instance choapitrên 2 vCPU.
4. Chống lỗi và chống chết hệ thống
Nguyên tắc: một thứ hỏng không kéo thứ khác; mọi thứ tự dậy; dữ liệu không mất, không trùng.
4.1 Cách ly tiến trình
| Tiến trình | Chết vì gì | Hệ quả khi chết | Cơ chế tự phục hồi |
|---|---|---|---|
api (PM2 cluster ×2) |
Lỗi mã, quá tải bộ nhớ | Người dùng không thao tác được vài giây | PM2 khởi động lại instance lỗi, instance còn lại vẫn phục vụ; max_memory_restart 512M; exp_backoff_restart_delay |
worker |
Job lỗi lặp, API ngoài treo | Xếp/gửi chậm, không mất dữ liệu | Job nằm ở Redis, PM2 khởi động lại → job tiếp tục; thời gian chờ mọi gọi ngoài ≤ 30 giây |
worker-zalo |
Zalo đổi giao thức, phiên hết hạn | Chỉ kênh Zalo cá nhân ngừng; tin tự chuyển SMS sau thời gian chờ | PM2 khởi động lại; đăng nhập lại QR; cảnh báo quản trị |
| PostgreSQL / Redis | Hết đĩa, OOM | Toàn hệ thống dừng | systemd Restart=always; cảnh báo đĩa > 80 %; swap 2 GB chống OOM; Redis appendonly yes |
| nginx (aaPanel) | Cấu hình sai | Không vào được | aaPanel tự kiểm tra cấu hình trước khi áp; Uptime Kuma báo |
4.2 Trong mã
- Bắt
unhandledRejection/uncaughtException→ ghi nhật ký, thoát có kiểm soát để PM2 dựng lại (không "nuốt" lỗi rồi chạy tiếp sai). - Tắt mềm: nhận SIGTERM → ngừng nhận yêu cầu/job mới, chờ job đang chạy ≤ 30 giây, đóng kết nối →
pm2 reloadkhông rớt yêu cầu. - Idempotency ở mọi biên:
nguon + ma_nguoncho vé nguồn,transaction_idcho lệnh VeXeRe,event_type + booking_code + event_timestampcho webhook,ve_id + kenh + loai_tincho tin gửi. - Giao dịch CSDL và khoá hàng khi chuyển trạng thái vé (
SELECT … FOR UPDATE), không bao giờ hai điều hành xếp cùng một vé. - Cầu dao (opossum) cho VeXeRe, SMS gateway, Zalo OA: lỗi liên tiếp → mở cầu dao 60 giây, job vào hàng chờ, không dồn dập gọi dịch vụ đang hỏng.
- Thử lại có lùi và hàng đợi chết (dead-letter) trong BullMQ: sau N lần thất bại, job chuyển sang "cần người xem", hiện ở bàn xếp vé.
- Kiểm tra đầu vào bằng zod ở API, webhook, tệp đẩy lên; giới hạn kích thước tệp; giới hạn tốc độ theo IP/tài khoản (
@nestjs/throttler+ nginxlimit_req). - Cờ mô-đun: mô-đun lỗi bị vô hiệu tự động, lõi chạy tiếp (tài liệu 00 mục 4.9).
4.3 Vận hành
- Giám sát:
/health(CSDL, Redis, phiên Zalo, SIM lần liên hệ cuối, token VeXeRe, sao lưu cuối); aaPanel hiển thị CPU/RAM/PID từng Node project và có cảnh báo (Monitor/alarm) qua Telegram/Email; bot Zalo/Telegram của hệ thống báo sự cố nghiệp vụ; Uptime Kuma là tuỳ chọn nếu muốn theo dõi từ ngoài VPS. - Tự dậy: Node project của aaPanel (PM2 bên trong) khởi động lại tiến trình chết, giới hạn bộ nhớ, boot cùng máy; Cron Access URL gọi
/healthmỗi 5 phút để ghi nhận sống/chết trong log. - Sao lưu: hoàn toàn bằng Cron aaPanel + plugin Google Drive (mục 5.3); Redis không cần sao lưu; kiểm tra khôi phục tự động hằng tuần.
- Nhật ký: pino JSON ra log của Node project; Cron Cut Log của aaPanel cắt và giữ 30 tệp; che SĐT.
- Bảo mật máy chủ: chỉ mở 22/80/443 (aaPanel Security), SSH bằng khoá, fail2ban, cập nhật bảo mật tự động (
unattended-upgrades), PostgreSQL/Redis chỉ localhost, bí mật trong.envquyền 600. - Phát hành theo bậc: UAT VeXeRe → nhánh
main→ VPS; có thể thêm Node projectcgv-api-thutrên cổng khác để thử bản mới trước khi chuyển domain.
5. Triển khai trên aaPanel như một ứng dụng web có domain riêng
Mục tiêu người dùng đặt ra: cài lên aaPanel như một ứng dụng web có domain riêng, chạy qua proxy, đơn giản, ít thao tác tay, dựa vào tính năng aaPanel có sẵn. Kết luận sau khi tra tài liệu chính thức (02/9/2026): aaPanel đã có đủ, không cần Docker, không cần cài PM2/nginx/PostgreSQL thủ công.
5.1 Tính năng aaPanel được dùng
| Tính năng | Dùng cho | Ghi chú từ tài liệu aaPanel |
|---|---|---|
| Website → Node project | Chạy api, worker, worker-zalo như ba "dự án Node"; gắn domain → aaPanel tự tạo site tĩnh và reverse proxy nginx vào cổng dự án; bật SSL Let's Encrypt; khởi động cùng máy; xem CPU/RAM/PID, log trong panel |
Hai kiểu: Default (lệnh khởi động đọc từ package.json hoặc tự nhập, cổng, phiên bản Node, user chạy) và PM2 Project (tệp khởi động, thư mục làm việc, số instance cluster, giới hạn bộ nhớ, npm/yarn). Cổng cố định theo dự án |
| App Store → Node.js version manager | Node 22 LTS, mirror npm | |
| App Store → PgSQL manager | Cài PostgreSQL cục bộ; tạo database/user; sao lưu/khôi phục thủ công, sao lưu hàng loạt | Không có lịch sao lưu tự động cho PostgreSQL |
| App Store → Redis | Redis cục bộ | Chỉ nghe 127.0.0.1 |
| App Store → Google Drive | Đích sao lưu | Xác thực OAuth một lần bằng liên kết + mã |
| Cron | Sao lưu tự động, cắt log, gọi /health |
8 loại tác vụ: Shell Script, Backup Site, Backup Database, Cut Log, Backup Directory, Sync Time, Free RAM, Access URL; chu kỳ ngày/N ngày/giờ/N giờ/N phút/tuần/tháng; đích Local/FTP/Google Drive/GCS/S3; giữ N bản mới nhất; loại trừ tệp; log thực thi; báo lỗi sao lưu qua Email/Telegram/…; Backup Database chỉ hỗ trợ MySQL |
| Security, Files, Terminal, Monitor | Tường lửa cổng, soạn .env, lệnh git, cảnh báo tài nguyên |
5.2 Các bước cài một lần (khoảng 30 phút, chi tiết trong deploy/HUONG-DAN-AAPANEL.md)
- App Store: cài Node.js version manager (chọn 22), PgSQL manager, Redis, Google Drive (xác thực tài khoản Drive).
- PgSQL manager: tạo database
cong_gui_vevà user; ghi mật khẩu vào.env. - Terminal:
git clone https://<PAT chỉ đọc>@github.com/biencuong/CongGuiVe.git /www/wwwroot/cong-gui-ve; sao chépdeploy/.env.examplethành.env, điền bí mật,chmod 600. - Website → Node project → Add ba dự án, đều bật Boot on startup, Node 22, user
www: -cgv-api— kiểu PM2 Project, tệp khởi độngdeploy/khoi-dong.cjs(tham sốapi), cổng 3000, giới hạn bộ nhớ 512 MB, 1 instance (2 khi ≥ 4 vCPU); Domainve.xekhachhagiang.vn→ bật Mapping → cài SSL, bắt buộc HTTPS. -cgv-worker— tệp khởi độngdeploy/khoi-dong.cjs(tham sốworker), cổng 3001 (chỉ/health), không domain. -cgv-zalo— tham sốworker-zalo, cổng 3002, giới hạn 384 MB, không domain.khoi-dong.cjs(GĐ0) tự kiểm tranode_modules→pnpm install --frozen-lockfile; chạyprisma migrate deploy; dựngapps/web/distnếu thiếu; rồi chạy tiến trình tương ứng. Nhờ vậy cập nhật =git pull+ bấm Restart trong panel, hoặc nút Cập nhật trong app gọideploy/cap-nhat.shrồi tự khởi động lại. - Site do Mapping tạo → Config: dán khối
deploy/nginx-them.confvào trongserver {}:/phục vụapps/web/dist(SPA fallback),/api/→127.0.0.1:3000,/api/ssetắtproxy_buffering, giới hạn tốc độ/api/vexere/webhookvà/api/sms/webhook, nén. Không muốn chạm nginx thì đểapitự phục vụ SPA (chậm hơn một chút, chấp nhận được ở GĐ1). - Cron: bốn tác vụ ở mục 5.3.
- Security: chỉ mở 22 (đổi cổng, SSH bằng khoá), 80, 443; các cổng 3000–3002, 5432, 6379 không mở ra ngoài.
Bố cục trên VPS:
/www/wwwroot/cong-gui-ve/ git clone; .env (600) không trong git
deploy/khoi-dong.cjs tệp khởi động cho Node project (cài phụ thuộc, di trú, dựng web, chạy)
deploy/cap-nhat.sh pull → khởi động lại 3 dự án qua API aaPanel → kiểm tra /health
deploy/nginx-them.conf khối dán vào site aaPanel
deploy/sao-luu.sh Cron A (mục 5.3)
deploy/khoi-phuc.sh khôi phục từ bản sao lưu
deploy/kiem-tra-khoi-phuc.sh Cron C
data/ phiên Zalo, tệp tải lên (được sao lưu, không trong git)
/www/backup/cong-gui-ve/ bản sao lưu cục bộ 7 ngày, aaPanel đẩy lên Google Drive
Cấu hình VPS tối thiểu: 2 vCPU, 4 GB RAM, 40 GB SSD. Nếu dùng chung VPS với Vé247 (STT faster-whisper tốn RAM), cần ≥ 8 GB hoặc tách VPS.
5.3 Sao lưu tự động và đẩy lên Google Drive bằng Cron của aaPanel
Vì Cron Backup Database chỉ hỗ trợ MySQL, PostgreSQL đi đường Shell Script rồi Backup Directory đẩy lên Google Drive. Nguyên tắc: CSDL + cấu hình + phiên Zalo là những gì không dựng lại được từ git; bí mật lên Drive phải ở dạng mã hoá; sao lưu chưa kiểm tra khôi phục thì chưa tính là sao lưu.
| # | Cron aaPanel (loại) | Lịch | Việc |
|---|---|---|---|
| A | Shell Script: bash /www/wwwroot/cong-gui-ve/deploy/sao-luu.sh |
Hằng ngày 02:00 | pg_dump --format=custom → /www/backup/cong-gui-ve/db/cgv-<ngày-giờ>.dump; gói .env + data/ thành cau-hinh/cgv-cau-hinh-<ngày-giờ>.tar.gz.enc mã hoá AES-256 (khoá tại /root/.cgv-sao-luu.key, quyền 600, phải chép khoá ra nơi an toàn ngoài VPS); ghi .sha256; xoá bản cục bộ quá 7 ngày; ghi trang-thai.json để /health báo "sao lưu cuối lúc …"; lỗi → thoát mã 1 (aaPanel ghi log đỏ) và gọi POST /api/noi-bo/canh-bao để bot báo Zalo/Telegram |
| B | Backup Directory: /www/backup/cong-gui-ve → Google Drive, giữ 30 bản, loại trừ *.tmp |
Hằng ngày 02:40 | aaPanel nén thư mục, tải lên Drive vào thư mục của plugin, tự xoá bản cũ trên Drive; bật thông báo lỗi sao lưu (Telegram/Email) trong Cron |
| C | Shell Script: bash /www/wwwroot/cong-gui-ve/deploy/kiem-tra-khoi-phuc.sh |
Chủ nhật 03:30 | Khôi phục bản .dump mới nhất vào CSDL tạm cgv_thu, đếm bảng và số vé, ghi kiem-tra.json, xoá CSDL tạm; sai → thoát mã 1 + cảnh báo |
| D | Cut Log: log của ba Node project | Hằng ngày | Giữ 30 tệp |
Khôi phục: tải hai tệp từ Drive về, chạy bash deploy/khoi-phuc.sh <tệp.dump> [<tệp.tar.gz.enc>] (giải mã, pg_restore --clean --if-exists, in nhắc khởi động lại ba dự án). Mục tiêu: khôi phục xong ≤ 30 phút; mất dữ liệu tối đa 24 giờ. Muốn hạ xuống 1 giờ: thêm Cron A′ chu kỳ "N giờ = 1" chỉ chạy pg_dump (bản .dump vài MB).
Các tệp deploy/sao-luu.sh, deploy/khoi-phuc.sh, deploy/kiem-tra-khoi-phuc.sh, deploy/.env.example, deploy/HUONG-DAN-AAPANEL.md đã có trong repo; khoi-dong.cjs, cap-nhat.sh, nginx-them.conf viết ở GĐ0 khi có mã.
6. Tích hợp AI (mô-đun ai-tro-ly)
6.1 Nguyên tắc
AI không ghi thẳng vào lõi. Mọi kết quả AI là đề xuất có điểm tin cậy; người (đại lý/điều hành) xác nhận trên bảng xem trước; lệnh vào lõi vẫn là lệnh chuẩn (taoVe, suaVe). Không có AI thì hệ thống vẫn chạy đủ (mô-đun tắt được).
6.2 Các ứng dụng xếp theo giá trị
| # | Ứng dụng | Đầu vào | Đầu ra | Ghi chú |
|---|---|---|---|---|
| 1 | Đọc danh sách vé từ văn bản tự do và ảnh | Đại lý dán tin Zalo, hoặc chụp sổ tay/bảng viết tay | Bảng vé có cấu trúc + cảnh báo thiếu/mơ hồ | Dùng thị giác của mô hình; structured outputs; luật chuẩn hoá SĐT/giờ/ngày chạy trước và sau AI |
| 2 | Trợ lý đại lý qua chat (Zalo/OA/trong app) | "vé 0912… đã xếp chưa", "đặt 2 giường 17h30 mai cho A" | Trả lời từ CSDL hoặc lệnh tạo vé chờ xác nhận | Kế thừa kinh nghiệm OSZalo; công cụ (tool use) đọc CSDL, không cho AI tự do ghi |
| 3 | Giải thích lệch đối soát | Dòng lệch giữa hệ thống / VeXeRe / đại lý | Nguyên nhân khả dĩ, đề xuất xử lý | Luật phát hiện lệch chạy trước; AI chỉ diễn giải |
| 4 | Tin nhắn khách hàng đa ngôn ngữ | Vé + ngôn ngữ khách (Hà Giang nhiều khách nước ngoài) | Bản tiếng Anh/Trung/Hàn của tin vé | Khuôn cố định, AI dịch phần biến; lưu để tái dùng |
| 5 | Gợi ý xếp chỗ | Ghi chú khách ("say xe", "người già", "đi 3 người") | Ràng buộc chọn ghế | Luật trước, AI chỉ khi ghi chú không khớp luật |
| 6 | Tóm tắt ngày cho quản trị | Số liệu ngày | Bản tin ngắn gửi Zalo nhóm | Chạy theo lịch, chi phí nhỏ |
6.3 Kỹ thuật
- SDK chính thức
@anthropic-ai/sdk(TypeScript); mô hình mặc địnhclaude-opus-4-8(5 USD/1M token vào, 25 USD/1M token ra); suy nghĩ thích ứngthinking: {type: "adaptive"}; structured outputs (client.messages.parse()với zod schema của vé) để không phải tự phân tích JSON. Nếu muốn giảm chi phí cho việc 1 và 6 ở khối lượng lớn, người dùng có thể chọnclaude-haiku-4-5(1 USD/5 USD) hoặcclaude-sonnet-5(3 USD/15 USD) — đó là quyết định của người dùng, không tự hạ. - Prompt caching: hướng dẫn hệ thống + danh mục tuyến/điểm đón cố định đặt trước, nội dung biến đặt sau; kiểm tra
cache_read_input_tokens> 0. - Batch API (giảm 50 %) cho việc 3 và 6 chạy đêm.
- Ước chi phí việc 1: một ảnh danh sách ~1.500 token vào + 500 token ra ≈ 0,02 USD với Opus 4.8; 100 ảnh/ngày ≈ 2 USD/ngày.
- Bảo vệ dữ liệu: gửi tối thiểu (không gửi email/ghi chú nhạy cảm nếu không cần), che SĐT khi chỉ cần suy luận cấu trúc, ghi nhật ký prompt/kết quả để kiểm tra, không dùng dữ liệu khách để huấn luyện (mặc định API không dùng).
- Chống lỗi: thời gian chờ 60 giây, cầu dao, khi AI lỗi thì rơi về luật chuẩn hoá (đại lý vẫn nhập tay được); ngưỡng tin cậy < 0,7 thì bắt buộc xem lại.
7. Tự động hoá
| Nhóm | Tự động hoá | Cơ chế |
|---|---|---|
| Nghiệp vụ | Xếp tự động/bán tự động, gửi vé, gửi lại khi đổi/huỷ, nhắc hết hạn giữ chỗ (T−30', T−10'), quét trạng thái VeXeRe, làm giàu biển số T−3h, tin "xe của bạn", đối soát nháp cuối kỳ, tóm tắt ngày | Job BullMQ (trễ, lặp) + sự kiện lõi |
| Vận hành | Sao lưu đêm + đẩy Google Drive + kiểm tra khôi phục tuần, cắt nhật ký, gọi /health, dọn dữ liệu quá hạn (ẩn danh sau 24 tháng) |
Cron của aaPanel (Shell Script, Backup Directory, Cut Log, Access URL) + script trong deploy/ |
| Phát hành | Kiểm thử + kiểm tra tài liệu khi push (GitHub Actions); dựng web; cập nhật VPS bằng nút hoặc webhook GitHub → deploy/cap-nhat.sh; gắn thẻ phiên bản |
Mục 9 |
| Tài liệu | Sinh OpenAPI, ERD, bảng mô-đun, ma trận quyền vào docs/ trong CI |
Script sinh + commit tự động |
| Cảnh báo | SIM im, Zalo đứt, tin lỗi liên tiếp, VeXeRe 5xx, pay lỗi, đĩa đầy |
Uptime Kuma + bot Zalo/Telegram |
8. Ứng dụng người dùng: PC và mobile
8.1 So sánh cách làm ứng dụng mobile
| Phương án | Mã nguồn | Phân phối | Đẩy thông báo | Camera / chia sẻ | Chi phí, rủi ro | Kết luận |
|---|---|---|---|---|---|---|
| PWA (React) | 1 (chung với PC) | Cài từ trình duyệt, không qua cửa hàng, cập nhật tức thì | Android: đầy đủ; iOS ≥ 16.4 khi thêm vào màn hình chính | Camera qua trình duyệt; Share Target trên Android (chia sẻ tin Zalo vào app để đọc thành vé); iOS không có Share Target | Thấp nhất | Chọn cho GĐ1 |
| PWA bọc Capacitor | 1 + lớp bọc mỏng | Google Play (25 USD một lần), App Store (99 USD/năm) | FCM/APNs gốc | Đầy đủ, kể cả nhận chia sẻ trên iOS | Thêm quy trình duyệt cửa hàng | GĐ2 nếu đại lý cần "app trên cửa hàng" |
| React Native / Expo | 2 (mobile riêng, PC riêng) | Cửa hàng | Gốc | Đầy đủ | Nhân đôi công sức giao diện | Không |
| Flutter | 2, ngôn ngữ khác (Dart) | Cửa hàng | Gốc | Đầy đủ | Thêm ngôn ngữ | Không |
| Kotlin/Swift gốc | 3 | Cửa hàng | Gốc | Đầy đủ | Cao nhất | Chỉ cho máy SIM gửi SMS nếu app gateway không đáp ứng |
| Zalo Mini App (tuỳ chọn) | React (ZMP), tái dùng thành phần | Trong Zalo, không cài | Qua OA | Camera, thanh toán Zalo | Cần OA xác thực; kinh nghiệm dự án Mini App trường học | Mô-đun NguonVe sau GĐ2 cho đại lý sống trong Zalo |
8.2 Thiết kế PWA cho đại lý (điện thoại) và điều hành (PC)
- Một ứng dụng, hai bố cục: mobile bố cục một cột, thanh điều hướng dưới (Vé mới · Vé của tôi · Đẩy danh sách · Thống kê · Tôi); PC bố cục hai cột cho bàn xếp vé và bảng quản trị.
- Nhập ít gõ: chọn tuyến/giờ gần đây, ô SĐT
type="tel", ngày bằng chip "hôm nay / mai / kia", điểm đón theo danh sách; dán tin Zalo → hệ thống tách thành vé (luật + AI); chụp ảnh danh sách (camera) → AI đọc. - Ngoại tuyến có kiểm soát: form vé lưu IndexedDB khi mất mạng, tự gửi khi có mạng (Android: Background Sync; iOS: gửi khi mở lại app); danh sách vé gần đây đọc được ngoại tuyến; dải "đang ngoại tuyến" rõ ràng.
- Thông báo đẩy: vé đã xếp, vé bị từ chối, yêu cầu được duyệt, giữ chỗ sắp hết hạn; Web Push (VAPID) qua
web-push; iOS chỉ khi đã thêm vào màn hình chính → hướng dẫn cài ngay lần đầu đăng nhập. - Chạm một tay: nút ≥ 44 px, thao tác chính ở vùng ngón cái, chip trạng thái màu thống nhất (tài liệu 00), chữ 16 px trở lên, tối/sáng theo máy.
- Máy yếu, mạng yếu: gói ban đầu < 300 KB nén, ảnh vé do máy chủ dựng, danh sách ảo hoá, tránh hoạt ảnh nặng; kiểm tra bằng Lighthouse mobile với Moto G (mô phỏng).
- Cập nhật: service worker báo "có bản mới" → tải lại; phiên bản hiện trong mục Tôi.
- Bảo mật: phiên cookie HttpOnly + SameSite; khoá màn hình ứng dụng (mã PIN tuỳ chọn) cho đại lý dùng máy chung; đăng xuất từ xa.
8.3 Ứng dụng trên máy gửi SMS
Máy Android + SIM đặt cố định chạy ứng dụng SMS Gateway (mã nguồn mở), kết nối máy chủ gateway tự host; trang quản trị hiển thị máy nào đang sống, số tin hôm nay, lỗi. Nếu app không đáp ứng (ví dụ cần nhiều SIM trên một máy, hoặc nhận SMS khách trả lời để ghi vào vé), viết app Kotlin riêng theo cùng giao thức REST + webhook.
9. Cơ chế phiên bản và đồng bộ git (đã cài trong repo)
9.1 Quy ước
- Nguồn phiên bản duy nhất: trường
versiontrongpackage.jsongốc (semverX.Y.Z). Khi có monorepo, lõi và từng mô-đun cóversionriêng trongpackage.json/manifest.tscủa chúng; phiên bản gốc là "bản phát hành toàn hệ thống". - Thẻ git
vX.Y.Zgắn vào commit phát hành;docs/CHANGELOG.mdtheo Keep a Changelog: mọi thay đổi ghi vào mục[Chưa phát hành], khi nâng phiên bản script chuyển thành[X.Y.Z] - ngày. - Thông điệp commit kiểu Conventional Commits, mô tả tiếng Việt:
feat(vexere): giữ chỗ tự động,fix(gui-tin): thử lại SMS,docs: cập nhật hướng dẫn đại lý. Quy tắc nâng:feat→ minor,fix→ patch,BREAKING CHANGE→ major. - Nhánh:
mainlà bản chạy trên VPS; việc lớn làm trên nhánhtinh-nang/<ten>và gộp bằng PR để CI kiểm tra.
9.2 Công cụ trong repo
| Tệp | Việc |
|---|---|
scripts/phien-ban.py |
python scripts/phien-ban.py patch|minor|major|X.Y.Z — nâng package.json, chuyển mục [Chưa phát hành] trong CHANGELOG thành mục phiên bản có ngày |
scripts/dong-bo-git.ps1 (Windows) / scripts/dong-bo-git.sh (Linux, Git Bash) |
Một lệnh: cài hook → git add -A → (tuỳ chọn nâng phiên bản) → commit → gắn thẻ → git push (+ thẻ); lần đầu chưa có remote thì tự tạo repo riêng tư bằng gh repo create |
scripts/kiem-tra-tai-lieu.py |
Chặn commit/PR đổi mã (apps/, packages/, deploy/) mà không đổi tài liệu (docs/, README, HUONG-DAN-SU-DUNG, CHANGELOG); bỏ qua bằng biến BO_QUA_TAI_LIEU=1 (phải có lý do trong thông điệp commit) |
.githooks/pre-commit |
Gọi kiểm tra tài liệu trước mỗi commit (git config core.hooksPath .githooks, script đồng bộ tự đặt) |
.github/workflows/kiem-tra-tai-lieu.yml |
Chạy cùng kiểm tra trên GitHub cho push/PR; PR gắn nhãn khong-doi-tai-lieu thì bỏ qua |
.gitattributes |
Kết thúc dòng LF chung, CRLF cho .ps1/.cmd/.bat (dev Windows, chạy Linux) |
9.3 Dùng hằng ngày
# Máy dev (Windows) — cuối buổi làm việc
powershell -ExecutionPolicy Bypass -File scripts\dong-bo-git.ps1 -ThongDiep "docs: bổ sung phân tích công nghệ"
# Phát hành bản mới (nâng minor, gắn thẻ, đẩy)
powershell -ExecutionPolicy Bypass -File scripts\dong-bo-git.ps1 -Nang minor -ThongDiep "feat: giai đoạn 1"
Đồng bộ tự động theo lịch (tuỳ chọn, chưa bật — chờ chốt ở mục 10): Task Scheduler chạy dong-bo-git.ps1 -ThongDiep "tự động" mỗi 30 phút; chỉ commit khi có thay đổi. Nhược điểm: lịch sử commit vụn; thích hợp cho giai đoạn tài liệu, không nên dùng cho mã.
9.4 Trên VPS
deploy/cap-nhat.sh (GĐ0) kéo main rồi khởi động lại ba Node project (qua API của aaPanel hoặc lệnh PM2 mà panel dùng), khoi-dong.cjs tự cài phụ thuộc, di trú, dựng web; script kiểm tra /health và ghi số bản (git describe --tags) để trang quản trị hiển thị; nút Cập nhật gọi script này; GitHub webhook có thể kích hoạt tự động khi có thẻ mới.
10. Điểm chưa rõ cần chốt
- VPS — đã chốt 02/9/2026: VPS mới riêng, Ubuntu + aaPanel. Còn lại: nhà cung cấp, cấu hình CPU/RAM/đĩa.
- Domain và tài khoản Google Drive cho sao lưu: tên miền phụ nào (đề xuất
ve.xekhachhagiang.vn); Drive của nhà xe riêng hay tài khoản đang dùng? Khoá mã hoá sao lưu sẽ giữ ở đâu ngoài VPS? - Zalo cá nhân: dùng bao nhiêu tài khoản, có số SIM riêng cho nhà xe không, chấp nhận thỉnh thoảng quét lại QR trên trang quản trị?
- Mobile — đã chốt 02/9/2026: PWA cài từ trình duyệt ở bản 1. Còn lại: tỉ lệ đại lý dùng iPhone (ảnh hưởng thông báo đẩy, chia sẻ tin).
- AI — đã chốt 02/9/2026: bản 1 bật "đọc danh sách vé từ văn bản dán và ảnh chụp", mô hình mặc định
claude-opus-4-8. Còn lại: ngân sách tháng; có đổi sang mô hình rẻ hơn cho khối lượng lớn không. - Đồng bộ git — đã chốt 02/9/2026: Claude tự chạy
scripts/dong-bo-git.ps1cuối mỗi lượt làm việc có thay đổi; không đặt lịch tự động. - Tên repo
CongGuiVe(riêng tư, tài khoảnbiencuong) có cần đổi không? - Máy gửi SMS: chấp nhận ứng dụng SMS Gateway mã nguồn mở, hay muốn app riêng ngay từ đầu?