Phân tích chi tiết API backend cho Zalo Mini App, ưu tiên Type 2 (Collect/Thu thập)
Version 1.0 Priority: Type 2 Collect Setting: Super Admin
┌─────────────────────────────────────────────────────────────┐
│ ZALO SUPER APP │
│ ┌───────────────────────────────────┐ │
│ │ ZALO MINI APP │ │
│ │ (React + ZaUI Components) │ │
│ │ │ │
│ │ 1. authorize() → Zalo User ID │ │
│ │ 2. getAccessToken() → token │ │
│ │ 3. getPhoneNumber() → SĐT │ │
│ │ 4. getLocation() → GPS │ │
│ │ │ │
│ │ Gọi BOM Gift API với token │ │
│ └──────────────┬────────────────────┘ │
│ │ HTTPS │
└─────────────────┼───────────────────────────────────────────┘
│
┌────────────▼────────────────────────┐
│ BOM GIFT BACKEND │
│ (Laravel — bom.gift) │
│ │
│ /api/v1/zalo/* (NEW endpoints) │
│ │
│ ┌─ ZaloAuthMiddleware ───────────┐ │
│ │ Nhận token từ Mini App │ │
│ │ → Gọi BOM.ASIA verify token │ │
│ │ → Nhận phone + Zalo user info │ │
│ │ → Resolve tenant + customer │ │
│ └──────────────┬────────────────┘ │
│ │ │
│ Campaign logic (existing): │
│ - Collect / Survey / Gift │
└────────────┬───────────────────────┘
│ sync Zalo user data
┌────────────▼────────────────────────┐
│ BOM.ASIA SERVER │
│ (Zalo Ready — existing system) │
│ │
│ ┌─ Zalo Integration Hub ─────────┐ │
│ │ │ │
│ │ POST /api/v2/zalo/verify │ │
│ │ → Verify Zalo access token │ │
│ │ → Decode phone_token │ │
│ │ → Return verified user info │ │
│ │ │ │
│ │ POST /api/v2/zalo/user/sync │ │
│ │ → Lưu Zalo user profile │ │
│ │ → Map phone ↔ Zalo User ID │ │
│ │ → Lưu avatar, display name │ │
│ │ │ │
│ │ ZNS OTP (existing) │ │
│ │ ZNS Templates (existing) │ │
│ │ ZNS Notification (existing) │ │
│ └────────────────────────────────┘ │
│ │
│ Single source of truth cho: │
│ - Zalo user data │
│ - Zalo OA connections │
│ - ZNS messaging │
│ - App credentials (app_id/secret) │
└────────────────────────────────────┘
BomApiService (đã có sẵn, JWT auth)| Field mới | Trên | Mô tả |
|---|---|---|
campaigns.zalo_miniapp | Campaign Edit → General tab | Boolean toggle — campaign này có hiển thị trên Mini App không |
// Quy tắc: // - Web landing: Campaign::active()->latest()->first() // - Zalo Mini App: Campaign::active()->where('zalo_miniapp', true)->first() // - Nếu không có campaign nào bật zalo_miniapp → Mini App hiện "Chưa có chương trình" // - Chỉ 1 campaign bật zalo_miniapp tại 1 thời điểm (per tenant) // API: GET /api/v1/zalo/campaign // → Trả 1 campaign có zalo_miniapp=true (giống /campaigns/active nhưng filter thêm)
Ví dụ tenant Star:
┌──────────────────────────────────────────────────┐
│ Campaign "Trưng bày POSM" (collect) │
│ status: active │
│ zalo_miniapp: ✅ true ← hiển thị trên Mini App│
│ web landing: ✅ (latest) ← hiển thị trên web │
│ │
│ Campaign "Khảo sát Q2" (survey) │
│ status: active │
│ zalo_miniapp: ❌ false ← không show Mini App │
│ web landing: ❌ (không phải latest) │
└──────────────────────────────────────────────────┘
| Tính năng | Web SPA (hiện tại) | Zalo Mini App (mới) |
|---|---|---|
| Xác thực | OTP qua Zalo ZNS | Zalo authorize() + getPhoneNumber() — không cần OTP |
| SĐT | User nhập thủ công | Tự động từ Zalo (đã verified) |
| GPS | Browser Geolocation API | getLocation() — chính xác hơn |
| User ID | Session-based | Zalo User ID (persistent) |
| UI | Custom CSS | ZaUI Components (50+ native) |
| Push | Không có | Zalo notification API |
| Payment | Không có | Zalo Checkout SDK (tương lai) |
// Step 1-3: Trong Zalo Mini App import { authorize, getAccessToken, getPhoneNumber } from 'zmp-sdk'; const auth = await authorize(); const { accessToken } = await getAccessToken(); const { token: phoneToken } = await getPhoneNumber(); // Step 4: Gửi lên backend const res = await fetch('https://tenant.bom.gift/api/v1/zalo/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ access_token: accessToken, phone_token: phoneToken, }) }); const { app_token, customer } = await res.json(); // Dùng app_token cho mọi API call sau đó
// POST /api/v1/zalo/auth // 1. Nhận access_token + phone_token từ Mini App // 2. Gọi Zalo API verify: GET graph.zalo.me/v2.0/me?access_token=... // 3. Decode phone_token → lấy SĐT thật // 4. Find/Create Customer by phone + tenant // 5. Lưu zalo_user_id vào customer // 6. Trả JWT app_token (dùng cho các API sau) return response()->json([ 'app_token' => $jwt, 'customer' => ['id', 'full_name', 'phone', ...], 'campaign' => [...], // active campaign data ]);
BOM.ASIA (https://bom.asia/api/v2) là Zalo Hub trung tâm — đã có sẵn hệ thống Zalo (ZNS, OA, templates). Thông tin Zalo user sẽ lưu về bom.asia, BOM Gift chỉ lưu mapping reference.
| Chức năng | BOM.ASIA (Zalo Hub) | BOM Gift (Campaign Logic) |
|---|---|---|
| Zalo token verify | ✅ Verify + decode | ❌ Gọi qua API |
| Phone decode | ✅ Decrypt phone_token | ❌ Nhận kết quả |
| Zalo user profile | ✅ Lưu avatar, name, ID | Chỉ lưu zalo_user_id |
| App credentials | ✅ app_id, app_secret | ❌ Không lưu |
| ZNS messaging | ✅ Send (existing) | Gọi qua BomApiService |
| Campaign data | ❌ | ✅ Toàn bộ |
| Customer data | ❌ | ✅ Toàn bộ |
| Gift/Redeem | ❌ | ✅ Toàn bộ |
BOM.ASIA đã có đầy đủ Zalo Mini App module — không cần tạo mới!
/api/miniapp)ĐÃ CÓ Decode phone token từ Zalo SDK → SĐT thật
POST https://bom.asia/api/miniapp/auth/decode-phone
{
"phone_token": "token_from_getPhoneNumber()"
}
// → Returns: decoded phone number
ĐÃ CÓ Verify phone + lưu Mini App user IDs
POST https://bom.asia/api/miniapp/auth/check-phone
{
"phone": "0938123456",
"mini_app_user_id": "zalo_mini_app_uid",
"oa_user_id": "zalo_oa_uid",
"zalo_app_id": "app_id",
"name": "Nguyễn Văn A",
"avatar": "https://..."
}
// → Lưu/Update user trên bom.asia + trả user info
ĐÃ CÓ Login via Zalo token hoặc phone → Sanctum token
POST https://bom.asia/api/miniapp/auth/login
{
"access_token": "zalo_access_token",
// hoặc phone-based login
}
// → Returns: Bearer token cho các API sau
ĐÃ CÓ Decode location token → GPS coordinates
| API | Endpoint | Mô tả |
|---|---|---|
| ZNS Send | POST /api/v2/zns/send | Gửi ZNS message (đang dùng cho OTP) |
| ZNS OTP | POST /api/v2/zns/send-otp | Gửi OTP qua ZNS |
| ZNS Bulk | POST /api/v2/zns/send-bulk | Gửi hàng loạt |
| OA Contacts | GET /api/v2/zalo-oa/contacts | DS contacts OA |
| OA Message | POST /api/v2/zalo-oa/send | Gửi message qua OA |
| Webhooks | POST /api/zalo-oa/webhook | 83 event types (follow, message, etc.) |
| E-commerce | /api/ecommerce/{appId}/* | Cart, Orders, ZaloPay |
/api/miniapp/auth/decode-phone để decode phone_token/api/miniapp/auth/check-phone để sync user info// POST /api/v1/zalo/auth — ZaloController public function auth(Request $request) { $bomApi = app(BomApiService::class); $tenant = $request->attributes->get('current_tenant'); // Step 1: Decode phone token → lấy SĐT thật (BOM.ASIA đã có API) $decode = $bomApi->decodeZaloPhone($request->phone_token); if (!$decode['ok']) abort(401, 'Phone decode failed'); $phone = $decode['data']['phone']; // Step 2: Sync Zalo user info lên BOM.ASIA (lưu trên bom.asia) $bomApi->syncZaloMiniAppUser([ 'phone' => $phone, 'mini_app_user_id' => $request->zalo_user_id, 'oa_user_id' => $request->oa_user_id, 'zalo_app_id' => $request->zalo_app_id, 'name' => $request->display_name, 'avatar' => $request->avatar_url, ]); // Step 3: Find/Create LOCAL customer (bom.gift chỉ lưu campaign data) $campaign = Campaign::active() ->where('tenant_id', $tenant->id)->latest()->first(); $customer = Customer::firstOrCreate( ['phone' => $phone, 'tenant_id' => $tenant->id, 'campaign_id' => $campaign?->id], [ 'source_type' => $campaign?->type ?? 'collect', 'full_name' => $request->display_name, 'zalo_user_id' => $request->zalo_user_id, 'verified_at' => now(), ] ); // Step 4: Issue JWT cho Mini App (dùng cho các API sau) $jwt = JwtService::issue($customer, $tenant); return response()->json([ 'app_token' => $jwt, 'customer' => [...], 'campaign' => [...], // active campaign + form_config + programs ]); }
customers.zalo_user_id = reference link giữa 2 hệ thốngcustomers + program_registrations — hoàn toàn giống khi đăng ký qua web. Chỉ khác: phone tự lấy từ Zalo (không nhập thủ công), Zalo profile sync về bom.asia.
// Thêm vào app/Services/BomApiService.php // Base URL: /api/miniapp (KHÔNG cần JWT auth — module public) public function decodeZaloPhone(string $phoneToken): array { // Gọi API đã có: POST /api/miniapp/auth/decode-phone $response = Http::timeout(10) ->post("{$this->baseUrl}/../miniapp/auth/decode-phone", [ 'phone_token' => $phoneToken, ]); return $response->successful() ? ['ok' => true, 'data' => $response->json('data')] : ['ok' => false, 'error' => $response->json('message', 'Decode failed')]; } public function syncZaloMiniAppUser(array $data): array { // Gọi API đã có: POST /api/miniapp/auth/check-phone $response = Http::timeout(10) ->post("{$this->baseUrl}/../miniapp/auth/check-phone", $data); return $response->successful() ? ['ok' => true, 'data' => $response->json('data')] : ['ok' => false, 'error' => $response->json('message')]; }
Tái sử dụng HTTP client. Module /api/miniapp trên bom.asia là public (không cần JWT auth).
| Tầng | Ai quản lý | Ở đâu | Nội dung |
|---|---|---|---|
| Tầng 1: Zalo Credentials | Super Admin | BOM.ASIA | app_id, app_secret, OA config — cấu hình trên bom.asia cho từng tenant |
| Tầng 2: Campaign Config | Tenant Admin | BOM Gift | Bật/tắt Mini App, mapping app_id, form fields, landing content |
Super Admin cấu hình Zalo credentials trên bom.asia (không phải bom.gift):
| Field | Per | Mô tả |
|---|---|---|
zalo_app_id | Per Mini App | Zalo Mini App ID từ Developer Console |
zalo_app_secret | Per Mini App | App Secret (encrypted, chỉ bom.asia giữ) |
zalo_oa_id | Per OA | Official Account ID |
zalo_oa_secret | Per OA | OA Secret Key |
// bom.asia quản lý nhiều Zalo app: // /api/miniapp/auth/decode-phone — tự detect app_id từ phone_token // /api/miniapp/auth/check-phone — truyền zalo_app_id để phân biệt // /api/v2/zalo-oa/accounts — list tất cả OA accounts // → Mỗi tenant map với 1 zalo_app_id + 1 oa_id
Tenant admin cấu hình tại BOM Gift, menu: /admin/branding hoặc /admin/setting
| Field | Ai setting | Mô tả |
|---|---|---|
zalo_enabled | Super Admin / Tenant Admin | Bật/tắt Zalo Mini App cho tenant này |
zalo_app_id | Super Admin | Mapping: tenant này dùng Zalo App nào (reference ID, không phải secret) |
zalo_oa_id | Super Admin | Mapping: tenant này dùng OA nào |
zalo_auto_create | Tenant Admin | Tự tạo customer khi user mở Mini App lần đầu |
zalo_welcome_msg | Tenant Admin | Tin nhắn chào mừng qua OA (tùy chọn) |
tenants.zalo_config (JSON)// Per-tenant, không chứa secret { "enabled": true, "app_id": "111222333", // reference → bom.asia app "oa_id": "444555666", // reference → bom.asia OA "auto_create": true, "welcome_msg": "Chào mừng bạn đến với Star!" }
Khi user mở Mini App của tenant Star:
// 1. Mini App gửi auth request POST https://star.bom.gift/api/v1/zalo/auth { "phone_token": "...", "zalo_user_id": "...", "zalo_app_id": "111222333" // Mini App tự biết app_id của mình } // 2. BOM Gift middleware ResolveTenant // Domain star.bom.gift → tenant_id = 2 (Star) // Kiểm tra: tenants.zalo_config.enabled === true // Kiểm tra: tenants.zalo_config.app_id === request.zalo_app_id // 3. BOM Gift → BOM.ASIA: decode phone POST https://bom.asia/api/miniapp/auth/decode-phone { "phone_token": "..." } // → phone: "0938123456" // 4. BOM Gift → BOM.ASIA: sync user (truyền app_id để bom.asia biết app nào) POST https://bom.asia/api/miniapp/auth/check-phone { "phone": "0938123456", "zalo_app_id": "111222333", // Star's app "mini_app_user_id": "...", "name": "Nguyễn Văn A" } // → bom.asia lưu user under app 111222333 // 5. BOM Gift: Create local customer Customer::firstOrCreate( ['phone' => '0938123456', 'tenant_id' => 2], // Star ['source_type' => 'collect', 'zalo_user_id' => '...', ...] ); // 6. Issue JWT chứa tenant_id + customer_id // Mọi API sau đều scope by tenant_id trong JWT
tenant_id — mọi query đều WHERE tenant_id = ?zalo_app_id validate — Mini App phải match với tenant config| Page | Super Admin | Tenant Admin |
|---|---|---|
/admin/setting → Zalo Mini App | ✅ Cấu hình zalo_app_id, oa_id cho từng tenant Toggle enabled per tenant Test connection | ❌ Không thấy |
/admin/branding → Zalo | ✅ Xem tất cả | ✅ Xem config của tenant mình Toggle auto_create Nhập welcome_msg |
/admin/landing → Form Fields | ✅ | ✅ Cấu hình field active/required |
/admin/customer | ✅ Xem tất cả tenant | ✅ Chỉ thấy KH của mình (bao gồm KH từ Mini App) |
bom.gift/admin/setting → chọn tenant → nhập app_id + oa_id (reference)bom.gift/admin/branding → bật Zalo Mini App → cấu hình form fields tại Landing Pagestenant.bom.gift/api/v1/zalo/*
PUBLIC Xác thực Zalo user, trả app_token
{
"access_token": "zalo_access_token_from_sdk",
"phone_token": "phone_number_token_from_getPhoneNumber"
}
{
"app_token": "eyJhbGci...", // JWT, valid 24h
"expires_at": "2026-03-19T12:00:00Z",
"customer": {
"id": 123,
"full_name": "Nguyễn Văn A", // null nếu mới
"phone": "0938123456",
"zalo_user_id": "8734526190",
"is_new": true
},
"campaign": {
"id": 2, "type": "collect", "name": "...",
"form_config": {...},
"landing_config": {...},
"programs": [...]
}
}
AUTH Lấy campaign active + form config + programs
// Giống /campaigns/active hiện tại // + form_config cho biết field nào active/required // + programs với image, stock, input_type
AUTH Đăng ký thông tin — phone tự lấy từ token, không cần nhập
{
"full_name": "Nguyễn Văn A",
"gender": "male", // theo form_config
"age": 25, // theo form_config
"ward_code": "27112", // theo form_config
"address": "120 NVL", // theo form_config
"pharmacy_name": "...", // theo form_config
"medical_rep_id": 5, // theo form_config
"notes": "...", // theo form_config
"lat": 10.776, // từ getLocation()
"lng": 106.700,
"consent_health": true
}
// phone + zalo_user_id + campaign_id → tự lấy từ auth token
{
"customer_id": 123,
"message": "Đăng ký thành công"
}
AUTH Tái sử dụng logic hiện có
GET /api/v1/zalo/collect/programs // → Giống /collect/programs POST /api/v1/zalo/collect/choose // → Giống /collect/choose GET /api/v1/zalo/collect/summary // → Giống /collect/summary GET /api/v1/zalo/wards/provinces // → Giống /wards/provinces GET /api/v1/zalo/wards?province_code= // → Giống /wards GET /api/v1/zalo/collect/medical-reps // → Giống /collect/medical-reps
Sự khác biệt duy nhất: middleware xác thực bằng JWT (từ Zalo auth) thay vì session.
POST /api/v1/zalo/auth // Xác thực (giống type 2) POST /api/v1/zalo/customers // Lưu thông tin KH GET /api/v1/zalo/survey // Câu hỏi khảo sát POST /api/v1/zalo/survey/submit // Gửi đáp án → scoring POST /api/v1/zalo/gift/generate // Tạo gift code + QR GET /api/v1/zalo/gift/{code} // Xem gift code status
// 1. Thêm zalo_config vào tenants Schema::table('tenants', function ($table) { $table->json('zalo_config')->nullable(); // {"app_id","app_secret","oa_id","enabled"} }); // 2. Thêm zalo fields vào customers (zalo_user_id đã có) // customers.zalo_user_id — đã tồn tại ✓ // 3. Tạo bảng zalo_sessions (JWT tracking) Schema::create('zalo_sessions', function ($table) { $table->id(); $table->foreignId('customer_id')->constrained(); $table->foreignId('tenant_id')->constrained(); $table->string('zalo_user_id'); $table->string('token_hash', 64)->unique(); // SHA256 of JWT $table->timestamp('expires_at'); $table->string('ip_address', 45)->nullable(); $table->timestamps(); });
Thêm tab "Zalo Mini App" tại /admin/branding — tenant admin tự cấu hình.
| Layer | Biện pháp |
|---|---|
| Zalo Token Verify | Backend gọi Zalo API xác minh access_token, không tin frontend |
| Phone Verify | Decode phone_token server-side bằng app_secret, không nhận phone từ client |
| JWT App Token | Signed bằng APP_KEY, expire 24h, chứa customer_id + tenant_id + zalo_user_id |
| Tenant Isolation | JWT chứa tenant_id, middleware validate tenant match domain |
| Rate Limiting | throttle:60/min cho authenticated, 20/min cho auth endpoint |
| CORS | Chỉ cho phép origin từ Zalo Mini App domain |
| App Secret | Encrypted trong DB (Laravel encrypt), không bao giờ expose ra API |
| # | Task | Chi tiết |
|---|---|---|
| 1.1 | DB Migration | zalo_config trên tenants, zalo_sessions table |
| 1.2 | ZaloAuthMiddleware | Verify Zalo token, decode phone, issue JWT |
| 1.3 | POST /zalo/auth | Auth endpoint — core flow |
| 1.4 | Zalo Collect APIs | register, programs, choose, summary, wards, medical-reps |
| 1.5 | Admin Setting UI | Zalo config form tại Super Admin settings |
| 1.6 | Test & Deploy | Test với Zalo Mini App sandbox |
| # | Task | Chi tiết |
|---|---|---|
| 2.1 | Zalo Survey APIs | customers, survey, submit, gift/generate |
| 2.2 | Gift Code in Mini App | QR display, pharmacy list |
| 2.3 | Test & Deploy | End-to-end survey flow |
| # | Task |
|---|---|
| 3.1 | Zalo Notification API — push gift expiry reminders |
| 3.2 | Zalo Checkout SDK — thanh toán trong app |
| 3.3 | Zalo Share — share campaign link trong Zalo |
| 3.4 | Analytics — track Mini App events |
| Method | Path | Auth | Mô tả |
|---|---|---|---|
| POST | /api/v1/zalo/auth | Public | Xác thực Zalo → JWT |
| GET | /api/v1/zalo/campaign | JWT | Campaign active + config |
| POST | /api/v1/zalo/collect/register | JWT | Đăng ký KH (type 2) |
| GET | /api/v1/zalo/collect/programs | JWT | DS chương trình |
| POST | /api/v1/zalo/collect/choose | JWT | Chọn chương trình |
| GET | /api/v1/zalo/collect/summary | JWT | Tổng kết + QR |
| GET | /api/v1/zalo/collect/medical-reps | JWT | DS TDV theo khu vực |
| GET | /api/v1/zalo/wards/provinces | JWT | DS tỉnh/TP |
| GET | /api/v1/zalo/wards | JWT | DS phường/xã |
| POST | /api/v1/zalo/customers | JWT | Lưu thông tin KH (type 1) |
| GET | /api/v1/zalo/survey | JWT | Câu hỏi khảo sát (type 1) |
| POST | /api/v1/zalo/survey/submit | JWT | Gửi đáp án (type 1) |
| POST | /api/v1/zalo/gift/generate | JWT | Tạo gift code (type 1) |
/api/v1/zalo/* — không ảnh hưởng API hiện tạiverifyZaloToken() + syncZaloUser(), tái sử dụng JWT auth + retryBOM Gift Platform · Zalo Mini App API Plan v1.0 · March 2026