Biz Telegram Payment Notify — Sepay 收款即時提醒
此技能將 Telegram 機器人通知 接入 Next.js 專案中既有的 Sepay Webhook。每當客戶成功轉帳至與 Sepay 連結的銀行帳戶時,技能將透過 Telegram Bot 即時推送一則訊息到擁有者(或業務團隊群組)的聊天室,訊息包含客戶姓名、手機號碼、金額、產品名稱及時間。
設計理念:給客戶的 Email 自動回覆(Resend)與給擁有者的 Telegram 警報服務兩個不同的角色。Email 是給客戶的正式文件。Telegram 則是給擁有者的「 Ping 」——當您在外出時,打開 Telegram 就能立即看到新訂單,無需在 Gmail 的眾多郵件中翻找。Telegram 延遲 < 2 秒,而 Email 則需 30-60 秒。
為什麼選擇 Telegram 而不是 Slack/Discord/Zalo:Telegram 機器人設定只需 5 分鐘(1 個 Token + 1 個 Chat ID),無需工作空間或伺服器管理員,免費且無限訊息,推播通知比 Email 更強大,且擁有原生的 iOS/Android/桌面應用程式。Slack 需要工作空間與邀請。Discord 需要伺服器與權限設定。Zalo OA 則需要經過驗證的商業帳號(審核需 3-7 天)以及每月活躍用戶費用。對於擁有者端的警示,Telegram 是阻力最小的路徑。
技能輸出:程式碼修補、機器人設定逐步指南、環境變數清單、三級測試計畫。技能不會自動測試發送 Telegram——要求使用者先提供 Bot Token 與 Chat ID,因為這些憑證屬於使用者的資產。
何時使用此技能
- 使用者剛完成 Sepay Webhook 的接入,並希望在客戶轉帳時透過 Telegram 獲得即時通知。
- 使用者已經擁有
/api/payment-success(或類似路徑)來處理 Resend Email,並希望平行新增 Telegram 警報——而不是建立新的路由或複製邏輯。 - 使用者說「有訂單時通知我」,且情境是使用 Sepay 整合的銷售頁面。
請勿使用此技能的情況:
- 使用者想要 Slack/Discord 通知 → 這是不同的使用案例。
- 使用者想要 Zalo OA 機器人 → 需要經過驗證的商業 OA 帳號且成本結構不同。
- 使用者尚未設定 Sepay Webhook(尚未有銀行帳戶連結 Sepay,尚未有接收 Webhook 的端點)→ 此技能不會從頭設定 Sepay,只會接入既有的 Webhook。
- 使用者只需要給擁有者的 Email →
biz-email-setup已經提供了 Email B(擁有者通知)。
總體工作流程(6 個階段)
階段 0:偵測既有的 Sepay Webhook 路由(路徑 + 框架 + 處理器結構)
↓
階段 0.5:偵測客戶資料儲存(來自 /biz-setup-sepay-payment 的 lib/leads-kv.ts)
→ 有:使用實際的 KV 查找
→ 無:提供 2 個選項(原始模式 或 先執行 /biz-setup-sepay-payment)
↓
階段 1:引導使用者建立 Telegram 機器人(BotFather → Token → Chat ID)
↓ [閘門 — 等待使用者提供 Token + Chat ID]
↓
階段 2:草擬訊息格式 → 向使用者顯示預覽 → 確認
↓
階段 3:接入程式碼(lib/telegram.ts 輔助函式 + 整合至 Webhook 處理器 + 環境變數)
↓
階段 4:測試計劃(直接 curl 測試機器人 → 模擬 Sepay 負載 → 正式環境驗證)
階段 1 和 4 設有閘門等待使用者,因為需要使用者手動操作(取得 Token、驗證正式環境)。階段 0/0.5/2/3 則由技能主動執行。
階段 0 — 偵測既有的 Sepay Webhook
此技能會接入既有的 Webhook,不會建立新的路由。首先定位處理器。
尋找 Webhook 路由 — 根據常見關鍵字進行 grep:
# 在專案根目錄中
grep -r "sepay" --include="*.ts" --include="*.js" --include="*.tsx" -l
grep -r "transferAmount\|transferType\|payment-success\|payment-webhook" --include="*.ts" --include="*.js" -l
3 種常見情境:
| 情境 | 既有的路徑 | 行動 |
|---|---|---|
使用者使用 biz-email-setup 管線 | app/api/payment-success/route.ts(或 pages/api/payment-success.ts)已有 Resend 呼叫 sendCustomerEmail + sendOwnerEmail | 將 sendTelegramNotification 接入同一個 Promise.allSettled 中 |
| 使用者有 Sepay 專用路由 | app/api/sepay-webhook/route.ts 或 app/api/webhook/sepay/route.ts | 在解析並驗證 Sepay 負載後(transferType === 'in' 分支)接入 |
| 找不到任何路由 | — | 停止。通知使用者:「我找不到接收 Sepay 的 Webhook 路由。請您先設定 Sepay Webhook(將銀行連結至 Sepay → 建立指向您網域的 Webhook URL),完成後再回來讓我接入 Telegram。或者,如果您的 Webhook 使用其他端點名稱,請告訴我路徑。」 |
讀取現有處理器以了解其結構:
- 是否對 Sepay 請求進行驗證?(
Authorization標頭的Apikey或 HMAC) - 是否根據
id或referenceCode進行去重?(Sepay 在收到非 200 回應時會重試 → 需要去重以避免重複通知) - 是否根據
content(包含訂單 ID 的轉帳訊息)查找客戶? - 回傳給 Sepay 的回應格式:
{ success: true }或純文字200 OK?
如果現有處理器尚未去重 — 提醒使用者,但不要阻止。此技能僅接入 Telegram,去重是付款邏輯的考量。若需要,建議使用者另外加入去重機制。
階段 0.5 — 偵測客戶資料儲存(關鍵)
Sepay Webhook 僅發送 content(客戶轉帳時貼上的文字)、transferAmount 和 referenceCode。Sepay 不知道客戶是誰——必須根據 content(包含訂單 ID)在本機儲存中進行查找。
技能會檢查 3 項:
# 1. 檢查客戶資料儲存輔助函式是否存在
test -f lib/leads-kv.ts || test -f lib/leads-kv.js && echo "lead store: ✓"
# 2. 檢查 Vercel KV 環境變數
grep -E "KV_REST_API_URL|KV_REST_API_TOKEN" .env.local && echo "KV env: ✓"
# 3. 檢查 @vercel/kv 是否已安裝
grep '"@vercel/kv"' package.json && echo "KV package: ✓"
3 種情境:
| 情境 | 行動 |
|---|---|
| 全部 ✓ — 客戶資料儲存已就緒 | 繼續階段 1,在 Webhook 處理器中使用 lib/leads-kv.ts 的 getLeadByOrderId() |
| 缺少 1 項以上 — 尚未設定 | 停止,詢問使用者 2 個選項(見下方) |
若缺少時,詢問使用者:
我發現您還沒有客戶資料儲存(Vercel KV)。Sepay Webhook 不知道客戶是誰——只知道轉帳內容與金額。您想要:
選項 1 — 透過
/biz-setup-sepay-payment進行完整的客戶資料儲存設定(建議):
- 該技能將設定 Vercel KV、建立 lib/leads-kv.ts 檔案,並接入表單提交以在使用者註冊時儲存客戶資料
- 完成後回到這裡,我就能接入 Telegram,查找完整的客戶姓名/電話/Email
- Telegram 警報將顯示:「Nguyễn Văn A — 0901234567 — [email protected] — 499K — AI Agent 課程」
選項 2 — 原始模式:直接轉發 Sepay 負載,不查找客戶資料:
- Telegram 僅顯示:「Vietcombank — 499.000đ — 內容: DH001 0901234567 — 14:32」
- 擁有者需自行根據內容判斷客戶,並與 Resend Email 日誌交叉比對
- 簡單、可立即運作,但使用者體驗較差
您要選擇 1 還是 2?
如果使用者選擇 1 → 暫停此技能,建議執行 /biz-setup-sepay-payment。待使用者完成設定後 → 再回到此技能。
如果使用者選擇 2 → 繼續階段 1,使用原始模板(請參閱 templates/lib-telegram-raw.ts)。
階段 1 — 引導使用者建立 Telegram 機器人
參閱 references/telegram-bot-setup.md 以獲得包含文字截圖的詳細指南。為使用者總結 3 個步驟:
步驟 1 — 透過 @BotFather 建立新機器人
1. 開啟 Telegram(手機/電腦)
2. 搜尋:@BotFather → 點選開始
3. 輸入指令:/newbot
4. BotFather 詢問機器人名稱 — 輸入顯示名稱(例如:「品牌 X 訂單」)
5. BotFather 詢問使用者名稱 — 輸入以 "_bot" 結尾的使用者名稱(例如:brandx_orders_bot)— 必須是全域唯一的
6. BotFather 回傳訊息,其中包含一行:
"Use this token to access the HTTP API: 1234567890:AAxxxxxxxxxxxxxxxxxxxxxx"
7. 複製該 Token,提供給我。
安全性注意事項:此 Token 等於機器人的控制權。請勿提交到 Git 或公開分享。如果不慎洩漏,請回到 @BotFather → 輸入 /token → /revoke 以產生新 Token。
步驟 2 — 取得 Chat ID
個人聊天(僅擁有者接收) — 推薦給個人創業者:
1. 在 Telegram 中,搜尋:@userinfobot → 點選開始
2. 機器人回傳訊息,其中包含 "Id: 123456789" — 複製該數字。
3. 這就是您的 Chat ID。
群組聊天(業務團隊共同接收) — 推薦給團隊:
1. 建立一個新的 Telegram 群組(或使用現有的群組)
2. 點選群組名稱 → 新增成員 → 搜尋您剛建立的機器人(根據使用者名稱 @brandx_orders_bot)→ 新增
3. 在群組中輸入:/start@brandx_orders_bot(或任何訊息)
4. 開啟瀏覽器,存取:
https://api.telegram.org/bot<TOKEN>/getUpdates
(將 <TOKEN> 替換為步驟 1 的 Token)
5. 在 JSON 回應中,尋找 "chat":{"id":-1001234567890,...} — 複製該數字(如果有負號,請一併保留)
6. 這就是群組的 Chat ID。
頻道(單向廣播,無法討論)— Chat ID 以 -100 開頭。取得方式與群組相同,但需將機器人新增為頻道管理員。
步驟 3 — 提供給技能
等待使用者提供:
TELEGRAM_BOT_TOKEN=1234567890:AAxxxxxxxxxxxxxxxxxxxxxx
TELEGRAM_CHAT_ID=123456789
重要閘門:在使用者提供這兩個值之前,請不要繼續階段 2。如果使用者說「我已經有了,在其他地方」→ 仍然等待提供,因為需要在階段 3 將它們注入到 .env.local 中。
階段 2 — 草擬訊息格式
預設為越南語,行動裝置友善(Telegram 支援 HTML 渲染):
🎉 <b>新訂單</b>
👤 <b>{{name}}</b>
📞 {{phone}}
📧 {{email}}
💰 <b>{{amount}}</b> — {{product_name}}
🕐 {{time_vn}}
🔗 交易代碼:<code>{{reference_code}}</code>
變數注入後,實際顯示如下:
🎉 新訂單
👤 Nguyễn Văn A
📞 0901 234 567
📧 [email protected]
💰 499.000đ — AI Agent 課程
🕐 14:32 14/05/2026
🔗 交易代碼:FT26134567890
向使用者顯示預覽,並詢問:
- 這個格式可以嗎?需要新增/刪除任何欄位嗎?(例如:新增 UTM 來源、今日訂單數量、客戶 Zalo 連結)
- 表情符號會不會太多?(有些使用者偏好純文字)
- 希望新增一個內聯按鈕(CTA)連結到 CRM/試算表嗎?(進階功能 — 技能可以新增內聯鍵盤)
格式規則:
- HTML 模式(
parse_mode: "HTML")— Telegram 支援<b>、<i>、<u>、<code>、<pre>、<a href>。不支援<br>— 請使用\n。 - 越南盾金額格式:
499.000đ(點號作為千分位分隔符,最後加上đ)或簡潔的499K/1.99M格式。如果有offer.json的voice設定,則依此讀取。 - 電話號碼格式:
0901 234 567(3-3-3 分組)— 在行動裝置上更易於閱讀。 - 越南時間:
HH:mm dd/mm/yyyy— 設定為越南時間(UTC+7),而非 UTC。 - 避免表情符號過多(每則訊息 >5 個表情符號會降低專業感)。
階段 3 — 接入程式碼
3 項變更:
3.1 建立輔助函式 lib/telegram.ts
請參閱 templates/lib-telegram.ts(TypeScript)或 templates/lib-telegram.js(JavaScript)。內容如下:
// lib/telegram.ts
type TelegramPayload = {
name: string;
phone: string;
email: string;
amount: number; // VND,原始數字(例如:499000)
productName: string;
referenceCode?: string;
};
export async function sendTelegramNotification(payload: TelegramPayload): Promise<void> {
const token = process.env.TELEGRAM_BOT_TOKEN;
const chatId = process.env.TELEGRAM_CHAT_ID;
if (!token || !chatId) {
console.error('[telegram] 缺少 TELEGRAM_BOT_TOKEN 或 TELEGRAM_CHAT_ID');
return; // 不拋出錯誤 — Telegram 失敗時不應阻止付款回應
}
const text = formatMessage(payload);
try {
const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text,
parse_mode: 'HTML',
disable_web_page_preview: true,
}),
});
if (!res.ok) {
const errBody = await res.text();
console.error(`[telegram] 發送失敗:${res.status} ${errBody}`);
}
} catch (err) {
console.error('[telegram] 網路錯誤:', err);
// 吞下錯誤 — 不要讓 Telegram 失敗導致 Sepay 重試
}
}
function formatMessage(p: TelegramPayload): string {
const amountStr = p.amount.toLocaleString('vi-VN') + 'đ';
const phoneStr = p.phone.replace(/(\d{4})(\d{3})(\d{3})/, '$1 $2 $3');
const now = new Date().toLocaleString('vi-VN', {
timeZone: 'Asia/Ho_Chi_Minh',
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
});
return [
'🎉 <b>新訂單</b>',
'',
`👤 <b>${escapeHtml(p.name)}</b>`,
`📞 ${phoneStr}`,
`📧 ${escapeHtml(p.email)}`,
`💰 <b>${amountStr}</b> — ${escapeHtml(p.productName)}`,
`🕐 ${now}`,
p.referenceCode ? `\n🔗 交易代碼:<code>${escapeHtml(p.referenceCode)}</code>` : '',
].filter(Boolean).join('\n');
}
function escapeHtml(s: string): string {
return s.replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c]!));
}
為何吞下錯誤:如果 Sepay 收到非 200 回應,它會重試 Webhook。如果 Telegram 超時或故障導致 Webhook 當機 → Sepay 重試 → 每次重試都會重新發送 Email 並記錄付款 → 使用者會為一筆訂單收到 5 封 Email 和 5 則 Telegram 警報。Telegram 是盡力而為的輔助通道,並非關鍵路徑。
3.2 接入 Sepay Webhook 處理器
讀取現有處理器,找到處理付款成功的位置(在解析 Sepay 負載並驗證 transferType === 'in' 之後)。新增:
// app/api/payment-success/route.ts(或類似路徑)
import { sendTelegramNotification } from '@/lib/telegram';
import { sendCustomerEmail, sendOwnerEmail } from '@/lib/resend'; // 如果已存在
export async function POST(req: Request) {
const payload = await req.json();
// [現有] 驗證 Sepay 授權並解析負載
if (payload.transferType !== 'in') {
return Response.json({ success: true }); // 忽略轉出交易
}
// [現有] 根據內容(包含訂單 ID)或 referenceCode 查找客戶
const lead = await lookupLeadByTransferContent(payload.content);
// [新增] 平行展開 3 個副作用 — Telegram 失敗不會封鎖 Email
await Promise.allSettled([
sendCustomerEmail(lead),
sendOwnerEmail(lead),
sendTelegramNotification({
name: lead.name,
phone: lead.phone,
email: lead.email,
amount: payload.transferAmount,
productName: lead.productName,
referenceCode: payload.referenceCode,
}),
]);
return Response.json({ success: true });
}
使用 Promise.allSettled 而非 Promise.all 的原因:
Promise.all若任何一個 Promise 失敗就會拒絕 → 處理器拋出錯誤 → Sepay 收到 500 狀態碼 → 重試 → 重複發送 Email。Promise.allSettled會等待所有 Promise 完成(無論成功或失敗),不會拋出錯誤。
3.3 環境變數 + .gitignore
# .env.local — 由參考文檔處理
TELEGRAM_BOT_TOKEN=1234567890:AAxxxxxxxxxxxxxxxxxxxxxx
TELEGRAM_CHAT_ID=123456789
確認這兩個變數存在於 .env.local 中。如果不存在,則新增。
將 .env.local 加入到 .gitignore 中(如果尚未加入):
# .gitignore
.env.local
如果 .env.local 已被追蹤(git check-ignore .env.local 顯示未忽略),警告使用者:
⚠️
.env.local檔案目前正在被 Git 追蹤。TELEGRAM_BOT_TOKEN是敏感資訊。請執行:git rm --cached .env.local echo '.env.local' >> .gitignore來停止追蹤。完成後請將變數重新貼上到
.env.local中。
階段 4 — 測試計劃
3 個測試級別,按順序執行:
測試 1 — 直接發送測試訊息(透過 curl)
無需等待 Webhook。使用 sendMessage API 直接測試 Bot:
curl -X POST "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/sendMessage" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "<TELEGRAM_CHAT_ID>",
"text": "🧪 <b>測試訊息</b>\n\n✅ Bot 設定成功!\n🕐 $(date +%H:%M\ %d/%m/%Y)",
"parse_mode": "HTML"
}'
預期結果:Telegram 聊天室中收到一則帶有「🧪 測試訊息」和「✅ Bot 設定成功!」的訊息。
測試 2 — 模擬 Sepay 負載
將 lib/telegram.ts 匯入並在建置後執行:
// scripts/test-telegram.js 或直接使用 REPL
import { sendTelegramNotification } from '../lib/telegram';
await sendTelegramNotification({
name: 'Nguyễn Văn A (測試)',
phone: '0901234567',
email: '[email protected]',
amount: 499000,
productName: '測試產品',
referenceCode: 'TEST' + Date.now(),
});
或者,直接向您的 Webhook 端點發送一個模擬請求:
curl -X POST "https://yourdomain.com/api/payment-success" \
-H "Content-Type: application/json" \
-d '{
"id": "test-123",
"transferType": "in",
"transferAmount": 499000,
"content": "測試 - 0901234567 - 訂單001",
"referenceCode": "TESTREF001",
"accountNumber": "1900123456789",
"bankName": "Vietcombank"
}'
預期結果:
- Resend Email 按預期發送(如果已設定)
- Telegram 收到您定義的格式的警報
- API 回應:
{ "success": true }且狀態碼為 200
測試 3 — 正式環境驗證
- 請銀行傳送一筆小額測試轉帳到連結的帳戶(或使用 Sepay 的「 Test Payment 」功能,如果有)
- 檢查使用者端:
- 收到一封格式良好的 Email → ✅
- 在 Telegram 聊天室收到即時警報(< 5 秒) → ✅
- 警報中的金額與實際轉帳金額相符 → ✅
- 故意測試錯誤情境:
- 使
TELEGRAM_BOT_TOKEN無效(暫時修改)→ 確認 Webhook 仍回傳 200 → Email 仍發送 - 還原 Token → 確認 Telegram 恢復正常
- 使
參考文檔
| 檔案 | 說明 |
|---|---|
templates/lib-telegram.ts | TypeScript 範本 — 輔助函式 + 格式化 |
templates/lib-telegram.js | JavaScript 範本(無 TypeScript 專案用) |
templates/lib-telegram-raw.ts | 原始模式範本 — 如果沒有 KV 客戶資料儲存時使用 |
references/telegram-bot-setup.md | 逐步的 Telegram Bot 建立指南(含文字截圖) |
references/sepay-ip-whitelist.md | 需要將 Telegram IP 加入白名單嗎?通常不需要,但做為參考 |
已知邊界案例與處理
| 邊界案例 | 處理方式 |
|---|---|
| 使用者尚未設定 Resend / Email 自動回覆 | 只接入 Telegram;Email 部分不是此技能的必要條件。但需警告使用者。 |
| Webhook 端點使用 HMAC 簽章驗證 | 讀取現有驗證流程,不要在驗證前呼叫 sendTelegramNotification。 |
| 同一個 bot 被多個專案使用 | 使用不同的 Chat ID(每個專案一個群組)。Token 可以相同。 |
| 使用者使用 Pages Router (pages/api) | 提供與 App Router 不同的範本:templates/lib-telegram-pages.ts |
| 使用者想要圖片/檔案類型的警報 | 此版本僅支援文字。圖片需要 sendPhoto API 不同的範本。 |