一句話:上傳一份 Excel / CSV,一次建立大量 行程 / 梯次(月開上百團人工逐筆太慢)。兩階段:dry-run 預覽驗證 → 提交;建檔走既有
createTrip/createDeparturerepo,不另開寫入路徑。畫面(UI = wireframe):
批次開團
荒野現行用 Google Sheet(總表-六月 / 總表-七月 月曆式排團主檔)規劃整月團期,再人工逐筆敲進系統。一個月上百團,逐筆建檔是上線最痛的瓶頸(0617 會議列為首要任務)。本 spec 讓營運上傳一份對齊欄位的試算表,系統解析、驗證、預覽、一次建檔。
範圍:建 trips(行程定義)+ departures(梯次);若 group-code 對照碼齊備則一併產團號。可對既有行程補梯次,也可新建行程含多梯次。
不做:AI / 對話式建檔(MCP/CLI)——0617 列為 future,本檔只做檔案上傳;定價解析、訂金、同業價各歸 trip-pricing;配套住宿 / 分房不在匯入範圍(§3.4 串接)。
匯入是寫入既有 trips / departures 的工作流,欄位對齊 trips §1:
slug、title、summary、duration_days、destination、is_climbing / is_overseas(price_from_twd 由匯入時取該行程各梯次最低 price_twd 帶入,不在檔表達;status 預設 draft)。departure_date、return_date、price_twd、capacity、notes、region_code / airline_code(團號來源碼,選填)。import_batches(新增;audit 用)一次匯入 commit 寫一列,逐筆結果不落表(回前端即可):
| 欄 | 型別 | 說明 |
|---|---|---|
id | text PK(imp_<ulid>) | — |
source_filename | text | 上傳檔名。 |
uploaded_by_user_id | text FK → user(同庫) | 上傳者。 |
row_count | integer | 資料列數(不含表頭)。 |
created_trip_count | integer | 新建行程數。 |
created_departure_count | integer | 新建梯次數。 |
skipped_count | integer | 去重 skip 的列數。 |
error_count | integer | 失敗列數。 |
created_at | timestamptz | — |
一列 = 一個梯次;同 slug 多列 = 同行程多梯次。提供可下載 .xlsx 範本(含表頭 + 範例列 + 中文說明分頁)。表頭解析以中文表頭或英文 key 皆可(去空白、大小寫不敏感);範本預設中文表頭。
| 範本表頭(中文) | key | 對應 | 必填 | 格式 / 驗證 |
|---|---|---|---|---|
| 行程代碼 | slug | trips.slug | ✅ | 既有→補梯次;不存在→新建行程 |
| 行程名稱 | title | trips.title | 新行程✅ | 補梯次時可空(以既有行程為準) |
| 目的地 | destination | trips.destination | 新行程✅ | 補梯次時可空 |
| 天數 | duration_days | trips.duration_days | 新行程✅ | 整數 > 0 |
| 行程簡介 | summary | trips.summary | — | |
| 登山團 | is_climbing | trips.is_climbing | — | 是/否(true/1/Y 等寬鬆解析),預設否;與既有行程衝突→error |
| 海外團 | is_overseas | trips.is_overseas | — | 同上 |
| 出發日 | departure_date | departures.departure_date | ✅ | YYYY-MM-DD(民國 / 西元 / Excel 序號容錯) |
| 回程日 | return_date | departures.return_date | ✅ | YYYY-MM-DD,≥ departure_date |
| 售價 | price_twd | departures.price_twd | ✅ | 整數 TWD、> 0 |
| 名額 | capacity | departures.capacity | ✅ | 整數、> 0 |
| 備註 | notes | departures.notes | — | |
| 地區碼 | region_code | departures.region_code | — | 與航空碼成對:兩者皆填→產團號;查無對照碼→error |
| 航空碼 | airline_code | departures.airline_code | — | 同上 |
訂金 / 同業價不在匯入檔(走 trip-pricing 的行程 / 梯次編輯)。
price_from_twd不由使用者填:新行程建立時取其各梯次最低price_twd。
packages/core/src/catalog/bulk-import.ts(first arg db: ScopedDb):
.xlsx 經 exceljs、.csv 內建解析)→ 逐列驗證 → 回預覽:每列標 new_trip / append_departure / skip(已存在)/ error(附原因)。不寫任何資料。new_trip / append_departure)建檔。沿用 createTrip / createDeparture;region_code + airline_code 皆備則建梯次後呼叫 generateGroupCode。寫 import_batches + 逐筆 audit_log。dry-run 與 commit 共用同一套純函式分類(解析後列 + DB snapshot + 模組授權 → 逐列狀態);commit 以提交當下 DB snapshot 與真實模組授權重判(防 dry-run 後資料變動的 TOCTOU),不得在 commit 端放寬模組 gate。
解析:server-side。.xlsx 用 exceljs(維護良好;SheetJS 的 npm 版已棄),.csv 用內建解析(UTF-8 + BOM 容錯)。上限 1000 資料列,超過於 dry-run 直接擋。
return ≥ departure、capacity > 0、price 為正整數、duration > 0)。(slug, departure_date) 第二列起標 skip(避免單檔複製貼上建出重複梯次)。is_climbing/is_overseas 與既有行程不符 → 報錯(不靜默覆寫;改行程屬性走行程編輯)。is_climbing/is_overseas=true → 報錯(modules 兩層 gate)。此 gate 於 dry-run 與 commit 兩階段皆強制。region_code / airline_code 只填其一 → 報錯;兩者皆填但 region_codes / airline_codes 對照表查無 → 報錯。(slug, departure_date) 已存在(DB)→ 標 skip,不重複建、不覆寫(重傳同檔安全);同檔內重複亦 skip(§3.2)。不支援更新既有梯次價格 / 容量(避免覆寫線上在賣梯次;要改走梯次編輯)。audit_log;批次寫一筆 import_batches。region_code + airline_code 兩欄皆備 → commit 建梯次後呼叫 generateGroupCode(db, { departureId, regionCode, airlineCode, departureDate }) 產團號(巢狀 savepoint,撞號使該行程 tx rollback);缺則團號留空(之後於團號頁產)。本檔不自刻團號邏輯。price_twd;通路 / 訂金 / 同業價走 trip-pricing,不在匯入檔表達。/admin/trips 加「批次匯入」;上傳 → 預覽表(逐列狀態 + 錯誤)→ 確認建檔 → 結果摘要(建 N 行程 / M 梯次、skip K、error J)。/admin/trips/import/template 產固定欄名的 .xlsx 範本(含範例列 + 中文說明分頁)。trip.manage(匯入屬寫入;比照 content-authoring 的 read-gates-write)。頁面、parse / commit action、範本 route 皆 requirePermission('trip.manage')。現況 admin / sales 持 trip.manage;若要 op 可匯入,於 dynamic-rbac 的 /admin/roles 授予 op trip.manage,不在本檔寫死角色。.xlsx 範本(穩、可驗證);不直接吃現行月曆式總表,總表轉範本由營運手動。(slug, departure_date) 重複一律 skip,不覆寫線上在賣梯次。| 版本 | 日期 | 變更 | Commit |
|---|---|---|---|
| 1 | 2026-06-20 | 初版(草案):Excel/CSV 兩階段批次建檔(dry-run 預覽 → 提交)、欄位契約、去重 / 部分失敗隔離、與團號 / 定價串接邊界。未實作。 | — |
| 2 | 2026-06-23 | 定義定案(過夜 /loop):§5 三開放議題裁決(固定 .xlsx 範本 / 不支援更新 / 同步上限 1000 列);§2.1 欄位契約做到可實作精度(中文表頭 ↔ key、寬鬆布林、團號碼成對欄);§2.1 新增 import_batches audit 表;§3.1 解析用 exceljs(.xlsx)+內建(.csv)、commit 以真實模組授權重判(不放寬 gate);§3.2 補批次內梯次去重;§3.4 團號已落地 → 碼齊備則 commit 後 generateGroupCode;§4 權限對齊 code(trip.manage = admin/sales,op 經 /admin/roles 授予)。 | — |
| 3 | 2026-06-23 | 一版實作落地:exceljs + import_batches(migration 0017)+ catalog/bulk-import.ts(parseAndValidate/commitImport,純函式 resolveRows)+ /admin/trips/import 三步驟 UI + .xlsx 範本下載 route。tsc 0 / vitest(bulk-import 22)。 | — |