🏛 Heritage Voice — Tài liệu Business Analysis

Tài liệu nội bộ dành cho BA, Dev, PM — Phiên bản 1.0 · Tháng 5/2026

v1.0.0 May 2026 Connect Vietnam Confidential

Heritage Voice

Nền tảng thuyết minh di sản văn hóa Việt Nam qua âm thanh — Progressive Web App hỗ trợ 6 ngôn ngữ, GPS geofencing, AI assistant, Tour Guide Mode và hệ thống quản lý nội dung đa cấp.

1. Giới thiệu dự án

1.1 Mục tiêu

Heritage Voice giải quyết bài toán thiếu hướng dẫn viên chất lượng tại các di tích lịch sử Việt Nam bằng cách số hóa nội dung thuyết minh, cho phép du khách tự khám phá qua điện thoại — không cần cài app, không rào cản ngôn ngữ.

1.2 Domain URL

# Production
Frontend:  https://disannoi.vn/
Admin CMS: https://disannoi.vn/admin/
API Base:  https://disannoi.vn/api/
AI Proxy:  https://hv-ai-proxy.hiepntt-moet.workers.dev/

1.3 Các trang chính (FE)

FileMô tảAuth
home.htmlTrang chủ: Resume, Nearby, Featured, AI chat, User menuOptional
detail.htmlChi tiết di sản: Hero, Stats, Tabs, AI float pillOptional
map.htmlBản đồ: GPS, Geofence, Audio points, Explore modeOptional
profile.htmlHồ sơ: Hoạt động, Yêu thích, HDV, Cài đặt ngôn ngữRequired
player.htmlTrình phát audio: Controls, Transcript, RatingOptional
tour-host.htmlMàn hình Guide: Tạo phiên, PIN, điều khiển audioRequired + can_guide
tour-guest.htmlMàn hình Khách: Join bằng PIN, chọn ngôn ngữOptional
qr.htmlQuét QR code → redirect đến điểm ngheOptional
login.htmlĐăng nhập / Đăng ký
chat.htmlTrang AI chat riêng (planned)Optional

2. Actors & Roles

Hệ thống có 6 role theo thứ tự quyền tăng dần:

guest
Xem công khai, nghe audio, quét QR, không đăng nhập
member
Guest + lưu yêu thích, lịch sử nghe, đăng ký làm HDV
reviewer
Member + phê duyệt/từ chối nội dung theo site được assign
editor
Reviewer + tạo/sửa audio points, voice tours, upload audio
admin
Editor + quản lý user, phân quyền, xem analytics, quản lý QR
superadmin
Admin + export data, xóa vĩnh viễn, cấu hình hệ thống

2.1 Phân quyền theo site (user_site_permissions)

Ngoài role toàn hệ thống, mỗi user có thể được cấp quyền chi tiết theo từng heritage_site:

PermissionMô tả
can_editTạo/sửa audio points và voice tours tại site này
can_reviewPhê duyệt nội dung tại site này
can_create_qrTạo QR code tại site này
can_guideDẫn tour (Tour Guide Mode) tại site này
ℹ️
Lưu ý quan trọng

can_guide không phụ thuộc vào role — user có role member vẫn có thể dẫn tour nếu được cấp can_guide tại site cụ thể. Admin có thể cấp trực tiếp hoặc duyệt đơn đăng ký từ user.

3. Tech Stack

LayerTechnologyChi tiết
FrontendVanilla HTML/CSS/JSPWA, Service Worker, Web Push API, Geolocation API
BackendPHP 8File-based routing qua index.php, PDO/MySQL
DatabaseMySQL (tranhiep_tads)Host: localhost, User: tranhiep_tads
MapLeaflet.js + OpenStreetMapGeofencing, GPS tracking, Custom divIcon
AIClaude API (claude-sonnet-4)Proxy qua Cloudflare Workers, RAG từ site data
AuthCustom token (base64 + HMAC)Access token 7 ngày, Refresh token 30 ngày
UploadPHP move_uploaded_fileAudio → /audio/, Image → /images/, Avatar → /images/avatars/
EmailPHPMailerNotification khi submit/approve/reject review
TTSWeb Speech APIClient-side TTS tiếng Việt (fallback)
i18n UICustom lang.js6 ngôn ngữ, data-i18n attribute, HV_applyLang()

4. Kiến trúc hệ thống

4.1 Request Flow

Browser → PWA (HTML/JS) → fetch() → /api/index.php (Router) → include relevant handler (auth.php, sites.php, audio.php, ...) → db() → MySQL → json_ok() / json_error() → Service Worker (cache strategy) → Cache First: static assets, audio files → Network First: API responses

4.2 API Router Pattern

Tất cả API đi qua /api/index.php — file này require_once tất cả handler files và dùng PHP match() để route:

// Pattern: $r = segment[0], $id = segment[1], $sub = segment[2], $sub2 = segment[3]
GET  /api/heritage-sites          → sites_list()
POST /api/heritage-sites          → site_create()
GET  /api/heritage-sites/{id}      → site_detail($id)
GET  /api/tour-guide/create        → tg_create()
POST /api/guide/applications/1/approve → guide_approve($sub)  // dùng $sub làm ID
⚠️
Lưu ý PDO params

PDO không cho phép dùng cùng tên named parameter (:lang) 2 lần trong 1 query. Dùng :lang1, :lang2, :lang3 cho các JOIN khác nhau.

4.3 Token Structure

// Format: base64(payload).HMAC_SHA256
payload: {uid, type, exp, jti}
type: 'access' (7 ngày) | 'refresh' (30 ngày)

// Lưu trong localStorage
hv_token         → access token
hv_refresh_token  → refresh token  
hv_user          → {id, email, display_name, role, avatar_url}
hv_lang          → ngôn ngữ UI (vi/en/zh/ja/ko/fr)

5. Database Schema

5.1 Core Tables

users Người dùng hệ thống
idINT UNSIGNEDPKAuto increment
emailVARCHAR(255)UQEmail đăng nhập
display_nameVARCHAR(255)Tên hiển thị
roleENUMguest/member/reviewer/editor/admin/superadmin
statusENUMactive/inactive/banned
providerVARCHAR(20)email / google / facebook
avatar_urlTEXTNULLURL ảnh đại diện
last_seen_atTIMESTAMPNULLLần cuối online
heritage_sites Di sản văn hóa
idVARCHAR(100)PKSlug ID (vd: dinh_vinh_binh)
nameVARCHAR(255)Tên di sản tiếng Việt
typeENUMrelic/museum/craft_village/historical_site/landscape/...
latitude/longitudeDECIMALTọa độ trung tâm
geofence_radius_mINTBán kính geofence (mét)
statusENUMdraft/active/inactive
featuredTINYINTHiện trong mục Nổi bật
audio_points Điểm nghe audio
idVARCHAR(100)PKSlug ID (vd: dvb_ap_01)
site_idVARCHAR(100)FK→ heritage_sites.id
tour_idVARCHAR(100)FK→ voice_tours.id
audio_urlTEXTNULLURL file audio tiếng Việt (gốc)
transcriptTEXTNULLNội dung thuyết minh đầy đủ (VI)
review_statusENUMdraft/pending_review/active/rejected
reviewerVARCHAR(255)NULLTên người thẩm định
reviewed_byINTNULL→ users.id
order_indexINTThứ tự trong tour

5.2 i18n Tables

audio_localizations File audio theo ngôn ngữ
audio_point_idVARCHAR(100)FK→ audio_points.id
language_codeVARCHAR(5)UQvi/en/zh/ja/ko/fr
audio_urlTEXTURL file audio bản dịch
sourceENUMmanual/tts
content_translations Nội dung text theo ngôn ngữ
entity_typeENUMaudio_point/voice_tour/heritage_site
entity_idVARCHAR(100)ID của entity
language_codeVARCHAR(5)vi/en/zh/ja/ko/fr
field_nameVARCHAR(50)title/short_description/script/name/transcript
contentTEXTNội dung đã dịch
sourceENUMmanual/ai_translated

5.3 Tour Guide Tables

tour_sessions Phiên tour của hướng dẫn viên
pinCHAR(6)UQMã PIN 6 số khách join
host_user_idINTFK→ users.id (guide)
site_idVARCHAR(100)FK→ heritage_sites.id
statusENUMwaiting/active/ended
playback_stateENUMidle/playing/paused
current_audio_idVARCHAR(100)NULLAudio đang phát
playback_positionFLOATVị trí phát (giây)

5.4 Guide Registration Tables

guide_applications Đơn đăng ký hướng dẫn viên
user_idINTFK→ users.id
site_idVARCHAR(100)FK→ heritage_sites.id
statusENUMpending/approved/rejected
noteTEXTNULLLý do đăng ký từ user
admin_noteTEXTNULLGhi chú từ admin

6. API Endpoints

Heritage Sites

GET/api/heritage-sitesDanh sách di sản. Params: status, type, province, lang, page, limit
GET/api/heritage-sites/nearbyDi sản gần vị trí. Params: lat, lng, radius
GET/api/heritage-sites/{id}Chi tiết di sản
POST/api/heritage-sitesTạo di sản [admin+]
PUT/api/heritage-sites/{id}Sửa di sản [admin+]

Audio Points

GET/api/audio-pointsDanh sách điểm nghe. Params: site_id, tour_id, status, lang
GET/api/audio-points/{id}Chi tiết điểm nghe
POST/api/audio-pointsTạo điểm nghe [editor+]
PUT/api/audio-points/{id}Sửa điểm nghe [editor+] — tự reset review_status về draft

Authentication

POST/api/auth/registerĐăng ký tài khoản
POST/api/auth/loginĐăng nhập → {access_token, refresh_token}
GET/api/auth/meThông tin user hiện tại + stats
POST/api/auth/refreshRefresh access token

Review (Thẩm định)

POST/api/review/submitGửi thẩm định → đổi review_status = pending_review
GET/api/review/pendingDanh sách chờ duyệt [reviewer+]
POST/api/review/approveDuyệt → review_status = active [reviewer+]
POST/api/review/rejectTừ chối → review_status = draft [reviewer+]

Tour Guide

POST/api/tour-guide/createTạo phiên tour → {pin, audio_points} [can_guide]
POST/api/tour-guide/joinKhách join bằng PIN + chọn lang
GET/api/tour-guide/{pin}/statePoll trạng thái (2s/lần)
POST/api/tour-guide/{pin}/commandLệnh: play/pause/seek/next/end [host only]
POST/api/tour-guide/{pin}/pingKhách báo còn online (8s/lần)

Guide Registration

POST/api/guide/applyUser đăng ký làm HDV tại site
GET/api/guide/my-applicationXem trạng thái đơn của mình
GET/api/guide/my-permissionsDanh sách site được dẫn tour
POST/api/guide/applications/{id}/approveAdmin duyệt → can_guide = 1
GET/api/guide/analyticsThống kê HDV [admin+]

i18n / Languages

GET/api/audio-localizationsDanh sách audio theo ngôn ngữ. Params: audio_point_id, lang
POST/api/audio-localizationsThêm/cập nhật audio bản dịch [editor+]
GET/api/content-translationsDanh sách bản dịch text. Params: entity_type, entity_id, lang
POST/api/content-translationsThêm/cập nhật bản dịch text [editor+]

7. Authentication Flow

[Login] POST /api/auth/login → Response: {access_token, refresh_token, user} → Lưu vào localStorage: hv_token, hv_refresh_token, hv_user [Request API] fetch(url, {headers: {Authorization: 'Bearer ' + token}}) → 401? → pwa.js tự động gọi /api/auth/refresh → Retry với token mới [Auto refresh] mỗi 5 phút, check exp token → timeLeft < 1 giờ → gọi refresh [Middleware PHP] require_auth() / require_editor() / require_admin_role() → Parse Bearer token → verify HMAC → check exp → query DB user → json_error('Unauthorized', 401) nếu fail

8. Frontend App Modules

8.1 Chọn ngôn ngữ (i18n UI)

Lần đầu mở home.html: → check localStorage['hv_lang'] → Không có → Hiện langFirstOverlay (fullscreen chọn ngôn ngữ) → Chọn → lưu hv_lang → ẩn overlay → hiện trang Đổi ngôn ngữ sau: → Tap nút 🇻🇳 VI trên header → langSheet bottom → Chọn → HV_setLang(lang) → HV_applyLang() → renderSites() Áp dụng ngôn ngữ: → HV_applyLang() quét tất cả [data-i18n] → thay textContent → API calls thêm ?lang=xx → server trả content đúng ngôn ngữ → Fallback: nếu không có bản dịch → trả về tiếng Việt

8.2 GPS & Geofencing

map.html init: → navigator.geolocation.watchPosition() → Mỗi update GPS: → Vẽ avatar marker với radio pulse animation → Loop tất cả audio_points: → haversine(userLat, userLng, ap.lat, ap.lng) < ap.trigger_radius_m? → Chưa nghe? → hiện GeoAlert bottom sheet → Tap "Nghe ngay" → player.html?id={ap.id}

8.3 Audio Ngôn ngữ trong Detail

detail.html load: → Đọc hv_lang từ localStorage → loadAvailableLangs(siteId): → GET /api/audio-localizations?site_id={id} → Build _availableLangs = ['vi', 'en', ...] → Update stat "VI ▾" User tap stat ngôn ngữ: → Hiện audioLangSheet với các ngôn ngữ có sẵn → Chọn → selectAudioLang(lang) → lưu hv_lang → GET /api/audio-points?site_id={id}&lang={lang} → renderAudioTab() với audio URLs bản dịch

9. Admin CMS

9.1 Các tab Admin

TabRoute keyQuyềnMô tả
📊 Analyticsanalyticseditor+KPI, biểu đồ, fullscreen mode
🏛 Điểm đếnsiteseditor+CRUD heritage_sites, nút HDV theo site
🎧 Điểm ngheaudioeditor+CRUD audio_points, tab ngôn ngữ, badge review, gửi thẩm định
🗺 Tuyến nghetourseditor+CRUD voice_tours
📷 QR Codesqreditor+Tạo/quản lý QR theo site permission
✅ Thẩm địnhreviewreviewer+Danh sách pending, approve/reject + lý do
👥 Người dùngusersadmin+CRUD users, phân quyền 4 loại theo site, search
🏷 Loại hìnhtypesadmin+Quản lý 11 loại hình di sản
🌐 Dịch thuậti18neditor+Badge ngôn ngữ, upload audio đa ngôn ngữ, bản dịch text
🎙 HDVguidesadmin+Danh sách đơn đăng ký, duyệt/từ chối, top HDV, analytics
📄 Trang tĩnhpagesadmin+FAQ, About, Terms...
⚙️ Cài đặtsettingsadmin+Cấu hình hệ thống

9.2 Form Điểm nghe — Tab ngôn ngữ

Form tạo/sửa audio_point có tab ngôn ngữ VI | EN | ZH | JA | KO | FR:

  • Tab VI: dùng fields gốc (af_url, af_transcript, af_tts_intro)
  • Tab EN-FR: fields riêng (af_loc_en, af_transcript_en...) → lưu vào audio_localizations + content_translations
  • Tên file audio upload: {lang}_{audio_point_id}_{timestamp}.mp3
  • Upload API: POST /api/upload.php?type=audio&lang={lang}&site={site_id}&point={point_id}

10. Tour Guide Mode

10.1 Kiến trúc

Server relay (polling 2s) — không dùng WebSocket/SSE để đơn giản và tương thích tốt:

Guide (host): 1. POST /api/tour-guide/create {site_id} → {pin: "123456", audio_points: [...]} 2. Chia sẻ PIN cho khách 3. POST /api/tour-guide/{pin}/command {cmd: "play", audio_id: "dvb_ap_01", position: 0} 4. Lệnh được lưu vào DB (tour_sessions) Guest (khách): 1. POST /api/tour-guide/join {pin: "123456", lang: "en"} → {session, audio_points với audio_url bản EN} 2. Poll GET /api/tour-guide/{pin}/state mỗi 2 giây 3. So sánh state với lần trước → có lệnh mới? → execute trên local audio element 4. POST /api/tour-guide/{pin}/ping mỗi 8 giây Cleanup: → Member không ping > 15s → tự động đánh dấu left_at → tour_sessions.member_count cập nhật realtime

10.2 Fallback ngôn ngữ

Guest chọn JA → tg_join() query: → LEFT JOIN audio_localizations al ON al.audio_point_id = ap.id AND al.language_code = :lang → ap.audio_url = COALESCE(al.audio_url, ap.audio_url) -- fallback về VI nếu không có JA → is_localized = (al.audio_url IS NOT NULL)

11. Hướng dẫn viên — Registration Flow

CON ĐƯỜNG 1 — Admin cấp thẳng: Admin CMS → Tab Người dùng → 🔑 Phân quyền → tick can_guide tại site → Save → PUT /api/users/{id}/permissions {site_id, can_guide: 1} Admin CMS → Tab Điểm đến → 👥 HDV (theo site) → Search user → Thêm HDV → PUT /api/users/{id}/permissions {site_id, can_guide: 1} CON ĐƯỜNG 2 — User đăng ký: profile.html → Tab 🎙 HDV → "Đăng ký làm HDV" → Chọn site + Nhập lý do → POST /api/guide/apply {site_id, note} → Status: pending Admin CMS → Tab HDV → Danh sách chờ duyệt → Xem đơn → Duyệt / Từ chối → POST /api/guide/applications/{id}/approve → Tự động: user_site_permissions.can_guide = 1 Kết quả: → User thấy danh sách site được dẫn tour trong profile → Nút 🎙 hiện trong detail.html khi vào site đó → Có thể mở tour-host.html và tạo phiên ngay

12. Đa ngôn ngữ

12.1 Hai lớp ngôn ngữ

LớpPhạm viLưu ở đâuCơ chế
UI LanguageNút, label, placeholder, thông báolang.js (client)data-i18n attribute + HV_applyLang()
Content LanguageTên di sản, mô tả, audio, transcriptDB: content_translations, audio_localizationsAPI ?lang=xx + COALESCE fallback

12.2 Fallback Chain

Request: GET /api/audio-points?lang=ja → Query: COALESCE(ct_title.content, ap.title) → Có bản JA trong content_translations? → trả JA → Không → trả tiếng Việt gốc (ap.title) → Audio: COALESCE(al.audio_url, ap.audio_url) → Có file JA trong audio_localizations? → trả JA → Không → trả file VI gốc + is_localized: false

12.3 Tên file audio

# Format: {lang}_{audio_point_id}_{timestamp}.ext
en_gioi_thieu_chung_1779638946.mp3
zh_pho_co_hoi_an_chua_cau_1779639100.mp3
vi_dinh_vinh_binh_dvb_ap_01_1779349817.mp3

# Upload endpoint
POST /api/upload.php?type=audio&lang=en&site=dinh_hoe_thi&point=gioi_thieu_chung

13. Workflow Thẩm định

audio_points.review_status: [draft] ──────────────────────────────────────────────────────┐ │ │ │ Editor bấm "Gửi thẩm định" │ │ POST /api/review/submit {target_type, target_id, site_id}│ ▼ │ [pending_review] ← nút disabled "⏳ Chờ thẩm định" │ │ │ ├── Reviewer Approve → [active] → published │ │ review_status = active │ │ status = active │ │ reviewer = display_name │ │ reviewed_by = user_id │ │ reviewed_at = NOW() │ │ │ └── Reviewer Reject → [draft] → quay lại biên tập ───┘ review_status = draft Editor sửa nội dung content fields (title/audio_url/transcript/...): → audio_update() tự động reset: review_status = 'draft' reviewed_by = NULL reviewed_at = NULL status = 'draft' (nếu đang active)
⚠️
Logic quan trọng

Khi editor sửa bất kỳ content field nào (title, audio_url, transcript, latitude, longitude...), hệ thống tự động reset review_status về draft và xóa thông tin reviewer. Editor phải gửi thẩm định lại từ đầu.

14. AI Assistant

14.1 Kiến trúc RAG

User gõ câu hỏi trong detail.html: → Build ragContext từ: - site.name, site.description_long - audio_points: title, short_description của tất cả điểm nghe - Lịch sử chat (3 lượt gần nhất) → POST https://hv-ai-proxy.hiepntt-moet.workers.dev/ { model: "claude-sonnet-4-20250514", system: "Trợ lý AI Heritage Voice. Context: {ragContext}", messages: [...history, {role: "user", content: userInput}] } → Stream response → render từng chunk vào chat bubble

14.2 Proxy (Cloudflare Workers)

API key Claude được lưu trong Cloudflare Workers secret — không expose ra client. Worker forward request đến api.anthropic.com và trả về response.

15. Key User Flows

15.1 Du khách khám phá di sản

1. Mở app → Chọn ngôn ngữ (lần đầu) 2. Home → Thấy di sản gần nhất (GPS) hoặc Nổi bật 3. Tap vào di sản → detail.html 4. Tap "Explore Map" → map.html 5. Bước vào geofence → Alert "Bạn đang ở [điểm nghe]" 6. Tap "Nghe ngay" → player.html 7. Nghe audio + đọc transcript 8. Hỏi AI về chi tiết di sản 9. Rating + yêu thích

15.2 Hướng dẫn viên dẫn tour đoàn

1. Guide đăng nhập (có can_guide tại site) 2. Vào detail.html của di sản → Tap 🎙 3. tour-host.html: Chọn di sản (nếu chưa truyền) → "Bắt đầu phiên tour" 4. Nhận PIN 6 số → Chia sẻ cho khách 5. Khách mở tour-guest.html → Nhập PIN → Chọn ngôn ngữ → "Tham gia tour" 6. Guide chọn audio point → Bấm Play 7. Khách tự động nghe bản dịch theo ngôn ngữ đã chọn 8. Guide pause/next/seek → khách sync theo 9. Guide bấm "Kết thúc" → session ended

15.3 Editor tạo nội dung

1. Login (editor role) 2. Admin CMS → Tab Điểm nghe → "+ Thêm điểm nghe" 3. Điền thông tin: site, tour, tiêu đề, tọa độ, ảnh 4. Tab ngôn ngữ VI: Upload audio, điền transcript 5. Tab EN: Upload audio EN, điền transcript EN 6. "Lưu nháp" → review_status = draft 7. Kiểm tra lại → "Gửi thẩm định" 8. Reviewer nhận notification email → vào tab Thẩm định 9. Approve → review_status = active, status = active 10. Di sản xuất hiện trên app cho user

16. Permission Matrix

Actionguestmemberreviewereditoradminsuperadmin
Xem di sản/audio
Lưu yêu thích
Đăng ký HDV
Dẫn tour (can_guide)✅*✅*✅*
Tạo/sửa audio points✅*
Upload audio/ảnh
Gửi thẩm định
Phê duyệt nội dung✅*
Quản lý user
Duyệt đơn HDV
Export data

* Cần có user_site_permissions tương ứng tại site đó

17. Cấu trúc file server

/home/tranhiep/disannoi.vn/
├── index.html          # Redirect về home.html
├── home.html           # Trang chủ FE
├── detail.html         # Chi tiết di sản
├── map.html            # Bản đồ
├── profile.html        # Hồ sơ người dùng
├── player.html         # Trình phát audio
├── tour-host.html      # Guide interface
├── tour-guest.html     # Guest interface
├── qr.html             # QR scanner/redirect
├── login.html          # Đăng nhập
├── sw.js               # Service Worker (CACHE_NAME: hv-v6)
├── manifest.json       # PWA manifest
├── mailer.php          # PHPMailer wrapper (root level)
│
├── admin/
│   └── index.html      # Admin CMS (single page app)
│
├── api/
│   ├── index.php       # Router chính
│   ├── config.php      # DB constants, TOKEN_SECRET
│   ├── db.php          # PDO singleton db()
│   ├── helpers.php     # json_ok/error, cors, auth helpers, geo, pagination
│   ├── auth.php        # Token create/verify, login/register/me/refresh
│   ├── users.php       # CRUD users + site permissions
│   ├── sites.php       # heritage_sites + nearby search
│   ├── audio.php       # audio_points CRUD + i18n support
│   ├── tours.php       # voice_tours CRUD
│   ├── tour.php        # Tour Guide Mode (tg_* functions)
│   ├── guide.php       # Guide registration + analytics
│   ├── languages.php   # audio_localizations + content_translations API
│   ├── review.php      # Submit/approve/reject + email notify
│   ├── qr.php          # QR code management
│   ├── analytics.php   # Listen events + summary
│   ├── favorites.php   # User favorites
│   ├── ratings.php     # Site ratings
│   ├── upload.php      # File upload (audio/image/avatar)
│   ├── ai.php          # AI proxy endpoint
│   ├── offline.php     # Offline packs
│   └── mailer.php      # Email sending
│
├── assets/
│   ├── style.css       # Design system (brand: #8B3E18, #C0622A)
│   ├── lang.js         # i18n UI strings (6 ngôn ngữ)
│   ├── pwa.js          # SW register, TTS, install prompt, token refresh
│   ├── logo-white.svg  # Logo trắng (dùng trên nền tối)
│   └── favicon.svg
│
├── audio/            # File audio upload ({lang}_{point_id}_{ts}.mp3)
├── images/           # File ảnh di sản
│   └── avatars/      # Avatar người dùng
└── PHPMailer/        # PHPMailer library

18. Backlog & Roadmap

18.1 Đã hoàn thành

ModuleStatusGhi chú
PWA core (home, detail, map, profile, player)DoneService Worker, offline mode
Auth systemDoneCustom token, auto refresh
GPS GeofencingDoneHaversine, auto-trigger audio
QR Code systemDoneCreate/scan/resolve
Admin CMSDone12 tabs, single page app
Review workflowDonedraft→pending→active→rejected + email
Tour Guide ModeDonePIN, sync audio, đa ngôn ngữ
Guide RegistrationDoneĐăng ký, duyệt, analytics
Đa ngôn ngữ 6 tiếngDoneUI + Content + Audio localizations
AI Assistant RAGDoneClaude API, Cloudflare Workers proxy
Rating & FavoritesDoneStars, yêu thích, lịch sử

18.2 Backlog (Planned)

FeaturePriorityEffortMô tả
chat.html — Trang AI riêngHighSTrang chat AI đầy đủ, lịch sử chat
TTS đa ngôn ngữ tự độngHighMGoogle/Azure TTS → tạo audio từ transcript
Offline audio downloadMediumMCache audio cho toàn bộ di sản để dùng offline
Rating & Review FEMediumSUser viết review sau khi nghe
Push notification địa lýMediumMNotify khi gần di sản yêu thích
Route planningMediumLGợi ý lộ trình tham quan tối ưu
AR Mode (WebXR)LowXLCamera nhận diện di tích, overlay AR
Native App (Capacitor)LowLiOS + Android native packaging
API mở cho đối tácLowLOAuth2, rate limiting, docs
Analytics nâng caoLowMHeatmap, export Excel/PDF, so sánh kỳ

18.3 Known Issues / Tech Debt

IssueẢnh hưởngGiải pháp đề xuất
Token format không phải JWT chuẩnLowMigrate sang JWT library khi scale
PWA flash khi chọn ngôn ngữ lần đầuLow UXPrerender overlay trước khi load content
Tour Guide polling 2s gây load DBMedium khi scaleChuyển sang SSE hoặc WebSocket
Audio file không CDNMedium performanceTích hợp Cloudflare R2 hoặc S3
content_translations chưa có index full-textLowThêm FULLTEXT index khi data lớn
Kiến trúc sẵn sàng mở rộng

Toàn bộ hệ thống được thiết kế stateless, file-based routing dễ deploy trên bất kỳ shared hosting nào. Database schema chuẩn hóa, API RESTful sẵn sàng tích hợp mobile app native hoặc đối tác bên thứ ba.