00Bản đồ hệ thống — bốn khối chạy, đừng trộn
Đọc xong mục này là biết mỗi lệnh sau đây gõ ở thư mục nào.
Nét liền: gọi trực tiếp. Nét đứt: sự kiện qua hàng đợi, có thể trễ.
| Khối | Thư mục | Chạy ở đâu | Đụng vào là ảnh hưởng |
|---|---|---|---|
| Mini App | /src | Máy khách, trong Zalo | Phải deploy + Zalo duyệt mới ra khách |
| Backend | /backend | Cloudflare Worker (biên) | Deploy là ăn ngay, cả app lẫn CRM |
| CRM quản trị | /admin | Cloudflare Pages (PWA) | Chỉ nội bộ, deploy tự do |
| Gateway Zalo | /gateway | Máy/VPS chạy Node 24/7 | Tắt là hộp thư mất tin mới |
Bản 1 có ba khối (app · backend · trang quản trị). Bản 2 thêm khối thứ tư (gateway Zalo cá nhân, buộc phải có máy chạy Node dài hạn — Worker không giữ được WebSocket của zca-js), và thêm các mục 07–12: theo dõi vận chuyển, tin ZBS, trợ lý ba kênh, hộp thư, CRM ngoài, đặt hàng ngoài app.
01Giấy tờ và tài khoản phải có trước
Khâu này không viết được dòng mã nào mà vẫn tốn 1–3 tuần. Làm song song với mục 02.
- Official Account đã xác thực doanh nghiệp Mini App bắt buộc gắn một OA. Xác thực cần giấy phép kinh doanh khớp tên chủ tài khoản. Chưa xác thực thì không xin được quyền lấy số điện thoại, không gửi được tin ZBS.
- Zalo App → lấy App ID và Secret Key
Tạo tại Zalo for Developers.
ZALO_APP_SECRETdùng để giải mã token số điện thoại (xem mục 13.4 — có cái bẫy IP ở đây). - Mini App → lấy Mini App ID
ID này nằm công khai trong URL app (
zalo.me/s/<appId>), khai vàoapp-config.jsonvàZALO_MINI_APP_IDcủa Worker. - Khai ngay trong lần đầu vào Portal
Tên · mô tả · icon · ảnh bìa · danh mục · chính sách quyền riêng tư ·
điều khoản sử dụng (hai trang này phải có URL thật — xem
docs/dieu-khoan-su-dung.html) · phạm vi quyền xin của app. - Pancake POS → Shop ID + API Key
Đây là nguồn sự thật về sản phẩm, tồn kho, đơn hàng. Lấy luôn danh sách kho để chọn
DEFAULT_WAREHOUSE_ID(mục 02 bước 6). - Cloudflare
Worker + D1 + Pages + một Durable Object. Gói Free chạy đủ, nhưng Durable Object phải khai kiểu
new_sqlite_classesmới dùng được ở gói Free. - Cổng thanh toán Zalo Checkout SDK (bắt buộc, xem mục 05) + SePay để đối soát chuyển khoản. SePay nên cấp một tài khoản định danh (VA) riêng cho Mini App để webhook không lẫn với web.
- Hạ tầng phụ trên IP Việt Nam
Một chỗ chạy PHP trên hosting Việt Nam (ở đây là
enzara.vn) để làm proxy giải mã số điện thoại. Cloudflare bị Zalo trả-501khi giải mã từ IP ngoài VN. - Nếu dùng Messenger Facebook App + Page Access Token + App Secret. Không dùng thì bỏ qua, hệ thống không gãy.
Xin duyệt Mini App khi chưa có trang điều khoản, chưa có luồng thanh toán qua Checkout SDK, hoặc app còn màn hình trống → bị trả về, và mỗi lượt trả về mất 3–5 ngày chờ.
02Backend — Worker + D1, dựng trước tất cả
Mini App và CRM đều chỉ là mặt tiền. Dựng backend xong thì hai cái kia chỉ còn là giao diện.
enzara-backend
Worker- Để làm gì
- Một Worker Hono đứng giữa mọi thứ: giữ khoá bí mật (Mini App không được giữ khoá nào),
gọi Pancake, tính lại giá và tổng tiền, ký chữ ký Checkout SDK, ghi điểm/hạng/voucher xuống D1,
nhận webhook của SePay · Pancake · Zalo · WooCommerce · Messenger, phục vụ trang
/dat-hangvà API/admin/*cho CRM. - Cách dùng
cd backend→npm run dev(cổng 8787) →npm run deploy. Xem log bản thật:npx wrangler tail.- Tệp
backend/src/index.ts(gắn route + cron) ·routes/*.ts·db.ts·env.ts(danh mục biến) ·wrangler.toml.
- Cài và đăng nhập
cd backend npm install npx wrangler login
- Tạo D1 và dán database_id
npm run db:create # in ra database_id → dán vào wrangler.toml npm run db:schema:local npm run db:schema:remote
- Chạy toàn bộ migration — bằng vòng lặp, không gõ tay 33 lần
Tên script có dấu hai chấm, mà
zshhiểu:rlà ký hiệu cắt đuôi tệp nênnpm run db:migrate$n:remotegọi sai script mà không báo lỗi rõ. Phải dựng tên trước:for n in {2..34}; do M=$(printf 'db:migrate%s:remote' "$n") npm run "$M" || echo "DỪNG ở $n" doneThứ tự là bắt buộc: bảng sau tham chiếu bảng trước. Danh mục từng migration ở Phụ lục B.
- Nạp khoá bí mật
Dev local:
backend/.dev.vars(đã gitignore). Bản thật:npx wrangler secret put <TÊN>. Bảng đầy đủ ở Phụ lục A — cái nào bắt buộc, cái nào thiếu thì tính năng nào tắt.Bắt buộc tối thiểu để bán được hàng:
PANCAKE_API_KEY,PANCAKE_SHOP_ID,ADMIN_JWT_SECRET,ZALO_CHECKOUT_PRIVATE_KEY. - Khai biến công khai trong
wrangler.tomlPANCAKE_BASE_URL·ALLOWED_ORIGINS·DEFAULT_WAREHOUSE_ID·SEPAY_ACCOUNT/SEPAY_BANK·PHONE_PROXY_URL·ZALO_MINI_APP_ID·BOT_BASE_URL·MESSENGER_GRAPH_VERSION. - Chọn kho mặc định — không được bỏ trống
curl "http://localhost:8787/api/_pancake/ping?token=<DIAG_TOKEN>" curl "http://localhost:8787/api/_pancake/warehouses?token=<DIAG_TOKEN>"
Lấy
idcủa kho muốn bán và đặt vàoDEFAULT_WAREHOUSE_ID. - Khai Durable Object cho hộp thư
HOP_THU→ lớpHopThuHub, migration taghop-thu-v1vớinew_sqlite_classes. Đây là trạm WebSocket duy nhất giữ kết nối của gateway và các tab hộp thư. - Khai hai cron — đọc kỹ giờ UTC
crons = ["0 23 * * *", "*/10 0-16,23 * * *"]
0 23 * * *= 6h sáng giờ VN (đồng bộ bài viết từ website).*/10 0-16,23 * * *= 10 phút/lần, 6h–24h giờ VN (lưới an toàn hàng đợi CRM; ban đêm nghỉ, sự kiện vẫn nằm trong bảng chờ sáng gom). Viết0 6 * * *là 13h chiều VN. - Gắn tên miền và deploy
npx wrangler deploy # route custom_domain: zalo-app.enzara.vn
Chờ ~1 phút rồi mới kết luận có lỗi: node biên cần thời gian lan bản mới.
Worker phản chiếu mọi origin và tự bảo vệ bằng Bearer token, nên thêm domain admin mới
không cần deploy lại. ALLOWED_ORIGINS chỉ còn là danh sách tham chiếu.
03CRM quản trị — làm trước Mini App
Lý do đảo thứ tự: có CRM thì nhập được sản phẩm, khuyến mãi, quà, hạng — Mini App mới có gì để hiện.
enzara-admin
Pages · PWA- Để làm gì
- Trang web riêng (không nhúng trong Zalo) để vận hành toàn bộ cửa hàng: 16 mục, gọi
/admin/*bằng JWT. Cài được lên màn hình chính điện thoại như một app. - Cách dùng
cd admin && npm run dev(cổng 5173, đọcVITE_API_BASE). Bản thật: build với API base production rồi đẩy lên Pages.- Tệp
admin/src/App.tsx(gate đăng nhập +NAV/NAV_CHINH) ·api.ts·pages/*.tsx·public/sw.js.
- Tạo tài khoản chủ đầu tiên (chạy một lần)
curl -X POST "https://zalo-app.enzara.vn/admin/seed" \ -H "Authorization: Bearer <ADMIN_JWT_SECRET>" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"<mật-khẩu-mạnh>"}'Mật khẩu lưu dạng băm PBKDF2. Đổi mật khẩu = chạy lại lệnh này, hoặc dùng mục Đổi mật khẩu trong trang.
- Build và deploy
cd admin VITE_API_BASE="https://zalo-app.enzara.vn" npm run build npx wrangler pages deploy dist --project-name enzara-admin
Rồi gắn tên miền cho project Pages.
- Thêm người dùng và chia vai
Thiết lập → Người dùng. Vainhan_vienchỉ dùng trang; mọi giá trị vai khác đều là CHỦ (cố ý: tài khoản cũ mangrole='admin'không bị bản cập nhật cướp quyền). Hệ thống chặn tự khoá mình ra ngoài: luôn còn ít nhất một chủ, không tự xoá tài khoản đang đăng nhập. - Cài như ứng dụng
iPhone: Chia sẻ → Thêm vào MH chính. Android: ⋮ → Cài đặt ứng dụng. Máy tính: biểu tượng cài ở thanh địa chỉ. Service worker chỉ cache vỏ app — mọi lời gọi API luôn đi thẳng ra mạng, số liệu không bao giờ cũ.
16 mục — mỗi mục để làm gì
| Mục | Dùng để | Nối với |
|---|---|---|
| Hộp thư | Chat với khách qua số Zalo cá nhân của shop + xem thẻ đơn của khách đang nói chuyện | gateway, mục 10 |
| Đơn hàng | Danh sách đơn từ app, đổi trạng thái (đẩy ngược lên Pancake), xác nhận đã nhận tiền (báo kết quả về Checkout SDK), xem chi tiết bóc tách voucher/điểm | Pancake, Zalo |
| Sản phẩm | Thêm/sửa/xoá, ẩn–hiện, sửa giá biến thể; thêm là đẩy thẳng lên Pancake | Pancake |
| Tồn kho & Giá | Bảng biến thể: giá, giá sau giảm, tồn (đỏ khi ≤ 5), tìm theo tên/barcode | Pancake |
| Khuyến mãi | Voucher: mã, mức giảm, điều kiện, hạn; khách tự thu thập trong app | D1 |
| Quà đổi điểm | Danh mục quà, số điểm đổi, trạng thái giao quà | D1 |
| Vòng quay | Giải thưởng, tỉ lệ, ảnh, lượt quay/ngày | D1 |
| Đại lý | Xét đơn xin làm đại lý gửi từ app | D1 |
| Giới thiệu | Mức thưởng người giới thiệu / người được giới thiệu, xem cây giới thiệu | D1 |
| Bài viết | Bài lấy tự động từ website; nút Đồng bộ để đăng xong lấy về ngay | enzara.vn |
| Thành viên | Hồ sơ khách, điểm, hạng, điều chỉnh điểm thủ công (có ghi lý do) | D1 |
| Trợ lý | Bật/tắt bot từng kênh, nạp khoá mô hình, xem hội thoại | mục 09 |
| Tin nhắn | Gửi tin ZBS/OA cho khách, nhật ký đã gửi, nối lại quyền OA | mục 08 |
| CRM | Khai URL + secret nơi nhận sự kiện đẩy ra ngoài (đổi không cần deploy) | mục 11 |
| Thiết lập | Quy đổi điểm, hạng & quyền lợi, phí ship, kho mặc định, người dùng | D1 |
| Đổi mật khẩu | Tài khoản đang đăng nhập | — |
16 mục chia đều thanh dưới thì mỗi ô chưa tới 24px, bấm không trúng. Nay thanh dưới giữ 4 mục hay dùng
(NAV_CHINH: Hộp thư · Đơn hàng · Sản phẩm · Tồn kho) cộng nút Thêm mở bảng trượt
chứa đủ 16 mục. Máy tính giữ sidebar dọc như cũ.
04Mini App — giao diện khách
Mini App (gốc repo)
zmp · React- Để làm gì
- App khách chạy trong Zalo: 24 tuyến đường (trang chủ, danh mục, sản phẩm, giỏ, đơn, điểm, quà,
vòng quay, đại lý, bài viết, ưu đãi, cá nhân, và
/chan-doanđể dò máy khách). - Cách dùng
npm start(dev trên trình duyệt) ·zmp start -Z(dev trên điện thoại thật). Bật/tắt giao diện v2 bằng?uiV2=1/?uiV2=0ngay trên URL — cách duy nhất tiện khi test máy thật vì không có console.- Tệp
src/router.tsx·src/state.ts(Jotai) ·src/hooks.ts·src/config.ts(cờ UI v2) ·src/utils/*·app-config.json.
Bốn sửa đổi bắt buộc trong vite.config.mts
Thiếu bất cứ cái nào thì preview trên điện thoại ra màn hình trắng, không báo lỗi:
| Sửa | Vì sao |
|---|---|
server.host = true | Vite mặc định chỉ nghe 127.0.0.1. iPhone không có adb, nó tải app qua IP LAN → cổng 2999 thật ra không mở. |
server.allowedHosts = true | Vite 5+ chặn Host header lạ; điện thoại gọi bằng IP LAN nên bị coi là lạ. |
| Plugin ép header CORS cho mọi response | Zalo bọc app trong trang HTTPS h5.zdn.vn nhưng tải tài nguyên từ localhost:2999. |
Tự phục vụ /src/app.js | Dev loader của Zalo xin entry này, không qua index.html → thiếu preamble React Refresh → component ném lỗi. Phải cài preamble rồi import() động app.ts (import tĩnh bị hoist, chạy trước preamble). |
Quy trình deploy — bắt buộc 3 bước
zmp build zmp sync-config www/index.html <-- thiếu bước này = MÀN HÌNH TRẮNG zmp deploy -p -e -m "mô tả" # thêm -t nếu cần bản TESTING
Zalo không upload index.html. Nó nạp CSS/JS theo listCSS / listSyncJS
trong app-config.json, mà mỗi lần build Vite sinh mã băm tên tệp mới. Không đồng bộ thì app đi tìm
tệp không tồn tại và dừng ở màn hình trắng, im lặng.
Kiểm nhanh sau khi build:
node -e "const c=require('./app-config.json'),fs=require('fs');
console.log([...c.listCSS,...c.listSyncJS.filter(x=>x.startsWith('./'))]
.every(f=>fs.existsSync('www/'+f.replace('./',''))) ? 'KHỚP' : 'LỆCH')"
Hạn mức deploy — đừng đốt bản TESTING
| Môi trường | Hạn mức | Lệnh | Dùng khi |
|---|---|---|---|
| DEVELOPMENT | 300 lượt | zmp deploy -p -e -m "…" | Sửa tới sửa lui, tự test |
| TESTING | 60 lượt | zmp deploy -p -t -e -m "…" | Chỉ khi cần bản cho người khác test |
Lệnh không có -t đi vào DEVELOPMENT, không phải production: Mini App không có đường
deploy thẳng ra khách, mọi bản phát hành phải gửi duyệt trên Portal. zmp deploy chỉ in mã QR,
link có dạng https://zalo.me/s/<appId>/?env=TESTING&version=<n>.
Test trên điện thoại thật
zmp start -Z # Android: qua adb reverse zmp start -Z -ios -iosH <IP-LAN> # iPhone: qua Wi-Fi adb shell screencap -p /sdcard/a.png # chụp màn hình khi không có console
Trang /chan-doan là bộ đo trên máy khách: nó gọi lần lượt các API zmp-sdk, đo bộ nhớ ba tầng,
báo cầu nối native sống hay chết, rồi gửi kết quả về /api/client-log. Đây là thứ duy nhất cho biết
máy khách hỏng ở đâu khi không có DevTools.
05Thanh toán — phần dễ bị trả về nhất
Zalo yêu cầu mọi giao dịch trực tuyến phải đi qua Checkout SDK. COD không tính là trực tuyến.
Zalo Checkout SDK
Worker kýApp gọi- Để làm gì
- Mở bảng thanh toán của Zalo, nhận kết quả, và cho phép shop báo ngược “đã nhận tiền”. Private Key là khoá bí mật nên mọi chữ ký phải ký ở Worker, không bao giờ ở app.
- Cách dùng
- Khai tại Portal → Checkout SDK → Cấu hình chung: Callback URL
/webhook/zalo-checkout/callback, Notify URL/webhook/zalo-checkout/notify, rồi nạpZALO_CHECKOUT_PRIVATE_KEYbằngwrangler secret put. - Tệp
backend/src/zalo-checkout.ts·routes/orders.ts·src/utils/checkout.ts·src/pages/cart/pay.tsx.
Bốn công thức chữ ký, khác nhau hoàn toàn
Đây là chỗ mất thời gian nhất. Chép sai một dấu & là Zalo báo “sai mac” mà không nói sai ở đâu.
| Dùng ở | Chuỗi đem ký (HmacSHA256 với privateKey) |
|---|---|
createOrderapp gọi, Worker ký trước | Nối các trường đơn theo thứ tự quy định — KHÔNG có &privateKey= ở cuối |
updateOrderStatusshop báo đã nhận tiền | appId=…&orderId=…&resultCode=…&privateKey=… |
get-statustra lại trạng thái | appId=…&orderId=…&privateKey=… |
Kiểm notify / callback | Nối key=value theo đúng thứ tự Zalo gửi tới (verifyOverallMac bảo toàn cả trường không nằm trong mac) |
So sánh chữ ký luôn dùng timingSafeEqual, không dùng ===.
1. Đừng await createOrder trước khi điều hướng. Bảng thanh toán của Zalo
chỉ trình ra khi app đang điều hướng. Chờ kết quả trước rồi mới navigate = nút Thanh toán xoay vĩnh viễn.
2. Zalo chặn tần suất -1409. Bấm dồn hai cú sát nhau là đường ngắn nhất tới đó,
và lần bấm sau nữa Zalo không trả lời gì cả. Khi đã dính -1409 thì
không được tự đổi đơn chuyển khoản sang COD — trước đây lỗi này làm khách bị thu tiền
khi nhận hàng oan. Chống bấm dồn ở src/utils/checkout.ts.
SePay — đối soát chuyển khoản
Worker- Để làm gì
- Nghe webhook “tiền đã về” rồi khớp với đơn đang chờ, tự chuyển đơn sang đã thanh toán và báo kết quả về Checkout SDK.
- Cách dùng
- Khai webhook SePay trỏ tới
/webhook/sepay, nạpSEPAY_WEBHOOK_APIKEY+SEPAY_API_TOKEN. Nên dùng VA riêng cho Mini App để tách khỏi webhook của web. - Lưu ý
- Đối soát không lọc theo số tài khoản: một hồ sơ SePay có thể gắn nhiều tài khoản, và số
định danh khác số hiển thị cho khách. Xem
findMatchingTransactiontrongsepay-api.ts.
Máy nào có cầu nối native chết cả phiên vẫn phải đặt được hàng: đơn COD, không qua
Checkout SDK (POST /api/orders/:id/cod). Và với máy không mở nổi app thì còn đường ngoài app ở mục 12.
06Khách hàng, điểm, hạng và các cần bán
Tất cả nằm trên D1, chỉnh được từ CRM, Mini App chỉ đọc — không chép hằng số xuống app.
| Module | Để làm gì | Cách dùng | Nơi ở |
|---|---|---|---|
| Hồ sơ & điểm | Một hồ sơ theo zaloUserId; tích 10.000đ = 1 điểm, tiêu 1 điểm = 1.000đ |
App gọi POST /api/points/sync mỗi lần mở; CRM điều chỉnh tay có ghi lý do |
loyalty.ts, mg 003 |
| Hạng thành viên | Xét theo tổng chi tiêu tích luỹ, hệ số nhân vào điểm tích mỗi đơn + quyền lợi riêng | Thiết lập → Hạng. App đọc tier/nextTier từ /points/sync |
mg 005, 014 |
| Voucher | Mã giảm giá; khách thu thập voucher về ví của mình rồi mới dùng | /api/vouchers, /api/vouchers/collect, /api/vouchers/mine |
promotions.ts, mg 002, 009 |
| Quà đổi điểm | Đổi điểm lấy quà, có trạng thái giao quà để shop theo dõi | /api/points/rewards → /redeem; CRM đổi trạng thái |
mg 003, 015 |
| Giới thiệu | Mỗi khách một mã; người giới thiệu và người được giới thiệu đều được thưởng (mặc định +10/+10) | Link app kèm mã → app lưu referredBy → cộng khi đơn đầu hoàn tất |
referrals.ts |
| Vòng quay | Giữ khách quay lại mỗi ngày; giải thưởng và tỉ lệ do shop đặt | /api/spin/config, /api/spin/play |
spin.ts, mg 010, 012 |
| Đại lý | Khách xin làm đại lý ngay trong app, shop xét trong CRM | /api/agents/apply, /api/agents/mine |
agents.ts, mg 011 |
| Bài viết | Kéo bài từ website vào app để có nội dung giữ chân | Cron 6h sáng + nút Đồng bộ trong CRM; /api/posts |
posts.ts, mg 018 |
| Điểm cho khách mua web | Khách mua trên enzara.vn cũng được tích điểm, tiêu điểm ngược lại trên web | Webhook WooCommerce /webhook/woocommerce + /web/points/* với WEB_POINTS_API_KEY |
routes/web.ts, mg 004 |
| Địa chỉ giao hàng | Tỉnh/huyện/xã + địa chỉ đã lưu của khách | /api/address/*; lưu ở server theo zaloUserId, không chỉ ở máy |
routes/address.ts, mg 020 |
Có iPhone không lưu được gì ở máy (cả localStorage lẫn bộ nhớ bền đều chết, im lặng không báo lỗi). Nếu địa chỉ chỉ nằm ở máy thì khách đó không bao giờ thanh toán được. Kèm theo: không cho xoá địa chỉ cuối cùng.
07Vận chuyển và theo dõi đơn
Theo dõi đơn + báo khách
Worker- Để làm gì
- Dò trạng thái đơn bên Pancake, khi đổi mốc thì nhắn cho khách (đã xác nhận · đang giao · đã giao), kèm mã vận đơn và link tra cứu của chính hãng vận chuyển. Mỗi mốc chỉ nhắn một lần.
- Cách dùng
- Chạy migration 031, khai mẫu tin cho từng mốc. Không có mẫu tin cho mốc nào thì mốc đó im lặng — đó là hành vi đúng, không phải lỗi. Thẻ đơn hiện luôn trong khung chat của Hộp thư.
- Tệp
backend/src/theo-doi-don.ts·van-chuyen.ts(link tra cứu từng hãng) · bảngdon_bao_khach.
Phí ship & điểm nhận hàng
WorkerCRM- Để làm gì
- Bảng phí theo vùng + tuỳ chọn “lấy tại cửa hàng”. Kho mặc định khi lấy tại cửa hàng đặt bằng khoá
default_station_id— không lấy phần tử đầu Pancake trả về (thứ tự đó không do mình quyết, đã có khách vô tình đặt về sai chi nhánh). - Cách dùng
Thiết lập → Phí ship; app đọc/api/pricingvà/api/stations.- Tệp
pricing.ts·settings.ts· mg 017.
08Tin nhắn ra khách — ZBS thay ZNS
Zalo gộp ZNS và tin UID thành ZBS. Có 4 loại tin, mỗi loại một cửa sổ thời gian và một mức giá,
và có trần tự đặt để không bao giờ vượt ngân sách. Khung giờ gửi trong hệ thống là 8h–20h
(hằng TRAN trong backend/src/zbs.ts) — hẹp hơn khung cron CRM, cố ý, vì đây là nhắn cho người.
Zalo OA + ZBS
WorkerCRM- Để làm gì
- Gửi tin có mẫu cho khách (đơn hàng, điểm thưởng, nhắc giao hàng) và nhận tin khách gửi vào OA.
- Cách dùng
- Nạp
ZALO_OA_APP_ID+ZALO_OA_SECRET, đăng ký callback/webhook/zalo-oauth, rồi bấm nút cấp quyền trong CRM (Hộp thư → Số Zalo → dòng Zalo OA, hoặc tab Tin nhắn). Token tự làm mới sau đó. - Tệp
zalo-oa.ts·zbs.ts· mg 006, 022.
Khách quan tâm OA được cộng điểm một lần trọn đời (/api/points/claim-oa-follow) — đây là cách đổi
lượt quan tâm OA thành thứ khách thấy đáng.
09Trợ lý tự động — ba kênh
Bot đa kênh
Worker- Để làm gì
- Trả lời khách tự động trên Mini App (khung chat trong app), Zalo OA và
Messenger. Bot tra được sản phẩm, tra địa chỉ, và lên đơn —
nó gọi lại chính
POST /api/orderscủa Worker nên đơn bot chốt giống hệt đơn khách tự đặt. - Cách dùng
- Chạy mg 023–026 → nạp
BOT_API_KEY(nạp qua CRM, khoá được mã hoá trước khi lưu, KEK làADMIN_JWT_SECRET) → bật từng kênh trong tab Trợ lý. Khi dev, đặtBOT_SELF_URLđể đơn thử không bay lên bản thật. - Tệp
backend/src/bot/engine.ts·bot/cong-cu.ts(công cụ bot gọi được) ·bot/kenh/{oa,messenger}.ts·bot/ma-khoa.ts.
Cổng đang dùng (BOT_BASE_URL) trả SSE dù đã xin stream:false,
nên phải tự bóc dòng data:. Và chỉ nhóm model gh/gpt-4* dùng được ổn định —
đổi sang nhóm khác thì bot câm mà không báo lỗi.
Bot im lặng là hành vi đúng trong ba trường hợp: khách đang được người thật trả lời, câu hỏi vượt phạm vi đã khai, và ngoài khung giờ cho phép. Khi bot câm ngoài ba cái đó, tra theo thứ tự: khoá mô hình → kênh đã bật chưa → nhật ký hội thoại trong tab Trợ lý.
10Hộp thư Zalo cá nhân — khối thứ tư
Đây là phần mới nhất của bản 2, và là phần duy nhất không chạy được trên Cloudflare.
zalo-gateway
Node dài hạn- Để làm gì
- Nối tài khoản Zalo cá nhân của shop (thư viện không chính thức
zca-js) với Hộp thư trong CRM: nhận tin khách, đẩy về Worker, và nhận lệnh gửi tin ngược lại. - Cách dùng
-
cd gateway && npm install # .env: WORKER_URL=https://zalo-app.enzara.vn # GATEWAY_SECRET=<khớp secret của Worker> npm run dang-nhap # quét QR thêm một số Zalo npm start # nghe mọi số đã đăng nhập
Thêm số / thoát số cũng làm được ngay trên trang CRM bằng QR. Xem trạng thái:curl http://127.0.0.1:18930/status; giành lại phiên:curl -X POST http://127.0.0.1:18930/ket-noi-lai. - Tệp
gateway/src/index.ts·kenh-lenh.ts(WebSocket nhận lệnh) ·hang-doi.ts·chuan-hoa.ts· Worker:routes/gw.ts,hop-thu/{hub,ws,kho,khach}.ts, mg 028–034.
Bốn sự thật phải thiết kế quanh nó
- Zalo không cho lấy lại lịch sử. zca-js chỉ phát lại tin cũ đúng lúc kết nối. Nên tin tới
phải ghi xuống đĩa trước (
data/hang-doi.ndjson) rồi mới gửi lô lên Worker — không được phép chỉ nằm trong RAM. - Một số Zalo chỉ giữ được một phiên web. Mở Zalo Web của số đó là gateway bị đá
(mã 3000/3003) — và khi đó gateway không tự giành lại, phải gọi
/ket-noi-lai. - Cổng 18930 chỉ một tiến trình. Bật bản mới khi bản cũ còn sống thì chết ngay vì
EADDRINUSE. Tìm tiến trình bằngps -Ao pid,command | grep index.ts—pgrep -f ".../src/index.ts"KHÔNG khớp vì dòng lệnh thật lànode src/index.ts. - “Đã gửi lệnh” ≠ “khách đã nhận”. Số đang mất phiên thì gateway nhận lệnh rồi mới từ chối.
Nên Worker kiểm
hop_thu_tai_khoan.trang_thai = 'dang_ket_noi'trước khi ghi nhật ký thành công.
Chữ ký giữa gateway và Worker:
X-Gw-Signature = hex(HMAC-SHA256("<X-Gw-Timestamp>.<thân>", GATEWAY_SECRET)).
Chưa nạp GATEWAY_SECRET thì /gw/* trả 503 (đóng hẳn); nạp rồi mà ký sai thì 401.
Nhịp tim 30s/lần; quá 3 phút không thấy thì Hộp thư báo “Gateway im lặng”.
zca-js dùng API Zalo không chính thức — tài khoản có thể bị khoá.
Chỉ dùng số Zalo riêng của shop, không dùng số cá nhân, và không nhắn hàng loạt cho người lạ.
11CRM ngoài — hai chiều
Nhánh /crm/*
Worker- Để làm gì
- Cho một hệ chăm sóc khách bên ngoài kéo dữ liệu (khách, điểm, hội thoại, góp ý, hạng, nhật ký tin đã gửi), nhận đẩy sự kiện real-time, và ghi ngược (sửa tên/SĐT, cộng–trừ điểm, đóng ticket).
- Cách dùng
-
npm run db:migrate27:remote npx wrangler secret put CRM_API_KEY # khoá CRM gọi vào npx wrangler secret put CRM_WEBHOOK_SECRET # khoá ký sự kiện đẩy ra # nơi nhận: khai URL trong tab CRM của trang quản trị (đổi không cần deploy)
Tài liệu đấu nối tự sinh, công khai tại/crm/docs(npm run docs:crm→docs/crm-docs.html) — gửi link này cho đối tác là đủ. - Tệp
backend/src/crm.ts·crm-su-kien.ts·routes/crm.ts·crm-docs.ts·docs/crm-integration.md.
- Chưa đặt
CRM_API_KEY→ toàn bộ/crm/*trả 503. Không có khoá thì không mở cửa. - Chưa khai nơi nhận → chiều đẩy tắt (sự kiện vẫn được ghi, cron dọn ngay), chiều kéo vẫn chạy.
- Không đặt
CRM_WEBHOOK_SECRET→ sự kiện gửi đi không có chữ ký; chỉ chấp nhận khi CRM nằm trong mạng nội bộ. - Đường đi chính là bắn thẳng ngay sau khi Worker trả lời xong lượt sinh ra sự kiện (trễ mili-giây). Cron 10 phút chỉ nhặt hai loại sót: lần gửi trước thất bại, và sự kiện sinh ra ban đêm.
- CRM phải tự chống nhận trùng theo id sự kiện.
- Không có ở đây: chi tiết đơn hàng.
/crm/customerschỉ trả tổng hợp; đơn gốc nằm ở Pancake.
CRM chiếm cổng 8787, nên wrangler dev phải đổi cổng (dự án đang dùng 8799).
12Đặt hàng ngoài Mini App
Trang /dat-hang
Worker phục vụ- Để làm gì
- Đường sống cho khách mà Zalo không khởi động nổi Mini App (đo được: 3/10 lần trên một số iPhone, app không chạy nổi một dòng mã nào). Mọi lưới an toàn bên trong app đều vô nghĩa với họ, nên đường thoát buộc phải nằm ngoài app: shop gửi link qua chat, khách mở bằng trình duyệt, đặt xong đơn vào Pancake y hệt đơn từ app.
- Cách dùng
- Không cần cài gì — trang do chính Worker phục vụ. Gửi link cho khách là xong. Đơn từ đây
luôn là COD và không cần
zaloUserId: backend tự tìm lại chủ đơn theo số điện thoại nên khách cũ vẫn vào đúng hồ sơ và vẫn được cộng điểm. - Tệp
backend/src/dat-hang.ts.
Ba ràng buộc thiết kế của trang này, không được vi phạm khi sửa:
- Tự chứa hoàn toàn Không CDN ngoài, không font ngoài, không thư viện. Khách đang ở đây vì app đã hỏng — không được để họ vấp thêm lần nữa.
- Cùng gốc với API Gọi
/api/...bằng đường dẫn tương đối: không CORS, không preflight. - Không tin giá ở client Trang chỉ hiện tạm tính; tổng tiền thật do
POST /api/orderstính lại từ Pancake. Khách sửa HTML cũng không ăn gian được.
13Mười cái bẫy đã trả giá
Mỗi dòng dưới đây từng làm mất nửa ngày đến hai ngày. Đọc trước sẽ rẻ hơn gặp lại.
| # | Hiện tượng | Nguyên nhân thật & cách xử |
|---|---|---|
| 1 | Mini App màn hình trắng sau deploy | Thiếu zmp sync-config giữa build và deploy. Luôn 3 bước. |
| 2 | Trang trắng 30 giây trên mạng VN | Google Fonts / CDN nước ngoài. Mọi font và thư viện phải tự host. |
| 3 | Nút Thanh toán xoay vĩnh viễn | Cầu nối native của Zalo im lặng. Mọi lời gọi zmp-sdk phải bọc goiZalo() có mốc chờ. |
| 4 | Giải mã SĐT trả -501 | Zalo chặn giải mã từ IP ngoài VN. Phải có proxy PHP trên hosting VN (PHONE_PROXY_URL, mẫu ở docs/zalo-phone.php.example). |
| 5 | iPhone mất địa chỉ vừa nhập, bấm “Xong” không phản ứng | localStorage ném lỗi khi GHI nhưng ĐỌC vẫn chạy → Jotai ghi đè giá trị RAM bằng rỗng. Dùng safeStorage ba tầng (RAM → nativeStorage → localStorage); cấm gọi localStorage trực tiếp. |
| 6 | iPhone kẹt mãi ở Splash Loading của Zalo | App chưa chạy dòng nào, không sửa được bằng mã. Giải pháp là đường ngoài app (mục 12). |
| 7 | Pancake từ chối đơn “tồn kho không đủ” dù còn hàng | Bỏ trống warehouse_id. Phải luôn gửi kho. |
| 8 | Đơn chuyển khoản tự biến thành COD | Bị Zalo chặn tần suất -1409 và code cũ coi im lặng là thất bại. Không được tự đổi phương thức. |
| 9 | npm run db:migrate9:remote chạy nhưng không migrate | zsh hiểu :r là cắt đuôi tệp. Dựng tên bằng printf rồi npm run "$M". |
| 10 | Bộ đo im lặng, tưởng app lỗi | Khoá bộ đo theo loại máy làm “im lặng” mang hai nghĩa. Bộ đo không được tự tắt — mất 35 phút vì đúng chuyện này. |
14Nghiệm thu và phát hành
Thứ tự nghiệm thu
- Máy tính, trình duyệt
npm start+wrangler dev: đủ luồng mua từ trang chủ tới đơn. - Điện thoại Android
zmp start -Z: kiểm cầu nối native, quyền, SĐT. - Điện thoại iPhone
zmp start -Z -ios -iosH <IP>+/chan-doan: kiểm ba tầng bộ nhớ và cầu nối. Coi iOS là máy chạy không trạng thái. - Bản DEVELOPMENT Đặt thử một đơn COD và một đơn chuyển khoản thật (số tiền nhỏ), xác nhận tiền về và trạng thái đổi.
- Bản TESTING Chỉ khi cần người khác test. 60 lượt, dùng dè.
- Gửi duyệt Kèm tài khoản test, video luồng mua, trang điều khoản + chính sách.
Checklist trước khi gửi duyệt
- Mọi giao dịch trực tuyến đi qua Checkout SDK (COD được để ngoài).
- Không còn màn hình trống, không còn dữ liệu mẫu sót lại.
- Trang chính sách quyền riêng tư + điều khoản có URL sống.
- Chỉ xin đúng những quyền app thật sự dùng, và có giải thích trong app trước khi xin.
- Đơn thật đã chạy trọn vòng: app → Worker → Pancake → SePay/Checkout → tin báo khách.
Ngay sau khi lên sóng
- Bật
npx wrangler tailtheo dõi ngày đầu; xem/api/client-logđể biết máy khách hỏng ở đâu. - Kiểm gateway còn sống (nhịp tim < 3 phút) và cron có chạy đúng giờ VN.
- Theo dõi hạn mức: ZBS (trần tự đặt), gọi mô hình của bot, và số lượt deploy còn lại.
Xác thực OA doanh nghiệp (1–3 tuần, không tự tăng tốc được) · bốn công thức mac của Checkout SDK (nửa ngày đến hai ngày nếu chép sai) · những cái bẫy ở mục 13 (mỗi cái nửa ngày nếu chưa biết trước).
APhụ lục — biến và khoá bí mật
Khoá bí mật nạp bằng npx wrangler secret put <TÊN>; biến công khai khai trong [vars] của wrangler.toml. Dev local: backend/.dev.vars.
| Tên | Loại | Thiếu thì sao |
|---|---|---|
PANCAKE_API_KEY · PANCAKE_SHOP_ID | Bí mật | Không có sản phẩm, không tạo được đơn |
ADMIN_JWT_SECRET | Bí mật | Không đăng nhập được CRM; cũng là KEK mã hoá khoá bot |
ZALO_CHECKOUT_PRIVATE_KEY | Bí mật | Không thanh toán trực tuyến được → app không được duyệt |
ZALO_APP_SECRET | Bí mật | Không giải mã được số điện thoại khách |
SEPAY_API_TOKEN · SEPAY_WEBHOOK_APIKEY | Bí mật | Không đối soát tự động được chuyển khoản |
ZALO_OA_APP_ID · ZALO_OA_SECRET | Bí mật | Không gửi/nhận tin OA, không gửi ZBS |
BOT_API_KEY | Bí mật | Trợ lý không trả lời câu nào |
MESSENGER_VERIFY_TOKEN · _APP_SECRET · _PAGE_TOKEN | Bí mật | Kênh Messenger tắt (phần còn lại chạy bình thường) |
CRM_API_KEY | Bí mật | /crm/* trả 503 — nhánh CRM ngoài đóng |
CRM_WEBHOOK_SECRET | Bí mật | Sự kiện đẩy đi không có chữ ký |
GATEWAY_SECRET | Bí mật | /gw/* trả 503 — hộp thư Zalo cá nhân đóng |
WOO_WEBHOOK_SECRET · WEB_POINTS_API_KEY | Bí mật | Khách mua trên web không được tích/tiêu điểm |
PHONE_PROXY_SECRET | Bí mật | Proxy SĐT từ chối |
DIAG_TOKEN | Bí mật | Không gọi được /api/_pancake/* để chẩn đoán |
DEFAULT_WAREHOUSE_ID | Công khai | Pancake từ chối đơn “tồn kho không đủ” |
ZALO_MINI_APP_ID | Công khai | Không gọi được updateOrderStatus |
PHONE_PROXY_URL | Công khai | Giải mã SĐT trả -501 |
ALLOWED_ORIGINS | Công khai | Chỉ để tham chiếu (Worker phản chiếu origin) |
BOT_BASE_URL · BOT_SELF_URL | Công khai | Bot gọi sai cổng mô hình / đơn thử bay lên bản thật |
MESSENGER_GRAPH_VERSION | Công khai | Lời gọi Meta trả 400 khi bản cũ bị khai tử |
Phía Mini App
VITE_API_URL (chỉ dev; bản thật đọc apiUrl trong app-config.json) ·
VITE_UI_V2 (đặt 0 để build ra giao diện cũ) · ZMP_TOKEN trong .env (đã gitignore).
Phía gateway
WORKER_URL · GATEWAY_SECRET trong gateway/.env. Phiên Zalo lưu ở
gateway/data/sessions/<ownId>.json (quyền 0600) — đây là dữ liệu đăng nhập thật, không commit, không sao lưu lên chỗ chung.
BPhụ lục — 33 migration, chạy đúng thứ tự
db/schema.sql chạy trước (có sẵn admin_users), rồi 002 → 034.
| Mã | Thêm gì | Mã | Thêm gì |
|---|---|---|---|
| 002 | Voucher | 019 | Nhật ký máy khách |
| 003 | Điểm & quà đổi điểm | 020 | Địa chỉ lưu ở server |
| 004 | Tích điểm từ web | 021 | Hồ sơ khách mở rộng |
| 005 | Hạng thành viên | 022 | Tin ZBS |
| 006 | Zalo OA | 023 | Trợ lý: hội thoại |
| 007 | Trạng thái đồng bộ | 024 | Trợ lý: hồ sơ |
| 008 | Zalo Checkout | 025 | Trợ lý: lên đơn |
| 009 | Ví voucher của khách | 026 | Trợ lý: chai tặng |
| 010 | Vòng quay | 027 | Hàng đợi sự kiện CRM |
| 011 | Đại lý | 028 | Hộp thư: nền tảng |
| 012 | Ảnh giải thưởng | 029 | Hộp thư: gửi tin |
| 013 | Bảng cấu hình app_settings | 030 | Hộp thư: bot gợi ý |
| 014 | Quyền lợi theo hạng | 031 | Theo dõi đơn, báo khách |
| 015 | Trạng thái giao quà | 032 | Hộp thư: ảnh & tệp |
| 016 | Siết an toàn dữ liệu | 033 | Hộp thư: @nhắc tên |
| 017 | Phí vận chuyển | 034 | Hộp thư: SĐT khách |
| 018 | Bài viết |
CPhụ lục — bản đồ endpoint
| Nhánh | Ai gọi | Xác thực | Gồm |
|---|---|---|---|
/api/* | Mini App, trang /dat-hang | Không (theo zaloUserId) | products · categories · stations · banners · pricing · vouchers(+mine,collect) · posts · spin · referral · agents · phone · client-log · orders · points · address · bot |
/admin/* | CRM quản trị | JWT | login · seed · nguoi-dung · products · inventory · orders(+status, mark-paid) · promotions · rewards · redemptions · members(+adjust) · spin · sync · messages · crm · settings |
/webhook/* | Hệ ngoài gọi vào | Chữ ký riêng từng hệ | sepay · pancake · woocommerce · zalo-oa · zalo-oauth · zalo-connect · zalo-checkout/{notify,callback} · messenger |
/crm/* | CRM ngoài | CRM_API_KEY | ping · customers(+points, PATCH) · points · conversations · feedback · messages · tiers · events(+drain, retry-failed) · docs |
/gw/* | zalo-gateway | HMAC GATEWAY_SECRET | su-kien · trang-thai · ws (WebSocket kênh lệnh) |
/web/points/* | enzara.vn | WEB_POINTS_API_KEY | summary · spend · refund |
/api/_pancake/* | Người dựng hệ thống | DIAG_TOKEN | ping · warehouses · variations |