實戰應用:自動化抓資料與發送通知

在前面的章節中,我們學會了各種排程的「載體」(無論是 Linux 主機、GitHub Actions、或是 Vercel Cron)。現在,我們要將這些火力集中,實作一個真實世界最常見的商業情境:「每日訂閱推播系統」

情境說明

我們即將打造一個「每日幣圈天氣預報」系統。這隻程式每天早上 8 點會做三件事情:

  1. 去免費的 Supabase 資料庫 讀取所有訂閱我們服務的使用者 (Line Notify Token)。
  2. 去呼叫外部 API 抓取最新的比特幣價格。
  3. 利用迴圈,自動將最新報價透過 Line Notify 發送給所有訂閱者。

你可以把這個腳本放在 GitHub Actions 上跑,也可以寫成 Next.js API 掛在 Vercel 上。這裡我們以 Node.js (TypeScript) 腳本為例。

前置準備

  1. Supabase 資料庫:建立一個名為 subscribers 的 Table,包含兩個欄位:id (主鍵)、line_token (字串)。
  2. 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 或是伺服器時,請務必注意以下三個業界常常會踩坑的地方:

  1. API Rate Limit (請求頻率限制): 就像上面的程式碼第 73 行示範的一樣,當你的訂閱者從 5 個變成 5000 個時,如果你用 Promise.all 瞬間併發 5000 個請求給 Line 的伺服器,你的 API Token 會立刻被 Line 當成阻斷服務攻擊 (DDoS) 並且永久封鎖。在迴圈內加入短暫的 sleep 或使用 Queue (佇列) 是排程腳本的必備素養。

  2. Supabase 的 Service Role Key: 因為排程腳本是在伺服器端 (Backend) 自動執行的,沒有使用者登入的 Context。因此在初始化 Supabase 時,我們不能使用一般的 ANON_KEY(會被 RLS 擋下),必須使用具有無敵權限的 SERVICE_ROLE_KEY。這把鑰匙非常危險,絕對不能外洩或放在前端程式碼中,請務必將其設定在環境變數裡!

  3. 錯誤處理 (Try-Catch) 與重試機制: 自動化腳本是在半夜無人看守時執行的。如果今天 Coindesk 的 API 剛好維修,你的腳本就會直接崩潰。請確保所有的網路請求都有用 try-catch 包覆,甚至在出錯時寫一段邏輯發信給你自己報警,這樣你隔天起床才會知道任務失敗了。

結語

恭喜你!到這裡為止,你已經完整掌握了「自動化排程」的精髓。從最基礎的 Linux Crontab,到雲端時代的 GitHub Actions 與 Vercel Cron,並且親手實作了一個能帶來商業價值的每日推播系統。

把這些技術組合起來,你可以做出自動化搶票機器人、自動備份資料庫的腳本、甚至是一人營運的電子報平台。這就是程式設計最迷人的地方:寫一次程式碼,讓電腦永遠為你工作!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!