實戰應用:自動化抓資料與發送通知
在前面的章節中,我們學會了各種排程的「載體」(無論是 Linux 主機、GitHub Actions、或是 Vercel Cron)。現在,我們要將這些火力集中,實作一個真實世界最常見的商業情境:「每日訂閱推播系統」。
情境說明
我們即將打造一個「每日幣圈天氣預報」系統。這隻程式每天早上 8 點會做三件事情:
- 去免費的 Supabase 資料庫 讀取所有訂閱我們服務的使用者 (Line Notify Token)。
- 去呼叫外部 API 抓取最新的比特幣價格。
- 利用迴圈,自動將最新報價透過 Line Notify 發送給所有訂閱者。
你可以把這個腳本放在 GitHub Actions 上跑,也可以寫成 Next.js API 掛在 Vercel 上。這裡我們以 Node.js (TypeScript) 腳本為例。
前置準備
- Supabase 資料庫:建立一個名為
subscribers的 Table,包含兩個欄位:id(主鍵)、line_token(字串)。 - Line Notify:前往 Line Notify 官方網站,申請並產生你自己的個人存取權杖 (Token)。並手動將這個 Token 新增到剛剛的 Supabase 資料庫中。
核心實戰程式碼
讓我們先安裝必要的套件:
npm install @supabase/supabase-js axios
接下來是這支威力強大的排程腳本 daily-job.ts:
import { createClient } from '@supabase/supabase-js';
import axios from 'axios';
// ==========================================
// 1. 環境變數設定
// ==========================================
// 在真實環境中,這些請使用 process.env 讀取,不要寫死在程式碼中!
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://your-project.supabase.co';
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || 'your-service-role-key';
// 初始化 Supabase 客戶端 (使用 Service Role Key 以繞過 RLS 讀取所有資料)
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// ==========================================
// 2. 獲取比特幣價格的函數
// ==========================================
async function getBitcoinPrice(): Promise<number | null> {
try {
const response = await axios.get('https://api.coindesk.com/v1/bpi/currentprice.json');
const priceStr = response.data.bpi.USD.rate;
// 移除千分位逗號並轉換成數字
return parseFloat(priceStr.replace(/,/g, ''));
} catch (error) {
console.error('抓取幣價失敗:', error);
return null;
}
}
// ==========================================
// 3. 發送 Line Notify 訊息的函數
// ==========================================
async function sendLineNotify(token: string, message: string) {
try {
await axios.post(
'https://notify-api.line.me/api/notify',
`message=${encodeURIComponent(message)}`,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${token}`
}
}
);
console.log('✅ 訊息發送成功');
} catch (error) {
console.error('❌ 訊息發送失敗:', error);
}
}
// ==========================================
// 4. 主程式排程邏輯 (Main Job)
// ==========================================
async function runDailyJob() {
console.log('🚀 開始執行每日排程任務...');
// 步驟 A:取得最新資料
const price = await getBitcoinPrice();
if (!price) {
console.log('無法取得價格,任務中止。');
return;
}
const message = `\n早安!💰\n今日比特幣最新報價為:$${price} USD\n祝您投資順利!`;
console.log('準備發送訊息:', message);
// 步驟 B:從資料庫撈取所有訂閱者
const { data: subscribers, error } = await supabase
.from('subscribers')
.select('line_token');
if (error || !subscribers) {
console.error('無法讀取訂閱者資料庫:', error);
return;
}
console.log(`總共找到 ${subscribers.length} 位訂閱者,開始群發...`);
// 步驟 C:利用迴圈逐一發送訊息
for (const sub of subscribers) {
if (sub.line_token) {
// 加上小小的延遲,避免瞬間發送太多請求被 Line 封鎖 (Rate Limit)
await new Promise(resolve => setTimeout(resolve, 500));
await sendLineNotify(sub.line_token, message);
}
}
console.log('🎉 每日排程任務執行完畢!');
}
// 啟動程式
runDailyJob();
實戰開發避坑指南
當你把這個腳本丟上 GitHub Actions 或是伺服器時,請務必注意以下三個業界常常會踩坑的地方:
-
API Rate Limit (請求頻率限制): 就像上面的程式碼第 73 行示範的一樣,當你的訂閱者從 5 個變成 5000 個時,如果你用
Promise.all瞬間併發 5000 個請求給 Line 的伺服器,你的 API Token 會立刻被 Line 當成阻斷服務攻擊 (DDoS) 並且永久封鎖。在迴圈內加入短暫的sleep或使用 Queue (佇列) 是排程腳本的必備素養。 -
Supabase 的 Service Role Key: 因為排程腳本是在伺服器端 (Backend) 自動執行的,沒有使用者登入的 Context。因此在初始化 Supabase 時,我們不能使用一般的
ANON_KEY(會被 RLS 擋下),必須使用具有無敵權限的SERVICE_ROLE_KEY。這把鑰匙非常危險,絕對不能外洩或放在前端程式碼中,請務必將其設定在環境變數裡! -
錯誤處理 (Try-Catch) 與重試機制: 自動化腳本是在半夜無人看守時執行的。如果今天 Coindesk 的 API 剛好維修,你的腳本就會直接崩潰。請確保所有的網路請求都有用
try-catch包覆,甚至在出錯時寫一段邏輯發信給你自己報警,這樣你隔天起床才會知道任務失敗了。
結語
恭喜你!到這裡為止,你已經完整掌握了「自動化排程」的精髓。從最基礎的 Linux Crontab,到雲端時代的 GitHub Actions 與 Vercel Cron,並且親手實作了一個能帶來商業價值的每日推播系統。
把這些技術組合起來,你可以做出自動化搶票機器人、自動備份資料庫的腳本、甚至是一人營運的電子報平台。這就是程式設計最迷人的地方:寫一次程式碼,讓電腦永遠為你工作!