FR-121 全庫無時區時間欄位改 timestamptz(資料庫統一存 UTC)

決策者 2026-09-27 裁:「資料庫應統一存 UTC」,並裁「這次就修」——進 b4。 來源:FR-114 第 9 棒 b3 總測回報 #16(CM-2231)、CM-2234 拆出、CM-2245 過渡不再需要。

§1

問題

  • DEV 實查:63 張表、140 個欄位是 timestamp without time zone(public 31 表 65 欄/compliance 20 表 51 欄/survey 10 表 21 欄/oscal 2 表 3 欄)。這種欄位存的數字沒有時區概念,寫入端用什麼時間就存什麼,讀取端只能「假設」它是某時區。
  • 現況兩種寫法並存:絕大多數表以本地時間(Asia/Taipei)寫入;少數欄位以 UTC 寫入(實查:remote_agents.last_seen_at、review_marks.reviewed_at;jedi-iam 改密碼兩張待 .1 真機核。原判 flow-engine 三張為 UTC 是 DEV 舊開發庫特例,安裝版實為本地寫入)。
  • 共用讀取 jedi_common/session/database/db_mw.py:to_local() 對 naive 一律當 UTC 轉 Asia/Taipei → 本地時間寫入的表畫面多 8 小時(b3 總測 #16)。
  • 任何「當成某時區」的修法都綁死伺服器時區,客戶裝在別的時區就壞。
§2

終局

所有時間欄位改 timestamp with time zone(timestamptz)。PostgreSQL 內部一律以 UTC 儲存,讀出依連線時區自動轉;Python 端一律用 aware datetime。之後不再有「naive 當什麼時區」的邏輯,db_mw.to_local() 的 naive 分支可刪。

§3

做法(分四張子卡,按 schema 拆,每張獨立 migration 只加不破)

  1. public(31 表 65 欄)— jedi-iam/jedi-system-core/jedi-bulletin/jedi-issue/jedi-log/jedi-file-upload/jedi-remote-agent 等套件表+主線表。
  2. compliance(20 表 51 欄)— jedi-flow-engine/jedi-task-platform/jedi-compliance-audit/主線 flow_control。
  3. survey+oscal(12 表 24 欄)— jedi-survey/jedi-oscal-v2。
  4. 收尾 — db_mw.to_local() 刪 naive 分支;grep 全 monorepo datetime.utcnow/naive datetime.now() 寫入改 datetime.now(timezone.utc);ORM DateTime 改 DateTime(timezone=True);CM-2245 的過渡邏輯拆掉;守衛測試「全庫不得再有 timestamp without time zone」。

每張子卡固定步驟:

  • 用 information_schema 列出該 schema 的欄位,逐欄判斷寫入端是本地還是 UTC(grep model default/server_default/寫入程式),列表進卡。
  • migration:ALTER TABLE t ALTER COLUMN c TYPE timestamptz USING c AT TIME ZONE '<Asia/Taipei 或 UTC>',依上一步逐欄選。只套 DEV。表屬套件的 migration 放該套件 migrations/,主線表放 scripts/sql/(先看既有放法)。
  • 有 view/SQL 字串/date_trunc/::date 比較引用這些欄位的,git grep 欄位名確認型別改動後語意不變(view 與 SQL 字串是守衛盲區)。
  • 驗證:套完再跑盤點 SQL 該 schema 歸零;抽三筆舊資料轉換前後讀出時間相同(本地 10:35 的那筆仍 10:35);受影響套件既有測試帶 PYTHONPATH 全綠;把 DB 時區改成 UTC 再讀一次仍正確(這是終局要驗的點)。
§4

驗收(決策者 190 手測)

  • 升級後既有資料時間不變(角色/公告/流程執行的建立時間與升級前一致)。
  • 190 容器 TZ 改成 UTC 重啟後畫面時間仍正確(驗不綁時區)。
  • 忘記密碼 5 分鐘限制、24 小時內不能再改、密碼過期天數三條期限判斷正常。
§5

出貨影響

  • 四張都是主線/套件 active migration → 出貨基線要重產(scripts/init/02-schema.sql)。
  • 升級路徑:migration 只加不破,--upgrade 時自動套;舊資料依表逐欄正確轉換。
  • 版號:jedi-common bump、各動到 models 的套件 bump。
§6

沿革

見 FR-114-LOG.md 第 9 棒與 CM-2231 #16/CM-2234/CM-2245。

§7

顯示時區原則(決策者 2026-09-27 補裁)

  • 資料庫存 UTC;UI 顯示依使用者所在地轉換。 這版先固定 Asia/Taipei,之後做個人 profile 時區設定。
  • db_mw.to_local() 不刪,改成「aware UTC → 顯示時區」;顯示時區單一來源(設定/TZ,預設 Asia/Taipei),留 get_display_tz(user_context) 介面給 profile 接。只刪 naive 分支。
  • API 回傳時間一律帶時區偏移(ISO 8601),前端不自行猜;前端把時間當本地字串解析的地方要一併查。
  • CM-2245 作廢,不進 b4;b4 的時間問題全由本 FR 解。
  • 子卡 .5(前端顯示收斂):與 .4 同派;業界做法是前端依使用者時區轉、後端回 UTC,這版先寫死台北、前端不轉,只把顯示點收成一支函式留 profile 接口。