0. Tóm tắt điều hành
Hệ thống cần xây là một cổng tập trung của nhà xe để nhiều đại lý bán vé gửi vé về, nhà xe xếp chỗ (tự động qua API VeXeRe hoặc thủ công), rồi xuất vé cho từng khách và gửi qua SMS (SIM thường), email, Zalo OA, Zalo cá nhân, kèm thống kê và đối soát riêng từng đại lý. Đại lý được đẩy lên, thêm, sửa, xoá vé của mình trong giới hạn quyền theo trạng thái vé.
Năm kết luận chính sau khi đọc tài liệu:
- Tài liệu VeXeRe cung cấp là góc nhìn ĐẠI LÝ (AMS + API đối tác), không phải góc nhìn nhà xe (BMS). Gửi vé "lên VeXeRe" bằng API này nghĩa là đặt chỗ như một đại lý: có hạn mức, có chiết khấu, có chính sách huỷ của VeXeRe. Cần xác nhận với VeXeRe tài khoản API cấp cho nhà xe (mục 11, câu 1).
- API đủ để tự động hoá vòng đời vé: tìm chuyến → sơ đồ ghế → giữ chỗ → thanh toán → tra cứu → huỷ → sửa điểm đón/trả, và webhook báo ngược khi vé bị đổi giờ/huỷ/cập nhật giá cọc. Thiếu hai thứ: API đổi ngày/giờ (chỉ làm tay trên AMS) và API tra hạn mức.
- Giữ chỗ chưa thanh toán sẽ tự huỷ đến
expired_time; chuyến bị tắt bán trực tuyến trước giờ khởi hành N giờ. Hệ thống phải có đồng hồ theo dõi hai mốc này, không được phó mặc cho người trực. - Xây mới hoàn toàn trên VPS Linux riêng: mã, CSDL, tiến trình mới; Vé247/OSZalo/web chỉ là tài liệu nghiệp vụ và mã mẫu. Bộ công nghệ đề xuất (mục 4.7, phân tích sâu ở tài liệu 01): Node.js + TypeScript (NestJS), PostgreSQL, Redis + BullMQ, React + Vite (PWA); chạy trên aaPanel như các Node project gắn domain riêng (nginx + SSL của aaPanel), PostgreSQL/Redis/Google Drive cài từ App Store, sao lưu bằng Cron aaPanel lên Google Drive; Zalo cá nhân chạy trong worker riêng bằng zca-js; SMS qua máy Android + SIM chạy ứng dụng gateway.
- Đối soát phải khoá kỳ: mỗi vé mang ba con số (giá bán, tiền VeXeRe ghi nhận, hoa hồng đại lý) và một trạng thái cuối; chốt kỳ thì vé không sửa được nữa; biên bản xuất Excel theo đại lý.
- Lõi quản lý vé + mô-đun cắm/rút: lõi chỉ giữ thực thể vé, vòng đời, sự kiện, quyền, vết, cấu hình; mọi thứ khác (VeXeRe, từng kênh gửi, đẩy danh sách, đối soát, nguồn liên thông) là mô-đun có hợp đồng và phiên bản riêng, bật/tắt bằng cấu hình, nâng cấp không đụng lõi và không đụng nhau (mục 4.9). Tài liệu đi cùng mã: thiết kế, hướng dẫn sử dụng, bàn giao, kinh nghiệm nằm trong kho mã, cập nhật trong cùng một lần thay đổi và được CI kiểm tra (mục 4.10).
1. Bối cảnh và những gì tài liệu cho biết
1.1 Rút ra từ HDSD AMS (bản 7/2022)
AMS là phần mềm web cho đại lý đặt vé trên mọi nhà xe có bán trên VeXeRe. Các quy tắc nghiệp vụ ảnh hưởng trực tiếp đến thiết kế:
| Quy tắc AMS | Hệ quả cho hệ thống |
|---|---|
| Hạn mức: số tiền khả dụng; bấm Thanh toán thì trừ đúng Giá dành cho đại lý (= tổng tiền − chiết khấu). Hoàn lại khi huỷ theo phí huỷ theo giá đại lý. | Phải theo dõi hạn mức, cảnh báo khi thấp; mỗi lần thanh toán/huỷ ghi một bút toán để đối soát với Báo cáo GD thay đổi hạn mức của AMS. |
| 3 trạng thái vé: Đã đặt chỗ → Đã thanh toán → Đã huỷ. Vé đặt chỗ đến giờ hẹn huỷ chưa thanh toán thì tự huỷ và VeXeRe nhắn khách. | Trạng thái nội bộ phải ánh xạ 1-1 và có bộ đếm ngược theo expired_time. |
| 3 hình thức thanh toán: tại đại lý (trừ hạn mức), tại nhà xe (chỉ giữ chỗ), online (VeXeRe thu hộ, phí 1,1–2,5 %). | Trường hình thức thanh toán trên vé quyết định có gọi pay hay không. |
| Sửa vé: vé đã thanh toán không được thêm ghế, không đổi sang ghế khác giá, không đổi ngày/giờ. Gần giờ chạy không nên sửa vì nhà xe đã in. | Sau khi xếp, đại lý chỉ yêu cầu sửa; điều hành duyệt; hệ thống chặn các sửa AMS không cho phép. |
| Đổi ngày/giờ ngang giá: chỉ 1 lần/vé, không áp dụng vé cọc/coupon, vé đã đổi phí huỷ mặc định 100 %, đại lý chịu trách nhiệm nếu chuyến có phí huỷ > 0. | Không có API tương ứng; "đổi chuyến" trong hệ thống = huỷ + đặt lại, có cảnh báo phí, hoặc điều hành làm tay trên AMS rồi ghi nhận lại. |
| Huỷ vé: tính phí huỷ theo giá gốc (khách chịu) và theo giá đại lý (đại lý chịu); có thể huỷ từng ghế. | Màn huỷ phải hiện hai con số này trước khi xác nhận (API GET /v3/booking/refund cung cấp). |
| Tắt bán trực tuyến trước giờ khởi hành N giờ: chuyến ẩn khỏi hệ thống. | Vé đẩy lên sát giờ phải chuyển sang luồng xếp thủ công (gọi nhà xe/điều hành). |
| "NX không báo mã ghế": ghế chọn chỉ tượng trưng, khách ra xe được xếp. | Vé xuất cho khách phải ghi "ghế do nhà xe sắp xếp khi lên xe" thay vì số ghế. |
| Vé cọc (Lễ, Tết): thanh toán giá cọc; khi có giá chính thức thì chờ thu thêm/chờ hoàn tiền. | Có trạng thái phụ deposit_status và xử lý webhook ticket_official_fare_updated. |
| Báo cáo tối đa 31 ngày; tìm vé tối đa 7 ngày theo ngày đặt hoặc ngày đi. | Hệ thống tự lưu toàn bộ lịch sử, không dựa vào AMS để tra cứu cũ. |
| SMS/Email xác nhận của VeXeRe gửi khách (tuỳ chọn, có bản tiếng Anh); email chứa ticket code. | Quyết định: có để VeXeRe tự nhắn khách song song với hệ thống hay không (mục 11, câu 6). |
| Tối đa 8 ghế/đơn (API). | Đoàn > 8 khách tách nhiều booking, vẫn gom một "vé nhóm" khi gửi khách. |
1.2 Rút ra từ tài liệu API đối tác (vdoc.vexere.net, đọc ngày 02/9/2026)
Môi trường: UAT (uat-api.vexere.net, uat-vroute.vexere.net, uat-account-service.vexere.net) và PRO (api.vexere.com, vroute.vexere.com, account-service.vexere.com). UAT có sẵn client_id/secret thử nghiệm trong tài liệu.
Xác thực: POST {account}/v3/token với grant_type=client_credentials; token sống 1 giờ; mọi API khác dùng Authorization: Bearer.
| Nhóm | Endpoint | Dùng để |
|---|---|---|
| Khu vực | GET /v3/area/city, /district, /city_district; GET {route}/v2/area |
Ánh xạ tên tỉnh/huyện của nhà xe ↔ id VeXeRe (đã có lưu ý sáp nhập đơn vị hành chính 2025). |
| Tìm chuyến | GET {route}/v2/route?filter[from,to,date,online_ticket…]&page&pagesize≤20&sort |
Lấy trip_code của chuyến đúng nhà xe, đúng giờ. |
| Sơ đồ ghế | GET /v3/trip/seat_map?trip_code |
Ghế trống, giá từng ghế, max_total_seats, danh sách điểm đón/trung chuyển/trả (id, phụ thu, min_customer, điểm cố định/không cố định). |
| Chính sách | GET /v3/company_policy/config_detail, GET /v3/cancellation/policy |
Ghi vào vé gửi khách (hành lý, trẻ em, huỷ). |
| Giữ chỗ | POST /v3/booking/reserve (form-urlencoded; trip_code, seats = mã ghế đầy đủ cách nhau dấu phẩy, customer_phone/name/email, pickup+pickup_id hoặc transfer+transfer_id, drop_off_info+drop_off_point_id hoặc drop_off_transfer_info+arrive_transfer_id, note) → booking_code, code (ticket code), tickets[], partner_code (mã nhà xe cấp để lên xe). |
Bước "xếp vé tự động". |
| Tra vé | GET /v3/booking?code= → status (1 giữ chỗ, 4 đã thanh toán, 5 hoàn/huỷ), expired_time, amount_booking, vehicle{number,driver,phone_info}, pickup_date, pickup_guide, partner_code, all_tickets_cancelled |
Làm giàu vé trước khi gửi khách (biển số, tài xế, giờ đón); đối soát. |
| Thanh toán | POST /v3/booking/pay (code, transaction_id = mã đối soát phía ta) → vxr_transaction_id |
Trừ hạn mức; khứ hồi gửi mảng. |
| Huỷ | GET /v3/booking/refund?code=booking (xem phí/hoàn, cancelable, bậc phí theo giờ) rồi POST /v3/booking/refund (code = ticket code) |
Huỷ có xem trước phí. |
| Sửa | PUT /v3/booking/{code}/agent_api — chỉ customer_name/email, điểm đón/trung chuyển/trả cùng loại, cùng mức phụ thu |
Sửa nhẹ sau khi xếp. |
| Coupon | GET /v3/campaign/coupon, POST /v3/booking/{code}/coupon |
Giai đoạn sau. |
| Nhà xe | GET /v3/company/{id}/info_for_agent, /image, đánh giá |
Hiển thị. |
| Webhook (VeXeRe → ta) | POST tới URL đăng ký, header x-signature = HMAC-SHA256(client_secret, chuỗi key=value&… theo thứ tự quy định); 3 sự kiện ticket_updated, ticket_cancelled, ticket_official_fare_updated; phải trả 200 {"code":"00"}; VeXeRe thử lại 4 lần, yêu cầu idempotent. |
Đồng bộ ngược khi nhà xe đổi giờ/huỷ trên BMS. |
| Webview | Trang đặt vé nhúng của VeXeRe cho đối tác | Không dùng (ta có UI riêng). |
Mã lỗi cần xử lý riêng: 3403 SEAT_IS_NOT_AVAILABLE (chọn lại ghế, thử tối đa 3 lần), 3308 TRIP_IS_NOT_BOOKABLE (chuyển thủ công), 3317 BOOKING_EXPIRED, 3301 BOOKING_PAID, 3302 BOOKING_CANCELLED, 3101/3115/3118/3120 tham số sai (lỗi dữ liệu, không thử lại).
Điểm mâu thuẫn trong tài liệu cần hỏi VeXeRe: mục Update ticket info ghi "booking status = 2 (booked)" trong khi Get booking ghi "1-Reserve". Thiết kế coi giá trị trả về của GET /v3/booking là chuẩn.
1.3 Hệ thống hiện có: chỉ là tài liệu tham khảo và mã mẫu
Cổng gửi vé được xây mới hoàn toàn trên một VPS Linux riêng: mã nguồn mới, CSDL mới, tiến trình mới; không dùng chung máy chủ, CSDL hay thư viện lúc chạy với Vé247/OSZalo. Ba hệ thống cũ chỉ đóng vai trò tài liệu nghiệp vụ và mã mẫu để rút ngắn thiết kế chi tiết:
| Hệ thống cũ | Tham khảo gì | Không dùng gì |
|---|---|---|
Vé247 (D:\CODE\30_SamsungS22u) |
Lược đồ vé và cách chuẩn hoá SĐT/giờ/ngày/loại chỗ (server/src/server.js mục tao-ve, brain.js); quy trình phát hành an toàn (kiểm tra tham chiếu trước khi triển khai); cách app Android giữ kết nối máy chủ. |
Không chạy chung VPS/CSDL; app S22U không sửa để phục vụ cổng. |
OSZalo-247 (d:\CODE\07_OSZalo_247) |
Cách dùng zca-js: đăng nhập QR, lưu phiên, giám sát kết nối, gửi tin/ảnh, nhận sự kiện đã xem, giới hạn nhịp gửi (src/channels/zaloLive.ts). |
Không gọi sang OSZalo lúc chạy; cổng có worker Zalo riêng. |
xekhachhagiang.com (d:\CODE\32_webVexe247\site-moi) |
Cách khách mô tả nhu cầu (tuyến, điểm đón), dòng vé chuẩn, mã tra cứu ngắn. | Không phụ thuộc. |
Sau khi cổng ổn định, các hệ thống cũ có thể đẩy vé vào cổng qua API liên thông (mục 4.6) với tư cách một "nguồn". Đó là tích hợp tuỳ chọn, không phải điều kiện để cổng chạy.
2. Mục tiêu, phạm vi, tác nhân
2.1 Mục tiêu đo được
- Đại lý nhập một vé dưới 30 giây trên điện thoại; đẩy danh sách 50 vé dưới 2 phút kể cả sửa lỗi.
- Vé hợp lệ được xếp tự động qua API và gửi khách trong vòng 2 phút (trường hợp ghế còn, chuyến còn bán online).
- Khách nhận vé qua ít nhất một kênh ≥ 99 % (SMS là kênh bảo đảm, các kênh khác là bổ sung).
- Mọi thay đổi vé đều có vết (ai, lúc nào, trước/sau).
- Chốt đối soát tháng cho một đại lý dưới 10 phút, có biên bản Excel.
2.2 Ngoài phạm vi (phiên bản 1)
- Không thay thế AMS/BMS của VeXeRe; không quản lý sơ đồ ghế của nhà xe.
- Không thu tiền online (dùng kênh thanh toán online của VeXeRe nếu cần).
- Không xây app di động gốc cho đại lý (web mobile-first/PWA là đủ).
- Không tích hợp cổng SMS thương hiệu (brandname) — chỉ SIM thường theo yêu cầu.
2.3 Tác nhân
| Tác nhân | Là ai | Việc chính |
|---|---|---|
| Quản trị nhà xe | Chủ/kế toán nhà xe | Cấu hình VeXeRe, kênh gửi, tuyến, đại lý, hoa hồng; xem toàn bộ; chốt đối soát. |
| Điều hành (bàn xếp vé) | Người trực tổng đài/điều độ | Nhận vé đẩy lên, xếp chỗ (tự động/thủ công), duyệt yêu cầu sửa/huỷ, xử lý sự cố gửi. |
| Đại lý (chủ đại lý, nhân viên đại lý) | Điểm bán, cộng tác viên | Đẩy lên, thêm, sửa, xoá vé của đại lý mình; theo dõi trạng thái; xem thống kê/đối soát của mình. |
| Nguồn hệ thống | Vé247, OSZalo, web | Đẩy vé bằng API với khoá riêng; được đối xử như một đại lý đặc biệt. |
| Khách hàng | Người đi xe | Nhận vé qua kênh; tra cứu vé bằng mã/SĐT (trang công khai tối giản). |
| VeXeRe | Hệ thống ngoài | Nhận đặt chỗ/thanh toán/huỷ; gửi webhook. |
3. Nghiệp vụ và quy trình
3.1 Vòng đời một vé
stateDiagram-v2
[*] --> NHAP: đại lý tạo (form/danh sách/API)
NHAP --> DA_DAY: đại lý bấm "Đẩy lên"
DA_DAY --> DANG_XEP: điều hành nhận / máy tự xếp
DANG_XEP --> DA_XEP: có ghế + mã VeXeRe (hoặc xếp tay)
DANG_XEP --> TU_CHOI: hết chỗ / chuyến ẩn / dữ liệu sai
TU_CHOI --> NHAP: đại lý sửa, đẩy lại
DA_XEP --> DA_XUAT: bộ xuất vé dựng nội dung
DA_XUAT --> DA_GUI: ≥ 1 kênh gửi thành công
DA_GUI --> HOAN_THANH: qua giờ khởi hành + 1 ngày
DA_XEP --> HUY: huỷ (đại lý yêu cầu → điều hành duyệt / webhook VeXeRe)
DA_XUAT --> HUY
DA_GUI --> HUY
DA_DAY --> HUY: đại lý tự huỷ khi chưa xếp
HUY --> [*]
HOAN_THANH --> [*]
Quy tắc quyền theo trạng thái (chi tiết ở mục 7):
| Trạng thái | Đại lý sửa | Đại lý xoá/huỷ | Điều hành |
|---|---|---|---|
| NHAP, DA_DAY, TU_CHOI | Sửa tự do | Xoá mềm tự do | Xem, xếp |
| DANG_XEP | Khoá (đang giữ chỗ) | Không | Xếp/từ chối |
| DA_XEP, DA_XUAT, DA_GUI | Chỉ gửi yêu cầu sửa (tên, email, điểm đón/trả, ghi chú) | Chỉ gửi yêu cầu huỷ, thấy phí huỷ dự kiến | Duyệt → gọi API sửa/huỷ; gửi lại vé |
| HUY, HOAN_THANH | Không | Không | Chỉ ghi chú |
| Vé thuộc kỳ đối soát đã chốt | Không | Không | Không (phải mở kỳ) |
Trạng thái gửi theo từng kênh là lớp riêng trên vé: mỗi vé có N bản ghi gui_tin (kênh, người nhận, CHO_GUI → DANG_GUI → DA_GUI/LOI → DA_XEM nếu kênh báo được). Vé đạt DA_GUI khi có ít nhất một kênh DA_GUI.
3.2 Luồng 1 — Đại lý nhập thủ công
- Đại lý mở cổng trên điện thoại → Vé mới. Form gồm: SĐT khách (bắt buộc, 10 số), tên khách, số khách, loại chỗ (ghế/giường/cabin/…), tuyến (chọn từ danh sách tuyến nhà xe), ngày đi, giờ đi (chọn trong lịch chuyến), điểm đón, điểm trả, ghi chú cho nhà xe, giá bán, tiền đã thu (đã thu tại đại lý / thu tại xe / chuyển khoản), kênh gửi cho khách (mặc định theo cấu hình đại lý), email (nếu chọn kênh email).
- Hệ thống kiểm tra tức thì: trùng SĐT + ngày + giờ (cảnh báo, không chặn); giờ đi đã qua mốc tắt bán (cảnh báo "sẽ xếp thủ công"); điểm đón có trong danh sách của chuyến.
- Bấm Lưu nháp (NHAP) hoặc Đẩy lên (DA_DAY). Đẩy lên tạo mã vé nội bộ dạng
VX-YYMMDD-NNNN. - Đại lý thấy vé trong Vé của tôi với trạng thái thời gian thực (SSE).
3.3 Luồng 2 — Đẩy danh sách
Ba cách nhập, cùng một bộ kiểm tra:
| Cách | Mô tả | Ai dùng |
|---|---|---|
| Tải Excel/CSV theo mẫu | Mẫu cột ở Phụ lục C; tải mẫu trống từ hệ thống. | Đại lý có sổ Excel. |
| Dán văn bản nhiều dòng | Mỗi dòng một vé theo dòng vé chuẩn của Vé247 (0912… · Nguyễn A · 2 giường · 17h30 2/9 · HG→HN · đón BX Hà Giang). Bộ tách dòng viết mới trong packages/shared, tham khảo cách chuẩn hoá của Vé247. |
Đại lý quen chat Zalo. |
API POST /api/dai-ly/ve/lo |
Mảng JSON, khoá API của đại lý/nguồn. | Vé247, OSZalo, web. |
Quy trình: tải lên → hệ thống dựng bảng xem trước có cột Kết quả kiểm tra (hợp lệ / cảnh báo / lỗi + lý do) → đại lý sửa tại chỗ trong bảng → bấm Đẩy các dòng hợp lệ → tạo một lô (lo_day) và N vé DA_DAY; dòng lỗi giữ lại trong lô để sửa tiếp. Toàn bộ hoặc không: không, vì thực tế một dòng sai không nên chặn 49 dòng đúng; nhưng lô ghi rõ số dòng thành công/lỗi.
Kiểm tra bắt buộc: SĐT hợp lệ; ngày ≥ hôm nay; giờ có trong lịch chuyến của tuyến; số khách 1–8 mỗi dòng (hơn thì tự tách); trùng lặp trong cùng lô (SĐT + ngày + giờ) hỏi gộp hay tách.
3.4 Luồng 3 — Xếp vé
Ba chế độ, cấu hình theo tuyến hoặc theo vé:
sequenceDiagram
autonumber
participant DL as Đại lý
participant C as Cổng gửi vé
participant DH as Điều hành
participant VX as VeXeRe API
DL->>C: Đẩy vé (DA_DAY)
C->>C: Tìm tuyến → from_id/to_id, company_id
C->>VX: GET /v2/route (ngày, from, to, online_ticket=1)
VX-->>C: danh sách chuyến của nhà xe
C->>C: Khớp giờ đi (± dung sai) → trip_code
C->>VX: GET /v3/trip/seat_map?trip_code
VX-->>C: ghế trống, giá, điểm đón/trả
C->>C: Chọn ghế theo luật (loại chỗ, tầng, kề nhau)
alt Tự động hoàn toàn
C->>VX: POST /v3/booking/reserve
VX-->>C: booking_code, ticket code, partner_code
opt Đại lý đã thu tiền
C->>VX: POST /v3/booking/pay (transaction_id = mã vé nội bộ)
end
C->>VX: GET /v3/booking?code (biển số, tài xế, giờ đón)
C->>C: DA_XEP → xuất vé → gửi
else Bán tự động (mặc định giai đoạn đầu)
C->>DH: Đề xuất ghế + giá, chờ bấm "Xác nhận"
DH->>C: Xác nhận / đổi ghế / từ chối
C->>VX: reserve (+ pay) như trên
else Thủ công (chuyến ẩn, hết ghế, lỗi API)
DH->>DH: Xếp trên AMS/BMS hoặc gọi nhà xe
DH->>C: Nhập mã vé/ghế/biển số → DA_XEP
end
C-->>DL: Trạng thái đổi thời gian thực
Luật chọn ghế (cấu hình được): đúng loại chỗ đại lý ghi; ưu tiên ghế kề nhau cho nhóm; ưu tiên tầng dưới cho người già/trẻ em nếu ghi chú; không lấy ghế có phụ thu khác giá bán trừ khi đại lý cho phép; nếu chuyến có nhiều mức giá thì hiện chênh lệch cho điều hành. Khi 3403 SEAT_IS_NOT_AVAILABLE → tải lại sơ đồ, chọn lại, tối đa 3 lần rồi chuyển bán tự động.
Đồng hồ theo dõi: vé đã reserve chưa pay có expired_time → nhắc điều hành trước 30 phút và 10 phút; đến hạn mà chưa quyết → tự chuyển TU_CHOI với lý do "hết hạn giữ chỗ" và báo đại lý. Vé đẩy lên sau mốc tắt bán của chuyến → vào hàng đợi thủ công ngay, không gọi API.
3.5 Luồng 4 — Xuất vé và gửi đa kênh
Nội dung vé (một nguồn dữ liệu, nhiều khuôn hiển thị): tên nhà xe, mã vé nội bộ + mã VeXeRe (ticket code, partner_code nếu có), tên khách, số khách, tuyến, ngày giờ khởi hành, giờ đón dự kiến + điểm đón + địa chỉ, số ghế (hoặc "nhà xe xếp chỗ khi lên xe"), biển số + SĐT xe (nếu VeXeRe trả), giá vé + đã thu/chưa thu, chính sách huỷ tóm tắt, số hotline, liên kết tra cứu ngắn.
Khuôn theo kênh:
| Kênh | Khuôn | Ghi chú |
|---|---|---|
| SMS SIM thường | Văn bản không dấu, ≤ 160 ký tự (1 phân đoạn GSM-7); nếu cần dấu thì ≤ 70 ký tự/phân đoạn (UCS-2) | Có dấu tốn 2–3 phân đoạn; mặc định không dấu, có liên kết tra cứu. |
| HTML có bảng, kèm mã QR nội bộ, đính kèm PDF tuỳ chọn | Cần SPF/DKIM cho tên miền gửi. | |
| Zalo OA | ZNS mẫu "xác nhận vé" (gửi theo SĐT, không cần khách theo dõi OA) hoặc tin OA thường nếu khách đã tương tác | ZNS phải đăng ký mẫu và được duyệt; có phí/tin (chưa xác minh mức phí hiện hành). |
| Zalo cá nhân | Văn bản có dấu + ảnh vé (PNG dựng từ khuôn) | Qua worker-zalo (zca-js); chỉ tới được khi tìm thấy tài khoản theo SĐT; tin từ người lạ có thể rơi vào "Tin nhắn chờ". |
Chính sách gửi: mỗi đại lý có thứ tự kênh mặc định (ví dụ Zalo cá nhân → SMS; hoặc SMS + Email). Bộ gửi chạy hàng đợi: thử kênh 1; nếu kênh 1 không thể gửi (không tìm thấy Zalo, email sai) thì tự chuyển kênh dự phòng; nếu kênh 1 đã gửi thì các kênh đánh dấu "song song" vẫn gửi. Giới hạn nhịp riêng từng kênh (mục 4.3). Mọi lần gửi lưu nội dung đã gửi để tra lại đúng chữ khách nhận.
Gửi lại: khi vé đổi (giờ, ghế, xe) → tự dựng bản "VÉ CẬP NHẬT" và gửi lại theo cùng kênh đã thành công trước đó; khi huỷ → "VÉ ĐÃ HUỶ" kèm phí/hoàn.
3.6 Luồng 5 — Sửa, huỷ, đổi chuyến
| Việc | Trước khi xếp | Sau khi xếp (có mã VeXeRe) |
|---|---|---|
| Sửa tên/email/điểm đón/trả/ghi chú | Đại lý sửa thẳng | Đại lý gửi yêu cầu sửa → điều hành duyệt → PUT /v3/booking/{code}/agent_api (chỉ điểm cùng loại, cùng phụ thu) → gửi lại vé |
| Đổi số khách/loại chỗ | Sửa thẳng | Không sửa được trên vé đã thanh toán → huỷ ghế thừa (refund từng ticket) hoặc đặt thêm booking mới |
| Đổi ngày/giờ | Sửa thẳng | Không có API. Hai lựa chọn hiện cho điều hành: (a) huỷ + đặt lại, hiện phí huỷ dự kiến; (b) điều hành đổi tay trên AMS (ngang giá, 1 lần, phí huỷ 100 % về sau) rồi bấm "Đã đổi tay" và nhập giờ mới. |
| Huỷ | Đại lý huỷ thẳng, xoá mềm | Đại lý gửi yêu cầu huỷ → hệ thống gọi GET /refund hiện phí huỷ theo giá gốc (khách chịu) và theo giá đại lý (đại lý chịu) → điều hành duyệt → POST /refund (ticket code) → HUY → báo khách + đại lý → ghi bút toán hoàn hạn mức |
| Xoá | Xoá mềm (giữ vết) | Không xoá; chỉ huỷ |
Điều hành có thể uỷ quyền cho một đại lý tin cậy tự duyệt yêu cầu của mình (cờ tu_duyet trên đại lý).
3.7 Luồng 6 — Đồng bộ ngược từ VeXeRe
- Webhook: nhận
ticket_updated(nhà xe đổi giờ/giờ đón) → cập nhật vé, gửi "VÉ CẬP NHẬT";ticket_cancelled→ HUY, ghicancel_fee/refund_amount, báo đại lý + khách;ticket_official_fare_updated(vé cọc) → đánh dấu chờ thu thêm/hoàn với hạnsurcharge_time_deadline, báo đại lý. Xác thựcx-signature, lưu sự kiện thô, idempotent theo (event_type,booking_code,event_timestamp), trả200 {"code":"00"}ngay rồi xử lý nền. - Quét định kỳ (dự phòng webhook lỗi): mỗi 15 phút với vé có ngày đi ≤ 2 ngày tới gọi
GET /v3/booking?code=&checking_status_only=1; mỗi giờ với vé xa hơn. Khác trạng thái → xử lý như webhook. - Làm giàu trước giờ chạy: 3 giờ trước khởi hành gọi
GET /v3/bookinglấy biển số/tài xế/SĐT xe; nếu mới có thì gửi khách tin "XE CỦA BẠN" (tuỳ chọn cấu hình).
3.8 Luồng 7 — Thống kê và đối soát theo đại lý
Số liệu trên mỗi vé (đóng băng khi chốt kỳ):
gia_ban: giá đại lý bán cho khách (đại lý nhập).gia_ncc: giá nhà cung cấp chỗ ghi nhận (với VeXeRe làamount_booking, giá bán lẻ VeXeRe).gia_dai_ly(bảng mô-đun vexere): giá dành cho đại lý (sau chiết khấu) — lấy từ AMS/GET bookingnếu API trả; nếu không thì tính theo % chiết khấu cấu hình.hoa_hong: hoa hồng nhà xe trả đại lý (theo % hoặc số cố định, cấu hình theo đại lý × tuyến × loại chỗ).phi_huy,hoan_tienkhi huỷ.hinh_thuc_thu: đã thu tại đại lý / thu tại xe / chuyển khoản nhà xe.
Công thức mặc định (điều chỉnh được ở cấu hình):
phai_nop = Σ(gia_ban − hoa_hong) cho vé đã thu tại đại lý và không huỷ; vé huỷ có phí thì phai_nop += phi_huy_dai_ly_chiu. Vé thu tại xe không tính vào phải nộp nhưng vẫn tính hoa hồng.
Kỳ đối soát: tuần hoặc tháng theo đại lý. Quy trình: quản trị tạo kỳ → hệ thống liệt kê vé theo ngày đi trong kỳ, ba cột đối chiếu (hệ thống, VeXeRe qua GET booking/nhập file Báo cáo chi tiết vé xuất từ AMS, đại lý xác nhận) → dòng lệch được tô và có ô ghi chú xử lý → chốt kỳ (khoá mọi vé trong kỳ) → xuất biên bản Excel (tổng hợp + chi tiết) → đại lý bấm Xác nhận trên cổng (ghi thời điểm, người). Mở lại kỳ chỉ quản trị làm, có lý do, ghi vết.
Bảng thống kê (đại lý thấy của mình, quản trị thấy tất cả, lọc ngày/tuần/tháng/khoảng): số vé, số ghế, doanh thu, hoa hồng, tỉ lệ huỷ, phí huỷ; theo tuyến, theo giờ, theo loại chỗ; tỉ lệ gửi thành công theo kênh và thời gian trung bình từ đẩy lên → đã gửi; xếp hạng đại lý; tồn đọng (vé DA_DAY quá 15 phút, vé giữ chỗ sắp hết hạn, tin gửi lỗi).
4. Kiến trúc hệ thống
4.1 Tổng thể
flowchart LR
subgraph NguoiDung[Người dùng]
DL[Đại lý
web mobile-first / PWA]
DH[Điều hành
bàn xếp vé]
QT[Quản trị]
KH[Khách
trang tra cứu]
end
subgraph VPS[VPS Ubuntu + aaPanel]
CADDY[nginx của aaPanel
HTTPS, reverse proxy từ Node project]
API[api — NestJS
REST + SSE, RBAC, audit]
WK[worker — NestJS
xếp vé, gửi tin, đồng bộ, đồng hồ hết hạn]
WZ[worker-zalo
zca-js, phiên Zalo cá nhân]
PG[(PostgreSQL 16)]
RD[(Redis 7
BullMQ, phiên, cache token)]
SMSG[sms-gateway server
tự host]
end
subgraph Kenh[Kênh gửi]
SIM[Máy Android + SIM
app SMS Gateway]
MAIL[SMTP]
OA[Zalo OA / ZNS]
end
subgraph Ngoai[Ngoài]
VX[VeXeRe API]
NG[Nguồn tuỳ chọn:
Vé247 / OSZalo / web]
end
DL & DH & QT & KH --> CADDY --> API
NG -->|API khoá riêng| CADDY
VX -->|webhook HMAC| CADDY
API <--> PG
API <--> RD
WK <--> PG
WK <--> RD
WZ <--> RD
WK <--> VX
WK --> SMSG --> SIM
SIM -->|đã gửi / đã nhận / lỗi| SMSG
WK --> MAIL & OA
Nguyên tắc: một kho mã (monorepo), ba tiến trình Node (api, worker, worker-zalo) dùng chung PostgreSQL và Redis; giao diện là SPA tĩnh do Caddy phục vụ; mọi việc nền (xếp vé, gửi tin, quét trạng thái, nhắc hết hạn) là job BullMQ có thử lại và có giới hạn nhịp theo hàng đợi. Tách worker-zalo để phiên Zalo cá nhân đứt không kéo API đứt. Mã viết bằng TypeScript; tên miền nghiệp vụ tiếng Việt không dấu (ve, daiLy, guiTin) để khớp tài liệu này. Quy mô vài trăm vé/ngày chạy thoải mái trên một VPS nhỏ; tách dịch vụ sau nếu lớn. Bên trong, mã tổ chức theo lõi quản lý vé + mô-đun cắm/rút (mục 4.9): api và worker chỉ là hai "vỏ" nạp lõi cùng các mô-đun đang bật.
4.2 Mô-đun
| Thành phần | Thuộc | Trách nhiệm | Ghi chú |
|---|---|---|---|
| Thực thể vé, máy trạng thái, bus sự kiện, quyền, vết, cấu hình, registry mô-đun | Lõi | Mọi lệnh tạo/đẩy/xếp/huỷ/sửa vé đi qua đây và phát sự kiện ve.* |
packages/core, mục 4.9 |
cong-dai-ly (giao diện đại lý) |
Lõi (vỏ) + màn hình do mô-đun đóng góp | Đăng nhập, form vé, vé của tôi, yêu cầu sửa/huỷ; menu tự hiện theo mô-đun đang bật | SPA React (PWA); trạng thái qua SSE |
ban-xep-ve |
Lõi | Hàng đợi vé DA_DAY, gọi nhà cung cấp chỗ đang bật (thủ công là mặc định, có sẵn trong lõi), đồng hồ hết hạn, duyệt yêu cầu, xử lý tin lỗi | Màn hình máy tính, phím tắt |
quan-tri |
Lõi (khung) + trang cấu hình do mô-đun đóng góp | Đại lý, người dùng, tuyến, điểm đón/trả, nhật ký, bật/tắt mô-đun | |
gui-tin (điều phối gửi) |
Lõi | Hàng đợi theo kênh, nhịp, dự phòng, thử lại, gửi lại khi đổi/huỷ; gọi kênh đang bật qua hợp đồng KenhGui |
Một hàng đợi BullMQ mỗi kênh; bảng gui_tin |
vexere |
Mô-đun (NhaCungCapCho + BaoCao) |
Bọc API (token 55 phút, thử lại có lùi, mã lỗi → hành động, nhật ký gọi/đáp), tìm chuyến, chọn ghế, giữ chỗ, thanh toán, huỷ, sửa; webhook nhận (HMAC, chống trùng); quét định kỳ, làm giàu biển số; ánh xạ tuyến/điểm ↔ id VeXeRe | UAT/PRO bằng .env; tắt mô-đun → cổng vẫn xếp thủ công |
xuat-ve |
Mô-đun (KhuonVe) |
Khuôn theo kênh, dựng PNG/PDF, liên kết tra cứu ngắn | Khuôn sửa được trong quản trị |
sms-gateway, email, zalo-oa, zalo-ca-nhan |
Mô-đun (KenhGui, mỗi kênh một mô-đun) |
Gửi, nhận trạng thái gửi/đã xem, trang cấu hình riêng | zalo-ca-nhan chạy tiến trình worker-zalo riêng |
day-danh-sach |
Mô-đun (NguonVe) |
Excel/CSV/dán, bảng xem trước, lô | |
api-nguon |
Mô-đun (NguonVe) |
API cho Vé247/OSZalo/web đẩy vé, webhook ra báo trạng thái | Khoá theo nguồn |
thong-ke, doi-soat |
Mô-đun (BaoCao) |
Tổng hợp theo đại lý/tuyến/kênh; kỳ đối soát, khoá kỳ (qua lệnh khoá vé của lõi), Excel | exceljs |
4.3 Thiết kế từng kênh gửi
| Tiêu chí | SMS SIM thường | Zalo OA (ZNS) | Zalo cá nhân | |
|---|---|---|---|---|
| Cách gửi | worker gọi REST của máy chủ SMS gateway tự host; máy Android + SIM chạy ứng dụng gateway nhận lệnh, gửi bằng SIM, báo đã gửi/đã nhận/lỗi qua webhook về worker |
nodemailer SMTP (tên miền nhà xe hoặc dịch vụ gửi) |
Gọi API OA với mẫu ZNS đã duyệt, gửi theo SĐT | Job vào hàng đợi zalo-ca-nhan; worker-zalo (zca-js) tìm UID theo SĐT, gửi bằng tài khoản bán vé của nhà xe, báo msgId/đã xem |
| Độ tin cậy tới khách | Cao (mọi máy) | Trung bình (khách ít đọc) | Cao nếu ZNS | Trung bình (tin người lạ vào "chờ") |
| Chi phí | Gói SIM; cần theo dõi số dư | Rất thấp | Phí/tin ZNS (chưa xác minh) | Không |
| Giới hạn | Nhà mạng chặn spam SIM thường: giữ ≤ 1 tin/5 giây, trần ngày cấu hình (mặc định 200), nội dung có mã vé khác nhau; số cụ thể chưa xác minh theo từng nhà mạng | Uy tín tên miền (SPF/DKIM/DMARC) | Mẫu phải duyệt; OA phải xác thực | Nhịp tự đặt theo kinh nghiệm OSZalo (20/phút, 150/giờ, 600/ngày; không có con số chính thức); nguy cơ khoá tài khoản nếu gửi hàng loạt tới người lạ |
| Nhận biết thất bại | RESULT_ERROR_* từ Android; không có DELIVERED sau 10 phút → nghi ngờ |
SMTP bounce | Mã lỗi API | "không tìm thấy người dùng", lỗi gửi |
| Vai trò đề xuất | Kênh bảo đảm, luôn bật dự phòng | Bổ sung cho khách doanh nghiệp/đoàn | Giai đoạn 3 | Kênh ưu tiên cho khách đã kết bạn/đã chat với tài khoản bán vé |
Thiết kế kênh SMS (chi tiết): dùng ứng dụng SMS Gateway for Android (mã nguồn mở) trên một máy Android + SIM đặt cố định, cùng máy chủ gateway tự host trong Docker Compose (ứng dụng có chế độ máy chủ riêng); worker gọi REST gửi tin kèm mã tham chiếu, nhận webhook đã gửi/đã nhận/lỗi để cập nhật gui_tin; nhịp gửi do BullMQ giới hạn (mặc định 1 tin/5 giây, trần ngày cấu hình); tạm dừng hàng đợi khi 5 lỗi liên tiếp và báo quản trị; hỗ trợ nhiều máy SIM (mỗi máy một tài khoản gateway, cổng chia đều). Nếu ứng dụng gateway không đáp ứng, viết app Kotlin riêng theo cùng giao thức (kinh nghiệm từ app Vé247). Cần kiểm tra lại phiên bản ứng dụng gateway hiện hành trước khi chốt.
4.4 Bộ kết nối VeXeRe — quy ước
- Token cache 55 phút, làm mới sớm; nhận 401 thì làm mới một lần rồi thử lại.
- Mọi lệnh ghi (
reserve,pay,refund,agent_api) là một giao dịch nội bộ cótransaction_id= mã vé nội bộ + hậu tố hành động, để đối soát vớivxr_transaction_id. - Không bao giờ gọi
paymà chưa lưubooking_code; không gọirefundmà chưa lưu kết quảGET refundđể hiện phí. - Ghi đầy đủ yêu cầu/đáp (che SĐT một phần) vào
mod_vexere_nhat_kyvới thời gian phản hồi để theo dõi. - Bảng ánh xạ
mod_vexere_anh_xa: tuyến nội bộ →from_id/to_id(area),company_idnhà xe, danh sách giờ, dung sai khớp giờ (mặc định ±10 phút), danh sách điểm đón/trả nội bộ ↔pickup_id/drop_off_point_id(đồng bộ từseat_map, quản trị chỉnh tay). - Kiểm thử: chạy trên UAT với
client_id/secretthử nghiệm trong tài liệu; bộ ca kiểm thử theo mục Testcases của tài liệu; sang PRO khi có tài khoản thật.
4.5 Webhook nhận từ VeXeRe
POST /api/vexere/webhook → đọc thô body → dựng chuỗi theo thứ tự trường trong tài liệu → HMAC-SHA256(client_secret) → so với x-signature (so sánh thời gian hằng) → sai thì 401 và ghi nhật ký → đúng thì lưu mod_vexere_su_kien (chống trùng) → trả ngay 200 {"code":"00","message":"Webhook received"} → xử lý nền. URL webhook đăng ký với VeXeRe qua đầu mối kỹ thuật (tài liệu không có API tự đăng ký).
4.6 API liên thông cho nguồn tuỳ chọn (Vé247, OSZalo, web)
Tích hợp tuỳ chọn, bật khi cần; cổng không phụ thuộc các hệ thống này.
POST /api/nguon/ve(khoá theo nguồn): nhận vé theo lược đồ tương thíchtao-vecủa Vé247 (maNguon, soDienThoai, soKhach, loaiGhe, tuyenTu, tuyenDen, ngayDi, gio, diemDon, diemTra, yeuCauKhac, nguon) → trảveId, maVe. Idempotent theonguon + maNguon.POST /api/nguon/ve/{id}/huy,PUT /api/nguon/ve/{id}: như đại lý.- Webhook ra
POST {url nguồn}khi vé đổi trạng thái, để hệ thống nguồn cập nhật phía mình.
4.7 Công nghệ đề xuất
Tiêu chí chọn: một người bảo trì được; chạy trên một VPS Linux bằng Docker; kênh Zalo cá nhân chỉ có thư viện Node (zca-js) nên một ngôn ngữ cho toàn bộ tiết kiệm nhất; có sẵn hàng đợi có nhịp, lịch, RBAC, audit; phục vụ nhiều đại lý ghi đồng thời và đối soát bằng SQL.
| Lớp | Chọn | Vì sao (và lựa chọn đã bỏ) |
|---|---|---|
| Ngôn ngữ | TypeScript trên Node.js 22 LTS | Một ngôn ngữ cho api, worker, web, kịch bản. Chọn Python/Go/PHP vẫn phải kèm một tiến trình Node cho Zalo cá nhân. |
| Khung máy chủ | NestJS 11 (adapter Fastify) | Có sẵn mô-đun, guard (RBAC), interceptor (audit), @nestjs/schedule, @nestjs/bullmq, @nestjs/swagger, kiểm tra đầu vào; khớp 11 mô-đun mục 4.2. Fastify thuần nhẹ hơn nhưng phải tự dựng các lớp này. |
| CSDL | PostgreSQL 16 | Nhiều đại lý ghi đồng thời; khoá hàng (SELECT … FOR UPDATE) khi hai điều hành xếp cùng vé; ràng buộc FK/UNIQUE (chống trùng webhook); JSONB cho payload; window function cho đối soát; pg_dump. SQLite đủ cho một máy nhưng yếu khi nhiều người ghi và khó tách tiến trình. |
| ORM, di trú | Prisma (+ SQL thô cho báo cáo) | Lược đồ khai báo, di trú có phiên bản, kiểu an toàn; báo cáo phức tạp viết SQL qua $queryRaw. Lược đồ chia theo tệp: lõi một tệp, mỗi mô-đun một tệp (bảng mod_<ten>_*), gom lại khi dựng. |
| Hàng đợi, lịch | BullMQ trên Redis 7 | Thử lại có lùi, job trễ (nhắc hết hạn giữ chỗ), job lặp (quét định kỳ), giới hạn nhịp theo hàng đợi (đúng nhu cầu SMS/Zalo), Bull Board để xem hàng đợi. |
| Phiên, cache | Redis | Phiên đăng nhập (cookie HttpOnly) lưu Redis; cache token VeXeRe 55 phút; khoá phân tán khi xếp. |
| Giao diện | React 19 + Vite + TypeScript, Ant Design 5, TanStack Query, vite-plugin-pwa |
Ant Design mạnh về bảng/biểu mẫu back-office, có locale tiếng Việt; PWA để đại lý "cài" lên màn hình điện thoại; TanStack Query nhận cập nhật từ SSE. |
| Thời gian thực | SSE | Một chiều máy chủ → trình duyệt là đủ (trạng thái vé, hàng đợi bàn xếp); qua Caddy không cần cấu hình riêng; đơn giản hơn WebSocket. |
| Kiểm tra dữ liệu | zod trong packages/shared |
Một lược đồ dùng chung cho web và api (form vé, tệp đẩy danh sách, API nguồn). |
| Gọi VeXeRe | undici (fetch) + lớp bọc thử lại trong packages/vexere-client |
Nhẹ; ghi nhật ký yêu cầu/đáp; có bản giả lập (mock) để chạy test không cần UAT. |
| Xuất vé | satori + @resvg/resvg-js (PNG), pdf-lib (PDF), Handlebars (văn bản/HTML) |
Dựng ảnh vé từ JSX không cần Chromium; nhẹ cho VPS nhỏ. |
| nodemailer + SMTP (Zoho Mail / Brevo / Google Workspace) | Cần SPF/DKIM/DMARC cho tên miền gửi. | |
| SMS SIM thường | Máy Android + SIM chạy SMS Gateway for Android (mã nguồn mở) + máy chủ gateway tự host | REST gửi tin, webhook trạng thái, nhiều máy; không phải viết app Android ngay. Phương án B: app Kotlin riêng. |
| Zalo cá nhân | zca-js trong tiến trình worker-zalo riêng |
Thư viện không chính thức, có thể đứt phiên → tách tiến trình, giám sát, đăng nhập lại bằng QR trên trang quản trị. |
| Zalo OA | API chính thức Zalo OA / ZNS | Giai đoạn 3. |
| Excel | exceljs | Biên bản đối soát, mẫu tệp đẩy danh sách. |
| Xác thực, phân quyền | Phiên cookie + CASL | Diễn đạt được "đại lý chỉ thấy vé của đại lý mình", "sửa khi trạng thái ≤ DA_DAY"; khoá API băm cho nguồn. |
| Nhật ký, giám sát | pino (JSON) ra log Node project, /health, Monitor/Cron của aaPanel, cảnh báo Zalo/Telegram; Uptime Kuma tuỳ chọn |
Đủ cho một VPS; chưa cần Prometheus/Grafana ở bản 1. |
| Triển khai | aaPanel Website → Node project (kiểu PM2) ×3, gắn domain riêng → reverse proxy nginx + SSL tự động; Node/PostgreSQL/Redis/Google Drive từ App Store; sao lưu bằng Cron aaPanel lên Google Drive | Chỉ dùng tính năng có sẵn của aaPanel, ít thao tác tay; không cần Docker (tài liệu 01, mục 5). |
| Kiểm thử | Vitest (đơn vị), Supertest (API), Playwright (luồng chính) | Bộ ca theo mục Testcases của tài liệu VeXeRe chạy trên UAT. |
Phương án đã cân nhắc và lý do không chọn:
- Node + Fastify + SQLite một tiến trình (khuôn Vé247): triển khai đơn giản nhất, nhưng yếu khi nhiều đại lý ghi đồng thời, không có hàng đợi có nhịp, khó tách worker Zalo. Vẫn là phương án dự phòng nếu muốn tối giản (mục 11, câu 12).
- Python Django: quản trị và ORM sẵn, nhưng Zalo cá nhân vẫn cần tiến trình Node; hai ngôn ngữ cho một người bảo trì.
- PHP Laravel (+ Filament): nhanh cho back-office, cùng vấn đề hai ngôn ngữ.
- Go: nhị phân gọn, nhưng làm giao diện/tích hợp chậm hơn, không có thư viện Zalo.
Cấu hình VPS tối thiểu: 2 vCPU, 4 GB RAM, 40 GB SSD, Ubuntu 24.04. Ước dùng: PostgreSQL ~300 MB, Redis ~50 MB, ba tiến trình Node ~450 MB, Caddy ~30 MB.
4.8 Cấu trúc mã nguồn (monorepo, pnpm workspaces)
cong-gui-ve/
apps/
api/ NestJS: nạp lõi + các mô-đun đang bật; REST + SSE
worker/ NestJS không HTTP: chạy job BullMQ của lõi và mô-đun, cron
worker-zalo/ tiến trình riêng của mô-đun zalo-ca-nhan (zca-js)
web/ React + Vite + AntD: vỏ giao diện lõi + màn hình do mô-đun đóng góp
packages/
core/ thực thể, máy trạng thái, bus sự kiện, quyền, vết, cấu hình, registry mô-đun
core-contracts/ interface điểm mở rộng, kiểu sự kiện, phiên bản hợp đồng (semver)
modules/
vexere/ sms-gateway/ email/ zalo-oa/ zalo-ca-nhan/ xuat-ve/
day-danh-sach/ api-nguon/ thong-ke/ doi-soat/
(mỗi mô-đun: manifest.ts, src/, prisma/ (bảng mod_*), test/,
README.md, HUONG-DAN-SU-DUNG.md, CHANGELOG.md)
shared/ zod schema, chuẩn hoá SĐT/giờ/ngày, tiện ích dùng chung
db/ gom lược đồ lõi + mô-đun, di trú, Prisma client
vexere-client/ bọc API VeXeRe (dùng bởi mô-đun vexere), có mock
deploy/ khoi-dong.cjs, cap-nhat.sh, nginx-them.conf (GĐ0); sao-luu.sh, khoi-phuc.sh,
kiem-tra-khoi-phuc.sh, .env.example, HUONG-DAN-AAPANEL.md (đã có)
scripts/ phien-ban.py, dong-bo-git.ps1 / .sh, kiem-tra-tai-lieu.py (đã có)
.githooks/ pre-commit kiểm tra tài liệu; .github/workflows/ CI kiểm tra tài liệu
package.json nguồn phiên bản toàn hệ thống (semver)
docs/
thiet-ke/ tài liệu này, 01-phan-tich-cong-nghe-nen-tang.md + mỗi mô-đun một tệp + ADR
huong-dan/ theo vai trò: dai-ly.md, dieu-hanh.md, quan-tri.md (+ ảnh chụp)
ban-giao/ HANDOVER-LOG.md
kinh-nghiem/ LESSONS.md
tham-khao/ bản trích tài liệu VeXeRe/AMS
CHANGELOG.md
CLAUDE.md chỉ dẫn cho AI: đọc tài liệu nào đầu phiên, cập nhật tài liệu nào khi đổi mã
Khung docs/, CLAUDE.md đã tạo sẵn trong repo này; tài liệu thiết kế hiện nằm ở docs/00-…md và sẽ chuyển vào docs/thiet-ke/ khi khởi tạo monorepo.
4.9 Lõi quản lý vé và mô-đun cắm/rút
Ý tưởng: lõi là "hệ điều hành vé", mô-đun là "ứng dụng" cài lên. Lõi không biết VeXeRe, không biết SMS; nó chỉ biết vé, trạng thái, sự kiện, quyền, vết. Một mô-đun nâng cấp, thêm hay gỡ không làm lõi và mô-đun khác phải đổi mã.
flowchart TB
subgraph LOI[Lõi quản lý vé — packages/core]
TT[Thực thể & máy trạng thái vé]
SK[Bus sự kiện ve.*]
QQ[Quyền · phiên · vết · cấu hình]
RG[Registry mô-đun
manifest · phiên bản · bật/tắt · cô lập lỗi]
HD[Hợp đồng — core-contracts
NhaCungCapCho · KenhGui · NguonVe · KhuonVe · BaoCao · TrangQuanTri]
end
subgraph MOD[Mô-đun cắm/rút — packages/modules/*]
M1[vexere
NhaCungCapCho · BaoCao · webhook]
M2[sms-gateway · email · zalo-oa · zalo-ca-nhan
KenhGui]
M3[day-danh-sach · api-nguon
NguonVe]
M4[xuat-ve
KhuonVe]
M5[thong-ke · doi-soat
BaoCao]
end
M1 & M2 & M3 & M4 & M5 -. thực thi hợp đồng .-> HD
HD --> RG
SK -- phát sự kiện --> M1 & M2 & M4 & M5
M3 -- lệnh tạo/sửa vé --> TT
M1 -- kết quả xếp chỗ --> TT
M2 -- kết quả gửi --> TT
Lõi (packages/core) gồm:
- Thực thể chuẩn:
ve,dai_ly,nguoi_dung,tuyen,diem_don_tra,ve_lich_su,yeu_cau_thay_doi,gui_tin,su_kien,mo_dun,cai_dat(mục 5). - Máy trạng thái vé (mục 3.1) và các lệnh chuyển trạng thái có kiểm tra quyền:
taoVe,dayVe,xepVe,tuChoi,xuatVe,ghiKetQuaGui,huyVe,suaVe,khoaVe,moKhoaVe. - Bus sự kiện:
ve.tao,ve.day,ve.xep,ve.tu_choi,ve.xuat,ve.gui,ve.sua,ve.huy,ve.het_han,ve.khoa; trong tiến trình qua event-emitter, liên tiến trình qua BullMQ; mọi sự kiện lưu bảngsu_kienđể phát lại cho mô-đun bật muộn hoặc xử lý lỗi. - Nhà cung cấp chỗ mặc định
thu-cong(điều hành nhập ghế/mã tay) nằm trong lõi, nên lõi tự chạy được khi không có mô-đun nào. - Registry mô-đun: đọc
manifest(tên, phiên bản,yeuCauLoi: ">=1.2 <2", điểm mở rộng cung cấp, quyền cần, bảng sở hữu, di trú, trang cấu hình), bật/tắt theo bảngmo_dun, kiểm tra tương thích khi khởi động, cô lập lỗi (mô-đun ném lỗi → ghi nhật ký, đánh dấu lỗi, lõi và mô-đun khác chạy tiếp). - Dịch vụ chung mô-đun gọi qua interface: quyền, phiên, vết, cấu hình (có mã hoá bí mật), nhật ký, hàng đợi, SSE.
Sáu điểm mở rộng (interface TypeScript trong packages/core-contracts):
| Điểm mở rộng | Hợp đồng tối thiểu | Cài đặt |
|---|---|---|
NhaCungCapCho |
xep(ve) → {ghe, maChoNgoai, giaNcc, hetHan?}, huy(ve) → {phi, hoan}, sua(ve, thayDoi), traCuu(ve) |
thu-cong (lõi), vexere; sau này BMS khác |
KenhGui |
gui(tin) → {maNgoai}, nhanTrangThai(payload) → {maNgoai, trangThai}, gioiHan() |
sms-gateway, email, zalo-oa, zalo-ca-nhan |
NguonVe |
nhan(duLieu) → lệnh taoVe/suaVe/huyVe + baoNguoc(suKien) |
form (lõi), day-danh-sach, api-nguon |
KhuonVe |
dung(ve, kenh, loaiTin) → {vanBan, html?, png?, pdf?} |
xuat-ve |
BaoCao |
danhSach() → [báo cáo], chay(thamSo) → bảng/tệp |
thong-ke, doi-soat, vexere (hạn mức, giao dịch) |
TrangQuanTri |
Đóng góp mục menu, trang cấu hình, ô trạng thái sức khoẻ | mọi mô-đun có cấu hình |
Quy tắc ràng buộc (kiểm tra bằng lint và CI):
- Mô-đun chỉ phụ thuộc
core-contractsvàshared; không import mô-đun khác; mô-đun nói chuyện với nhau qua sự kiện lõi. - Mô-đun sở hữu bảng riêng tiền tố
mod_<ten>_, chỉ tham chiếu lõi quave_id/dai_ly_id/tuyen_id; không thêm cột vào bảng lõi; cần lưu thêm thì dùngthuoc_tinh_mo_rong(JSONB, khoá theo tên mô-đun). - Phiên bản: lõi và từng mô-đun đánh semver riêng; lõi giữ tương thích ngược trong cùng phiên bản lớn; đổi hợp đồng = tăng phiên bản lớn kèm bảng chuyển đổi; mô-đun khai báo dải phiên bản lõi hỗ trợ; registry từ chối nạp mô-đun không tương thích và nêu rõ lý do.
- Di trú CSDL của mô-đun nằm trong thư mục mô-đun, chạy theo thứ tự lõi → mô-đun; tắt mô-đun không xoá dữ liệu; xoá dữ liệu là thao tác riêng có xác nhận.
- Kiểm thử hợp đồng: mỗi điểm mở rộng có bộ test chuẩn (ví dụ
KenhGui: gửi thành công, thất bại, webhook trùng, quá nhịp); mô-đun mới phải qua bộ test đó trước khi được nạp. - Bật/tắt không cần dựng lại; giao diện tự ẩn/hiện menu, trang cấu hình, cột báo cáo theo mô-đun đang bật.
- Mọi gọi từ lõi sang mô-đun có thời gian chờ và bắt lỗi; mô-đun chậm/hỏng không chặn máy trạng thái.
Ví dụ vận hành: nhà xe muốn thử kênh Telegram → viết mô-đun telegram thực thi KenhGui, chạy bộ test hợp đồng, bật trong quản trị; lõi, VeXeRe, SMS không đổi một dòng. Nâng lõi 1.x → 2.0 vì đổi hợp đồng NhaCungCapCho → chỉ mô-đun vexere phải cập nhật; các kênh gửi vẫn chạy.
4.10 Tài liệu đồng bộ với mã
Nguyên tắc: một thay đổi chưa xong nếu tài liệu chưa đổi theo. Bốn bộ tài liệu sống trong kho mã, cùng nhánh, cùng lần cập nhật với mã:
| Bộ | Ở đâu | Nội dung | Ai đọc |
|---|---|---|---|
| Thiết kế | docs/thiet-ke/ (tài liệu này + mỗi mô-đun một tệp + ADR) |
Kiến trúc, hợp đồng, dữ liệu; ADR ngắn cho mỗi quyết định: bối cảnh, quyết định, hệ quả | Người phát triển, AI |
| Hướng dẫn sử dụng | docs/huong-dan/ theo vai trò + HUONG-DAN-SU-DUNG.md trong từng mô-đun |
Từng màn hình, thao tác, ảnh chụp; mục "Có gì mới" theo phiên bản | Đại lý, điều hành, quản trị; nút ? trong app trỏ đúng mục |
| Bàn giao | docs/ban-giao/HANDOVER-LOG.md |
Việc đang dở, cách chạy, vị trí cấu hình (không chứa bí mật), điều cần biết khi tiếp quản; xoá mục khi xong | Người tiếp quản, AI đầu phiên |
| Kinh nghiệm | docs/kinh-nghiem/LESSONS.md |
Lỗi đã gặp và cách xử lý, quyết định đã đảo và vì sao | Người phát triển, AI |
Cơ chế bảo đảm đồng bộ toàn hệ thống:
- Định nghĩa hoàn thành cho mọi thay đổi: mã + test + di trú + cập nhật đúng bộ tài liệu bị ảnh hưởng + một dòng
CHANGELOG.md(theo Keep a Changelog, ghi phiên bản lõi/mô-đun). - CI chặn: kịch bản kiểm tra khi thay đổi đụng
packages/core,packages/modules/<x>hoặcapps/webmà không đụngdocs/hayREADME/HUONG-DAN-SU-DUNGcủa mô-đun tương ứng thì thất bại, trừ khi gắn nhãnkhong-doi-tai-lieukèm lý do. Mẫu PR có ô kiểm bốn bộ tài liệu. - Sinh tự động những gì sinh được, không viết tay hai lần: OpenAPI từ NestJS Swagger →
docs/thiet-ke/api.md; ERD từ lược đồ Prisma; bảng mô-đun và phiên bản từmanifest; sơ đồ trạng thái từ hằng số máy trạng thái; ma trận quyền từ định nghĩa CASL. Chạy trong CI, tệp sinh ra commit kèm. - Phiên bản tài liệu = phiên bản phần mềm: đầu mỗi tài liệu ghi "áp dụng cho lõi x.y / mô-đun z"; trang Giới thiệu trong app hiện phiên bản và liên kết đúng bản hướng dẫn; nâng lõi lên phiên bản lớn mới thì CI bắt mọi mô-đun cập nhật dòng tương thích trong
README. - Đồng bộ với bộ nhớ AI:
CLAUDE.mdtrong kho chỉ dẫn đầu phiên đọcHANDOVER-LOG.md+LESSONS.md, cuối phiên cập nhật; khớp thói quen bàn giao đang dùng ở các dự án khác. - Rà soát định kỳ: mỗi lần phát hành chạy danh mục "tài liệu nào nhắc đến màn hình/API/bảng vừa đổi" (tìm theo tên) và duyệt; hằng quý đọc lại hướng dẫn cùng một đại lý thật.
5. Thiết kế dữ liệu
Dữ liệu chia hai lớp theo mục 4.9: bảng lõi (mô-đun chỉ đọc/ghi qua dịch vụ lõi) và bảng mô-đun (tiền tố mod_<ten>_, mô-đun toàn quyền, tham chiếu lõi qua ve_id/dai_ly_id/tuyen_id).
erDiagram
DAI_LY ||--o{ NGUOI_DUNG : "có"
DAI_LY ||--o{ VE : "sở hữu"
TUYEN ||--o{ VE : "thuộc"
TUYEN ||--o{ DIEM_DON_TRA : "có"
VE ||--o{ VE_LICH_SU : "vết"
VE ||--o{ YEU_CAU_THAY_DOI : "yêu cầu"
VE ||--o{ GUI_TIN : "gửi"
VE ||--o{ SU_KIEN : "phát"
VE ||--o| MOD_VEXERE_DAT_CHO : "mô-đun vexere"
VE ||--o{ MOD_VEXERE_GIAO_DICH : "mô-đun vexere"
MOD_DAY_DANH_SACH_LO ||--o{ VE : "mô-đun đẩy danh sách"
MOD_DOI_SOAT_KY ||--o{ MOD_DOI_SOAT_CT : "gồm"
MOD_DOI_SOAT_CT }o--|| VE : "ghi nhận"
Bảng lõi
| Bảng | Cột chính |
|---|---|
dai_ly |
id, ma, ten, sdt, email, loai (dai_ly / nguon_he_thong), hoa_hong_json, kenh_mac_dinh_json, tu_duyet, api_key_hash, trang_thai, thuoc_tinh_mo_rong, tao_luc |
nguoi_dung |
id, dai_ly_id (null = nhà xe), ten_dang_nhap, mat_khau_hash, ho_ten, vai_tro (quan_tri / dieu_hanh / dai_ly_chu / dai_ly_nv), trang_thai |
tuyen |
id, ten, tu_ten, den_ten, gio_json, dung_sai_phut, che_do_xep (tu_dong / ban_tu_dong / thu_cong), tat_ban_truoc_phut, thuoc_tinh_mo_rong |
diem_don_tra |
id, tuyen_id, loai (don / tra / trung_chuyen), ten, dia_chi, phu_thu, thuoc_tinh_mo_rong |
ve |
id, ma_ve, dai_ly_id, nguoi_tao_id, nguon (tên mô-đun NguonVe), ma_nguon, ten_khach, sdt, email, tuyen_id, ngay_di, gio_di, so_khach, loai_cho, diem_don_id, diem_don_text, diem_tra_id, diem_tra_text, ghi_chu, gia_ban, hinh_thuc_thu, kenh_json, trang_thai, ly_do_tu_choi, nha_cung_cap_cho (thu_cong / vexere / …), ma_cho_ngoai, ghe_json, xe_bien_so, xe_sdt, gio_don_du_kien, gia_ncc, hoa_hong, phi_huy, hoan_tien, khoa_luc, khoa_boi (mô-đun + lý do), thuoc_tinh_mo_rong, tao_luc, sua_luc, xoa_luc |
ve_lich_su |
id, ve_id, nguoi_id, mo_dun, hanh_dong, truoc_json, sau_json, luc |
yeu_cau_thay_doi |
id, ve_id, loai (sua / huy / doi_chuyen), noi_dung_json, phi_du_kien, trang_thai (cho_duyet / da_duyet / tu_choi), nguoi_duyet_id, ly_do, tao_luc, duyet_luc |
gui_tin |
id, ve_id, kenh (tên mô-đun KenhGui), nguoi_nhan, loai_tin (ve_moi / cap_nhat / huy / xe_cua_ban), noi_dung, trang_thai, so_lan, loi, ma_ngoai, tao_luc, gui_luc, xem_luc |
su_kien |
id, ten (ve.tao / ve.xep / …), ve_id, du_lieu_json, phat_luc, xu_ly_json (mô-đun nào đã xử lý, kết quả) — nhật ký sự kiện để phát lại |
mo_dun |
ten, phien_ban, bat, cau_hinh_json (bí mật mã hoá), trang_thai, loi_cuoi, cap_nhat_luc — registry mô-đun |
cai_dat |
khoá-giá trị cấu hình lõi; phiên đăng nhập lưu Redis, không có bảng |
Bảng mô-đun
| Mô-đun | Bảng | Cột chính |
|---|---|---|
vexere |
mod_vexere_dat_cho |
ve_id, trip_code, booking_code, ticket_code, partner_code, status, expired_time, amount_booking, gia_dai_ly, is_deposit, deposit_status, dong_bo_luc |
mod_vexere_giao_dich |
id, ve_id, loai (reserve / pay / refund / update), transaction_id, vxr_transaction_id, so_tien, phi, yeu_cau_json, dap_json, ma_loi, luc |
|
mod_vexere_su_kien |
id, event_type, booking_code, event_timestamp, payload_json, chu_ky_hop_le, xu_ly_luc, ket_qua (UNIQUE event_type + booking_code + event_timestamp) |
|
mod_vexere_nhat_ky |
id, endpoint, method, ma_http, thoi_gian_ms, tom_tat, luc |
|
mod_vexere_anh_xa |
tuyen_id → from_id, to_id, company_id; diem_don_tra_id → point_id, ten_vxr, dong_bo_luc |
|
sms-gateway |
mod_sms_thiet_bi |
id, ten, so_sim, tai_khoan_gateway, lan_cuoi_hoi, so_gui_hom_nay, tran_ngay, trang_thai |
zalo-ca-nhan |
mod_zalo_tai_khoan |
id, ten, uid, phien_ma_hoa, trang_thai, lan_cuoi_song, nhip_json |
xuat-ve |
mod_xuat_ve_mau |
id, kenh, loai_tin, ten, noi_dung, kich_hoat, phien_ban |
day-danh-sach |
mod_day_danh_sach_lo |
id, dai_ly_id, nguoi_id, cach (excel / dan), ten_tep, so_dong, so_hop_le, so_loi, ket_qua_json, tao_luc |
doi-soat |
mod_doi_soat_ky |
id, dai_ly_id, tu_ngay, den_ngay, so_ve, doanh_thu, hoa_hong, phai_nop, trang_thai (mo / da_chot / da_xac_nhan), tep_xlsx, chot_boi, chot_luc, xac_nhan_luc |
mod_doi_soat_ct |
ky_id, ve_id, gia_ban, gia_ncc, hoa_hong, phi_huy, phai_nop, lech_json, ghi_chu |
|
thong-ke |
(không bảng riêng) | Materialized view làm mới theo lịch |
Chỉ mục: ve(dai_ly_id, trang_thai), ve(ngay_di, gio_di), ve(sdt), ve(nguon, ma_nguon) UNIQUE, gui_tin(trang_thai, kenh), mod_vexere_dat_cho(booking_code), mod_vexere_su_kien(booking_code).
6. Giao diện
Đại lý (điện thoại là chính): Vé mới (form một cột, chọn nhanh tuyến/giờ đã dùng gần đây), Đẩy danh sách (tải tệp/dán, bảng xem trước sửa tại chỗ), Vé của tôi (thẻ vé, màu theo trạng thái, lọc ngày đi, tìm SĐT, nút Sửa/Huỷ/Gửi lại tuỳ trạng thái), Thống kê của tôi, Đối soát (kỳ, biên bản, nút Xác nhận).
Điều hành (máy tính): Bàn xếp vé hai cột — trái là hàng đợi DA_DAY nhóm theo chuyến (ngày/giờ/tuyến), phải là chi tiết vé + sơ đồ ghế VeXeRe + nút Xác nhận/Đổi ghế/Từ chối/Xếp tay; Yêu cầu chờ duyệt; Tin gửi lỗi; Sắp hết hạn giữ chỗ (đếm ngược); Tra cứu mọi vé.
Quản trị: Tuyến & điểm (đồng bộ id VeXeRe), Đại lý & hoa hồng, Kênh gửi (trạng thái SIM, OSZalo, OA, SMTP; bộ đếm ngày), Khuôn tin (xem trước theo kênh), Người dùng & quyền, Nhật ký.
Khách: trang /v/{mã ngắn} hiện vé (không cần đăng nhập, mã ngắn khó đoán 8 ký tự), cập nhật theo trạng thái mới nhất, nút "Gọi nhà xe".
Điểm UX quan trọng: trạng thái đổi tức thời (SSE); thao tác trên điện thoại một tay; chữ to, nút to; màu trạng thái thống nhất toàn hệ thống.
7. Phân quyền và an toàn
| Quyền | Quản trị | Điều hành | Đại lý chủ | Đại lý NV | Nguồn hệ thống |
|---|---|---|---|---|---|
| Tạo/đẩy vé | ✓ | ✓ | ✓ (đại lý mình) | ✓ (đại lý mình) | ✓ (API) |
| Sửa/xoá vé chưa xếp | ✓ | ✓ | ✓ | chỉ vé mình tạo | ✓ |
| Yêu cầu sửa/huỷ vé đã xếp | ✓ | ✓ | ✓ | ✓ | ✓ |
| Duyệt yêu cầu, xếp vé, gọi API VeXeRe | ✓ | ✓ | nếu tu_duyet |
– | – |
| Xem vé | tất cả | tất cả | đại lý mình | đại lý mình | vé của nguồn |
| Thống kê | tất cả | tất cả | đại lý mình | đại lý mình | – |
| Tạo/chốt/mở kỳ đối soát | ✓ | – | xác nhận kỳ của mình | – | – |
| Cấu hình hệ thống, kênh, khuôn tin, người dùng | ✓ | – | quản lý NV của mình | – | – |
An toàn:
- Mật khẩu băm (scrypt/argon2), phiên cookie HttpOnly, khoá API đại lý băm; giới hạn lần đăng nhập sai.
- Bí mật (
client_secretVeXeRe, SMTP, OA token, khoá OSZalo, khoá SIM) trong.env, không commit; giá trị trongkenh_cau_hinhmã hoá bằng khoá máy chủ. - Mọi hành động ghi vào
ve_lich_su/nhật ký; xoá là xoá mềm. - Dữ liệu cá nhân (SĐT, tên, email) theo Nghị định 13/2023/NĐ-CP: chỉ dùng cho việc đi xe; tự ẩn SĐT trong thống kê tổng hợp; lịch xoá/ẩn danh vé sau N tháng (mặc định 24) cấu hình được; thông báo xử lý dữ liệu trên trang tra cứu.
- Webhook: chữ ký HMAC, chống phát lại theo
event_timestamp, giới hạn tốc độ theo IP. - HTTPS bắt buộc (aaPanel + Let's Encrypt), CSDL sao lưu hằng ngày ra thư mục ngoài web root như Vé247, giữ 30 bản.
8. Vận hành
- Triển khai: VPS Ubuntu + aaPanel. Ba Node project (
cgv-apigắn domain đề xuấtve.xekhachhagiang.vnvới Mapping + SSL,cgv-worker,cgv-zalo), Node 22 / PostgreSQL / Redis / Google Drive cài từ App Store. Cập nhật:git pull+ Restart trong panel (tệp khởi động tự cài phụ thuộc, di trú, dựng web) hoặc nút Cập nhật trong app. Tệp.envchỉ có trên VPS. Chi tiết: tài liệu 01 mục 5,deploy/HUONG-DAN-AAPANEL.md. - Giám sát:
/health(CSDL, Redis, token VeXeRe, hàng đợi,worker-zalo, SMS gateway lần liên hệ cuối, webhook nhận cuối, sao lưu cuối); aaPanel Monitor + Cron Access URL; bot Zalo/Telegram báo sự cố; Bull Board xem hàng đợi (chỉ quản trị). Cảnh báo khi: máy SIM im > 10 phút, tin lỗi > 5 liên tiếp, vé giữ chỗ sắp hết hạn, VeXeRe lỗi 5xx liên tục,paylỗi (nghi hạn mức thấp), phiên Zalo đứt, sao lưu lỗi. - Hạn mức VeXeRe: không có API tra, nên hệ thống tự ước tính (nạp − đã thanh toán + đã hoàn) và quản trị nhập lại số thực từ AMS khi lệch.
- Sao lưu/khôi phục: Cron aaPanel — Shell
deploy/sao-luu.sh(02:00,pg_dump+ gói cấu hình mã hoá) → Backup Directory lên Google Drive giữ 30 bản (02:40) →deploy/kiem-tra-khoi-phuc.shhằng tuần; Redis không cần sao lưu; khôi phục bằngdeploy/khoi-phuc.sh. - Nhật ký: pino JSON ra log Node project, Cron Cut Log giữ 30 tệp; che SĐT một phần.
- Bảo mật máy chủ: chỉ mở 22 (khoá SSH, tắt mật khẩu), 80, 443; UFW + fail2ban; PostgreSQL/Redis chỉ trong mạng nội bộ Docker; cập nhật bảo mật hệ điều hành tự động.
9. Lộ trình và ước lượng
| Giai đoạn | Nội dung | Ước lượng |
|---|---|---|
| GĐ0 — Nền tảng | Monorepo, Docker Compose, PostgreSQL/Redis, lõi (thực thể, máy trạng thái, bus sự kiện, quyền, vết, registry mô-đun) + hợp đồng core-contracts + bộ test hợp đồng, khung NestJS + React, CI (kể cả kiểm tra tài liệu), bốn bộ tài liệu khởi tạo, VPS + tên miền + HTTPS, sao lưu tự động |
1–1,5 tuần |
| GĐ1 — Lõi vé & gửi bảo đảm | Form vé, đẩy danh sách (Excel/dán), bàn xếp vé thủ công, xuất vé, gửi SMS SIM (máy Android + app gateway) + Zalo cá nhân (worker-zalo), trang tra cứu, thống kê cơ bản, API liên thông nguồn |
2–3 tuần |
| GĐ2 — Tự động với VeXeRe | Bộ kết nối đầy đủ trên UAT, ánh xạ tuyến/điểm, xếp bán tự động → tự động, thanh toán, huỷ/sửa qua API, webhook, quét định kỳ, làm giàu biển số, email | 2–3 tuần (+ thời gian VeXeRe cấp tài khoản PRO và đăng ký webhook) |
| GĐ3 — Đối soát & OA | Kỳ đối soát, khoá kỳ, biên bản Excel, xác nhận đại lý, đối chiếu với báo cáo AMS, Zalo OA/ZNS, cảnh báo vận hành, ẩn danh dữ liệu | 2 tuần |
Ước lượng tính theo một người làm chính với Claude Code, thử nghiệm thật với 1–2 đại lý ngay cuối GĐ1.
10. Rủi ro và giảm thiểu
| Rủi ro | Mức | Giảm thiểu |
|---|---|---|
| VeXeRe không cấp API đối tác cho nhà xe, hoặc cấp với chiết khấu/hạn mức bất lợi | Cao | Hỏi ngay (mục 11). GĐ1 chạy được không cần API (xếp thủ công); bộ kết nối là adapter, thay được bằng cách khác sau. |
| SIM thường bị nhà mạng chặn khi gửi nhiều | Trung bình | Nhịp chậm, trần ngày, nội dung cá nhân hoá, nhiều SIM luân phiên, dự phòng Zalo; theo dõi tỉ lệ DELIVERED. |
| Zalo cá nhân bị khoá do gửi tới người lạ | Trung bình | Chỉ gửi Zalo cá nhân cho khách đã chat/kết bạn; còn lại đi SMS; nhịp gửi tự đặt theo kinh nghiệm OSZalo; worker-zalo tách riêng, đăng nhập lại bằng QR. |
| Vé giữ chỗ hết hạn tự huỷ mà không ai thấy | Cao | Đồng hồ hết hạn, nhắc 2 lần, tự chuyển TU_CHOI và báo đại lý. |
| Tài liệu API có điểm không nhất quán (trạng thái 1/2) | Thấp | Kiểm chứng trên UAT; coi GET booking là chuẩn. |
| Đại lý nhập sai tuyến/giờ hàng loạt | Trung bình | Chọn từ danh sách, xem trước lô, cảnh báo trùng, giới hạn sửa sau khi xếp. |
| Sai lệch đối soát do huỷ/đổi sau khi chốt | Trung bình | Khoá kỳ; thay đổi sau chốt đi vào kỳ sau dưới dạng điều chỉnh có vết. |
| Vận hành thêm PostgreSQL/Redis/ba Node project trên aaPanel cho một người | Thấp | Mọi thứ cài từ App Store và Website của aaPanel, sao lưu và cảnh báo tự động bằng Cron, hướng dẫn từng bước trong deploy/HUONG-DAN-AAPANEL.md. |
| Ứng dụng SMS gateway mã nguồn mở đổi giao thức hoặc ngừng phát triển | Thấp | Bọc trong một adapter; phương án B là app Kotlin riêng theo cùng giao thức. |
11. Câu hỏi cần chốt trước khi lập trình
- Tài khoản VeXeRe: nhà xe đã có (hoặc xin được)
client_id/secretAPI đối tác chưa? Chiết khấu và hạn mức thế nào? Hay mỗi đại lý dùng tài khoản AMS riêng (khi đó hệ thống chỉ xếp thủ công và ghi nhận)? company_idcủa nhà xe trên VeXeRe và danh sách tuyến/giờ chính thức để dựng bảng ánh xạ.- Thanh toán: đại lý thu tiền rồi hệ thống
payngay (trừ hạn mức), hay chỉ giữ chỗ và thu tại xe? Ai chịu phí huỷ theo giá đại lý? - Gửi SMS: mua một máy Android + SIM riêng đặt cố định (đề xuất) chạy ứng dụng SMS Gateway mã nguồn mở, hay tự viết app? Dự kiến bao nhiêu tin/ngày, mấy SIM?
- Zalo OA đã có và đã xác thực chưa? Có ngân sách ZNS không, hay để giai đoạn sau?
- Có để VeXeRe tự nhắn SMS/email cho khách song song không? Nếu không,
customer_emailgửi lên VeXeRe dùng hộp thư của nhà xe. - 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 (đề xuất 2 vCPU / 4 GB / 40 GB SSD, Ubuntu 24.04), tên miền (đề xuất
ve.xekhachhagiang.vn), ai giữ quyền root? - Hoa hồng đại lý: % hay số cố định, theo tuyến/loại chỗ? Kỳ đối soát tuần hay tháng?
- Nguồn tuỳ chọn: có cần Vé247/OSZalo/web đẩy vé vào cổng ngay GĐ1 không, hay để sau khi cổng ổn định?
- Đại lý được xoá vé tới trạng thái nào: chỉ trước khi xếp (đề xuất) hay cả sau khi xếp nếu chưa gửi khách?
- Có cần vé khứ hồi và vé cọc Lễ/Tết ở phiên bản 1 không?
- Bộ công nghệ: chốt theo mục 4.7 (NestJS + PostgreSQL + Redis/BullMQ + React/Vite + Docker Compose) hay muốn tối giản hơn (Fastify + SQLite một tiến trình, chấp nhận hạn chế đã nêu)?
- Mức "cắm/rút" mong muốn: mô-đun nằm trong cùng kho mã, bật/tắt bằng cấu hình không cần dựng lại (đề xuất, mục 4.9) hay cần cài/gỡ gói mô-đun lúc chạy như plugin thật (phức tạp và rủi ro hơn nhiều)?
Đã chốt ngày 02/9/2026 (trả lời trực tiếp): VPS mới riêng Ubuntu + aaPanel (câu 7); ứng dụng đại lý bản 1 là PWA cài từ trình duyệt; đồng bộ git: Claude tự chạy scripts/dong-bo-git.ps1 cuối mỗi lượt làm việc; AI bản 1: đọc danh sách vé từ văn bản dán và ảnh chụp (tài liệu 01, mục 6). Các câu còn lại vẫn mở.
Phụ lục A — Ánh xạ trạng thái VeXeRe ↔ hệ thống
VeXeRe status |
Ý nghĩa | Trạng thái nội bộ | Hành động |
|---|---|---|---|
| 1 | Reserve (giữ chỗ, có expired_time) |
DA_XEP (chưa thanh toán) | Theo dõi hết hạn |
| 4 | Paid | DA_XEP (đã thanh toán) | Xuất & gửi |
| 5 | Refund/huỷ | HUY | Báo khách + đại lý, bút toán hoàn |
is_deposit=1, deposit_status 1/2/3/4 |
Vé cọc: chưa có giá / chờ thu thêm / hoàn / xong | Cờ phụ trên vé | Báo đại lý thu thêm/hoàn theo surcharge_time_deadline |
Phụ lục B — Khuôn tin mẫu
SMS (không dấu, ≤ 160 ký tự)
XKH: Ve 17h30 02/9 Ha Giang-Ha Noi, 2 giuong A5 A6, don BX Ha Giang 17h15. Ma ve NO97T9. Xe 24B-123.45 0912xxxxxx. Xem: xkh.vn/v/AB12CD34
Zalo cá nhân / OA (có dấu, kèm ảnh vé)
🎫 VÉ XE KHÁCH HÀ GIANG
Khách: Nguyễn Văn A (0912…)
Chuyến: 17h30 Thứ Tư 02/9/2026 · Hà Giang → Hà Nội
Chỗ: 2 giường A5, A6 (tầng dưới)
Đón: 17h15 tại Bến xe Hà Giang
Xe: 24B-123.45 · 0912 xxx xxx
Mã vé: NO97T9 · Đã thanh toán 400.000đ
Huỷ trước 12 giờ không mất phí. Tra cứu: xkh.vn/v/AB12CD34
Email: tiêu đề Vé xe 17h30 02/9 Hà Giang → Hà Nội — mã NO97T9; thân HTML bảng như trên, mã QR nội bộ, nút "Xem vé".
Phụ lục C — Mẫu cột tệp đẩy danh sách
| Cột | Bắt buộc | Ví dụ | Ghi chú |
|---|---|---|---|
| SĐT | ✓ | 0912345678 | 10 số, tự bỏ khoảng trắng/dấu chấm |
| Tên khách | Nguyễn Văn A | ||
| Số khách | ✓ | 2 | 1–8/dòng |
| Loại chỗ | giường / ghế / cabin | Chuẩn hoá như Vé247 | |
| Tuyến | ✓ | Hà Giang - Hà Nội | Khớp danh sách tuyến |
| Ngày đi | ✓ | 02/9/2026 | Cho phép 2/9, 02-09-2026 |
| Giờ đi | ✓ | 17h30 | Khớp lịch chuyến |
| Điểm đón | BX Hà Giang | Khớp danh sách; không khớp → cảnh báo | |
| Điểm trả | Mỹ Đình | ||
| Giá bán | 200000 | Trống → giá mặc định tuyến | |
| Đã thu | dai_ly / xe / ck | Mặc định theo đại lý | |
| a@gmail.com | Khi gửi email | ||
| Kênh gửi | zalo,sms | Trống → mặc định đại lý | |
| Ghi chú | Người già, cần tầng dưới | Gửi nhà xe |