一句話:
travelers是訂單明細層的旅客 PII(綁order_id,非綁 user)——一個「人」可橫跨多筆訂單、出現為多筆 traveler,所以它不是會員/owner 的錨點。本檔是旅客實體的單一入口;欄位以 code 為準(packages/core/src/schema/business.ts的travelers),畫面以 wireframe 為準(§6)。定位:domain/core spec(實體與邏輯)。功能切片(分房 / 驗證 / 護照 / 同意書)在各自 spec,§5 索引串接。狀態見 roadmap。
travelers(schema:packages/core/src/schema/business.ts):
order_id(FK → orders.id,onDelete: cascade)。綁訂單,不綁 user(帳號歸戶另有 user_id,見下)。full_name、birth_date、id_number(身分證,存 id_number_enc jsonb envelope 加密欄)、passport_no(護照,存 passport_no_enc jsonb envelope 加密欄)、phone、email。emergency_contact_name / emergency_contact_phone。is_primary(是否同訂購人)、room_id(FK → departure_rooms.id,分房,onDelete: set null)、created_at。user_id(nullable FK → user,onDelete: set null):這位旅客是哪個帳號的人。建單時 primary 旅客掛下單 user(公開結帳 = 登入者、K 單 = 客戶 user);非 primary 留 NULL(M2 person 目錄再對齊)。PDPA 匯出/匿名化以「order 歸屬 ∪ user_id 歸戶」聯集涵蓋(見 member-pdpa-self-service)。id_number_checksum_ok(nullable boolean):台灣身分證檢查碼於寫入時以同一份 normalize 後的值派生落欄——讀取端驗證零解密(不再為驗 checksum 逐列打 KMS)。NULL = 未填 / 非台灣格式。id_number 寫入前統一 trim + 去空白 + 大寫(normalizeIdNumberInput),同一份 normalized 值餵密文(*_enc)、blind-index(id_number_bidx)、checksum 三者——密文與 bidx 不再各自正規化不一致。與
orders/ 訂購人user的關係:訂購人是orders.user_id(帳號);travelers 是該訂單的實際出行旅客(可含或不含訂購人本人,is_primary標記)。詳見 orders §1、member-ownership §「會員/旅客/customer 三者釐清」。
member_profiles / user,見 member-ownership)。id_number / passport_no 無明文欄,分別存 id_number_enc / passport_no_enc(jsonb EncryptedField)—— AES-256-GCM envelope(per-row 隨機 DEK,GCP Cloud KMS 管 KEK),AAD 綁 dbName:table:column:rowId:mask 防跨租戶/跨列/跨欄移植。讀取走持久化遮罩(零 KMS);全碼僅經 order-scoped reveal 解密 + traveler.decrypt audit(防 IDOR)。低敏感度欄(如 gender)解耦、可存明文。遮罩規則的理由見 ADR 0005,KEK provider 判定見 ADR 0006。room_id set null。寫入層有獨立 repo packages/core/src/travelers/repo.ts(addTraveler / updateTraveler)——加密、bidx、checksum 三者共用同一份 normalized 值在此收斂(SSOT)。讀取一律隨訂單帶出(first arg db: ScopedDb):
getAdminOrderDetail → AdminOrderDetail.travelers: AdminTravelerRow[](packages/core/src/orders/admin-repo.ts)。getOrderById / getOrderByIdForUser → … & { travelers: TravelerInput[] }(packages/core/src/orders/repo.ts)。createOrderWithReservation 的 travelers[] 一併寫入(同 tx;primary 旅客帶 user_id 歸戶)。manifest.ts。分房讀寫:rooms/repo.ts(room_id 回寫)。無獨立 traveler resource——讀寫經訂單權限:order.read(看)、order.update(新增/編輯旅客)。可見範圍同訂單(owner-id query 層)。
| Spec | 擴充什麼 |
|---|---|
| room-assignment | gender(硬約束分房,明文)、room_preference / roommate_pref、room_id 配房 |
| traveler-validation | address / validation_exempt、身分證檢查碼 / 緊急聯絡 / 必填 / 金額一致即時驗證 |
| document-expiry | passport_expiry_date + 效期警戒(海外) |
| passport-ocr | 護照 OCR、20+ 護照欄位、purge cron(海外,KMS 前置) |
| orders | 母實體(訂單外殼 + 建單雙插) |
同意書(consent_signatures) | 每旅客/每訂單同意書簽署(append-only) |
../apps/wireframes/app/admin/order-detail/page.tsx../apps/wireframes/app/admin/passport-review/page.tsx