難的不是加 9 小時,而是搞不清楚現在是哪一邊

UTC 與日本時間(JST)的關係其實非常單純:JST = UTC + 9,固定不變。 日本沒有實施日光節約時間,時差不會隨季節改變。對台灣的開發者來說還要多記一件事:台灣時間是 UTC+8,所以台灣與日本之間固定相差 1 小時(日本比台灣快 1 小時)。

即使如此,時區事故仍然層出不窮,原因不在計算,而在於你會搞不清楚手上的時間是哪一邊的。當日誌是 UTC、資料庫是本地時間、CI 是 UTC、開發用筆電是 UTC+8 時,光看 2026-08-06 09:00:00 這個字串,沒有人知道它到底是幾點。

這篇是換算對照表,加上實際會出事的地方。

時差對照表

UTC日本 JST(同日)台灣(同日)
00:0009:0008:00
03:0012:0011:00
09:0018:0017:00
12:0021:0020:00
15:00隔日 00:0023: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:00ZUTC 的 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,顯示時再換算

歸納成三句話:

  1. 儲存與內部運算統一使用 UTC(或 Unix 時間)
  2. 只在顯示時換算成當地時間
  3. 以字串傳遞時一定要帶偏移量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:002026-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 時間轉換 都在瀏覽器內完成處理,輸入的日期時間不會傳送到伺服器。