Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
難的不是加 9 小時,而是搞不清楚現在是哪一邊
UTC 與日本時間(JST)的關係其實非常單純:JST = UTC + 9,固定不變。 日本沒有實施日光節約時間,時差不會隨季節改變。對台灣的開發者來說還要多記一件事:台灣時間是 UTC+8,所以台灣與日本之間固定相差 1 小時(日本比台灣快 1 小時)。
即使如此,時區事故仍然層出不窮,原因不在計算,而在於你會搞不清楚手上的時間是哪一邊的。當日誌是 UTC、資料庫是本地時間、CI 是 UTC、開發用筆電是 UTC+8 時,光看 2026-08-06 09:00:00 這個字串,沒有人知道它到底是幾點。
這篇是換算對照表,加上實際會出事的地方。
時差對照表
| UTC | 日本 JST(同日) | 台灣(同日) |
|---|---|---|
| 00:00 | 09:00 | 08:00 |
| 03:00 | 12:00 | 11:00 |
| 09:00 | 18:00 | 17:00 |
| 12:00 | 21:00 | 20:00 |
| 15:00 | 隔日 00:00 | 23:00 |
| 16:00 | 隔日 01:00 | 隔日 00:00 |
| 21:00 | 隔日 06:00 | 隔日 05:00 |
反方向(JST → UTC)減 9 小時。JST 00:00~08:59 在 UTC 是前一天。
日期換日的界線,對日本是 UTC 15:00,對台灣是 UTC 16:00。每日批次或報表統計期間對不上的問題,幾乎都發生在跨過這條線的地方。若要統計「台灣時間的一整天」,在 UTC 是 前一天 16:00 ~ 當天 16:00。
ISO 8601 的 Z 與 +09:00
| 寫法 | 意義 |
|---|---|
2026-08-06T00:00:00Z | UTC 的 0 點。Z 代表 UTC(Zulu time) |
2026-08-06T09:00:00+09:00 | 與上一列完全相同的瞬間,只是以 JST 書寫 |
2026-08-06T09:00:00 | 沒有偏移量=無法判斷,解讀由解析器決定 |
2026-08-06 | 只有日期。時刻與時區的解讀因實作而異 |
中間兩列指向同一個瞬間。只要帶著偏移量,用哪一種寫法都不會遺失資訊。
問題在第三列。沒有偏移量的日期時間字串,單獨存在時沒有確定的意義。 在 API 回應或 CSV 中看到這種格式,請先確認規格。
JavaScript 中日期位移的條件
這是最常遇到的陷阱。兩個看起來是同一天的字串,會被解析成不同的瞬間。
// 只有日期的格式會被視為 UTC
new Date('2026-08-06').toISOString();
// => '2026-08-06T00:00:00.000Z' (台灣時間為 8/6 08:00)
// 有時刻但沒有偏移量的格式會被視為「本地時間」
new Date('2026-08-06T00:00:00').toISOString();
// => '2026-08-05T16:00:00.000Z' (在 UTC+8 環境執行時,日期退回前一天)
只是多加了 T00:00:00,結果就位移了,連日期都變了。這是符合 ES 規格的行為,不是 bug:只有日期=UTC,有時刻但沒偏移量=本地時間。
安全的做法是永遠把偏移量寫出來:
new Date('2026-08-06T00:00:00+09:00').toISOString();
// => '2026-08-05T15:00:00.000Z' (如預期:日本時間 8/6 0 點)
要顯示成特定時區時,把時區傳給 toLocaleString,結果就不再依賴執行環境的設定:
const d = new Date('2026-08-06T00:00:00Z');
d.toLocaleString('zh-TW', { timeZone: 'Asia/Tokyo' }); // => '2026/8/6 上午9:00:00'
d.toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' }); // => '2026/8/6 上午8:00:00'
另外請記得 toISOString() 一律輸出 UTC。若把它當成「本地時間的 ISO 字串」使用,存進去的會是差了 8 或 9 小時的值。
伺服器端的換算
Python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo # Python 3.9+
now_utc = datetime.now(timezone.utc)
now_jst = now_utc.astimezone(ZoneInfo('Asia/Tokyo')) # 台灣為 'Asia/Taipei'
# 原則:不要讓 naive(沒有時區資訊)的 datetime 進入系統
MySQL
SELECT CONVERT_TZ('2026-08-06 00:00:00', '+00:00', '+09:00');
-- => 2026-08-06 09:00:00
MySQL 要注意的是:TIMESTAMP 型別會依連線時區自動換算,而 DATETIME 型別完全不換算。 同一張表混用兩種型別,就會出現看起來一樣、意義卻不同的欄位並排。使用具名時區('Asia/Taipei')需要載入時區資料表,因此直接指定偏移量比較保險。
PostgreSQL
SELECT TIMESTAMPTZ '2026-08-06 00:00:00+00' AT TIME ZONE 'Asia/Taipei';
-- => 2026-08-06 08:00:00
PostgreSQL 的慣例是以 TIMESTAMPTZ(含時區)儲存,顯示時再用 AT TIME ZONE 轉換。型別的選擇整理在 CREATE TABLE 參考手冊。
四個會出事的地方
1. cron 依伺服器的時區執行
GitHub Actions 的 schedule 一律是 UTC。 想要「每天早上 9 點執行」而寫成 0 9 * * *,實際會在台灣時間下午 5 點跑。
on:
schedule:
# 台灣時間早上 9 點 = UTC 當天 01:00(日本時間早上 9 點則是 '0 0 * * *')
- cron: '0 1 * * *'
cron 語法本身的讀法請見 cron 運算式的寫法,工作流程的相依關係請見 GitHub Actions needs 設計模式。
2. Docker 容器預設是 UTC
多數基礎映像檔沒有時區設定,直接以 UTC 運行。本機開發(UTC+8)與容器內(UTC)的日誌時間差 8 小時就是這個原因。如果應用程式把時間當成「本地時間」處理,結果就會隨環境而變。
3. 日誌的時區混雜
存取日誌是伺服器本地時間、應用程式日誌是 UTC、監控服務顯示的是瀏覽器時區——這種狀態很常見。事故調查要比對多份日誌時,先全部統一成 UTC 或 Unix 時間再排列最保險。Unix 時間戳沒有時區概念,是絕對的秒數,適合當作比對基準(詳見 什麼是 Unix 時間)。
4. 只儲存「日期」
像 2026-08-06 這種只有日期的值,指涉的範圍會隨時區改變。結帳日、統計日這類業務上的日期,不要轉成日期時間,維持日期原樣(使用 DATE 型別)比較安全。一旦轉成日期時間,就得回答「是哪個時區的 0 點」。
原則:儲存用 UTC,顯示時再換算
歸納成三句話:
- 儲存與內部運算統一使用 UTC(或 Unix 時間)
- 只在顯示時換算成當地時間
- 以字串傳遞時一定要帶偏移量(
Z或+09:00)
「只服務國內所以存本地時間」乍看合理,但只要 CI 和容器以 UTC 運行,某處就會產生界線。界線越少,事故越少。
直接換算
想立刻確認日誌上的 UTC 時間對應日本時間幾點,把它貼進 UTC/JST 轉換工具 就能雙向換算;再減 1 小時就是台灣時間。工具支援 ISO 8601 格式,也可以直接取得目前時間並複製。
要把 Unix 時間戳(像 1785974400 這樣的數字)還原成日期時間,可以用 Unix 時間轉換;要把逾時或 TTL 設定值在秒、分、小時之間換算,可以用 時間單位轉換。這些工具都在瀏覽器內完成處理,輸入的日期時間不會傳送到外部。
總結
- JST = UTC + 9 固定,日本沒有日光節約時間;台灣是 UTC+8,與日本固定差 1 小時
- 換日界線:日本是 UTC 15:00,台灣是 UTC 16:00
- ISO 8601 中沒有偏移量(
Z/+09:00)的字串,單獨存在時意義不確定 - JavaScript 對只有日期=UTC、有時刻無偏移量=本地時間的解讀不同
- MySQL 只有
TIMESTAMP會自動換算,DATETIME不會 - GitHub Actions 的 cron 一律是 UTC;台灣早上 9 點是
0 1 * * * - 儲存用 UTC、顯示時換算、字串一定帶偏移量
常見問題
台灣時間與日本時間差幾小時?
固定差 1 小時,日本比台灣快。台灣是 UTC+8,日本(JST)是 UTC+9,兩地都沒有日光節約時間,因此全年都是 1 小時的差距。也就是說,日本時間早上 9 點就是台灣時間早上 8 點。
2026-08-06T09:00:00+09:00 和 2026-08-06T00:00:00Z 是不同的時間嗎?
是同一個瞬間,只是以不同時區書寫。用哪一種格式儲存都不會遺失資訊,但在系統內部統一成其中一種,比較和排序時比較不會混淆。
為什麼 JavaScript 的日期會差一天?
因為只有日期的字串('2026-08-06')會被解析為 UTC,而有時刻但沒有偏移量的字串('2026-08-06T00:00:00')會被解析為本地時間。在 UTC+8 環境下,後者會指向 8 小時前(前一天 16:00 UTC),以 UTC 為基準輸出日期時就看起來差了一天。請務必在字串中加上偏移量。
要在 GitHub Actions 於台灣時間早上 9 點執行該怎麼寫?
schedule 的 cron 以 UTC 解讀,所以寫成 - cron: '0 1 * * *'(台灣早上 9 點 = UTC 01:00)。若要對應日本時間早上 9 點則是 0 0 * * *。另外,GitHub Actions 的排程執行可能因負載而延遲數分鐘到十幾分鐘,不適合需要分鐘級精準度的工作。
貼上要換算的日期時間,資料會被傳送嗎?
不會。UTC/JST 轉換工具 與 Unix 時間轉換 都在瀏覽器內完成處理,輸入的日期時間不會傳送到伺服器。