# EsimLao Authentication API Documentation ## Overview API xác thực người dùng qua email với OTP (One-Time Password). URL_UAT : http://149.28.132.56:8360/ --- ## 1. Request OTP Gửi mã OTP đến email người dùng để xác thực đăng nhập. ### Endpoint ``` POST /apis/auth/request-otp ``` ### Request Headers | Header | Value | Required | |--------|-------|----------| | Content-Type | application/json | Yes | ### Request Body ```json { "email": "user@example.com", "lang": "lo" // Default: "lo" / en } ``` ### Parameters | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | email | string | Yes | - | Email address của người dùng | | lang | string | No | "vi" | Ngôn ngữ email: `vi` (Tiếng Việt), `en` (English), `lo` (ລາວ) | ### Response Success (200) ```json { "errorCode": "0", "message": "", "data": { "email": "user@example.com", "expireInSeconds": 300 } } ``` ### Response Error (200) ```json // Email không được cung cấp { "errorCode": "-801", "message": "", "data": {} } // Lỗi hệ thống { "errorCode": "-6", "message": "", "data": {} } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | errorCode| string | "0" = Success, khác "0" = Error (xem Error Codes) | | message | string | Thông báo từ CONFIG table (theo ngôn ngữ) | | data.email | string | Email đã gửi OTP | | data.expireInSeconds | int | Thời gian OTP hết hạn (giây) | ### Notes - OTP gồm 6 chữ số - OTP có hiệu lực trong 5 phút - Mỗi lần request mới sẽ hủy các OTP cũ chưa sử dụng - Nếu email chưa tồn tại, hệ thống tự động tạo tài khoản mới --- ## 1.1 Resend OTP Gửi lại mã OTP mới đến email (hủy OTP cũ). ### Endpoint ``` POST /apis/auth/resend-otp ``` ### Request Headers | Header | Value | Required | |--------|-------|----------| | Content-Type | application/json | Yes | ### Request Body ```json { "email": "user@example.com", "lang": "lo" // Default: "lo" / en / vi } ``` ### Response Tương tự **Request OTP**. --- ## 2. Verify OTP Xác thực mã OTP và hoàn tất đăng nhập. ### Endpoint ``` POST /apis/auth/verify-otp ``` ### Request Headers | Header | Value | Required | |--------|-------|----------| | Content-Type | application/json | Yes | ### Request Body ```json { "email": "user@example.com", "otpCode": "123456", "lang": "lo" // Default: "lo" / en } ``` ### Parameters | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | email | string | Yes | - | Email đã nhận OTP | | otpCode | string | Yes | - | Mã OTP 6 số | | lang | string | No | "lo" | Ngôn ngữ thông báo: `lo` (ລາວ), `en` (English) | ### Response Success (200) ```json { "errorCode": "0", "message": "", "data": { "userId": 12345, "email": "user@example.com", "fullName": "Nguyen Van A", "avatarUrl": "https://example.com/avatar.jpg", "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...", "expiresAt": "2024-12-30T10:00:00Z" } } ``` ### Response Error Cases ```json // Thiếu email hoặc OTP { "errorCode": "-801", "message": "", "data": {} } // OTP không hợp lệ { "errorCode": "-201", "message": "", "data": {} } // OTP đã được sử dụng { "errorCode": "-203", "message": "", "data": {} } // OTP đã hết hạn { "errorCode": "-202", "message": "", "data": {} } // Không tìm thấy người dùng { "errorCode": "-300", "message": "", "data": {} } // Lỗi hệ thống { "errorCode": "-6", "message": "", "data": {} } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | errorCode| string | "0" = Success, khác "0" = Error (xem Error Codes) | | message | string | Thông báo từ CONFIG table (theo ngôn ngữ) | | data.userId | int | ID người dùng | | data.email | string | Email người dùng | | data.fullName | string | Họ tên đầy đủ | | data.avatarUrl | string | URL ảnh đại diện (nullable) | | data.accessToken | string | JWT access token | | data.refreshToken | string | Refresh token để làm mới access token | | data.expiresAt | datetime | Thời điểm access token hết hạn | ### Notes - Access token có hiệu lực 24 giờ - Refresh token có hiệu lực 30 ngày - Mỗi lần đăng nhập thành công, các token cũ sẽ bị thu hồi --- ## 2.1 Google Login - Get Authorization URL Lấy URL để redirect người dùng đến Google OAuth consent screen. ### Endpoint ``` POST /apis/auth/google-login ``` ### Request Headers | Header | Value | Required | |--------|-------|----------| | Content-Type | application/json | Yes | ### Request Body ```json { "lang": "lo" // Optional: "lo" (default), "en" } ``` ### Parameters | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | lang | string | No | "lo" | Ngôn ngữ thông báo: `lo` (ລາວ), `en` (English) | ### Response Success (200) ```json { "errorCode": "0", "message": "", "data": { "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=xxx&redirect_uri=xxx&response_type=code&scope=email%20profile" } } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | errorCode | string | "0" = Success | | message | string | Thông báo từ CONFIG table (theo ngôn ngữ) | | data.url | string | URL để redirect user đến Google OAuth | ### Flow 1. Frontend gọi API này để lấy Google OAuth URL 2. Frontend redirect user đến URL nhận được 3. User đăng nhập Google và cho phép quyền 4. Google redirect về `redirect_uri` với `code` parameter 5. Frontend gọi API `/apis/auth/google-callback` với `code` này ### Response Error ```json { "errorCode": "-6", "message": "", "data": {} } ``` --- ## 2.2 Google Callback - Complete Login Xác thực authorization code từ Google và hoàn tất đăng nhập. - Nếu email chưa tồn tại: Tự động tạo tài khoản mới trong `CUSTOMER_INFO` - Nếu email đã tồn tại: Cập nhật thông tin và đăng nhập ### Endpoint ``` POST /apis/auth/google-callback ``` ### Request Headers | Header | Value | Required | |--------|-------|----------| | Content-Type | application/json | Yes | ### Request Body ```json { "code": "4/0AXEWy...", "redirectUri": "https://your-app.com/callback", "lang": "lo" // Optional: "lo" (default), "en" } ``` ### Parameters | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | code | string | Yes | - | Authorization code từ Google (nhận qua URL callback) | | redirectUri | string | No | - | Redirect URI đã đăng ký với Google (nếu khác default) | | lang | string | No | "lo" | Ngôn ngữ thông báo: `lo` (ລາວ), `en` (English) | ### Response Success (200) - GIỐNG API verify-otp ```json { "errorCode": "0", "message": "", "data": { "userId": 12345, "email": "user@gmail.com", "fullName": "Nguyen Van A", "avatarUrl": "https://lh3.googleusercontent.com/...", "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...", "expiresAt": "2024-12-30T10:00:00Z" } } ``` ### Response Fields (giống verify-otp) | Field | Type | Description | |-------|------|-------------| | errorCode | string | "0" = Success | | message | string | Thông báo từ CONFIG table (theo ngôn ngữ) | | data.userId | int | ID người dùng (từ CUSTOMER_INFO.ID) | | data.email | string | Email người dùng | | data.fullName | string | Họ tên đầy đủ (từ Google profile) | | data.avatarUrl | string | URL ảnh đại diện từ Google | | data.accessToken | string | JWT access token (24 giờ) | | data.refreshToken | string | Refresh token (30 ngày) | | data.expiresAt | datetime | Thời điểm access token hết hạn | ### Database Operations Khi login thành công: 1. **CUSTOMER_INFO**: - User mới: INSERT với `SUR_NAME`, `LAST_NAME`, `EMAIL`, `AVATAR_URL`, `IS_VERIFIED=1` - User có sẵn: UPDATE `AVATAR_URL` (nếu trống), `LAST_LOGIN_DATE`, `IS_VERIFIED=1` 2. **USER_TOKEN**: Revoke tokens cũ và tạo token mới ### Response Error Cases ```json // Code không được cung cấp { "errorCode": "-801", "message": "", "data": {} } // Lỗi trao đổi token với Google { "errorCode": "-700", "message": "", "data": { "error": "..." } } // Không nhận được email từ Google { "errorCode": "-700", "message": "", "data": {} } // Lỗi hệ thống { "errorCode": "-6", "message": "", "data": {} } ``` ### Notes - Access token có hiệu lực 24 giờ - Refresh token có hiệu lực 30 ngày - Mỗi lần đăng nhập thành công, các token cũ sẽ bị thu hồi - User đăng nhập qua Google tự động được đánh dấu `IS_VERIFIED = 1` - Nếu user chưa có avatar và Google cung cấp, sẽ tự động lưu --- ## Google OAuth Flow Diagram ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Client │ │ API │ │ Google │ │ Database │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ POST /google-login│ │ │ │──────────────────>│ │ │ │ │ │ │ │ { url: "..." } │ │ │ │<──────────────────│ │ │ │ │ │ │ │ Redirect to URL │ │ │ │──────────────────────────────────────>│ │ │ │ │ │ │ │ │ User Login │ │ │ │ Grant Permission │ │ │ │ │ │ Callback with code│ │ │ │<──────────────────────────────────────│ │ │ │ │ │ │ POST /google-callback │ │ │ { code: "..." }│ │ │ │──────────────────>│ │ │ │ │ │ │ │ │ Exchange code │ │ │ │──────────────────>│ │ │ │ │ │ │ │ access_token │ │ │ │<──────────────────│ │ │ │ │ │ │ │ Get user info │ │ │ │──────────────────>│ │ │ │ │ │ │ │ email, name, pic │ │ │ │<──────────────────│ │ │ │ │ │ │ │ Insert/Update CUSTOMER_INFO │ │ │ Create USER_TOKEN │ │ │──────────────────────────────────────>│ │ │ │ │ │ JWT Token Response │ │ │<──────────────────│ │ │ │ │ │ │ ``` --- ## Example Usage (cURL) - Google Login ### Step 1: Get Google OAuth URL ```bash curl -X POST http://149.28.132.56:8360/apis/auth/google-login \ -H "Content-Type: application/json" ``` ### Step 2: Complete Login with Code ```bash curl -X POST http://149.28.132.56:8360/apis/auth/google-callback \ -H "Content-Type: application/json" \ -d '{ "code": "4/0AXEWy..." }' ``` --- ## Error Codes ### Success | errorCode| Constant | Description | |------|----------|-------------| | "0" | Success | Thành công (mọi request thành công đều trả về "0") | ### General Errors (-1 to -99) | errorCode| Constant | Description | |------|----------|-------------| | "-1" | Error | Lỗi chung | | "-6" | SystemError | Lỗi hệ thống | ### OTP Errors (-200 to -299) | errorCode| Constant | Description | |------|----------|-------------| | "-200" | OtpRequired | Yêu cầu OTP | | "-201" | OtpInvalid | OTP không hợp lệ | | "-202" | OtpExpired | OTP đã hết hạn | | "-203" | OtpAlreadyUsed | OTP đã được sử dụng | | "-204" | OtpMaxAttemptsExceeded | Vượt quá số lần thử | | "-205" | OtpSendFailed | Gửi OTP thất bại | | "-206" | OtpTooManyRequests | Request quá nhiều | ### User Errors (-300 to -399) | errorCode| Constant | Description | |------|----------|-------------| | "-300" | UserNotFound | Không tìm thấy người dùng | | "-304" | InvalidEmail | Email không hợp lệ | ### External Service Errors (-700 to -799) | errorCode| Constant | Description | |------|----------|-------------| | "-700" | ExternalServiceError | Lỗi từ dịch vụ bên ngoài (Google OAuth, etc.) | ### Validation Errors (-800 to -899) | errorCode| Constant | Description | |------|----------|-------------| | "-801" | RequiredFieldMissing | Thiếu trường bắt buộc | --- ## Authentication Sau khi đăng nhập thành công, sử dụng `accessToken` trong header cho các API yêu cầu xác thực: ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` --- ## Flow Diagram ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Client │ │ API │ │ Email │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ POST /request-otp │ │ │──────────────────>│ │ │ │ │ │ │ Generate OTP │ │ │ Save to DB │ │ │ Queue Email │ │ │ │ │ Response │ │ │<──────────────────│ │ │ │ │ │ │ Send OTP Email │ │ │──────────────────>│ │ │ │ │ │ │ OTP Email │<──────────────────────────────────────│ │ │ │ │ POST /verify-otp │ │ │──────────────────>│ │ │ │ │ │ │ Verify OTP │ │ │ Generate JWT │ │ │ │ │ Token Response │ │ │<──────────────────│ │ │ │ │ ``` --- ## Example Usage (cURL) ### Request OTP ```bash curl -X POST https://api.esimlao.com/apis/auth/request-otp \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "lang": "vi" }' ``` ### Verify OTP ```bash curl -X POST https://api.esimlao.com/apis/auth/verify-otp \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "otpCode": "123456" }' ``` ### Use Token ```bash curl -X GET https://api.esimlao.com/apis/user/profile \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` --- ## 3. Article Category Lấy danh sách danh mục bài viết. ### Endpoint ``` POST /apis/article/category ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "parentId": null } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ: "lo", "en" (có thể truyền qua header Accept-Language) | | pageNumber | int | 0 | Trang hiện tại | | pageSize | int | 10 | Số item mỗi trang | | parentId | int? | null | ID danh mục cha (null = root) | ### Response Success ```json { "errorCode": "0", "message": "Success", "data": { "categories": [ { "id": 1, "categoryName": "Cẩm nang du lịch", "categorySlug": "cam-nang-du-lich", "description": "Mô tả...", "iconUrl": "/icons/travel.png", "parentId": null, "displayOrder": 1 } ], "pagination": { "pageNumber": 0, "pageSize": 10, "totalCount": 5, "totalPages": 1 } } } ``` ### Response Fields - categories[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | ARTICLE_CATEGORY.ID | ID danh mục bài viết (Primary Key) | | categoryName | string | ARTICLE_CATEGORY.CATEGORY_NAME
CATEGORY_NAME_EN
CATEGORY_NAME_LO | Tên danh mục (theo ngôn ngữ được chọn) | | categorySlug | string | ARTICLE_CATEGORY.CATEGORY_SLUG | URL-friendly slug cho danh mục | | description | string | ARTICLE_CATEGORY.DESCRIPTION
DESCRIPTION_EN
DESCRIPTION_LO | Mô tả chi tiết danh mục | | iconUrl | string | ARTICLE_CATEGORY.ICON_URL | Đường dẫn icon của danh mục | | parentId | int? | ARTICLE_CATEGORY.PARENT_ID | ID danh mục cha (null = danh mục gốc) | | displayOrder | int | ARTICLE_CATEGORY.DISPLAY_ORDER | Thứ tự hiển thị (số nhỏ hơn hiển thị trước) | ### Response Fields - pagination | Field | Type | Description | |-------|------|-------------| | pageNumber | int | Trang hiện tại (bắt đầu từ 0) | | pageSize | int | Số items trên mỗi trang | | totalCount | int | Tổng số items trong database | | totalPages | int | Tổng số trang (= ceiling(totalCount / pageSize)) | --- ## 4. Article Load (List) Lấy danh sách bài viết với pagination và filters. ### Endpoint ``` POST /apis/article/load ``` ### Request (tất cả bài viết) ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10 } ``` ### Request (lọc theo category) ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "categoryId": 1, "isFeatured": false } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ (hoặc header Accept-Language) | | pageNumber | int | 0 | Trang hiện tại (0-indexed) | | pageSize | int | 10 | Số item mỗi trang | | categoryId | int? | **null** | **null = lấy tất cả**, có giá trị = lọc theo danh mục | | isFeatured | bool? | null | true = chỉ bài nổi bật, null = tất cả | ### Response Success ```json { "errorCode": "0", "message": "Success", "data": { "articles": [ { "id": 1, "title": "Hướng dẫn cài đặt eSIM", "slug": "huong-dan-cai-dat-esim", "summary": "Tóm tắt...", "thumbnailUrl": "/images/article1.jpg", "categoryId": 1, "viewCount": 150, "isFeatured": true, "isPinned": false, "publishedDate": "2024-12-25" } ], "pagination": { "pageNumber": 0, "pageSize": 10, "totalCount": 25, "totalPages": 3 } } } ``` ### Response Fields - articles[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | ARTICLE.ID | **ID bài viết** - dùng để lấy chi tiết qua `/apis/article/detail` | | title | string | ARTICLE.TITLE
TITLE_EN
TITLE_LO | Tiêu đề bài viết (theo ngôn ngữ) | | slug | string | ARTICLE.SLUG | URL-friendly slug (unique) | | summary | string | ARTICLE.SUMMARY
SUMMARY_EN
SUMMARY_LO | Tóm tắt ngắn gọn | | thumbnailUrl | string | ARTICLE.THUMBNAIL_URL | Ảnh thumbnail (dùng cho list) | | categoryId | int | ARTICLE.CATEGORY_ID | ID danh mục (FK → ARTICLE_CATEGORY) | | viewCount | int | ARTICLE.VIEW_COUNT | Số lượt xem | | isFeatured | bool | ARTICLE.IS_FEATURED | Bài viết nổi bật (true/false) | | isPinned | bool | ARTICLE.IS_PINNED | Bài viết được ghim (true/false) | | publishedDate | datetime | ARTICLE.PUBLISHED_DATE | Ngày xuất bản | ### Important Notes - **Ordering**: Danh sách sắp xếp theo: 1. `IS_PINNED` DESC (bài ghim lên đầu) 2. `PUBLISHED_DATE` DESC (mới nhất trước) 3. `CREATED_DATE` DESC - **Filter logic**: - `categoryId = null` → Lấy **tất cả** bài viết - `categoryId = 1` → Chỉ lấy bài viết thuộc category 1 ### cURL Examples #### Lấy tất cả bài viết ```bash curl -X POST http://localhost:8360/apis/article/load \ -H "Content-Type: application/json" \ -d '{ "lang": "lo", "pageNumber": 0, "pageSize": 10 }' ``` #### Lấy bài viết theo category ```bash curl -X POST http://localhost:8360/apis/article/load \ -H "Content-Type: application/json" \ -d '{ "categoryId": 1, "lang": "en", "pageSize": 5 }' ``` #### Lấy bài viết nổi bật ```bash curl -X POST http://localhost:8360/apis/article/load \ -H "Content-Type: application/json" \ -d '{ "isFeatured": true, "lang": "lo", "pageSize": 6 }' ``` --- ## 4.1 Article Detail Lấy chi tiết 1 bài viết theo ID hoặc slug. ### Endpoint ``` POST /apis/article/detail ``` ### Request (theo ID - khuyến nghị) ```json { "id": 1, "lang": "lo" } ``` ### Request (theo slug - SEO friendly) ```json { "slug": "huong-dan-cai-dat-esim", "lang": "lo" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | id | int? | Conditional | ID bài viết (lấy từ list API) | | slug | string? | Conditional | Slug của bài viết | | lang | string | No | Ngôn ngữ (default: "lo") | **Note**: Phải có **ít nhất một** trong `id` hoặc `slug` ### Response Success ```json { "errorCode": "0", "message": "Success", "data": { "article": { "id": 1, "title": "Hướng dẫn cài đặt eSIM", "slug": "huong-dan-cai-dat-esim", "summary": "Tóm tắt...", "content": "

Nội dung HTML đầy đủ...

", "thumbnailUrl": "/images/article1.jpg", "coverImageUrl": "/images/cover1.jpg", "metaDescription": "SEO description", "metaKeywords": "esim, laos", "categoryId": 1, "viewCount": 151, "isFeatured": true, "publishedDate": "2024-12-25", "createdDate": "2024-12-20" } } } ``` ### Response Fields - article | Field | Type | DB Column | Description | |-------|------|----------|-------------| | id | int | ARTICLE.ID | ID bài viết (Primary Key) | | title | string | ARTICLE.TITLE
TITLE_EN
TITLE_LO | Tiêu đề bài viết (theo ngôn ngữ) | | slug | string | ARTICLE.SLUG | URL-friendly slug (unique) | | summary | string | ARTICLE.SUMMARY
SUMMARY_EN
SUMMARY_LO | Tóm tắt ngắn gọn | | content | string | ARTICLE.CONTENT
CONTENT_EN
CONTENT_LO | **Nội dung HTML đầy đủ** | | thumbnailUrl | string | ARTICLE.THUMBNAIL_URL | Ảnh thumbnail | | coverImageUrl | string | ARTICLE.COVER_IMAGE_URL | Ảnh bìa (dùng cho detail page) | | metaDescription | string | ARTICLE.META_DESCRIPTION
META_DESCRIPTION_EN
META_DESCRIPTION_LO | SEO meta description | | metaKeywords | string | ARTICLE.META_KEYWORDS | SEO keywords (comma-separated) | | categoryId | int | ARTICLE.CATEGORY_ID | ID danh mục | | viewCount | int | ARTICLE.VIEW_COUNT | Số lượt xem (**tự động +1 khi gọi API này**) | | isFeatured | bool | ARTICLE.IS_FEATURED | Bài viết nổi bật | | publishedDate | datetime | ARTICLE.PUBLISHED_DATE | Ngày xuất bản | | createdDate | datetime | ARTICLE.CREATED_DATE | Ngày tạo bài viết | ### Important Notes - **Auto view count**: Mỗi lần gọi API này, `viewCount` tự động +1 - **Recommended flow**: 1. Gọi `/apis/article/load` → Lấy `id` từ danh sách 2. Gọi `/apis/article/detail` với `id` đó ### cURL Examples #### Lấy chi tiết theo ID ```bash curl -X POST http://localhost:8360/apis/article/detail \ -H "Content-Type: application/json" \ -d '{ "id": 1, "lang": "lo" }' ``` #### Lấy chi tiết theo slug ```bash curl -X POST http://localhost:8360/apis/article/detail \ -H "Content-Type: application/json" \ -d '{ "slug": "huong-dan-cai-dat-esim", "lang": "en" }' ``` --- ## 5. Banner Load Lấy danh sách banner. ### Endpoint ``` POST /apis/content/banner ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "position": "home" } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ (hoặc header Accept-Language) | | pageNumber | int | 0 | Trang hiện tại | | pageSize | int | 10 | Số item mỗi trang | | position | string? | null | Vị trí: "home", "sidebar"... | ### Response ```json { "errorCode": "0", "data": { "banners": [ { "id": 1, "title": "Banner Title", "subtitle": "Subtitle", "imageUrl": "/images/banner1.jpg", "imageMobileUrl": "/images/banner1_m.jpg", "linkUrl": "/promo", "linkTarget": "_blank", "position": "home", "displayOrder": 1 } ], "pagination": {...} } } ``` ### Response Fields - banners[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | BANNER.ID | ID banner (Primary Key) | | title | string | BANNER.TITLE
TITLE_EN
TITLE_LO | Tiêu đề banner (theo ngôn ngữ) | | subtitle | string | BANNER.SUBTITLE
SUBTITLE_EN
SUBTITLE_LO | Phụ đề banner | | imageUrl | string | BANNER.IMAGE_URL | Ảnh banner (desktop) | | imageMobileUrl | string | BANNER.IMAGE_MOBILE_URL | Ảnh banner (mobile) | | linkUrl | string | BANNER.LINK_URL | URL đích khi click banner | | linkTarget | string | BANNER.LINK_TARGET | Target (_self, _blank,...) | | position | string | BANNER.POSITION | Vị trí hiển thị (home, category,...) | | displayOrder | int | BANNER.DISPLAY_ORDER | Thứ tự hiển thị | **Lọc tự động**: Chỉ trả về banners với `STATUS = 1` và trong khoảng `START_DATE ≤ now ≤ END_DATE` --- ## 6. Customer Review Load Lấy đánh giá của khách hàng. ### Endpoint ``` POST /apis/content/review ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "isFeatured": true } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ | | pageNumber | int | 0 | Trang | | pageSize | int | 10 | Số item | | isFeatured | bool? | null | Lọc review nổi bật | ### Response ```json { "errorCode": "0", "data": { "reviews": [ { "id": 1, "customerName": "Nguyen Van A", "avatarUrl": "/avatars/user1.jpg", "rating": 1, "reviewContent": "Dich vu rat tot...", "destination": "Vientiane, Laos", "isFeatured": true, "createdDate": "2024-12-25" } ], "pagination": {...} } } ``` ### Response Fields - reviews[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | CUSTOMER_REVIEW.ID | ID đánh giá (Primary Key) | | customerName | string | CUSTOMER_REVIEW.CUSTOMER_NAME | Tên khách hàng | | avatarUrl | string | CUSTOMER_REVIEW.AVATAR_URL | Ảnh đại diện (nullable) | | rating | int | CUSTOMER_REVIEW.RATING | Số sao (1-5) | | reviewContent | string | CUSTOMER_REVIEW.REVIEW_CONTENT
REVIEW_CONTENT_EN
REVIEW_CONTENT_LO | Nội dung đánh giá (theo ngôn ngữ) | | destination | string | CUSTOMER_REVIEW.DESTINATION
DESTINATION_EN
DESTINATION_LO | Địa điểm du lịch | | isFeatured | bool | CUSTOMER_REVIEW.IS_FEATURED | Review nổi bật | | createdDate | datetime | CUSTOMER_REVIEW.CREATED_DATE | Ngày tạo review | **Lọc tự động**: Chỉ trả về reviews với `STATUS = 1` (đã duyệt) --- ## 6.1 Customer Review Create Khách hàng gửi đánh giá (chờ duyệt). ### Endpoint ``` POST /apis/content/review/create ``` ### Request ```json { "lang": "lo", "customerName": "Nguyen Van A", "reviewContent": "Dich vu rat tot, toi rat hai long!", "destination": "Vientiane, Laos", "rating": 5 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | lang | string | No | Ngôn ngữ | | customerName | string | Yes | Tên khách hàng | | reviewContent | string | Yes | Nội dung đánh giá | | destination | string | No | Địa điểm | | rating | int | No | Đánh giá (1-5) | ### Response Success ```json { "errorCode": "0", "message": "Review submitted successfully", "data": { "reviewId": 123 } } ``` ### Note - Review mới sẽ có `Status = false` (chờ admin duyệt) --- ## 7. FAQ Category Load Lấy danh mục FAQ (hỗ trợ cấu trúc phân cấp). ### Endpoint ``` POST /apis/content/faq-category ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "parentId": null } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ (hoặc header Accept-Language) | | pageNumber | int | 0 | Trang hiện tại | | pageSize | int | 10 | Số item mỗi trang | | parentId | int? | null | ID danh mục cha (null = lấy root categories) | ### Response ```json { "errorCode": "0", "data": { "categories": [ { "id": 1, "categoryName": "Cài đặt eSIM", "categorySlug": "cai-dat-esim", "description": "Hướng dẫn cài đặt", "iconUrl": "/icons/setup.png", "parentId": null, "displayOrder": 1 } ], "pagination": {...} } } ``` ### Response Fields - categories[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | FAQ_CATEGORY.ID | ID danh mục FAQ (Primary Key) | | categoryName | string | FAQ_CATEGORY.CATEGORY_NAME
CATEGORY_NAME_EN
CATEGORY_NAME_LO | Tên danh mục FAQ | | categorySlug | string | FAQ_CATEGORY.CATEGORY_SLUG | URL-friendly slug | | description | string | FAQ_CATEGORY.DESCRIPTION
DESCRIPTION_EN
DESCRIPTION_LO | Mô tả danh mục | | iconUrl | string | FAQ_CATEGORY.ICON_URL | Icon của danh mục | | parentId | int? | FAQ_CATEGORY.PARENT_ID | **ID danh mục cha (null = danh mục gốc)** | | displayOrder | int | FAQ_CATEGORY.DISPLAY_ORDER | Thứ tự hiển thị | ### Use Cases - **Homepage FAQ**: `parentId = null` → Lấy danh mục gốc - **Support Center**: `parentId = ` → Lấy danh mục con - Hỗ trợ cấu trúc phân cấp không giới hạn cấp độ --- ## 8. FAQ Load Lấy danh sách câu hỏi thường gặp. ### Endpoint ``` POST /apis/content/faq ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "categoryId": 1, "isFeatured": false } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ | | pageNumber | int | 0 | Trang | | pageSize | int | 10 | Số item | | categoryId | int? | null | Lọc theo danh mục | | isFeatured | bool? | null | Lọc FAQ nổi bật | ### Response ```json { "errorCode": "0", "data": { "faqs": [ { "id": 1, "question": "Làm sao để cài đặt eSIM?", "answer": "

Hướng dẫn chi tiết...

", "categoryId": 1, "viewCount": 100, "isFeatured": true } ], "pagination": {...} } } ``` ### Response Fields - faqs[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | FAQ.ID | ID câu hỏi (Primary Key) | | question | string | FAQ.QUESTION
QUESTION_EN
QUESTION_LO | Câu hỏi (theo ngôn ngữ) | | answer | string | FAQ.ANSWER
ANSWER_EN
ANSWER_LO | Câu trả lời (HTML format) | | categoryId | int | FAQ.CATEGORY_ID | ID danh mục (FK → FAQ_CATEGORY) | | viewCount | int | FAQ.VIEW_COUNT | Số lượt xem | | isFeatured | bool | FAQ.IS_FEATURED | FAQ nổi bật | **Lọc tự động**: Chỉ trả về FAQs với `STATUS = 1` --- ## 9. Device eSIM Compatibility - Get Metadata Lấy danh sách brands và categories để hiển thị tabs/filters cho tính năng kiểm tra thiết bị. ### Endpoint ``` GET /apis/content/device-metadata ``` ### Request Không cần body (GET request) ### Response ```json { "errorCode": "0", "message": "Success", "data": { "brands": [ { "brand": "Apple", "deviceCount": 24, "popularCount": 24, "devices": [ { "id": 1, "modelName": "iPhone XS", "category": "Phone", "isPopular": true, "displayOrder": 1, "modelNameEn": "iPhone XS", "modelNameLo": "iPhone XS", "notes": "Tất cả phiên bản trừ China Mainland, Hong Kong, Macao", "notesEn": "All versions except China Mainland, Hong Kong, Macao", "notesLo": "ທຸກເວີຊັນຍົກເວັ້ນ China Mainland, Hong Kong, Macao" } ] }, { "brand": "Samsung", "deviceCount": 19, "popularCount": 19, "devices": [...] } ], "categories": [ { "category": "Phone", "deviceCount": 67 }, { "category": "Tablet", "deviceCount": 4 } ] } } ``` ### Response Fields - brands[] | Field | Type | Description | |-------|------|-------------| | brand | string | Tên hãng (Apple, Samsung, Google,...) | | deviceCount | int | Tổng số thiết bị của hãng này | | popularCount | int | Số thiết bị phổ biến (IS_POPULAR = 1) | | **devices[]** | array | **Danh sách tất cả thiết bị của brand** | ### Response Fields - brands[].devices[] | Field | Type | Description | |-------|------|-------------| | id | int | ID thiết bị | | modelName | string | Tên model (mặc định Vietnamese) | | category | string | Loại thiết bị (Phone, Tablet, Laptop, Watch) | | isPopular | bool | Thiết bị phổ biến | | displayOrder | int | Thứ tự sắp xếp | | modelNameEn | string | Tên model tiếng Anh | | modelNameLo | string | Tên model tiếng Lào | | notes | string | Ghi chú (Vietnamese) | | notesEn | string | Ghi chú (English) | | notesLo | string | Ghi chú (Lao) | ### Response Fields - categories[] | Field | Type | Description | |-------|------|-------------| | category | string | Loại thiết bị (Phone, Tablet, Laptop, Watch) | | deviceCount | int | Tổng số thiết bị thuộc category này | ### Use Case API này trả về **TẤT CẢ** devices trong một lần gọi để: - Frontend có thể filter/search ở client-side - Hiển thị tabs cho từng brand với danh sách devices - Không cần pagination vì dùng cho client-side filtering - Giảm số lượng API calls ### Important Notes - **Trả về toàn bộ devices** trong response (không phân trang) - Frontend tự filter theo brand/category/search - Chỉ devices có `STATUS = 1` và `SUPPORTS_ESIM = 1` - Sắp xếp theo `DISPLAY_ORDER`, `BRAND`, `MODEL_NAME` --- ## 10. Device eSIM Compatibility - Search & Filter Tìm kiếm và lọc danh sách thiết bị hỗ trợ eSIM. ### Endpoint ``` POST /apis/content/device-compatibility ``` ### Request Examples #### Example 1: Lấy tất cả devices phổ biến (Quick Lookup) ```json { "isPopular": true, "lang": "lo", "pageNumber": 0, "pageSize": 50 } ``` #### Example 2: Filter theo brand (Tab Apple) ```json { "brand": "Apple", "isPopular": true, "lang": "en", "pageNumber": 0, "pageSize": 50 } ``` #### Example 3: Search keyword ```json { "searchKeyword": "iPhone 13", "lang": "lo", "pageNumber": 0, "pageSize": 20 } ``` #### Example 4: Filter brand + category ```json { "brand": "Apple", "category": "Tablet", "lang": "en", "pageNumber": 0, "pageSize": 10 } ``` ### Request Parameters | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | brand | string | No | null | Filter theo hãng (Apple, Samsung, Google,...) | | category | string | No | null | Filter theo loại (Phone, Tablet, Laptop, Watch) | | searchKeyword | string | No | null | Tìm kiếm trong tên model (all languages) | | isPopular | bool | No | null | `true` = chỉ lấy thiết bị phổ biến (quick lookup) | | lang | string | No | "lo" | Ngôn ngữ response: "lo", "en" | | pageNumber | int | No | 0 | Trang hiện tại (0-indexed) | | pageSize | int | No | 50 | Số items/page (mặc định 50 cho device list) | ### Response Success ```json { "errorCode": "0", "message": "Success", "data": { "devices": [ { "id": 1, "brand": "Apple", "modelName": "iPhone XS", "category": "Phone", "notes": "ທຸກເວີຊັນຍົກເວັ້ນ iPhone ຈາກ ຈີນແຜ່ນດິນໃຫຍ່, ຮ່ອງກົງ ແລະ ມາເກົາ", "supportsEsim": true, "isPopular": true, "displayOrder": 1 }, { "id": 2, "brand": "Apple", "modelName": "iPhone 13", "category": "Phone", "notes": null, "supportsEsim": true, "isPopular": true, "displayOrder": 12 } ], "pagination": { "pageNumber": 0, "pageSize": 50, "totalCount": 24, "totalPages": 1 } } } ``` ### Response Fields - devices[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | DEVICE_ESIM_COMPATIBILITY.ID | ID thiết bị (Primary Key) | | brand | string | DEVICE_ESIM_COMPATIBILITY.BRAND | Hãng sản xuất (Apple, Samsung,...) | | modelName | string | DEVICE_ESIM_COMPATIBILITY.MODEL_NAME
MODEL_NAME_EN
MODEL_NAME_LO | Tên model (theo ngôn ngữ được chọn) | | category | string | DEVICE_ESIM_COMPATIBILITY.CATEGORY | Loại thiết bị (Phone, Tablet, Laptop, Watch) | | notes | string | DEVICE_ESIM_COMPATIBILITY.NOTES
NOTES_EN
NOTES_LO | Lưu ý đặc biệt (nullable, theo ngôn ngữ) | | supportsEsim | bool | DEVICE_ESIM_COMPATIBILITY.SUPPORTS_ESIM | Hỗ trợ eSIM (luôn = true cho kết quả được trả về) | | isPopular | bool | DEVICE_ESIM_COMPATIBILITY.IS_POPULAR | Thiết bị phổ biến (hiển thị trong quick lookup) | | displayOrder | int | DEVICE_ESIM_COMPATIBILITY.DISPLAY_ORDER | Thứ tự sắp xếp | ### Filter Logic - **Auto-filter**: Chỉ trả về devices với `STATUS = 1` và `SUPPORTS_ESIM = 1` - **Search**: Tìm kiếm keyword trong `MODEL_NAME`, `MODEL_NAME_EN`, `MODEL_NAME_LO` (case-insensitive) - **Ordering**: `DISPLAY_ORDER` ASC → `BRAND` ASC → `MODEL_NAME` ASC ### cURL Examples #### Get metadata (brands & categories) ```bash curl -X GET http://localhost:8360/apis/content/device-metadata \ -H "Content-Type: application/json" ``` #### Search for "iPhone 13" ```bash curl -X POST http://localhost:8360/apis/content/device-compatibility \ -H "Content-Type: application/json" \ -d '{ "searchKeyword": "iPhone 13", "lang": "en", "pageSize": 20 }' ``` #### Get popular Apple devices ```bash curl -X POST http://localhost:8360/apis/content/device-compatibility \ -H "Content-Type: application/json" \ -d '{ "brand": "Apple", "isPopular": true, "lang": "lo", "pageSize": 50 }' ``` ### Implementation Notes - **Sample Data**: 70+ devices đã có sẵn trong `Database/DeviceCompatibility_Schema.sql` - **Quick Lookup**: Set `isPopular=true` để chỉ lấy thiết bị phổ biến (Apple 24 devices, Samsung 19 devices, Google Pixel 10 devices) - **Full Search**: Không set `isPopular` và dùng `searchKeyword` để tìm kiếm toàn bộ database - **Multi-language**: Notes field hỗ trợ 3 ngôn ngữ (vi, en, lo) ---