Thông tin Server
| OS | Ubuntu 24.04 LTS |
| Backend | Go 1.24 + Fiber v2.52 |
| Database | MySQL 8.0 (GORM ORM) |
| Auth | JWT Bearer Token (HS256) |
| Process Manager | PM2 |
| Web Server | Nginx 1.24 + SSL (Let's Encrypt) |
| Node.js | v20.20 (Frontend SSR) |
Subdomains
| Trang chủ | duocnamviet.site → :3010 |
| Admin Panel | admin.duocnamviet.site → :3013 |
| API Docs | api.duocnamviet.site → :8080 |
| Bán lẻ (B2C) | banle.duocnamviet.site → :3011 |
| Bán sỉ (B2B) | bansi.duocnamviet.site → :3012 |
| phpMyAdmin | mysql.duocnamviet.site |
Cấu trúc thư mục
| Backend | /opt/duocnamviet.site/backend/ |
| Frontend | /opt/duocnamviet.site/frontend/ |
| Admin | /opt/duocnamviet.site/admin/ |
| Bán lẻ | /opt/duocnamviet.site/banle/ |
| Bán sỉ | /opt/duocnamviet.site/bansi/ |
| Uploads | /opt/duocnamviet.site/uploads/ |
| API Docs | /opt/duocnamviet.site/api/ |
| Nginx Configs | /etc/nginx/sites-available/ |
| SSL Certs | /etc/letsencrypt/live/duocnamviet.site/ |
| Scripts | /opt/duocnamviet.site/scripts/ |
Ports & Services
| Backend (Go) | :8080 (PM2: backend) |
| Frontend (Next.js) | :3010 (PM2: frontend) |
| Admin (Next.js) | :3013 (PM2: admin) |
| Bán lẻ (Next.js) | :3011 (PM2: banle) |
| Bán sỉ (Next.js) | :3012 (PM2: bansi) |
| MySQL | :3306 |
| Nginx HTTP | :80 (→ 301 HTTPS) |
| Nginx HTTPS | :443 |
| PHP-FPM | unix socket (php8.3-fpm) |
Hướng dẫn kết nối API
1. Base URL
https://api.duocnamviet.site
2. Đăng nhập lấy Token
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "123456"
}
→ Response: { "success": true, "data": { "access_token": "eyJ...", "refresh_token": "eyJ..." } }
3. Gửi request có Auth
GET /api/products Authorization: Bearer eyJ...your_access_token... Content-Type: application/json
4. Frontend connect (Next.js)
// src/services/api.ts const API_URL = 'https://api.duocnamviet.site'; // hoặc trong .env.local: NEXT_PUBLIC_API_URL=https://api.duocnamviet.site
5. Response format chuẩn
// Thành công:
{ "success": true, "data": { ... } }
// Lỗi:
{ "success": false, "error": "Mô tả lỗi" }
// Phân trang:
{ "success": true, "data": { "data": [...], "total": 100, "page": 1, "limit": 20 } }
6. Upload ảnh
- Ảnh sản phẩm lưu tại: /opt/duocnamviet.site/uploads/products/ - URL truy cập: https://duocnamviet.site/uploads/products/ten-anh.jpg - Hỗ trợ: jpg, jpeg, png, webp (max 10MB) - Trong request body gửi URL ảnh vào field "images": ["https://...url1", "https://...url2"]
Hệ thống Roles
| Role Code | Tên | Quyền hạn |
|---|---|---|
| super_admin | Super Admin | Toàn quyền hệ thống |
| admin | Quản trị viên | Quản lý users, sản phẩm, đơn hàng, kho, nhân sự, tài chính, marketing, y tế, bài viết, cài đặt |
| warehouse | Kho hàng | Quản lý sản phẩm, danh mục, kho hàng, phiếu nhập/xuất |
| sales | Bán hàng | Quản lý đơn hàng, khách hàng |
| accountant | Kế toán | Giao dịch thu chi, hóa đơn VAT |
| marketing | Marketing | Chiến dịch, voucher, bài viết |
| doctor | Bác sĩ | Hồ sơ khám bệnh |
Hướng dẫn tích hợp API cho Frontend & App
Tài liệu chi tiết giúp team frontend, mobile app kết nối API backend Dược Nam Việt. Bao gồm authentication, fetch dữ liệu, hiển thị ảnh, xử lý lỗi.
1. Authentication Flow
Bước 1: Đăng nhập lấy Token
// JavaScript / TypeScript
const res = await fetch('https://api.duocnamviet.site/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: '123456' })
});
const data = await res.json();
// data.data.access_token → JWT token (24h)
// data.data.refresh_token → Refresh token (7 ngày)
// data.data.user → Thông tin user
// Lưu token
localStorage.setItem('token', data.data.access_token);
localStorage.setItem('refresh_token', data.data.refresh_token);
Bước 2: Gửi request có Auth
// Mọi API protected đều cần header Authorization
const res = await fetch('https://api.duocnamviet.site/api/products', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
const data = await res.json();
// data.success → true/false
// data.data → kết quả
Bước 3: Xử lý Token hết hạn (401)
// Khi nhận HTTP 401 → token hết hạn → dùng refresh_token để lấy token mới
const res = await fetch('https://api.duocnamviet.site/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: localStorage.getItem('refresh_token') })
});
const data = await res.json();
if (data.success) {
localStorage.setItem('token', data.data.access_token);
// Retry request gốc với token mới
} else {
// Redirect về trang login
window.location.href = '/login';
}
Bước 4: Lấy thông tin user đang đăng nhập
GET /api/auth/me
Authorization: Bearer {access_token}
→ Response:
{
"success": true,
"data": {
"id": 2,
"username": "admin",
"email": "admin@duocnamviet.site",
"full_name": "Super Admin",
"role_code": "super_admin",
"is_active": true,
"last_login_at": 1772555598
}
}
2. Response Format chuẩn
// Thành công (đơn giản)
{ "success": true, "data": { ... } }
// Thành công (có phân trang)
{
"success": true,
"data": {
"data": [ {...}, {...}, ... ], // mảng records
"total": 150, // tổng số records trong DB
"page": 1, // trang hiện tại
"limit": 20 // số records/trang
}
}
// Lỗi
{ "success": false, "error": "Mô tả lỗi tiếng Việt" }
// HTTP Status Codes:
// 200 → OK
// 201 → Created (POST thành công)
// 400 → Bad Request (thiếu field, sai format)
// 401 → Unauthorized (chưa login hoặc token hết hạn)
// 403 → Forbidden (không đủ quyền role)
// 404 → Not Found
// 500 → Server Error
3. Hướng dẫn Fetch Ảnh, Icon, Logo
3.1 Logo website
// Lấy logo từ Settings API (public, không cần auth)
GET /api/public/settings
→ Response:
{
"success": true,
"data": {
"logo_url": "/uploads/settings/logo.png",
"logo_type": "image", // "image" | "text" | "both"
"favicon_url": "/uploads/settings/favicon.ico",
"site_name": "Dược Nam Việt",
...
}
}
// Hiển thị logo trong React:
function Logo() {
const [settings, setSettings] = useState(null);
useEffect(() => {
fetch('/api/public/settings').then(r => r.json()).then(d => setSettings(d.data));
}, []);
if (!settings) return null;
if (settings.logo_type === 'image' || settings.logo_type === 'both')
return <img src={settings.logo_url} alt={settings.site_name} />;
if (settings.logo_type === 'text')
return <span>{settings.site_name}</span>;
}
3.2 Ảnh sản phẩm
// Product có field "images" là mảng URL
GET /api/public/products/1
→ data.images = [
"/uploads/products/paracetamol-1.jpg",
"/uploads/products/paracetamol-2.jpg"
]
// URL đầy đủ: https://duocnamviet.site/uploads/products/paracetamol-1.jpg
// Hoặc: https://api.duocnamviet.site/uploads/products/paracetamol-1.jpg
// Cả 2 domain đều serve được ảnh từ cùng folder /opt/duocnamviet.site/uploads/
// React component:
function ProductImage({ src }) {
const imgUrl = src.startsWith('http') ? src : 'https://duocnamviet.site' + src;
return <img src={imgUrl} alt="" loading="lazy" />;
}
// Nếu images rỗng, dùng placeholder:
const defaultImg = '/images/no-image.svg';
3.3 Icon danh mục (Category)
// Category có field "icon" là emoji string
GET /api/public/categories
→ data = [
{ "id": 1, "name": "Thuốc giảm đau", "slug": "thuoc-giam-dau", "icon": "💊" },
{ "id": 2, "name": "Vitamin", "slug": "vitamin", "icon": "🧴" },
{ "id": 3, "name": "Thiết bị y tế", "slug": "thiet-bi-y-te", "icon": "🩺" },
...
]
// Hiển thị trong React:
function CategoryCard({ cat }) {
return (
<a href={'/danh-muc/' + cat.slug}>
<span style={{fontSize: '2rem'}}>{cat.icon || ''}</span>
<span>{cat.name}</span>
</a>
);
}
3.4 Ảnh banner
// Banner có image_url
GET /api/public/banners?position=main
→ data = [
{
"title": "Miễn phí giao hàng",
"image_url": "/uploads/banners/banner1.jpg",
"link_url": "/san-pham",
"bg_color": "from-[#1a56db] to-[#2563eb]",
"text_color": "#FFFFFF"
}
]
// Nếu banner có image_url → dùng ảnh
// Nếu không có → dùng bg_color gradient + text
3.5 Logo thương hiệu (Brand)
GET /api/public/brands
→ data = [
{ "name": "La Roche-Posay", "slug": "la-roche-posay", "logo_url": "/uploads/brands/lrp.jpg" },
...
]
// Hiển thị: <img src={'https://duocnamviet.site' + brand.logo_url} alt={brand.name} />
3.6 Upload ảnh
// Upload qua multipart form data
POST /api/upload
Authorization: Bearer {token}
Content-Type: multipart/form-data
// FormData:
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('folder', 'products'); // products | banners | brands | settings
const res = await fetch('/api/upload', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token },
body: formData // KHÔNG set Content-Type, browser tự thêm boundary
});
→ { "success": true, "data": { "url": "/uploads/products/abc123.jpg" } }
// Lưu ý: ảnh tĩnh được serve bởi Nginx, cache 30 ngày
// Folder vật lý: /opt/duocnamviet.site/uploads/
// Hỗ trợ: jpg, jpeg, png, webp, svg (max 10MB)
4. Tích hợp Frontend (Next.js / React)
4.1 API Service file mẫu
// src/services/api.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL || '';
// LƯU Ý QUAN TRỌNG:
// - Admin panel (admin.duocnamviet.site): dùng API_URL = '' (relative)
// → request gửi tới admin.duocnamviet.site/api/...
// → Nginx proxy /api/ tới backend :8080
// - Frontend chính (duocnamviet.site): tương tự dùng relative URL
// - App mobile / bên thứ 3: dùng API_URL = 'https://api.duocnamviet.site'
async function request(path: string, options: RequestInit = {}) {
const token = localStorage.getItem('token');
const headers: any = { 'Content-Type': 'application/json', ...options.headers };
if (token) headers['Authorization'] = 'Bearer ' + token;
const res = await fetch(API_URL + path, { ...options, headers });
if (res.status === 401) {
localStorage.removeItem('token');
window.location.href = '/admin/login';
throw new Error('Token hết hạn');
}
const data = await res.json();
if (!data.success) throw new Error(data.error || 'Lỗi');
return data;
}
// Sử dụng:
export const productsAPI = {
list: (params?: string) => request('/api/products' + (params ? '?' + params : '')),
get: (id: string) => request('/api/products/' + id),
create: (data: any) => request('/api/products', { method: 'POST', body: JSON.stringify(data) }),
};
4.2 Phân trang
// API hỗ trợ query params: ?page=1&limit=20&search=keyword
// Response format:
{
"data": {
"data": [...items],
"total": 150, // tổng trong DB
"page": 1,
"limit": 20
}
}
// Tính tổng số trang:
const totalPages = Math.ceil(data.total / data.limit);
// Frontend gọi:
const res = await productsAPI.list('page=2&limit=20&search=para&category_id=1');
4.3 Lọc và tìm kiếm
// Products: ?search=para&category_id=1&sort=created_at&order=desc // Orders: ?type=b2c&status=pending&search=0901 // Customers: ?type=b2b&search=nguyen // Inventory: ?page=1&limit=50 // Ví dụ URL đầy đủ: GET /api/products?page=1&limit=20&search=vitamin&category_id=2&sort=retail_price&order=asc
5. CORS, Network & Proxy
// ⚠️ QUAN TRỌNG: Cấu hình CORS
// Backend Go Fiber đã cấu hình CORS cho tất cả origins (development)
// Production: Nginx proxy xử lý routing
// ARCHITECTURE:
//
// Browser → admin.duocnamviet.site/api/products
// → Nginx (port 443)
// → location /api/ { proxy_pass http://127.0.0.1:8080 }
// → Go Backend (port 8080)
//
// Tất cả subdomain đều có /api/ proxy tới cùng backend.
// Không có CORS issue vì request đi cùng domain.
// Admin panel: fetch('/api/products') → OK (relative, cùng domain)
// Frontend: fetch('/api/public/products') → OK (relative)
// Mobile app: fetch('https://api.duocnamviet.site/api/products') → OK (CORS *)
// TRÁNH: fetch('https://api.duocnamviet.site/...') từ admin.duocnamviet.site
// → Cross-origin, có thể gặp CORS issue
6. Debug & Troubleshooting
6.1 Test nhanh bằng cURL
# Login
curl -s https://api.duocnamviet.site/api/auth/login \
-X POST -H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}' | python3 -m json.tool
# Lấy token vào biến
TOKEN=$(curl -s https://api.duocnamviet.site/api/auth/login \
-X POST -H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['access_token'])")
# Test auth/me
curl -s https://api.duocnamviet.site/api/auth/me \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# List products
curl -s "https://api.duocnamviet.site/api/products?page=1&limit=5" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# Health check
curl -s https://api.duocnamviet.site/health | python3 -m json.tool
6.2 Lỗi thường gặp
| Lỗi | Nguyên nhân | Giải pháp |
|---|---|---|
| 401 Chưa đăng nhập | Thiếu header Authorization | Thêm Authorization: Bearer {token} |
| 401 Token hết hạn | Access token expired (24h) | Gọi /api/auth/refresh để lấy token mới |
| 401 Token không hợp lệ | Token sai hoặc đã bị revoke | Login lại để lấy token mới |
| 403 Forbidden | User không có role phù hợp | Kiểm tra role_code. VD: chỉ admin/warehouse tạo SP |
| CORS Error | Frontend gọi cross-domain | Dùng relative URL hoặc proxy qua Nginx |
| Network Error | Backend chưa start hoặc Nginx lỗi | Kiểm tra: pm2 status, nginx -t |
| 500 Server Error | Lỗi backend code hoặc DB | Xem log: pm2 logs backend --lines 50 |
6.3 Kiểm tra hệ thống
# PM2 status pm2 status # Backend logs pm2 logs backend --lines 50 # Nginx test config nginx -t # MySQL check mysql -u duocnamviet -p'Duocnamviet.site' duocnamviet_erp -e "SHOW TABLES;" # Disk space df -h /opt/duocnamviet.site/ # SSL cert expiry certbot certificates
7. Mobile App Integration
// Base URL cho mobile app: const BASE_URL = 'https://api.duocnamviet.site'; // React Native / Flutter / Swift / Kotlin: // 1. Login → lưu token vào AsyncStorage / SharedPreferences / Keychain // 2. Mọi request thêm header Authorization // 3. Ảnh URL: BASE_URL + image_path // VD: 'https://api.duocnamviet.site/uploads/products/img1.jpg' // Public APIs (không cần auth) cho app khách hàng: // GET /api/public/products → Danh sách SP // GET /api/public/products/:slug → Chi tiết SP // GET /api/public/categories → Danh mục + icon // GET /api/public/settings → Logo, liên hệ // GET /api/public/banners → Banner quảng cáo // GET /api/public/brands → Thương hiệu // GET /api/public/articles → Bài viết sức khỏe // POST /api/public/orders → Đặt hàng online // GET /api/public/orders?phone=... → Tra cứu đơn hàng
Kênh:
Data Models — Cấu trúc bảng MySQL
Mỗi model đều có các trường cơ bản: id (uint, auto increment), created_at, updated_at, deleted_at (soft delete).
User
| Field | Type | Bắt buộc | Mô tả |
|---|---|---|---|
| username | string(50) | ✓ | Tên đăng nhập, unique |
| string(100) | ✓ | Email, unique | |
| password | string | ✓ | Mật khẩu (bcrypt hash) |
| full_name | string(100) | Họ tên | |
| phone | string(20) | Số điện thoại | |
| role_id | uint | ✓ | FK → roles.id |
| role_code | string(30) | Mã role (denormalized) | |
| is_active | bool | Trạng thái hoạt động | |
| refresh_token | text | JWT refresh token |
Product
| Field | Type | Bắt buộc | Mô tả |
|---|---|---|---|
| sku | string(50) | ✓ | Mã sản phẩm, unique |
| name | string(255) | ✓ | Tên sản phẩm |
| slug | string(255) | ✓ | URL slug, unique |
| description | text | Mô tả chi tiết | |
| category_id | uint | FK → categories.id | |
| category_name | string(100) | Tên danh mục (denormalized) | |
| unit | string(20) | Đơn vị: hộp, viên, chai... | |
| images | JSON | Mảng URL ảnh ["url1","url2"] | |
| cost_price | float64 | Giá nhập (VND) | |
| retail_price | float64 | ✓ | Giá bán lẻ (VND) |
| wholesale_price | float64 | Giá bán sỉ (VND) | |
| vat_rate | float64 | Thuế VAT (%) | |
| min_stock | int | Tồn kho tối thiểu | |
| is_active | bool | Đang bán / Ngừng bán |
Category
| Field | Type | Bắt buộc | Mô tả |
|---|---|---|---|
| name | string(100) | ✓ | Tên danh mục |
| slug | string(100) | ✓ | URL slug, unique |
| description | text | Mô tả | |
| icon | string(10) | Emoji icon | |
| sort_order | int | Thứ tự sắp xếp | |
| is_active | bool | Hiển thị / Ẩn |
Order
| Field | Type | Bắt buộc | Mô tả |
|---|---|---|---|
| order_no | string(30) | ✓ | Mã đơn, unique (auto) |
| type | string(10) | ✓ | b2c / b2b |
| source | string(20) | website / pos / phone | |
| status | string(20) | pending/confirmed/shipping/completed/cancelled | |
| customer_id | uint | FK → customers.id | |
| customer_name | string(100) | ✓ | Tên khách |
| customer_phone | string(20) | ✓ | SĐT khách |
| items | JSON | ✓ | Mảng sản phẩm [{product_id, quantity, unit_price, total}] |
| sub_total | float64 | Tổng trước thuế/giảm giá | |
| discount_amount | float64 | Số tiền giảm | |
| total_amount | float64 | ✓ | Tổng thanh toán |
| payment_method | string(20) | cash/transfer/momo/zalopay/vnpay | |
| payment_status | string(20) | unpaid/paid/refunded | |
| shipping_addr | JSON | {full_name, phone, address, ward, district, city} | |
| note | text | Ghi chú đơn hàng |
Customer
| Field | Type | Bắt buộc | Mô tả |
|---|---|---|---|
| type | string(10) | b2c / b2b | |
| full_name | string(100) | ✓ | Họ tên / Tên cty |
| phone | string(20) | ✓ | Số điện thoại |
| string(100) | |||
| address | text | Địa chỉ | |
| gender | string(10) | male / female | |
| company_name | string(200) | Tên công ty (B2B) | |
| tax_code | string(20) | Mã số thuế (B2B) | |
| total_orders | int | Tổng đơn hàng | |
| total_spent | float64 | Tổng chi tiêu (VND) |
Inventory & InventoryMovement
| Field | Type | Mô tả |
|---|---|---|
| product_id | uint | FK → products.id |
| warehouse | string(50) | Tên kho: main, branch1... |
| quantity | int | Số lượng tồn kho |
| movement.type | string | import / export / adjust |
| movement.movement_no | string | Mã phiếu NK-001, XK-001 |
| movement.items | JSON | [{product_id, quantity, cost_price}] |
| movement.status | string | draft / confirmed / cancelled |
Employee & Payroll
| Field | Type | Mô tả |
|---|---|---|
| employee_code | string(20) | Mã NV, unique |
| full_name | string(100) | Họ tên |
| department | string(50) | Phòng ban |
| position | string(50) | Chức vụ |
| base_salary | float64 | Lương cơ bản (VND) |
| contract_type | string | permanent / probation / contract |
| payroll.period | string | Kỳ lương: 2026-03 |
| payroll.net_salary | float64 | Lương thực nhận |
Transaction & Invoice
| Field | Type | Mô tả |
|---|---|---|
| transaction.type | string | income / expense |
| transaction.category | string | sale / purchase / salary / rent... |
| transaction.amount | float64 | Số tiền (VND) |
| invoice.type | string | sales / purchase |
| invoice.items | JSON | [{product_name, quantity, unit_price, vat_rate, total}] |
| invoice.vat_amount | float64 | Tiền VAT |
Campaign, Voucher, MedicalRecord, Article
Xem chi tiết request body mẫu ở tab API Routes → mỗi endpoint POST đều có đầy đủ fields.
SiteSettings
| Field | Type | Mô tả |
|---|---|---|
| site_name | string | Tên website |
| site_description | text | Mô tả website |
| logo_url | string | URL logo |
| logo_type | string | Kiểu hiển thị logo: image | text | both |
| favicon_url | string | URL favicon |
| og_image_url | string | URL ảnh OG (chia sẻ MXH) |
| keywords | text | Từ khóa SEO |
| tags | json | Danh sách tags |
| contact_phone | string | SĐT liên hệ |
| contact_email | string | Email liên hệ |
| contact_address | string | Địa chỉ |
| facebook_url | string | URL Facebook |
| zalo_url | string | URL Zalo |
| google_analytics | string | Google Analytics ID |
| meta_title | string | Meta title SEO |
| meta_description | text | Meta description SEO |
| footer_text | text | Text footer website |
| maintenance_mode | bool | Chế độ bảo trì |
| theme_mode | string | light / dark |
| primary_color | string | Màu chủ đạo (#hex) |
| bg_color | string | Màu nền |
| text_color | string | Màu chữ |
| sidebar_color | string | Màu sidebar |
| hover_color | string | Màu hover |
| gradient_bg | string | Gradient CSS |
| border_radius | int | Bo góc (px), mặc định 8 |
| glass_opacity | float | Độ trong suốt glass, mặc định 0.35 |
| shadow_intensity | string | sm / md / lg / xl |
| animation_speed | string | slow / normal / fast |
| theme_preset | string | light / dark / midnight / ocean / sunset / forest / cherry |
Language
| Field | Type | Mô tả |
|---|---|---|
| id | uint | ID (auto increment) |
| key | string(10) | Mã ngôn ngữ (vi, en, ja...) — unique |
| name | string(100) | Tên hiển thị (Tiếng Việt, English...) |
| is_active | bool | Kích hoạt? Mặc định: true |
| is_default | bool | Ngôn ngữ mặc định (chỉ 1 record = true) |
| translations | json | Object key-value bản dịch: {"menu.home": "Trang chủ", "btn.save": "Lưu", ...} |
🎨 SiteTheme
| Field | Type | Mô tả |
|---|---|---|
| site_type | string | Loại site: admin / frontend / banle / bansi — unique |
| theme_mode | string | light / dark |
| primary_color | string | Màu chủ đạo (#hex) |
| bg_color | string | Màu nền |
| text_color | string | Màu chữ |
| sidebar_color | string | Màu sidebar |
| hover_color | string | Màu hover |
| gradient_bg | string | Gradient CSS |
| border_radius | int | Bo góc (px) |
| glass_opacity | float | Độ trong suốt glass |
| shadow_intensity | string | sm / md / lg / xl |
| animation_speed | string | slow / normal / fast |
| theme_preset | string | Preset tên: light / dark / midnight / ocean / sunset / forest / cherry |
📧 Email System
| Field | Type | Mô tả |
|---|---|---|
| EmailDomain | ||
| domain | string | Tên domain (unique) |
| mx_verified | bool | Đã xác minh MX record |
| spf_verified | bool | Đã xác minh SPF record |
| dkim_verified | bool | Đã xác minh DKIM record |
| dmarc_verified | bool | Đã xác minh DMARC record |
| dkim_selector | string | DKIM selector (mặc định: default) |
| is_active | bool | Kích hoạt |
| EmailAccount | ||
| string | Địa chỉ email (unique) | |
| display_name | string | Tên hiển thị khi gửi |
| domain | string | Domain (tự trích từ email) |
| daily_send_limit | int | Giới hạn gửi/ngày (mặc định 100) |
| today_sent_count | int | Số đã gửi hôm nay |
| signature | html | Chữ ký email (HTML) |
| EmailMessage | ||
| account_id | uint | FK → EmailAccount |
| direction | string | in / out |
| from_email / from_name | string | Người gửi |
| to_email / cc_email / bcc_email | string | Người nhận, CC, BCC |
| subject | string | Tiêu đề |
| body_html / body_text | text | Nội dung HTML / plaintext |
| attachments | json | [{name, url, size, content_type}] |
| is_read / is_starred / is_archived / is_trash / is_spam | bool | Trạng thái |
| status | string | queued / sending / sent / failed / draft |
| sent_at | datetime | Thời gian gửi |
| EmailTemplate | ||
| name / slug | string | Tên mẫu và URL slug |
| type | string | 2fa / forgot_password / verify_email / promotion / newsletter / welcome / custom |
| subject | string | Tiêu đề email (hỗ trợ {{variable}}) |
| body_html | html | Nội dung HTML (hỗ trợ {{variable}}) |
| variables | json | [{key, description}] — biến thay thế |
| EmailCronJob | ||
| name | string | Tên cron job |
| template_id / account_id | uint | Mẫu email và tài khoản gửi |
| schedule | string | Cron expression (VD: 0 8 * * *) |
| recipients | string | all_users / active_users / custom |
| custom_emails | json | Danh sách email nếu recipients=custom |
| max_per_day | int | Tối đa gửi/ngày |
| is_enabled | bool | Bật/tắt |
| total_sent | int | Tổng đã gửi |
Lịch sử cập nhật
v2.1.0
2026-04-02 21:00
NEW
API
POST /api/public/upload — Upload ảnh không cần đăng nhập (cộng dồng, prescription, review). Chỉ nhận ảnh, tối đa 2MB.
NEW
API
GET /api/public/orders/:order_number — Tra cứu chi tiết đơn hàng bằng mã đơn, không cần đăng nhập. Xác minh bằng phone hoặc email.
NEW
API Prescription bổ sung trường
product_ids: [123, 456] — gửi kèm danh sách sản phẩm muốn mua.
UX
API Docs: Click API trong sidebar/search tự động scroll đến và highlight card (animation glow teal 2 giây). Auto-expand nếu chưa mở.
UX
API Docs: Sidebar Treeview ẩn/hiện linh hoạt theo nhóm, nút mở rộng/thu gọn tất cả. Tab chả núnh trên header cố định.
FIX
API docs bỏ response hardcode — luôn hiển thị response thực tế từ server khi test.
v2.0.0
2026-04-02 16:00
NEW
Phân loại B2C/B2B bao gồm API public dùng chung (sản phẩm, danh mục, đặt hàng, bài viết, config, reviews, chatbot)
NEW
Thêm /api/public/email/fetch-local (internal)
FIX
Sửa sticky toolbar dùng CSS sticky + JS đo header height thay vì position:fixed
FIX
Kiểm tra 100% API endpoints từ backend, bổ sung thiếu vào api-groups.js
🕐 2026-04-01 10:00
NEW
Ra mắt trang API Documentation tại api.duocnamviet.site
NEW
325+ API endpoints đầy đủ với phân loại theo kênh (Public, B2C, B2B, Admin, Main)
NEW
Tích hợp test API trực tiếp với multi-scenario, live response
NEW
Data Models — Cấu trúc bảng MySQL đầy đủ