把難的留給雲端,把爽的留給你:自動擴縮、低維運、穩穩推播 👍
先放一句人話版摘要:Scheduler 定時叫醒 → Cloud Run 抓 ibon & 比對 → 有變化就 LINE 推播。
指令互動(/watch、/list、/check)走 Webhook,任務狀態放 Firestore。
下面用圖、例子、以及幾段輕量 Python,幫你把「技術名詞」變成「啊原來就這樣」。
[Day3 架構(cron + webhook 強化版)]
/cron/tick
。/watch
、/list
、/check
等指令口訣:「cron 負責掃、webhook 負責聊、Firestore 記事情、LINE 發報告」。
[排程時間軸(示意)]
TICK_SOFT_DEADLINE_SEC
):例如一輪最多跑 25 秒、每輪最多 N 個任務,避免超時。[Auto-scaling 直覺圖(示意)]
[Latency Budget(示意)]
一個端到端 ≈ 720ms(示意):
Ingress/Cold-start(80ms)→ HTTP(40ms)→ 抓 ibon(160ms)→ 解析(120ms)→ Firestore 讀(110ms)→ 差異與格式化(70ms)→ LINE push(140ms)
真的只有幾行,先確認「路通不通」。
# 1) Cloud Run /health(確認服務醒著)
curl -fsS "$SERVICE_URL/health" || echo "health check failed"
# 2) 假裝 Scheduler 的 cron(GET/POST 都示意)
curl -fsS -X POST "$SERVICE_URL/cron/tick" || echo "cron tick failed"
# 3) 假裝 LINE webhook(/watch 指令)
curl -fsS -X POST "$SERVICE_URL/webhook" \
-H "Content-Type: application/json" \
-d '{"user_id":"Uxxx","text":"/watch https://ibon.example 60"}' | jq .
這些 endpoint 名稱只是示意,對應你自己的路由即可。
from flask import Flask, request, jsonify
import os, time
app = Flask(__name__)
# --- Firestore 封裝(示意) ---
def fetch_jobs(limit=50):
# return [{"id":"W1","user_id":"U1","url":"https://ibon...","interval":60,"areas_cache":[...]}]
...
def update_job_cache(job_id, areas):
...
def should_notify(job, change, always=os.getenv("ALWAYS_NOTIFY","0")=="1"):
return always or bool(change["changed"] or change["added"] or change["removed"])
# --- 核心功能(與 Day 4 解析會接起來) ---
def extract_ticket_info(url: str) -> dict:
# 解析 ibon:活動資訊、座位圖、各區狀態(數字/熱賣中/售完)
...
def diff_areas(old: list, new: list):
old_map = {a["code"]: a for a in old}
new_map = {a["code"]: a for a in new}
changed, added, removed = [], [], []
for code, a in new_map.items():
if code not in old_map:
added.append(a); continue
if old_map[code]["status"] != a["status"]:
changed.append({"from": old_map[code], "to": a})
for code, a in old_map.items():
if code not in new_map:
removed.append(a)
return {"changed": changed, "added": added, "removed": removed}
def format_line_message(info, change=None):
ev = info["event"]
lines = [f"🎫 {ev['title']}", f"📍 {ev['venue']} 🗓 {ev['datetime']}", ""]
avail = [a for a in info["areas"] if a["status"].isdigit()]
hot = [a for a in info["areas"] if a["status"] == "熱賣中"]
sold = [a for a in info["areas"] if a["status"] == "已售完"]
if avail: lines.append("✅ 可售: " + " ".join(f"{a['name']} {a['status']}" for a in avail[:8]))
if hot: lines.append("🟢 熱賣中: " + "、".join(a["name"] for a in hot[:10]))
if sold: lines.append("🔴 售完: " + "、".join(a["name"] for a in sold[:10]))
return "\n".join(lines)
# --- cron:被 Scheduler 叫醒 ---
@app.route("/cron/tick", methods=["GET","POST"])
def cron_tick():
start = time.time()
jobs = fetch_jobs()
processed = 0
for job in jobs:
info = extract_ticket_info(job["url"])
change = diff_areas(job.get("areas_cache", []), info["areas"])
if should_notify(job, change):
# push_line(os.environ["LINE_CHANNEL_ACCESS_TOKEN"], job["user_id"], format_line_message(info, change))
...
update_job_cache(job["id"], info["areas"])
processed += 1
if time.time() - start > int(os.getenv("TICK_SOFT_DEADLINE_SEC","25")):
break
return jsonify(ok=True, processed=processed, elapsed_ms=int((time.time()-start)*1000))
# --- webhook:處理 /watch /list /check ---
@app.route("/webhook", methods=["POST"])
def webhook():
body = request.get_json(force=True)
text = (body.get("text") or "").strip()
# 解析文字,對應到你的 /watch /list /check...
...
return jsonify(ok=True, echo=text)
@app.route("/health")
def health():
return jsonify(status="ok")
// collection: jobs
{
id: "W123456",
user_id: "Uxxxxx",
url: "https://tickets.ibon.com/Show/Index/...",
interval: 60, // 秒
enabled: true,
last_run: 1737800000, // unix
areas_cache: [ // 上次解析的各區狀態
{ code: "B09P2J33", name: "5F B區 3800", status: "25" },
{ code: "C02X...", name: "3F C區 2800", status: "熱賣中" }
]
}
變數 | 說明 | 建議值 |
---|---|---|
LINE_CHANNEL_ACCESS_TOKEN |
LINE 推播金鑰 | 你的 LINE channel token |
LINE_CHANNEL_SECRET |
LINE Webhook 驗證 | 你的 LINE channel secret |
DEFAULT_PERIOD_SEC |
預設監看間隔 | 60 |
ALWAYS_NOTIFY |
是否每次都通知 | 0 (僅變化時通知) |
MAX_PER_TICK |
單輪最多處理任務數 | 5 |
TICK_SOFT_DEADLINE_SEC |
單輪軟性截止秒數 | 25 |
開啟你的 LINE,傳送 /watch 你的活動頁URL 60。
五分鐘後看一下通知有沒有跳。
覺得吵就把 ALWAYS_NOTIFY=0;覺得太安靜就把間隔改短(30s)。
要進洞穴了:如何用 DevTools & BeautifulSoup 把 ibon 頁面解析乾淨(jsonData 在哪、代碼怎麼翻成中文區名)。
放心,我會放超好懂的圖和可以直接跑的程式小片段。