呂凱崴kaiweilu.tw

徵求計畫批次匯入系統

太遙中心衛星影像徵求計畫的訂單,原本以 Excel 逐筆輸入、公式檢核與配號,再把 SQL 手動貼進資料庫執行,規則只存在於公式與承辦經驗中。本系統把規則逐條還原為領域規格與前後端共用的程式,做成試算表式網頁,可即時檢核、配號並直接寫入中心的 Oracle 資料庫。2026 年 6 月起於太遙中心實習,依中心需求主責完成並交付。

期間
2026-08-18 至 09-15(與 E-2 並行);已部署供中心內部使用
角色
依中心需求主責完成並交付:規則還原、領域規格與決策紀錄、前後端程式、測試、部署與交接文件;交付後持續參與中心的後續開發
技術
TypeScript、pnpm monorepo;Fastify、Prisma、Zod、MySQL 8;Oracle(node-oracledb);Vue 3、Vite、Pinia;Vitest、Playwright;Docker Compose、nginx
規模
896 項自動化測試;25 則設計決策紀錄;188 筆歷史訂單全流程驗證;56 欄 Oracle 正式表鏡像編輯

書審對應:附件 E-3

中心 Oracle 正式表的鏡像編輯器:56 欄一欄不少,流水號由系統配發、不可修改,改完按「儲存」才一次寫入(局部;客戶編號已遮蔽)
中心 Oracle 正式表的鏡像編輯器:56 欄一欄不少,流水號由系統配發、不可修改,改完按「儲存」才一次寫入(局部;客戶編號已遮蔽)
本頁目錄
  1. 原本的流程與交接難題
  2. 規則還原:領域規格與 25 則決策紀錄
  3. 架構:pnpm monorepo,領域層前後端共用
  4. 規則設計
  5. 試算表式前端
  6. 兩條匯入路徑、表名後綴與 Oracle 替身
  7. 登入、權杖與稽核
  8. 鏡像編輯與統計的安全設計
  9. 測試:896 項自動化測試
  10. 等價驗證:逐字元比對與 188 筆歷史訂單
  11. 部署與交接

原本的流程與交接難題

中心的衛星影像徵求計畫受理學術研究與基礎教育的申請,提供的影像要在中心的 Oracle 資料庫建立訂單。原本以一份 Excel 活頁簿處理:

  1. 在「資料輸入」表逐筆填入客戶編號、影像編號與可選的範圍座標;
  2. 篩出要處理的客戶,把狀態設為 1、按 F9 重算,由公式配發流水號;
  3. 從「SQL 輸出」表複製整段 INSERT ALL,貼進資料庫工具執行,並手動去掉首尾多餘的引號;
  4. 完成後把狀態改為 2,資料留在「資料留存」表。

規則散在儲存格公式、資料驗證與設定格式裡:流水號以 NOW() 加上依列號遞增的秒數產生,靠自我參照與反覆運算維持不變;SQL 以 TEXTJOIN 串接固定範圍,註記可處理 999 筆,實際上限是 499 筆;TWD97 與經緯度同時填寫時,經緯度會被靜默忽略。規則沒有文件,也沒有自動化測試,交接只能從公式反推。難點在於把規則還原成可測試的邏輯,並證明輸出與原流程一致。

規則還原:領域規格與 25 則決策紀錄

逐條讀出公式、資料驗證與設定格式,寫成 10 章的領域規格,有疑義處與中心逐條確認(例如 Excel 的每一欄都要顯示、作廢列的畫面行為)。規格是唯一真相:規則要改先改規格,再改程式與測試;與 Excel 不同之處都寫明理由,例如兩種座標同時填寫改為明確錯誤、配號改由伺服器集中配發、超過筆數自動分段而不截斷。

設計取捨寫成 25 則決策紀錄(8 月 19 日至 9 月 15 日),每則記載背景、選項、決策、後果與刻意不做的事。需求在四週內多次改變形狀:8 月 25 日是批次工具,26 日改為直接編輯中心資料表,28 日再改為整張資料表的編輯器加 SQL 執行頁,29 日加入前台,之後陸續加入單一登入、統計與自動追蹤。

主題決策紀錄
資料與寫入自有資料存 MySQL(0001);直接寫入 Oracle、保留產生 SQL(0002);作廢不刪除(0003);SQL 執行頁(0009);中心資料頁改指正式表(0023)
介面形狀欄序照資料表(0004);原生核取方塊(0005);讀取並直接編輯中心資料表(0006 至 0008);Excel/CSV 匯入(0010);整表一次顯示(0011);導覽與版面(0015、0016、0019)
權限與身分前後台分離(0012);入口網單一登入(0017);帳號刪除條件(0018);權限收斂為管理員(0020)
前台功能追蹤表(0013);客戶影像紀錄(0014);統計只送加總(0021);已結案分頁(0022、0025);自動追蹤依原公式計算(0024)

架構:pnpm monorepo,領域層前後端共用

套件內容與邊界
packages/core檢核、衍生欄位、流水號、SQL 產生、貼上與 CSV 解析、自動追蹤、統計加總;純函式,lint 禁止 import Vue、Fastify、Prisma 與 Node 內建模組,可直接在瀏覽器執行
apps/apiFastify;Prisma 存取 MySQL 8(系統自有的 9 張表);只有 Oracle 連線層可 import 驅動;授權集中於 policy 層
apps/webVue 3 試算表介面;即時檢核直接呼叫 core;鍵盤與格線各只有一份實作

前端檢核是體驗,後端以同一份程式碼再驗一次才是權威。需求多次改變骨架,core 的領域邏輯都沒有改過形狀;新需求先問「這是規則還是版面」,規則一律進 core。lint 另限單檔 300 行、函式 50 行、巢狀 3 層。

規則設計

  • 影像編號:格式為 SSS_TTT_YYYYMMDD_HHMMSS,去除空白、全形轉半形後檢核。長度不為 23、第 4、8、17 字元不是底線、日期段不合法或在未來為錯誤;應為數字處出現 I 或 O(既有 6 筆)、取像時間不合理(既有 2 筆)、同批重複只列警告,以免擋掉真實存在的編號。
  • 座標:八格全空為全幅;TWD97 只輸入兩個對角、展開為四角;經緯度四格皆為數值;其餘為錯誤(188 筆歷史訂單為 159 筆全幅、29 筆經緯度)。另加 Excel 沒有的範圍警告,檢查座標是否落在臺灣附近、兩對角是否相同。資料庫存使用者輸入的原字串,正規化只在檢核與輸出時重算,因為數值型別會讓輸出對不上 Excel(121.3 會被補成 121.3000000000000)。
  • 流水號:Excel 的「每列加一秒」在多人操作時會撞號,改為伺服器集中配號:以 SELECT … FOR UPDATE 鎖住配號表,下一號取「目前時刻」與「上一號加一」的較大者,原子配發連號;唯一索引為最後防線,撞號即回滾重試(上限 3 次),並有併發測試。
  • 處理狀態與作廢:沿用 Excel 的 0、1、2(待確認、準備匯入、已完成),到 2 之後不得退回。已配號的列不可刪除、只能作廢;作廢以獨立欄位記錄、與狀態正交,事後仍看得出那個號是否真的進了中心資料庫;作廢不可逆並寫入稽核。
  • SQL 產生:每筆訂單三段 INTO(主資料依座標型態三選一、DTM 檔、影像處理方式)。逐字元細節寫入規格:INTO 前兩個空白、VALUES 前四個、欄位清單後一個尾隨空白(最後一行除外)、客戶編號加引號而取像日期不加、座標照原始精度、換行一律 LF。超過設定筆數(預設 500)自動分段並標明「第 n 段/共 m 段」。core 的 SQL 模組是唯一組字串之處:單引號加倍、數值須為有限數、拒絕控制字元,並以注入樣本測試。

試算表式前端

中心要求從 Excel 換到網頁後,使用體驗不能差太多。操作規格逐鍵對照 Excel,18 個按鍵逐一實作並各有元件測試:

  • 編輯與選取:Enter 往下一列,F2 或直接打字進入編輯,離開儲存格即確認、只有 Esc 放棄;點列號或欄標題選整列整欄,⌘/Ctrl 加選、⇧ 連選;⌘+、⌘- 插入與刪除列;複製出去是 TSV,可直接貼回 Excel;復原 100 步;所有改動按「儲存」才在一個交易內寫入。
  • 貼上與匯入:有標題列依欄名對應,沒有時依欄數推斷版面並提醒,寫入前列出逐欄對應供核對;八成以上的列在同一欄報同一錯時判定為整欄錯位,只回一句提示;500 列貼上實測 3.6 秒。Excel 與 CSV 檔在瀏覽器內解析,不上傳伺服器。
  • 整表與呈現:整張表一次載入(上限 5,000 列),只把看得見的列畫進 DOM;錯誤不用顏色,改用側槽的校對符號;深淺兩套主題各量測 24 個位置,文字對比皆達 WCAG AA;主線流程有純鍵盤的端對端測試。
前台追蹤表:試算表式介面,自動追蹤欄由系統依三個日期計算(局部)
前台追蹤表:試算表式介面,自動追蹤欄由系統依三個日期計算(局部)
依計畫年度的統計頁:逐年趨勢與衛星種類(局部;金額已遮蔽)
依計畫年度的統計頁:逐年趨勢與衛星種類(局部;金額已遮蔽)

兩條匯入路徑、表名後綴與 Oracle 替身

中心於 8 月 25 日決定由系統直接寫入中心資料庫,產生 SQL 保留為並存的退路。同一份已驗證的資料列產生兩種輸出:給人複製的文字版逐字元照規格,給系統執行的版本以繫結變數傳值。繫結值一律以字串攜帶,因為 121.3 經過浮點數可能變成 121.30000000000000426。

直接寫入(預設)產生 SQL(退路)
送出前重新檢核,有錯即整批拒絕並指出列號同左
判重本系統的寫入紀錄,加上在同一個 Oracle 交易內查中心的流水號不需要
完成單一交易,commit 成功自動轉為已完成;失敗整批回滾並指出哪一列承辦人員確認後手動標記
留底SQL 全文與 SHA-256 檢核碼同左
  • 表名後綴由一個設定同時套用在直接寫入與產生的 SQL:留空為正式表,填入後綴則整組指向結構相同的測試表,不會出現「寫進測試表、貼出去的 SQL 卻打正式表」。目標 schema 以連線後設定 CURRENT_SCHEMA 決定,給人複製的文字因此不必加前綴;兩個設定都走嚴格白名單,格式不符即啟動失敗。
  • 中心資料庫版本較舊,驅動須用 thick mode 並打包 Instant Client 19.x;未設定連線時直接寫入自動停用、系統照常運作。流水號欄為定長 CHAR,判重先去除尾端空白再比對;NUMBER 一律以十進位字串取回,避免經緯度多出浮點雜訊。
  • Oracle 替身:開發環境的 Oracle XE 容器照抄 8 月 26 日取得的真實建表語法,連測試表組與附屬表主鍵停用的狀態一併重現,免得開發環境擋下正式環境擋不下的東西;8 月 28 日以只下 SELECT 的腳本逐欄比對中心三張表,零差異(主表 56 欄)。替身只綁本機與容器橋接位址;整合測試若連線字串不是本機即直接失敗。

登入、權杖與稽核

  • 帳號與權杖:密碼以 Argon2id 雜湊(64 MiB、迭代 3、平行度 4),首次登入強制改密碼。access 為 15 分鐘的 JWT,refresh 為 7 天的隨機字串、雜湊後存資料庫,兩者都放在 httpOnly cookie;refresh 每次換發即輪替,偵測到重用就撤銷該使用者的全部工作階段。登入失敗 5 次鎖定 15 分鐘,另以來源 IP 限制速率。
  • 單一登入:實作中央大學入口網的 OAuth 2.0 授權碼流程。回呼指向後端,前端看不到授權碼;state 為 32 位元組亂數;登入後的落點只收站內路徑,避免開放轉址;以入口網的 identifier 對應帳號(數字 id 跨系統會變);自動開通者一律為一般使用者,且不以同名帳號自動接上,避免權限提升;保留本機密碼作為入口網故障時的退路。以 25 項整合測試對真的 HTTP 伺服器驗證。
  • 授權:判斷集中在 policy 層;前端路由守衛為純函式,並以不變式測試要求除少數公開頁外,每一頁都標為管理員專用;後端每個端點再檢查一次。
  • 稽核只寫不改:登入、帳號異動、訂單列增改刪、配號與匯出全部記錄;中心資料表每一列異動記下操作者、時間、來源 IP 與改前改後;SQL 執行頁與客戶影像查詢也都記錄。帳號只有未留下營運資料者刪得掉。

鏡像編輯與統計的安全設計

中心於 8 月 28 日把需求改為:網站要和資料庫一樣完整,但具備 Excel 的操作。管理員的中心資料頁因此鏡像整張資料表:

  • 欄位定義向資料庫取得,欄名逐一過白名單才拼進 SQL;主鍵(流水號)只出現在 WHERE、不可修改,其餘欄位皆可編輯。
  • 檢核對齊 Oracle 收不收:型別、精度、長度、必填不符即整批擋下;影像編號與座標等領域規則降為提醒,因為中心既有資料本來就有這類列。
  • 一次儲存一個交易,全成或全敗。中心的資料表不只本系統在寫、又沒有版本欄位,因此改或刪之前以 SELECT … FOR UPDATE 鎖列,比對這次要寫的欄位與使用者載入時的值,對不上就整批中止;刪除前先把整列內容寫入稽核(本系統的訂單列則只作廢、不刪除)。
  • SQL 執行頁只接受查詢與資料異動,結構變更、權限與交易控制一律拒絕;一次一句,查詢自動加上筆數上限並偵測截斷,每分鐘 60 句,每次執行都寫稽核。
  • 統計頁是唯一對一般使用者開放的資料頁,只送出加總,不含申請人姓名、計畫名稱、客戶編號與單筆影像編號。時間軸為計畫年度(6 月 1 日至翌年 5 月 31 日),對應中心成果報告的統計表;加總在 core 以 bigint 精確計算,金額與面積全程以字串攜帶;圖表手寫 SVG、單一色相、不畫雙軸。客戶影像紀錄頁同樣只送出四欄、一律唯讀。

測試:896 項自動化測試

套件測試數範圍
領域層 core346 項規則正反例、SQL 逐字元樣本、自動追蹤對照、統計加總
後端 api389 項連真實 MySQL 與 Oracle 替身;每個端點正常、未登入、越權、輸入錯誤四條路徑;配號併發;直接寫入的成功、回滾與重複寫入
前端 web161 項鍵盤、選取、貼上解析、列虛擬化、錯誤呈現

2026 年 9 月 25 日於中心伺服器實跑,896 項全數通過;core 的驗收標準含分支覆蓋率 100%。另以 Playwright 開真的瀏覽器、打真的後端做端對端測試,涵蓋主線(登入、建批次、貼上、修錯、配號、產生 SQL、標記完成)、帳號管理、純鍵盤操作與前台各頁,也對正式 Compose 堆疊跑過;登入的速率限制不為測試調鬆,改為整份測試共用一次登入。

等價驗證:逐字元比對與 188 筆歷史訂單

  1. SQL 逐字元樣本:依規格獨立推導四份預期輸出(三種座標型態各一、混合一),推導時不參考實作程式碼;測試逐字元比對,行尾空白與檔尾無換行都算數。
  2. 兩種輸出等價:從文字版剖析出值,與繫結參數逐項比對,確認兩者寫入相同的值。
  3. 188 筆歷史訂單:以中心實際的 188 筆歷史訂單跑完整流程;歷史資料匯入指令完成後校驗筆數、座標型態分布與客戶數,不符即中止;實跑兩次,第一次新增 188 筆、第二次全數略過。
  4. 自動追蹤:以追蹤表 128 列的三個日期與 Excel 自己算出的結果為對照組,固定「今天」為原工作簿最後重算的 2026 年 8 月 1 日,128 列全數相符;Excel 的比較語意(數字恆小於文字、空白等於 0)一併照抄。

部署與交接

  • 部署:Docker Compose 三個容器,只有 nginx 對外,MySQL 與 api 不開埠;映像多階段建置、以非 root 身分執行並設健康檢查;HTML 的 CSP 於建置時由行內 script 的雜湊算出。已部署於中心伺服器供內部使用。
  • 交接文件:交接說明(閱讀順序、現況、環境建置、逐條記錄症狀與解法的環境紀錄)、領域規格、架構、操作規格、設計語言、部署文件(含上線前檢查清單與中心資料庫的實際結構)、25 則決策紀錄,以及每項附驗收標準的工作清單。