USJ 等待時間即時看板:資料管線設計與實作
為什麼要做這個專案
去過大阪環球影城的人都知道,排隊是無可避免的一環。雖然官方 App 有提供等待時間,但使用體驗並不理想——你必須點進每個設施才能看到時間,無法一目瞭然。
我們想做的是一個簡潔的看板頁面,讓使用者可以在同一個畫面上快速掌握所有設施的等待狀況,方便規劃遊園路線。
系統架構概覽
整個系統分為四層:
- 資料蒐集層:定期從資料源取得最新的等待時間
- 資料處理層:清洗、標準化並儲存時間序列資料
- API 層:提供 RESTful API 給前端查詢
- 前端展示層:即時顯示各設施的等待時間
資料蒐集
我們每 5 分鐘執行一次資料蒐集任務,將最新的等待時間寫入資料庫。蒐集程式用 Python 撰寫,部署在 Docker 容器中,由 cron job 觸發。
每次蒐集的資料包含:
- 設施名稱(日文 / 英文)
- 目前等待時間(分鐘)
- 設施運作狀態(營運中 / 暫停 / 維護中)
- 資料取得時間戳記
資料處理與儲存
原始資料進來後,需要經過幾道處理:
設施名稱標準化:同一個設施可能有不同的名稱變體(例如「ハリー・ポッター」和「Harry Potter」)。我們建立了一個對照表,將所有變體映射到統一的識別碼。
異常值偵測:偶爾會出現不合理的等待時間(例如負數或超過 500 分鐘),這些需要被標記並過濾。
時間序列儲存:我們使用 PostgreSQL 儲存歷史資料,每筆紀錄包含設施 ID、等待時間和時間戳記。這樣不僅能提供即時資料,還能做歷史趨勢分析。
CREATE TABLE wait_times (
id SERIAL PRIMARY KEY,
ride_id VARCHAR(50) NOT NULL,
wait_minutes INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
recorded_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_wait_times_ride_recorded
ON wait_times (ride_id, recorded_at DESC);
API 設計
API 提供兩個主要端點:
GET /api/rides/current:取得所有設施的最新等待時間GET /api/rides/:id/history?date=YYYY-MM-DD:取得特定設施的歷史資料
回傳格式設計得盡量精簡,減少傳輸量:
{
"rides": [
{
"id": "harry-potter",
"name": "哈利波特禁忌之旅",
"wait": 75,
"status": "operating",
"updatedAt": "2026-03-01T10:05:00+09:00"
}
],
"parkStatus": "open",
"lastSync": "2026-03-01T10:05:00+09:00"
}
前端即時更新
前端使用 Next.js 開發,頁面載入時先透過 Server Components 取得初始資料(避免白畫面),之後在客戶端透過輪詢機制每 60 秒更新一次。
為什麼用輪詢而不是 WebSocket?因為等待時間的更新頻率不高(每 5 分鐘),使用 WebSocket 反而會增加伺服器的連線維護成本。對這個場景來說,簡單的輪詢就夠了。
資料洞察
累積了一段時間的歷史資料後,我們發現了一些有趣的模式:
- 開園後第一個小時是等待時間最短的黃金時段,大部分設施的等待時間不超過 20 分鐘
- 下午 1 點到 3 點是等待高峰,熱門設施平均要等 90 分鐘以上
- 平日與假日的差異巨大:假日的平均等待時間是平日的 2-3 倍
- 季節性活動期間(如萬聖節、聖誕節)整體等待時間會上升 30-50%
這些洞察也被整合到前端,以圖表的方式呈現給使用者參考。
維運經驗
監控
我們設定了幾個關鍵指標的告警:
- 資料蒐集失敗超過 3 次連續
- API 回應時間超過 2 秒
- 資料新鮮度超過 15 分鐘(表示蒐集可能中斷)
資料庫管理
時間序列資料會持續增長,我們設定了自動清理機制,只保留最近 90 天的詳細資料。更早的資料會被聚合為每小時的平均值,大幅節省儲存空間。
結語
這個專案雖然功能看似簡單——就是顯示等待時間——但背後涉及的資料工程環節一點都不少。從穩定的資料蒐集、即時的處理管線,到高效的前端呈現,每一個環節都需要精心設計。
對我們來說,這也是一次很好的實踐:用資料工程的思維,解決日常生活中的小痛點。