軟體開發平行測試(Parallel Run / Parallel Testing)標準程序與計劃書教學手冊

版本:1.1
最後更新:2026-02-13
適用對象:專案經理、系統架構師、開發工程師、測試工程師、品質保證人員
文件等級:內部標準作業文件

目錄


第一章 平行測試概論

學習目標

  • 理解平行測試的定義、目的與價值
  • 區分平行測試與其他測試階段的差異
  • 掌握適用場景與成功關鍵因素

1.1 什麼是平行測試

平行測試(Parallel Run / Parallel Testing) 是指在系統轉換、升級或重寫過程中,同時運行新舊兩套系統,以相同的輸入資料執行相同的業務流程,再將兩套系統的產出進行逐項比對,以驗證新系統的正確性與一致性。

核心概念

┌─────────────┐     相同輸入資料     ┌─────────────┐
│   舊系統      │ ◄──────────────► │   新系統      │
│  (Baseline)  │                    │  (Target)    │
└──────┬───────┘                    └──────┬───────┘
       │                                    │
       ▼                                    ▼
  舊系統產出                           新系統產出
       │                                    │
       └──────────► 差異比對 ◄──────────────┘
                       │
                       ▼
                  差異報告與分析

平行測試的本質

面向說明
What新舊系統同步運行,比對結果一致性
Why確保新系統在正式上線前,業務邏輯與資料處理結果與舊系統完全一致
When系統轉換 / 升級 / 重寫的驗證階段
Who開發團隊、測試團隊、業務單位、風控單位
How使用相同輸入,比對輸出差異,分析並修正

實務提醒:平行測試不是「跑一次就好」,而是需要多個完整業務週期(如日結、月結、季結)的反覆驗證。


1.2 與其他測試類型的差異

測試類型目的比對對象執行時機資料來源
SIT(系統整合測試)驗證系統內部模組整合規格文件 vs 系統行為開發完成後測試資料
UAT(使用者驗收測試)驗證業務需求滿足度業務需求 vs 系統行為SIT 通過後模擬真實資料
Regression Test(回歸測試)確保修改未影響既有功能修改前 vs 修改後每次變更後自動化測試資料
Parallel Run(平行測試)驗證新舊系統一致性舊系統產出 vs 新系統產出UAT 通過後、上線前真實營運資料

關鍵差異

flowchart LR
    A[需求分析] --> B[系統設計]
    B --> C[開發實作]
    C --> D[SIT 系統整合測試]
    D --> E[UAT 使用者驗收測試]
    E --> F[Parallel Run 平行測試]
    F --> G[上線決策]
    G --> H[正式上線]
    
    style F fill:#ff9900,stroke:#333,stroke-width:3px,color:#000

注意事項:平行測試是上線前的「最後防線」,其重要性高於其他測試階段。若平行測試未通過,嚴禁上線


1.3 適用場景

必須執行平行測試的場景

場景說明風險等級
核心系統轉換如銀行帳務系統、保險理賠系統遷移🔴 極高
舊系統下線以新系統取代舊系統的完整功能🔴 極高
批次系統重寫如日結、月結報表、利息計算批次改寫🟠 高
金融交易系統涉及金額計算、帳務處理的系統變更🔴 極高
法規驅動變更因法規要求修改計算邏輯(如稅率、費率)🟠 高
平台遷移從大型主機遷移至開放平台🔴 極高
資料庫遷移更換資料庫引擎(如 DB2 → PostgreSQL)🟠 高

可選擇性執行的場景

場景說明建議
前端改版純 UI 變更,後端邏輯不變可用回歸測試替代
非核心系統內部管理工具等低風險系統依風險評估決定
新功能開發無舊系統可比對不適用平行測試

1.4 平行測試的目標

主要目標

  1. 正確性驗證:確認新系統的業務邏輯計算結果與舊系統一致
  2. 完整性驗證:確認所有業務場景都已覆蓋,無遺漏
  3. 效能驗證:確認新系統的處理效能符合上線要求
  4. 穩定性驗證:確認新系統在持續運行多個週期後仍保持穩定
  5. 資料一致性:確認資料遷移與轉換的正確性

次要目標

  • 發現潛在的設計缺陷
  • 驗證異常處理機制
  • 建立運維團隊對新系統的信心
  • 為上線決策提供客觀數據依據

1.5 平行測試成功的關鍵因素

mindmap
  root((平行測試<br/>成功關鍵))
    明確的範圍定義
      功能邊界
      資料邊界
      時間邊界
    完善的資料準備
      真實資料同步
      異常資料涵蓋
      邊界值設計
    清晰的差異判定
      容忍誤差定義
      差異分類標準
      升級處理流程
    自動化比對
      比對工具
      報表自動產出
      CI/CD 整合
    充足的時間
      多週期驗證
      含日結月結
      緩衝時間
    高層支持
      資源保障
      決策時效
      風險承擔

十大關鍵成功因素

#因素說明
1明確定義成功標準不能含糊——「零差異」或「容差範圍內」都要事先定義
2使用真實營運資料模擬資料無法覆蓋所有業務場景
3自動化差異比對人工比對效率低、易遺漏
4足夠的測試週期至少涵蓋日結、月結、季結完整週期
5明確的 RACI 矩陣每個角色的責任清楚定義
6差異即時處理機制發現差異後的升級與修正流程
7完整的稽核軌跡所有比對結果、修正紀錄都需留存
8高層管理支持資源調配、決策時效的保障
9回滾計畫新系統出問題時的退路
10充分的溝通開發、測試、業務、風控各方對齊

最佳實務:在專案啟動時就規劃平行測試,而非在 UAT 完成後才臨時安排。平行測試的準備時間通常占整體測試時程的 30% 以上。


第二章 平行測試標準作業流程(SOP)

學習目標

  • 掌握平行測試的完整作業流程
  • 理解每個階段的產出物與控制點
  • 明確角色責任分配

2.1 流程總覽

flowchart TD
    A[1. 平行測試評估] --> B[2. 測試範圍定義]
    B --> C[3. 測試資料準備]
    C --> D[4. 測試環境建置]
    D --> E[5. 平行執行]
    E --> F[6. 差異比對]
    F --> G[7. 差異分析]
    G --> H{差異數量<br/>是否可接受?}
    H -->|否| I[8. 修正與重測]
    I --> E
    H -->|是| J[9. 結案判定]
    J --> K[10. 切換上線決策]
    
    style A fill:#4CAF50,color:#fff
    style E fill:#2196F3,color:#fff
    style H fill:#FF9800,color:#fff
    style J fill:#9C27B0,color:#fff
    style K fill:#f44336,color:#fff

流程時間估算(大型專案參考)

階段建議時間佔比
1-2. 評估與範圍定義2-4 週10%
3. 資料準備2-3 週10%
4. 環境建置1-2 週5%
5-8. 執行與修正(多輪)8-16 週60%
9-10. 結案與上線決策2-3 週15%

2.2 各階段詳細說明

階段 1:平行測試評估

目的:評估是否需要執行平行測試、規模與資源需求

輸入

  • 專案計畫書
  • 系統變更影響分析
  • 風險評估報告

活動

  1. 評估系統變更的風險等級
  2. 決定是否需要平行測試
  3. 估算所需資源(人力、環境、時間)
  4. 取得管理層核准

產出

  • 平行測試可行性評估報告
  • 資源需求清單
  • 管理層核准文件

控制點

  • ✅ 風險等級為「高」或以上,必須執行平行測試
  • ✅ 管理層書面核准

階段 2:測試範圍定義

目的:明確定義平行測試的功能範圍、資料範圍與時間範圍

活動

  1. 列出所有受影響的業務功能
  2. 識別需要比對的資料欄位
  3. 定義測試週期與時間窗口
  4. 確認不納入範圍的項目及其原因

產出

  • 測試範圍說明書(含 In-Scope / Out-of-Scope)
  • 比對項目清單
  • 測試時程表

控制點

  • ✅ 業務單位簽核確認範圍
  • ✅ 風控單位審核範圍的完整性

階段 3:測試資料準備

目的:準備可用於平行測試的真實或仿真資料

活動

  1. 確認資料來源(Production 複製 / 數據脫敏)
  2. 設計資料同步機制
  3. 準備邊界值與異常測試資料
  4. 執行資料驗證(完整性、一致性)

產出

  • 資料準備計畫
  • 資料脫敏規則(如適用)
  • 資料驗證報告

控制點

  • ✅ 資料脫敏符合個資法規
  • ✅ 資料完整性驗證通過
  • ⚠️ 嚴禁使用正式環境資料直接作為測試資料而不脫敏

階段 4:測試環境建置

目的:建立獨立、隔離的平行測試環境

活動

  1. 建置新系統測試環境
  2. 確保舊系統可正常運行(或使用 Production 資料快照)
  3. 設置資料同步管道
  4. 部署比對工具與報表系統
  5. 執行環境驗證測試

產出

  • 環境建置檢查表
  • 環境驗證報告

控制點

  • ✅ 環境與 Production 隔離
  • ✅ 環境驗證測試通過
  • ✅ 資料同步管道正常運作

階段 5:平行執行

目的:同時運行新舊系統,產出比對所需的結果資料

活動

  1. 將相同輸入資料饋入新舊系統
  2. 同步觸發批次任務
  3. 記錄執行日誌
  4. 監控系統效能與穩定性
  5. 每日匯報執行狀況

產出

  • 執行日誌
  • 效能監控報告
  • 每日狀況報告

控制點

  • ✅ 輸入資料完全一致
  • ✅ 執行時序同步
  • ⚠️ 任何系統異常需立即通報

階段 6:差異比對

目的:比對新舊系統的輸出結果,產出差異報告

活動

  1. 執行自動化比對工具
  2. 彙整差異報告
  3. 標記差異嚴重等級
  4. 初步分類差異類型

產出

  • 差異比對報告
  • 差異統計摘要

階段 7:差異分析

目的:深入分析每筆差異的根因,判定是否為問題

差異分類

分類說明處理方式
A:程式缺陷新系統邏輯錯誤必須修正
B:設計差異新系統改進設計,結果正確但不同評估後決定
C:資料差異輸入資料不一致造成修正資料同步
D:時序差異執行時間差異導致調整同步機制
E:容差內差異在預設容忍誤差範圍內記錄但不修正
F:已知差異預期內的變更記錄,標註為已知

產出

  • 差異分析報告(含根因分析)
  • 修正清單

階段 8:修正與重測

目的:修正已確認的問題,並重新執行驗證

活動

  1. 開發團隊修正程式
  2. 經過 Code Review 與 SIT
  3. 部署至平行測試環境
  4. 重新執行受影響範圍的平行測試
  5. 驗證修正結果

控制點

  • ✅ 修正程式碼經過 Code Review
  • ✅ 修正不引入新問題(回歸驗證)

階段 9:結案判定

目的:依據成功標準判定平行測試是否通過

判定條件

  1. 所有 A 類(程式缺陷)差異已修正並驗證
  2. 所有 B 類差異已評估並取得業務簽核
  3. 連續 N 個完整業務週期無新增 A 類差異
  4. 效能指標符合上線標準
  5. 各單位簽署結案確認

產出

  • 平行測試結案報告
  • 各單位簽核紀錄

階段 10:切換上線決策

目的:依據平行測試結論,做出上線 / 延後上線的決策

決策會議參與者

  • 專案負責人
  • 系統架構師
  • 品質保證負責人
  • 業務代表
  • 風控 / 法遵代表
  • 高階管理者

決策依據

  • 平行測試結案報告
  • 殘留風險清單
  • 回滾計畫可行性
  • 上線時間窗口

產出

  • 上線決策會議紀錄
  • 上線 / 延後上線通知

2.3 RACI 責任矩陣

階段PM架構師開發測試業務風控高管
1. 評估ARCCCRI
2. 範圍定義ARCRRCI
3. 資料準備ACRRCCI
4. 環境建置ARRCIII
5. 平行執行ACRRCII
6. 差異比對ACCRIII
7. 差異分析ARRRCCI
8. 修正重測ACRRCII
9. 結案判定ARCRRRI
10. 上線決策RCICRRA

R = Responsible(執行者)、A = Accountable(當責者)、C = Consulted(諮詢者)、I = Informed(知會者)

實務提醒:RACI 矩陣必須在專案啟動時確認,並經各單位主管簽核。若角色模糊,平行測試必然失敗。


第三章 平行測試計劃書範本

學習目標

  • 能獨立撰寫完整的平行測試計劃書
  • 了解每個章節的內容要求與撰寫技巧

以下為計劃書範本,粗體字 為需填入的項目。


3.1 專案基本資訊

項目內容
文件編號PT-YYYY-NNN
專案名稱例:核心帳務系統升級專案
系統名稱例:核心帳務處理系統(Core Banking System)
平行測試期間YYYY/MM/DD ~ YYYY/MM/DD
預計測試週期數例:3 個完整月結週期
專案負責人姓名 / 職稱
測試負責人姓名 / 職稱
文件版本v1.0
核准日期YYYY/MM/DD

文件審核紀錄

版本日期修改說明作者審核者
v1.0YYYY/MM/DD初版
v1.1YYYY/MM/DD

3.2 測試範圍

3.2.1 功能範圍(In-Scope)

序號功能模組說明優先等級
1例:存款利息計算包含活存、定存利息日計與月計P0
2例:匯款交易處理國內匯款、跨行轉帳P0
3例:日結批次日終帳務結算P0
4例:報表產出資產負債表、損益表P1

3.2.2 批次範圍

序號批次名稱執行頻率說明
1例:BATCH-001 日結每日日終帳務處理
2例:BATCH-002 利息計算每月活存利息計算
3例:BATCH-003 報表每日監理報表產製

3.2.3 不納入範圍(Out-of-Scope)

序號項目排除原因
1例:信用卡系統不在本次遷移範圍
2例:電子銀行界面前端無變更

3.3 測試策略

3.3.1 同步方式

類型說明適用場景
Real-time 即時同步新舊系統同時處理每筆交易線上交易系統
Batch 批次同步以批次方式將資料同步處理批次處理系統
Replay 重播模式錄製舊系統交易,在新系統重播高流量線上系統

3.3.2 比對方式

flowchart LR
    A[比對方式] --> B[欄位級比對]
    A --> C[總額級比對]
    A --> D[筆數級比對]
    A --> E[報表級比對]
    
    B --> B1["逐筆逐欄比對<br/>精準度:最高<br/>成本:最高"]
    C --> C1["金額加總比對<br/>精準度:中<br/>成本:低"]
    D --> D1["筆數統計比對<br/>精準度:低<br/>成本:最低"]
    E --> E1["報表內容比對<br/>精準度:高<br/>成本:中"]
比對層級精準度適用場景說明
欄位級★★★★★核心交易、金額計算逐筆逐欄位比對
總額級★★★☆☆初步驗證、快速檢核比對加總金額
筆數級★★☆☆☆資料完整性檢查確認筆數一致
報表級★★★★☆報表產出驗證比對報表內容

3.3.3 自動化工具規劃

工具類型建議工具用途
資料比對自建比對程式 / Beyond Compare逐欄位差異比對
SQL 比對DBeaver / 自建 SQL Script資料庫層級比對
API 比對Postman / RestAssuredAPI 回應比對
報表比對自建工具 / diff 工具產出報表比對
效能監控Grafana / Prometheus系統效能指標

3.4 測試資料設計

3.4.1 資料策略

策略說明適用場景
全量測試使用完整 Production 資料核心系統、高風險系統
抽樣測試選取代表性樣本資料量過大時
組合測試全量 + 特殊設計資料建議的最佳方式

3.4.2 特殊測試資料設計

資料類型範例目的
邊界值金額 = 0、最大金額、最小金額驗證邊界條件
異常資料不合法的帳號、超額交易驗證錯誤處理
高風險交易大額匯款、跨幣種交易驗證高風險場景
歷史資料跨年度、跨季度的資料驗證歷史資料相容
併發資料同帳號同時多筆交易驗證並發處理

3.5 差異判定標準

3.5.1 容忍誤差(Tolerance)定義

資料類型容忍誤差說明
金額(整數)0(零容差)金額不允許任何差異
金額(含小數)±0.01 元因四捨五入可能產生 1 分差異
利率計算±0.0001%浮點數計算容差
筆數0(零容差)筆數必須完全一致
日期0(零容差)日期不允許差異
文字欄位完全一致不允許差異(含空白)
時間戳記±5 秒系統處理時間差異

3.5.2 差異嚴重等級

等級名稱定義處理時限範例
S1致命金額計算錯誤、資料遺失4 小時利息少算 100 元
S2嚴重功能異常但不影響金額1 工作天交易狀態碼不一致
S3一般非核心欄位差異3 工作天摘要文字不同
S4輕微格式差異、排序不同下次修正日期格式差異

3.6 風險評估

3.6.1 風險評估矩陣

風險項目類別可能性影響度風險值因應措施
資料同步失敗技術🟠建立資料同步監控與自動重試
測試時間不足專案🔴預留 20% 緩衝時間
人力不足資源🟡提前確認人力配置
環境不穩定技術🟠環境日常健檢
法規變更影響法規🟡持續追蹤法規動態
資料外洩風險資安極高🟠資料脫敏、存取控制

3.6.2 風險處理策略

flowchart TD
    A[風險識別] --> B{風險等級}
    B -->|🔴 紅| C[立即啟動應變計畫<br/>通報高階管理層]
    B -->|🟠 橙| D[密切監控<br/>每日回報]
    B -->|🟡 黃| E[定期追蹤<br/>週報回報]
    B -->|🟢 綠| F[記錄備查]

3.7 成功標準(Exit Criteria)

必要條件(Must Have)

  • 所有 S1(致命)差異歸零
  • 所有 S2(嚴重)差異歸零
  • 連續 3 個完整業務日 無新增 S1/S2 差異
  • 批次處理 100% 成功完成
  • 效能指標 ≤ 舊系統的 110%(或符合 SLA)
  • 筆數比對 100% 一致
  • 金額比對在容差範圍內

建議條件(Should Have)

  • S3(一般)差異 ≤ 5 筆
  • 自動化比對覆蓋率 ≥ 90%
  • 所有測試場景已執行並通過
  • 運維團隊完成培訓
  • 回滾演練至少執行 1 次

文件條件

  • 平行測試結案報告完成
  • 差異清單已全數處理(修正或標註已知)
  • 各單位簽核完成
  • 上線檢核清單填寫完成

實務提醒:Exit Criteria 必須在平行測試開始前由各單位共同確認,不得在測試過程中隨意調降標準


3.8 溝通計畫

平行測試期間的溝通計畫是確保各利害關係人對齊資訊、及時處理差異的關鍵。

3.8.1 溝通矩陣

溝通事項頻率方式發送者接收者內容
每日摘要報告每日Email + 系統測試 Lead全體成員比對結果摘要、差異趨勢
S1 差異通報即時電話 + 簡訊 + Email測試工程師開發 Lead、PM、風控差異詳情、初步影響評估
S2 差異通報4 小時內Email + IM測試工程師開發 Lead、PM差異詳情、建議處理方式
週報每週會議 + 書面PM管理層、業務代表進度摘要、風險更新、決策需求
里程碑報告每輪結束正式會議PM全部利害關係人測試結果、修正摘要、下一步計畫
升級通報視需求會議PM高階主管重大風險、延遲預警、決策請求

3.8.2 會議機制

會議名稱頻率時長必要參與者議程
每日站會每日15 min測試 Lead、開發 Lead昨日差異處理、今日計畫、阻礙
差異分析會視需求30-60 min開發、測試、業務S1/S2 差異根因分析與對策
週檢視會每週60 minPM、測試、開發、業務週進度回顧、風險評估、決策
管理層簡報每 2 週30 minPM、高階主管整體進度、關鍵指標、風險

3.8.3 通報升級機制

flowchart TD
    A[發現問題] --> B{嚴重等級?}
    B -->|S1 致命| C[立即通報]
    C --> C1[測試 Lead → 開發 Lead → PM]
    C1 --> C2[30 分鐘內召開緊急會議]
    C2 --> C3[4 小時內提出修正方案]
    B -->|S2 嚴重| D[4 小時內通報]
    D --> D1[測試 Lead → 開發 Lead]
    D1 --> D2[1 工作天內分析根因]
    B -->|S3/S4| E[列入每日報告]
    E --> E1[依排程處理]
    
    C3 --> F{超過 24 小時未解決?}
    F -->|是| G[升級至高階主管]
    F -->|否| H[持續追蹤]

實務提醒

  1. 溝通計畫應在平行測試啟動前經各方確認
  2. 所有重要溝通需留下書面紀錄(會議紀錄、Email 存檔)
  3. 測試期間建議建立專屬的即時通訊群組(如 Teams/Slack 頻道)
  4. S1 差異的通報不可僅依賴 Email,必須搭配電話或即時通訊確認收到

第四章 差異比對設計

學習目標

  • 掌握各種比對策略的設計原則
  • 理解數值容差與精度處理的技巧
  • 能設計 SQL、批次、API 層級的比對邏輯

4.1 比對策略總覽

比對架構設計

flowchart TD
    A[新系統產出] --> C[資料擷取層]
    B[舊系統產出] --> C
    C --> D[資料標準化]
    D --> E[比對引擎]
    E --> F{差異判定}
    F -->|有差異| G[差異記錄]
    F -->|無差異| H[通過記錄]
    G --> I[差異分類引擎]
    I --> J[差異報告]
    H --> K[通過報告]
    J --> L[匯總儀表板]
    K --> L

比對層級與策略

層級比對方式工具適用場景
資料庫層SQL 查詢比對自建 SQL Script結構化資料、批次結果
檔案層逐行/逐欄比對diff、自建工具報表、輸出檔案
API 層Request/Response 比對Postman、自建框架線上交易、API 服務
訊息層MQ 訊息比對MQ 監控工具訊息佇列系統
報表層報表內容比對PDF/Excel diff 工具監理報表、對帳單

4.2 數值容差與精度處理

4.2.1 浮點數誤差處理

浮點數(float/double)在二進位表示時會有精度損失,這是平行測試中最常見的差異來源之一。

/**
 * 錯誤示範:使用 double 計算金額
 */
public class BadExample {
    public static void main(String[] args) {
        double amount1 = 0.1 + 0.2;
        System.out.println(amount1); // 輸出:0.30000000000000004 ❌
    }
}

/**
 * 正確做法:使用 BigDecimal 計算金額
 */
import java.math.BigDecimal;
import java.math.RoundingMode;

public class GoodExample {
    public static void main(String[] args) {
        BigDecimal amount1 = new BigDecimal("0.1");
        BigDecimal amount2 = new BigDecimal("0.2");
        BigDecimal result = amount1.add(amount2);
        System.out.println(result); // 輸出:0.3 ✅
        
        // 利息計算範例:本金 × 年利率 ÷ 365
        BigDecimal principal = new BigDecimal("1000000");
        BigDecimal rate = new BigDecimal("0.025");
        BigDecimal dailyInterest = principal.multiply(rate)
            .divide(new BigDecimal("365"), 2, RoundingMode.HALF_UP);
        System.out.println("日利息:" + dailyInterest); // 68.49
    }
}

4.2.2 四捨五入差異

新舊系統可能使用不同的捨入方式,導致結果差異:

捨入方式Java 對應說明範例(2.5)
四捨五入HALF_UP最常見3
銀行家捨入HALF_EVEN金融系統常用2
無條件進位CEILING向上取整3
無條件捨去FLOOR向下取整2

實務提醒:在比對前,必須確認新舊系統使用的捨入方式是否一致。若不一致,需在比對工具中設定對應的容差規則。

4.2.3 日期與時區差異

差異類型原因處理方式
時區差異新舊系統使用不同時區統一轉換為 UTC
格式差異YYYY/MM/DD vs YYYY-MM-DD標準化格式後比對
精度差異毫秒 vs 秒截斷至相同精度
營業日差異新舊系統營業日定義不同統一營業日設定

4.2.4 BigDecimal 精度問題

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 平行測試比對工具 - 金額比對邏輯
 */
public class AmountComparator {
    
    /** 容忍誤差:0.01 元 */
    private static final BigDecimal TOLERANCE = new BigDecimal("0.01");
    
    /**
     * 比對兩筆金額是否在容忍範圍內
     * @param oldAmount 舊系統金額
     * @param newAmount 新系統金額
     * @return 比對結果
     */
    public static CompareResult compareAmount(BigDecimal oldAmount, BigDecimal newAmount) {
        BigDecimal diff = oldAmount.subtract(newAmount).abs();
        
        if (diff.compareTo(BigDecimal.ZERO) == 0) {
            return new CompareResult("EXACT_MATCH", diff);
        } else if (diff.compareTo(TOLERANCE) <= 0) {
            return new CompareResult("WITHIN_TOLERANCE", diff);
        } else {
            return new CompareResult("MISMATCH", diff);
        }
    }
    
    /**
     * 比對結果紀錄
     */
    public record CompareResult(String status, BigDecimal difference) {}
}

4.3 SQL 比對範例

4.3.1 筆數比對

-- ============================================
-- 筆數比對:確認新舊系統交易筆數一致
-- ============================================

-- 舊系統交易筆數
SELECT 
    TXN_DATE,
    TXN_TYPE,
    COUNT(*) AS OLD_COUNT
FROM OLD_SYSTEM.TRANSACTION_LOG
WHERE TXN_DATE = '2026-02-12'
GROUP BY TXN_DATE, TXN_TYPE
ORDER BY TXN_TYPE;

-- 新系統交易筆數
SELECT 
    TXN_DATE,
    TXN_TYPE,
    COUNT(*) AS NEW_COUNT
FROM NEW_SYSTEM.TRANSACTION_LOG
WHERE TXN_DATE = '2026-02-12'
GROUP BY TXN_DATE, TXN_TYPE
ORDER BY TXN_TYPE;

-- 差異比對(JOIN 查詢)
SELECT 
    COALESCE(o.TXN_TYPE, n.TXN_TYPE) AS TXN_TYPE,
    COALESCE(o.OLD_COUNT, 0) AS OLD_COUNT,
    COALESCE(n.NEW_COUNT, 0) AS NEW_COUNT,
    COALESCE(n.NEW_COUNT, 0) - COALESCE(o.OLD_COUNT, 0) AS DIFF
FROM (
    SELECT TXN_TYPE, COUNT(*) AS OLD_COUNT
    FROM OLD_SYSTEM.TRANSACTION_LOG
    WHERE TXN_DATE = '2026-02-12'
    GROUP BY TXN_TYPE
) o
FULL OUTER JOIN (
    SELECT TXN_TYPE, COUNT(*) AS NEW_COUNT
    FROM NEW_SYSTEM.TRANSACTION_LOG
    WHERE TXN_DATE = '2026-02-12'
    GROUP BY TXN_TYPE
) n ON o.TXN_TYPE = n.TXN_TYPE
WHERE COALESCE(o.OLD_COUNT, 0) <> COALESCE(n.NEW_COUNT, 0);

4.3.2 金額比對

-- ============================================
-- 金額比對:逐筆比對交易金額
-- ============================================
SELECT 
    o.TXN_ID,
    o.ACCOUNT_NO,
    o.TXN_TYPE,
    o.AMOUNT AS OLD_AMOUNT,
    n.AMOUNT AS NEW_AMOUNT,
    ABS(o.AMOUNT - n.AMOUNT) AS DIFF_AMOUNT,
    CASE 
        WHEN o.AMOUNT = n.AMOUNT THEN 'MATCH'
        WHEN ABS(o.AMOUNT - n.AMOUNT) <= 0.01 THEN 'WITHIN_TOLERANCE'
        ELSE 'MISMATCH'
    END AS COMPARE_RESULT
FROM OLD_SYSTEM.TRANSACTION_LOG o
JOIN NEW_SYSTEM.TRANSACTION_LOG n 
    ON o.TXN_ID = n.TXN_ID 
    AND o.TXN_DATE = n.TXN_DATE
WHERE o.TXN_DATE = '2026-02-12'
    AND o.AMOUNT <> n.AMOUNT
ORDER BY ABS(o.AMOUNT - n.AMOUNT) DESC;

4.3.3 總額比對

-- ============================================
-- 總額比對:比對每日交易總額
-- ============================================
SELECT 
    'OLD_SYSTEM' AS SYSTEM_NAME,
    TXN_DATE,
    TXN_TYPE,
    COUNT(*) AS TXN_COUNT,
    SUM(AMOUNT) AS TOTAL_AMOUNT,
    MIN(AMOUNT) AS MIN_AMOUNT,
    MAX(AMOUNT) AS MAX_AMOUNT,
    AVG(AMOUNT) AS AVG_AMOUNT
FROM OLD_SYSTEM.TRANSACTION_LOG
WHERE TXN_DATE = '2026-02-12'
GROUP BY TXN_DATE, TXN_TYPE

UNION ALL

SELECT 
    'NEW_SYSTEM' AS SYSTEM_NAME,
    TXN_DATE,
    TXN_TYPE,
    COUNT(*) AS TXN_COUNT,
    SUM(AMOUNT) AS TOTAL_AMOUNT,
    MIN(AMOUNT) AS MIN_AMOUNT,
    MAX(AMOUNT) AS MAX_AMOUNT,
    AVG(AMOUNT) AS AVG_AMOUNT
FROM NEW_SYSTEM.TRANSACTION_LOG
WHERE TXN_DATE = '2026-02-12'
GROUP BY TXN_DATE, TXN_TYPE
ORDER BY TXN_TYPE, SYSTEM_NAME;

4.3.4 舊系統有但新系統沒有的資料

-- ============================================
-- 找出舊系統有但新系統缺少的交易
-- ============================================
SELECT 
    o.TXN_ID,
    o.ACCOUNT_NO,
    o.TXN_TYPE,
    o.AMOUNT,
    o.TXN_DATETIME,
    'MISSING_IN_NEW' AS ISSUE_TYPE
FROM OLD_SYSTEM.TRANSACTION_LOG o
LEFT JOIN NEW_SYSTEM.TRANSACTION_LOG n 
    ON o.TXN_ID = n.TXN_ID
WHERE n.TXN_ID IS NULL
    AND o.TXN_DATE = '2026-02-12';

-- ============================================
-- 找出新系統有但舊系統沒有的交易(可能是多產出的資料)
-- ============================================
SELECT 
    n.TXN_ID,
    n.ACCOUNT_NO,
    n.TXN_TYPE,
    n.AMOUNT,
    n.TXN_DATETIME,
    'EXTRA_IN_NEW' AS ISSUE_TYPE
FROM NEW_SYSTEM.TRANSACTION_LOG n
LEFT JOIN OLD_SYSTEM.TRANSACTION_LOG o 
    ON n.TXN_ID = o.TXN_ID
WHERE o.TXN_ID IS NULL
    AND n.TXN_DATE = '2026-02-12';

4.4 批次比對邏輯範例

4.4.1 Java 批次比對框架

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 平行測試批次比對引擎
 * 
 * <p>用於比對新舊系統批次產出的結果檔案</p>
 */
public class BatchCompareEngine {

    /** 比對設定 */
    private final CompareConfig config;
    
    /** 差異記錄清單 */
    private final List<DiffRecord> diffRecords = new ArrayList<>();

    public BatchCompareEngine(CompareConfig config) {
        this.config = config;
    }

    /**
     * 執行批次比對
     * 
     * @param oldRecords 舊系統記錄
     * @param newRecords 新系統記錄
     * @return 比對結果報告
     */
    public CompareReport execute(
            List<Map<String, Object>> oldRecords,
            List<Map<String, Object>> newRecords) {

        // 1. 以主鍵建立索引
        Map<String, Map<String, Object>> oldIndex = buildIndex(oldRecords);
        Map<String, Map<String, Object>> newIndex = buildIndex(newRecords);

        // 2. 檢查舊系統有但新系統沒有的記錄
        for (String key : oldIndex.keySet()) {
            if (!newIndex.containsKey(key)) {
                diffRecords.add(new DiffRecord(
                    key, "MISSING_IN_NEW", "整筆記錄",
                    oldIndex.get(key).toString(), "N/A", "S1"
                ));
            }
        }

        // 3. 檢查新系統有但舊系統沒有的記錄
        for (String key : newIndex.keySet()) {
            if (!oldIndex.containsKey(key)) {
                diffRecords.add(new DiffRecord(
                    key, "EXTRA_IN_NEW", "整筆記錄",
                    "N/A", newIndex.get(key).toString(), "S2"
                ));
            }
        }

        // 4. 逐欄比對共同存在的記錄
        for (String key : oldIndex.keySet()) {
            if (newIndex.containsKey(key)) {
                compareFields(key, oldIndex.get(key), newIndex.get(key));
            }
        }

        // 5. 產出報告
        return generateReport(oldRecords.size(), newRecords.size());
    }

    /**
     * 逐欄位比對
     */
    private void compareFields(
            String key,
            Map<String, Object> oldRecord,
            Map<String, Object> newRecord) {

        for (String field : config.getCompareFields()) {
            Object oldVal = oldRecord.get(field);
            Object newVal = newRecord.get(field);
            
            // 取得該欄位的容差設定
            BigDecimal tolerance = config.getTolerance(field);
            
            if (!isEqual(oldVal, newVal, tolerance)) {
                String severity = config.getSeverity(field);
                diffRecords.add(new DiffRecord(
                    key, "FIELD_MISMATCH", field,
                    String.valueOf(oldVal),
                    String.valueOf(newVal),
                    severity
                ));
            }
        }
    }

    /**
     * 判斷兩個值是否相等(含容差判斷)
     */
    private boolean isEqual(Object oldVal, Object newVal, BigDecimal tolerance) {
        if (oldVal == null && newVal == null) return true;
        if (oldVal == null || newVal == null) return false;
        
        // 數值型別:使用容差比對
        if (oldVal instanceof Number && newVal instanceof Number) {
            BigDecimal oldNum = new BigDecimal(oldVal.toString());
            BigDecimal newNum = new BigDecimal(newVal.toString());
            return oldNum.subtract(newNum).abs().compareTo(tolerance) <= 0;
        }
        
        // 其他型別:字串比對
        return oldVal.toString().trim().equals(newVal.toString().trim());
    }

    /**
     * 以主鍵建立索引
     */
    private Map<String, Map<String, Object>> buildIndex(
            List<Map<String, Object>> records) {
        return records.stream().collect(Collectors.toMap(
            r -> config.getPrimaryKeyFields().stream()
                    .map(f -> String.valueOf(r.get(f)))
                    .collect(Collectors.joining("|")),
            r -> r,
            (r1, r2) -> r1 // 重複 key 取第一筆
        ));
    }

    /**
     * 產出比對報告
     */
    private CompareReport generateReport(int oldCount, int newCount) {
        long s1Count = diffRecords.stream()
            .filter(d -> "S1".equals(d.severity())).count();
        long s2Count = diffRecords.stream()
            .filter(d -> "S2".equals(d.severity())).count();

        return new CompareReport(
            oldCount, newCount, diffRecords.size(),
            s1Count, s2Count, diffRecords
        );
    }

    // ===== 內部資料結構 =====

    /** 差異記錄 */
    public record DiffRecord(
        String primaryKey,
        String diffType,
        String fieldName,
        String oldValue,
        String newValue,
        String severity
    ) {}

    /** 比對報告 */
    public record CompareReport(
        int oldSystemCount,
        int newSystemCount,
        int totalDiffs,
        long s1Count,
        long s2Count,
        List<DiffRecord> details
    ) {}
}

4.4.2 比對設定範例

/**
 * 比對設定類別
 */
public class CompareConfig {
    
    /** 主鍵欄位 */
    private List<String> primaryKeyFields = List.of("TXN_ID");
    
    /** 需比對的欄位 */
    private List<String> compareFields = List.of(
        "ACCOUNT_NO", "TXN_TYPE", "AMOUNT", 
        "BALANCE", "TXN_DATE", "STATUS"
    );
    
    /** 欄位容差設定 */
    private Map<String, BigDecimal> toleranceMap = Map.of(
        "AMOUNT", new BigDecimal("0.01"),
        "BALANCE", new BigDecimal("0.01"),
        "INTEREST", new BigDecimal("0.001")
    );
    
    /** 欄位嚴重等級 */
    private Map<String, String> severityMap = Map.of(
        "AMOUNT", "S1",    // 金額差異 → 致命
        "BALANCE", "S1",   // 餘額差異 → 致命
        "STATUS", "S2",    // 狀態差異 → 嚴重
        "TXN_DATE", "S2",  // 日期差異 → 嚴重
        "ACCOUNT_NO", "S1" // 帳號差異 → 致命
    );

    // ... getters ...
    
    public BigDecimal getTolerance(String field) {
        return toleranceMap.getOrDefault(field, BigDecimal.ZERO);
    }
    
    public String getSeverity(String field) {
        return severityMap.getOrDefault(field, "S3");
    }
    
    public List<String> getPrimaryKeyFields() { return primaryKeyFields; }
    public List<String> getCompareFields() { return compareFields; }
}

4.5 API 回傳比對範例

4.5.1 API 比對架構

sequenceDiagram
    participant Client as 比對客戶端
    participant Old as 舊系統 API
    participant New as 新系統 API
    participant Engine as 比對引擎
    participant DB as 差異資料庫

    Client->>Old: 發送請求
    Old-->>Client: 回傳結果 A
    Client->>New: 發送相同請求
    New-->>Client: 回傳結果 B
    Client->>Engine: 送入 A 與 B
    Engine->>Engine: 逐欄位比對
    Engine->>DB: 儲存差異結果
    Engine-->>Client: 回傳比對摘要

4.5.2 API 比對工具範例

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.http.*;
import java.net.URI;
import java.util.*;

/**
 * API 回傳比對工具
 * 
 * <p>同時呼叫新舊系統 API,比對回傳結果</p>
 */
public class ApiCompareClient {

    private final HttpClient httpClient = HttpClient.newHttpClient();
    private final ObjectMapper mapper = new ObjectMapper();
    
    private final String oldSystemBaseUrl;
    private final String newSystemBaseUrl;

    public ApiCompareClient(String oldSystemBaseUrl, String newSystemBaseUrl) {
        this.oldSystemBaseUrl = oldSystemBaseUrl;
        this.newSystemBaseUrl = newSystemBaseUrl;
    }

    /**
     * 比對 API 回傳結果
     * 
     * @param endpoint API 路徑
     * @param requestBody 請求內容
     * @return 比對結果
     */
    public ApiCompareResult compare(String endpoint, String requestBody) 
            throws Exception {
        
        // 1. 呼叫舊系統
        String oldResponse = callApi(oldSystemBaseUrl + endpoint, requestBody);
        
        // 2. 呼叫新系統
        String newResponse = callApi(newSystemBaseUrl + endpoint, requestBody);
        
        // 3. 解析 JSON
        JsonNode oldJson = mapper.readTree(oldResponse);
        JsonNode newJson = mapper.readTree(newResponse);
        
        // 4. 比對
        List<String> diffs = new ArrayList<>();
        compareJsonNodes("", oldJson, newJson, diffs);
        
        return new ApiCompareResult(
            endpoint, oldResponse, newResponse, 
            diffs, diffs.isEmpty()
        );
    }

    /**
     * 遞迴比對 JSON 節點
     */
    private void compareJsonNodes(
            String path, JsonNode oldNode, JsonNode newNode, 
            List<String> diffs) {
        
        if (oldNode == null && newNode == null) return;
        
        if (oldNode == null) {
            diffs.add(String.format("[%s] 舊系統無此欄位, 新系統=%s", 
                path, newNode));
            return;
        }
        if (newNode == null) {
            diffs.add(String.format("[%s] 舊系統=%s, 新系統無此欄位", 
                path, oldNode));
            return;
        }
        
        if (oldNode.isObject()) {
            Iterator<String> fields = oldNode.fieldNames();
            while (fields.hasNext()) {
                String field = fields.next();
                compareJsonNodes(
                    path + "." + field,
                    oldNode.get(field), 
                    newNode.get(field), 
                    diffs
                );
            }
            // 檢查新系統多出的欄位
            Iterator<String> newFields = newNode.fieldNames();
            while (newFields.hasNext()) {
                String field = newFields.next();
                if (!oldNode.has(field)) {
                    diffs.add(String.format(
                        "[%s.%s] 新系統多出欄位=%s", 
                        path, field, newNode.get(field)));
                }
            }
        } else if (!oldNode.equals(newNode)) {
            diffs.add(String.format(
                "[%s] 舊系統=%s, 新系統=%s", 
                path, oldNode, newNode));
        }
    }

    private String callApi(String url, String body) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        
        HttpResponse<String> response = httpClient.send(
            request, HttpResponse.BodyHandlers.ofString());
        return response.body();
    }

    /** API 比對結果 */
    public record ApiCompareResult(
        String endpoint,
        String oldResponse,
        String newResponse,
        List<String> differences,
        boolean isMatch
    ) {}
}

實務提醒:API 比對時需注意以下事項:

  1. 排除動態欄位(如 timestamp、requestId)
  2. 處理 JSON 欄位順序差異
  3. 注意回傳 null 與空字串的差異
  4. 記錄 API 回應時間差異作為效能參考

第五章 自動化平行測試設計

學習目標

  • 設計自動化比對架構
  • 將平行測試整合至 CI/CD 流程
  • 建立自動化報表與差異分類機制

5.1 自動化架構

5.1.1 整體架構圖

flowchart TB
    subgraph 資料來源
        A1[舊系統 DB]
        A2[舊系統 API]
        A3[舊系統 File]
    end
    
    subgraph 資料擷取
        B1[DB Extractor]
        B2[API Recorder]
        B3[File Collector]
    end
    
    subgraph 比對引擎
        C1[Schema Validator]
        C2[Data Normalizer]
        C3[Compare Engine]
        C4[Diff Classifier]
    end
    
    subgraph 報告系統
        D1[Diff Report]
        D2[Summary Dashboard]
        D3[Alert System]
    end
    
    subgraph 新系統
        E1[新系統 DB]
        E2[新系統 API]
        E3[新系統 File]
    end
    
    A1 --> B1
    A2 --> B2
    A3 --> B3
    E1 --> B1
    E2 --> B2
    E3 --> B3
    
    B1 --> C1
    B2 --> C1
    B3 --> C1
    C1 --> C2
    C2 --> C3
    C3 --> C4
    
    C4 --> D1
    C4 --> D2
    C4 --> D3

5.1.2 自動化比對設計原則

原則說明
可配置比對規則透過設定檔管理,不寫死在程式中
可擴充支援新增比對類型(DB、API、File 等)
冪等性相同輸入重複執行產出相同結果
可追溯每次執行保留完整日誌與差異記錄
漸進式支援增量比對,不需每次全量

5.1.3 比對設定檔範例(YAML)

# parallel-test-config.yaml
# 平行測試比對設定檔

compare-jobs:
  - name: "daily-transaction-compare"
    description: "每日交易比對"
    schedule: "0 2 * * *"  # 每日凌晨 2 點執行
    source:
      old-system:
        type: database
        connection: "jdbc:oracle:thin:@old-host:1521:OLDDB"
        query: "SELECT * FROM TRANSACTION_LOG WHERE TXN_DATE = :date"
      new-system:
        type: database
        connection: "jdbc:postgresql://new-host:5432/newdb"
        query: "SELECT * FROM transaction_log WHERE txn_date = :date"
    
    primary-key: ["txn_id"]
    
    compare-fields:
      - field: "account_no"
        type: string
        severity: S1
      - field: "amount"
        type: decimal
        severity: S1
        tolerance: 0.01
      - field: "balance"
        type: decimal
        severity: S1
        tolerance: 0.01
      - field: "txn_type"
        type: string
        severity: S2
      - field: "status"
        type: string
        severity: S2
    
    exclude-fields: ["created_at", "updated_at", "system_id"]
    
    report:
      format: ["html", "csv", "json"]
      output-dir: "/reports/parallel-test/"
      notify:
        - type: email
          recipients: ["team@company.com"]
          condition: "has_s1_diff"
        - type: slack
          channel: "#parallel-test"
          condition: "always"

5.2 CI/CD 整合

5.2.1 Jenkins Pipeline 範例

// Jenkinsfile - 平行測試 Pipeline
pipeline {
    agent any
    
    parameters {
        string(name: 'TEST_DATE', defaultValue: '', 
               description: '測試日期 (YYYY-MM-DD)')
        choice(name: 'TEST_SCOPE', choices: ['DAILY', 'MONTHLY', 'FULL'],
               description: '測試範圍')
    }
    
    environment {
        OLD_SYSTEM_URL = credentials('old-system-url')
        NEW_SYSTEM_URL = credentials('new-system-url')
    }
    
    stages {
        stage('環境檢查') {
            steps {
                script {
                    echo "檢查新舊系統連線狀態..."
                    sh './scripts/check-connectivity.sh'
                }
            }
        }
        
        stage('資料擷取') {
            parallel {
                stage('擷取舊系統資料') {
                    steps {
                        sh """
                            java -jar parallel-test-tool.jar extract \
                                --system old \
                                --date ${params.TEST_DATE} \
                                --scope ${params.TEST_SCOPE}
                        """
                    }
                }
                stage('擷取新系統資料') {
                    steps {
                        sh """
                            java -jar parallel-test-tool.jar extract \
                                --system new \
                                --date ${params.TEST_DATE} \
                                --scope ${params.TEST_SCOPE}
                        """
                    }
                }
            }
        }
        
        stage('執行比對') {
            steps {
                sh """
                    java -jar parallel-test-tool.jar compare \
                        --date ${params.TEST_DATE} \
                        --config parallel-test-config.yaml
                """
            }
        }
        
        stage('產出報告') {
            steps {
                sh """
                    java -jar parallel-test-tool.jar report \
                        --date ${params.TEST_DATE} \
                        --format html,csv
                """
                publishHTML(target: [
                    reportDir: 'reports',
                    reportFiles: 'parallel-test-report.html',
                    reportName: '平行測試報告'
                ])
            }
        }
        
        stage('差異檢查') {
            steps {
                script {
                    def result = readJSON file: 'reports/summary.json'
                    if (result.s1_count > 0) {
                        currentBuild.result = 'FAILURE'
                        error "發現 ${result.s1_count} 筆 S1 差異!"
                    }
                    if (result.s2_count > 0) {
                        currentBuild.result = 'UNSTABLE'
                        echo "警告:發現 ${result.s2_count} 筆 S2 差異"
                    }
                }
            }
        }
    }
    
    post {
        failure {
            emailext(
                subject: "⚠️ 平行測試失敗 - ${params.TEST_DATE}",
                body: '${FILE,path="reports/parallel-test-report.html"}',
                to: 'team@company.com'
            )
        }
        always {
            archiveArtifacts artifacts: 'reports/**', fingerprint: true
        }
    }
}

5.2.2 GitHub Actions 範例

# .github/workflows/parallel-test.yml
name: Parallel Test

on:
  schedule:
    - cron: '0 18 * * 1-5'  # UTC 18:00 = 台灣凌晨 2:00
  workflow_dispatch:
    inputs:
      test_date:
        description: '測試日期'
        required: true
        type: string

jobs:
  parallel-test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      
      - name: Run Compare
        run: |
          java -jar tools/parallel-test-tool.jar compare \
            --date ${{ github.event.inputs.test_date || 'today' }} \
            --config config/parallel-test-config.yaml
      
      - name: Upload Report
        uses: actions/upload-artifact@v4
        with:
          name: parallel-test-report
          path: reports/
      
      - name: Check Results
        run: |
          S1_COUNT=$(jq '.s1_count' reports/summary.json)
          if [ "$S1_COUNT" -gt 0 ]; then
            echo "::error::發現 $S1_COUNT 筆 S1 致命差異!"
            exit 1
          fi

5.3 測試報表與差異分類

5.3.1 報表格式範本

每日摘要報表

╔══════════════════════════════════════════════════════╗
║     平行測試每日摘要報表                               ║
║     日期:2026-02-12                                  ║
╠══════════════════════════════════════════════════════╣
║                                                      ║
║  📊 比對統計                                          ║
║  ├─ 舊系統筆數:125,430                               ║
║  ├─ 新系統筆數:125,430                               ║
║  ├─ 比對筆數 :125,430                               ║
║  └─ 差異筆數 :3                                     ║
║                                                      ║
║  🔍 差異分布                                          ║
║  ├─ S1 致命:0                                       ║
║  ├─ S2 嚴重:1                                       ║
║  ├─ S3 一般:1                                       ║
║  └─ S4 輕微:1                                       ║
║                                                      ║
║  📈 趨勢                                             ║
║  ├─ 較昨日差異:減少 2 筆 ✅                           ║
║  └─ 連續零 S1 天數:5 天                              ║
║                                                      ║
║  ⚡ 效能指標                                          ║
║  ├─ 批次執行時間(舊):45 min                         ║
║  ├─ 批次執行時間(新):38 min                         ║
║  └─ 效能改善:15.6% ✅                                ║
║                                                      ║
╚══════════════════════════════════════════════════════╝

5.3.2 差異自動分類規則

flowchart TD
    A[發現差異] --> B{金額欄位?}
    B -->|是| C{差額 > 0.01?}
    C -->|是| D[🔴 S1 致命]
    C -->|否| E[容差內,記錄但不報警]
    B -->|否| F{狀態/代碼欄位?}
    F -->|是| G[🟠 S2 嚴重]
    F -->|否| H{文字/摘要欄位?}
    H -->|是| I[🟡 S3 一般]
    H -->|否| J{格式/排序差異?}
    J -->|是| K[🟢 S4 輕微]
    J -->|否| L[未分類,人工判斷]

5.3.3 日誌與追蹤機制

日誌類型內容保存期限儲存位置
執行日誌每次比對的開始/結束時間、筆數1 年Log Server
差異明細每筆差異的新舊值、欄位、時間2 年差異資料庫
修正記錄差異修正的人員、時間、修正內容永久版本控制系統
決策記錄結案/上線決策的會議紀錄永久文件管理系統

實務提醒

  1. 自動化比對覆蓋率目標 ≥ 90%,剩餘 10% 由人工抽驗
  2. 每日自動執行比對,異常即時通知
  3. 報表需同時提供摘要版(給管理層)與明細版(給技術團隊)
  4. 所有比對結果需保留至少至上線後 6 個月,供稽核查核

第六章 金融系統平行測試實務案例

學習目標

  • 透過實際案例了解金融系統平行測試的執行方式
  • 掌握銀行核心系統升級的平行測試關鍵要點

案例:銀行核心帳務系統升級平行測試

案例背景

項目說明
專案名稱核心帳務系統現代化專案
遷移方向IBM 大型主機 COBOL → Java / PostgreSQL
影響範圍存款、放款、匯款、外匯等核心模組
帳戶數量約 300 萬戶
日交易量約 50 萬筆
風險等級🔴 極高

6.1 測試週期規劃

gantt
    title 核心帳務系統平行測試時程
    dateFormat  YYYY-MM-DD
    
    section 準備階段
    環境建置與驗證       :a1, 2026-01-05, 14d
    資料準備與同步       :a2, 2026-01-12, 10d
    比對工具部署         :a3, 2026-01-19, 7d
    
    section 第一輪(試跑)
    第一輪平行執行       :b1, 2026-01-26, 14d
    差異分析與修正       :b2, 2026-02-09, 10d
    
    section 第二輪(正式)
    第二輪平行執行       :c1, 2026-02-23, 21d
    差異分析與修正       :c2, 2026-03-16, 7d
    
    section 第三輪(驗證)
    第三輪平行執行       :d1, 2026-03-23, 14d
    結案評估             :d2, 2026-04-06, 5d
    
    section 上線
    上線決策會議         :e1, 2026-04-11, 2d
    正式切換             :e2, 2026-04-13, 3d
    上線後監控           :e3, 2026-04-16, 14d

6.2 批次日結流程比對

flowchart TD
    A[營業日結束] --> B[交易資料截止]
    B --> C1[舊系統日結批次]
    B --> C2[新系統日結批次]
    
    C1 --> D1[舊系統產出]
    C2 --> D2[新系統產出]
    
    D1 --> E[比對項目]
    D2 --> E
    
    E --> F1[1. 交易筆數比對]
    E --> F2[2. 交易金額比對]
    E --> F3[3. 帳戶餘額比對]
    E --> F4[4. 利息計算比對]
    E --> F5[5. 報表內容比對]
    E --> F6[6. 資金調撥比對]
    
    F1 --> G[差異報告]
    F2 --> G
    F3 --> G
    F4 --> G
    F5 --> G
    F6 --> G
    
    G --> H{有 S1/S2 差異?}
    H -->|是| I[通報處理]
    H -->|否| J[記錄通過]

日結比對檢核項目

序號比對項目比對層級容差嚴重等級
1存款交易筆數筆數級0S1
2存款交易總額總額級0S1
3放款交易筆數筆數級0S1
4放款交易總額總額級0S1
5各帳戶餘額欄位級±0.01S1
6活存利息計算欄位級±0.01S1
7定存利息計算欄位級±0.01S1
8逾期放款利息欄位級±0.01S1
9資產負債表報表級0S1
10監理報表報表級0S1

6.3 差異處理流程

flowchart TD
    A[發現差異] --> B[自動分類]
    B --> C{差異等級}
    
    C -->|S1 致命| D[立即通知]
    D --> D1[開發 Lead]
    D --> D2[測試 Lead]
    D --> D3[PM]
    D1 --> E[4 小時內分析根因]
    E --> F[修正程式]
    F --> G[Code Review]
    G --> H[部署重測]
    
    C -->|S2 嚴重| I[當日通知]
    I --> J[1 工作天內分析]
    J --> F
    
    C -->|S3 一般| K[列入追蹤清單]
    K --> L[排程處理]
    
    C -->|S4 輕微| M[記錄備查]
    
    H --> N{重測通過?}
    N -->|是| O[標記已解決]
    N -->|否| P[升級處理]
    P --> D

6.4 高風險交易監控

監控項目門檻值監控頻率通知方式
大額交易差異金額 ≥ 100 萬即時簡訊 + Email
跨幣種交易任何差異即時Email
聯行交易金額差異 > 0每小時Email
利息計算差異 > ±0.01每日系統告警
帳戶開銷戶筆數不一致每日系統告警

6.5 上線決策會議

決策會議議程

議程時間報告人
1. 平行測試結案報告30 min測試負責人
2. 殘留問題清單15 min開發負責人
3. 效能評估報告15 min架構師
4. 回滾計畫確認15 minPM
5. 風險評估報告15 min風控代表
6. 法遵確認10 min法遵代表
7. 上線 / 延後決策20 min高階主管

上線決策判定表

條件狀態判定
S1 差異歸零✅ / ❌必要
S2 差異歸零✅ / ❌必要
連續 3 天零 S1 差異✅ / ❌必要
效能符合 SLA✅ / ❌必要
回滾計畫經過演練✅ / ❌必要
運維團隊準備就緒✅ / ❌必要
法遵審核通過✅ / ❌必要
所有單位簽核✅ / ❌必要

以上條件全部為 ✅ 方可決議上線。任一項為 ❌,必須延後上線。

實務提醒

  1. 金融系統平行測試建議至少跨越 一個完整月結週期
  2. 若涉及利息計算,建議包含 季結或年結 的驗證
  3. 上線時間建議選在 長假前的非月底時段,預留問題處理時間
  4. 上線後至少 30 天 持續比對,確認新系統穩定

第七章 風險管理與內控設計

學習目標

  • 理解平行測試中的風險管理框架
  • 掌握內控機制的設計要點
  • 了解 ISO 27001 與法遵要求如何落實

7.1 分權機制

7.1.1 職責分離原則(Segregation of Duties)

核心原則:開發者不得自行執行比對、簽核結案。

flowchart LR
    subgraph 開發團隊
        A[修正程式碼]
    end
    subgraph 測試團隊
        B[執行比對]
        C[產出報告]
    end
    subgraph 業務單位
        D[驗證業務邏輯]
    end
    subgraph 風控/稽核
        E[覆核報告]
        F[簽核結案]
    end
    
    A --> B
    B --> C
    C --> D
    D --> E
    E --> F

7.1.2 權限矩陣

權限項目開發測試PM業務風控稽核
修改程式碼
部署至測試環境
執行比對工具
檢視差異報告
標記差異為「已知」
修改容差設定
簽核結案報告
決議上線

7.2 雙人覆核(Four-Eyes Principle)

需要雙人覆核的作業

作業項目第一覆核人第二覆核人說明
容差標準修改PM風控主管容差不得未經審核調寬
差異標記為「已知」測試 Lead業務負責人需業務確認為已知變更
結案判定測試 LeadPM + 風控至少三方確認
上線決策PM高階主管書面核准
回滾執行運維工程師架構師雙人在場執行

覆核紀錄格式

欄位說明
作業編號系統自動產生
作業類型結案判定 / 容差修改 / …
申請人提出申請的人員
申請時間時間戳記
第一覆核人姓名與時間戳記
第二覆核人姓名與時間戳記
覆核結果核准 / 退回
備註覆核意見

7.3 日誌保存

7.3.1 日誌保存規範

日誌類型內容最短保存期限格式
操作日誌人員操作紀錄(登入、執行、核准)3 年Structured Log(JSON)
比對日誌每次比對的輸入/輸出/結果2 年JSON + CSV
差異明細差異欄位、原值、新值2 年資料庫 + CSV
修正記錄程式修正的 Commit、CR 編號永久Git + 變更管理系統
決策紀錄會議紀錄、核准文件永久PDF(含簽章)
效能日誌系統效能指標1 年監控系統

7.3.2 日誌防竄改設計

日誌寫入 → 即時雜湊(SHA-256) → 寫入區塊鏈或 WORM 儲存 → 定期驗證完整性
防竄改措施說明
寫入即鎖定日誌寫入後不可修改、刪除
雜湊校驗每筆日誌計算 SHA-256,鏈式串接
異地備份日誌同步備份至獨立儲存
存取控制僅稽核人員有權查閱日誌

7.4 稽核需求

7.4.1 稽核查核項目

查核項目查核內容頻率
比對完整性是否所有範圍內項目都已比對每輪結束
差異處理所有差異是否都有處理紀錄每日
權限合規操作是否符合權限矩陣抽查
覆核落實雙人覆核是否確實執行每輪結束
日誌完整日誌是否完整且未被竄改每週
時效性S1/S2 差異是否在時限內處理每日

7.4.2 稽核報告範本結構

1. 稽核範圍與期間
2. 稽核方法
3. 查核發現
   3.1 合規事項
   3.2 不合規事項(含嚴重等級)
4. 改善建議
5. 管理層回應
6. 後續追蹤計畫

7.5 法遵要求

7.5.1 適用法規對應

法規 / 規範相關要求平行測試對應措施
個資法個人資料保護測試資料脫敏、存取控制
銀行法資訊系統安全管理完整變更管理、回滾計畫
金管會資安規範系統變更管理SOP 文件化、稽核軌跡
ISO 27001資訊安全管理分權、日誌、變更控管
ISO 22301營運持續管理回滾計畫、緊急應變
Basel III作業風險管理風險識別、控制點設計

7.5.2 ISO 27001 控制項對應

ISO 27001 控制項說明平行測試對應
A.8.1 資產管理測試環境與資料分類管理環境清單、資料分類標記
A.9.2 使用者存取管理測試系統存取控制權限矩陣、帳號管理
A.12.1 作業程序與責任標準作業流程SOP 文件化
A.12.4 日誌記錄與監控操作日誌保存日誌保存規範
A.14.2 開發與支援過程安全安全開發流程Code Review、分權機制
A.16.1 資安事件管理差異通報流程S1 差異即時通報

實務提醒

  1. 金融機構平行測試全程須留存稽核軌跡,不可事後補建
  2. 每一筆差異的處理都必須可追溯到具體的人、時間、決策理由
  3. 測試資料若包含真實客戶資料,必須脫敏處理
  4. 風控與法遵代表需全程參與,不可僅在結案時簽核

第八章 常見錯誤與失敗案例分析

學習目標

  • 從失敗案例中學習,避免重複犯錯
  • 建立風險意識與預防措施

8.1 常見錯誤總覽

#常見錯誤發生頻率影響嚴重度預防措施
1平行時間太短🔴 高🔴 極高至少 3 個完整業務週期
2測試資料不完整🔴 高🟠 高使用全量 Production 資料
3忽略例外交易🟠 中🔴 極高設計邊界值與異常資料
4沒有定義容忍誤差🔴 高🟠 高事前定義並經各方確認
5沒有設計回滾計畫🟠 中🔴 極高上線前必須演練回滾
6人工比對為主🟠 中🟠 高自動化比對 ≥ 90%
7範圍定義不清🟠 中🟠 高明確的 In/Out Scope
8跳過月結/季結🟠 中🔴 極高必須涵蓋完整週期
9缺乏管理層關注🟡 低🟠 高定期管理層簡報
10修正後忘記回歸🔴 高🔴 極高自動化回歸驗證

8.2 失敗案例分析

案例一:平行時間太短

情境:某銀行進行核心系統升級,因專案時程壓力,平行測試僅執行 5 個營業日。 結果:上線第一個月結,利息計算出現大量差異。
根因:5 天內未涵蓋月結、跳息等特殊場景。
教訓:平行測試必須至少涵蓋一個完整月結週期,高風險系統建議包含季結。

案例二:測試資料不完整

情境:使用抽樣的 10% 客戶資料做平行測試。
結果:上線後發現特定帳戶類型(如外幣帳戶、信託帳戶)計算邏輯錯誤。
根因:抽樣資料未涵蓋所有帳戶類型。
教訓:核心系統應使用全量資料,至少確保每種帳戶類型都有代表性樣本。

案例三:忽略例外交易

情境:平行測試只比對正常交易,未測試沖正、退匯、凍結等例外流程。
結果:上線後沖正交易導致帳務不平。
根因:例外交易佔比低但影響重大,被忽略。
教訓:必須設計包含所有交易類型的測試案例,特別是例外流程。

案例四:沒有定義容忍誤差

情境:新舊系統因浮點數計算方式不同,利息計算差 0.01 元。
結果:比對報告出現數十萬筆「差異」,團隊花費大量時間逐筆分析。
根因:未預先定義容忍誤差標準。
教訓:在開始比對前,必須定義每個欄位的容差,並經業務與風控確認。

案例五:沒有設計回滾計畫

情境:新系統上線後第 3 天發現嚴重問題,需要切回舊系統。
結果:因未設計回滾流程,花費 72 小時才恢復,造成嚴重營運損失。
根因:只規劃「上線」流程,未規劃「失敗回退」流程。
教訓:回滾計畫必須詳細到每個步驟,且至少演練一次。


8.3 預防機制

flowchart TD
    A[預防措施] --> B[流程面]
    A --> C[技術面]
    A --> D[管理面]
    
    B --> B1[完整的 SOP]
    B --> B2[嚴格的 Exit Criteria]
    B --> B3[回滾演練]
    
    C --> C1[自動化比對 ≥ 90%]
    C --> C2[容差預定義]
    C --> C3[全量資料測試]
    
    D --> D1[管理層定期檢視]
    D --> D2[獨立稽核]
    D --> D3[充足的預算與時間]

實務提醒:失敗的平行測試通常不是技術問題,而是時間不足範圍定義不清。務必在專案初期就爭取足夠的平行測試時間。


第九章 標準表單範本

學習目標

  • 熟悉各類標準表單的結構與用途
  • 能依實際需求調整表單內容

9.1 差異記錄表

欄位說明範例
差異編號系統自動產生DIFF-2026-0001
發現日期發現差異的日期2026-02-12
比對批次哪一輪哪一日R2-D05
交易日期交易發生日期2026-02-11
主鍵值識別紀錄的唯一鍵TXN-20260211-12345
差異欄位哪個欄位不同AMOUNT
舊系統值舊系統的值1,000.00
新系統值新系統的值999.99
差異量差異的絕對值0.01
嚴重等級S1 / S2 / S3 / S4S1
差異類型A-程式缺陷 / B-設計差異 / …A
根因分析差異原因說明四捨五入方式不同
處理狀態待分析 / 修正中 / 已修正 / 已知修正中
負責人負責修正的人員王大明
修正日期完成修正的日期2026-02-13
驗證結果修正後是否通過驗證✅ 通過

差異記錄表範例

差異編號發現日期差異欄位舊系統值新系統值等級類型狀態負責人
DIFF-000102-10AMOUNT50,000.0049,999.99S1A✅ 已修正王大明
DIFF-000202-10STATUS011S3B📝 已知李小華
DIFF-000302-11BALANCE1,234,567.891,234,567.90S1A🔧 修正中王大明
DIFF-000402-12MEMO轉帳轉帳交易S4B📝 已知

9.2 風險評估表

風險編號風險描述類別可能性影響度風險值因應措施負責人狀態
RISK-001測試資料同步失敗技術🟠建立同步監控與重試機制張工程師監控中
RISK-002平行測試時間不足專案🔴預留 20% 緩衝、每日報告進度PM監控中
RISK-003關鍵人員異動人力🟡知識轉移、文件化PM備查
RISK-004舊系統異常影響比對技術🟡設立舊系統異常排除規則架構師備查
RISK-005個資外洩風險資安極高🟠資料脫敏、存取控管資安主管監控中

風險評估矩陣

影響:低影響:中影響:高影響:極高
可能性:高🟡🟠🔴🔴
可能性:中🟢🟡🟠🔴
可能性:低🟢🟢🟡🟠

9.3 上線核准單

╔══════════════════════════════════════════════════════╗
║               上  線  核  准  單                      ║
╠══════════════════════════════════════════════════════╣
║                                                      ║
║  專案名稱:____________________                       ║
║  系統名稱:____________________                       ║
║  預定上線日期:____/__/__                             ║
║  申請人:__________  日期:____/__/__                  ║
║                                                      ║
╠══════════════════════════════════════════════════════╣
║  一、平行測試結果摘要                                  ║
║                                                      ║
║  測試期間:____/__/__ ~ ____/__/__                    ║
║  測試輪次:共 __ 輪                                   ║
║  最終差異數:S1=__ S2=__ S3=__ S4=__                  ║
║  連續零 S1 天數:____ 天                              ║
║                                                      ║
╠══════════════════════════════════════════════════════╣
║  二、核准條件確認                                      ║
║                                                      ║
║  □ S1 差異歸零                                       ║
║  □ S2 差異歸零                                       ║
║  □ 連續 3 天無新增 S1/S2 差異                         ║
║  □ 效能符合 SLA                                      ║
║  □ 回滾計畫已演練                                     ║
║  □ 運維團隊準備就緒                                   ║
║  □ 法遵審核通過                                      ║
║                                                      ║
╠══════════════════════════════════════════════════════╣
║  三、核准簽章                                         ║
║                                                      ║
║  PM:__________ 簽章:________ 日期:____/__/__        ║
║  測試Lead:_____ 簽章:________ 日期:____/__/__       ║
║  業務代表:_____ 簽章:________ 日期:____/__/__       ║
║  風控主管:_____ 簽章:________ 日期:____/__/__       ║
║  法遵代表:_____ 簽章:________ 日期:____/__/__       ║
║  高階主管:_____ 簽章:________ 日期:____/__/__       ║
║                                                      ║
╠══════════════════════════════════════════════════════╣
║  四、核准結果                                         ║
║                                                      ║
║  □ 核准上線    □ 延後上線    □ 退回補件              ║
║                                                      ║
║  備註:                                               ║
║  ________________________________________________    ║
║  ________________________________________________    ║
║                                                      ║
╚══════════════════════════════════════════════════════╝

9.4 測試每日報表

項目內容
報表日期YYYY/MM/DD
測試輪次第 __ 輪,第 __ 天

比對統計

比對項目舊系統筆數新系統筆數差異筆數較昨日
存款交易85,23085,2300
放款交易12,45012,4501↓1
匯款交易28,75028,7500↓2
合計126,430126,4301↓3

差異分布

等級今日新增今日修正累計未解趨勢
S1000
S2011
S3002
S4103

效能指標

指標舊系統新系統差異狀態
日結批次時間45 min38 min-15.6%
API 平均回應120 ms85 ms-29.2%
CPU 使用率(峰值)85%65%-23.5%

今日重點事項

事項說明處理狀態

9.5 問題追蹤清單

問題編號發現日期描述等級狀態負責人預計解決實際解決備註
ISS-00102-10利息計算四捨五入差異S1✅ 已關閉王大明02-1202-11修改為 HALF_UP
ISS-00202-11交易狀態碼格式不一致S3📝 已知李小華列為已知差異
ISS-00302-12餘額計算差 0.01S1🔧 處理中王大明02-14分析中

實務提醒:所有表單應存放於統一的文件管理系統(如 SharePoint、Confluence),並設定適當的存取權限。紙本文件需掃描歸檔。


9.6 回滾計畫範本

回滾計畫是平行測試與上線規劃中不可或缺的一環,確保新系統出現重大問題時能安全地切回舊系統。

9.6.1 回滾計畫基本資訊

項目內容
專案名稱_____________________
系統名稱_____________________
回滾計畫版本v1.0
編制人_____________________
審核人_____________________
最後更新____//

9.6.2 回滾觸發條件

#觸發條件說明決策權責
1上線後 S1 差異累計 ≥ 3 筆核心邏輯出現多處錯誤高階主管
2帳務不平新系統日結帳務不平衡PM + 風控主管
3資料遺失交易資料出現遺失或損壞架構師 + PM
4效能嚴重劣化回應時間超過 SLA 200%架構師
5外部系統連線中斷與聯行/央行系統無法正常通訊PM + 運維主管

9.6.3 回滾步驟

flowchart TD
    A[觸發回滾決策] --> B[召開緊急會議]
    B --> C[確認回滾範圍]
    C --> D[通知上下游系統]
    D --> E[暫停新系統服務]
    E --> F[執行資料回復]
    F --> G[啟動舊系統]
    G --> H[驗證舊系統正常]
    H --> I[恢復對外服務]
    I --> J[持續監控 2 小時]
    J --> K[發布回滾完成通知]
    K --> L[事後檢討會議]

9.6.4 回滾操作檢核表

步驟操作內容預估時間負責人確認人完成
1宣布進入回滾程序5 minPM高階主管
2暫停新系統所有服務入口10 min運維架構師
3執行資料庫回復至切換前快照30-60 minDBA架構師
4驗證資料庫回復一致性15 minDBA測試 Lead
5啟動舊系統應用服務10 min運維架構師
6切換 DNS/負載平衡器至舊系統5 min運維架構師
7執行舊系統健康檢查10 min測試 LeadPM
8執行樣本交易驗證15 min測試工程師業務代表
9通知上下游系統恢復連線10 minPM運維
10恢復對外服務5 min運維PM
11持續監控系統指標120 min運維架構師
12發送回滾完成通知5 minPM

9.6.5 回滾後處理

項目時限負責人說明
事件報告24 小時內PM記錄回滾原因、時間線、影響範圍
根因分析(RCA)3 工作天內開發 Lead深入分析導致回滾的技術原因
改善方案5 工作天內架構師制定修正方案與驗證計畫
重新排程1 週內PM更新專案時程,安排重新上線
檢討會議回滾後 2 天內PM全體參與,萃取教訓

實務提醒

  1. 回滾計畫必須在上線前至少演練一次(Dry Run)
  2. 回滾操作應有雙人到場執行(Four-Eyes Principle)
  3. 預估回滾總時間應控制在 2 小時以內(金融系統建議 1 小時內)
  4. 回滾期間的資料處理策略(回滾窗口內的新交易如何處理)必須事先定義

第十章 企業級最佳實踐(Best Practice)

學習目標

  • 掌握平行測試的企業級最佳實踐
  • 建立組織級的平行測試成熟度

10.1 核心最佳實踐

實踐 1:充足的測試週期

系統風險等級最少週期建議週期必須涵蓋
🔴 極高2 個月結週期3 個月結 + 1 季結日結、月結、季結、年結(如跨年)
🟠 高1 個月結週期2 個月結週期日結、月結
🟡 中2 週1 個月日結
🟢 低1 週2 週關鍵功能

實踐 2:全量比對為原則

全量比對(推薦)  ────────────────────────  抽樣比對(退而求其次)
   │                                              │
   ├─ 核心系統必須全量                                ├─ 非核心系統可抽樣
   ├─ 金融數據必須全量                                ├─ 抽樣率 ≥ 30%
   └─ 監理報表必須全量                                └─ 必須覆蓋所有類型

實踐 3:自動化比對比例 ≥ 90%

自動化等級自動化比例評估
Level 1< 30%❌ 不合格,風險極高
Level 230% - 60%⚠️ 勉強可用,建議改善
Level 360% - 80%🟡 尚可
Level 480% - 90%✅ 良好
Level 5≥ 90%✅✅ 優秀(目標)

實踐 4:完整稽核軌跡

flowchart LR
    A[操作紀錄] --> B[日誌收集器]
    C[比對結果] --> B
    D[修正記錄] --> B
    E[核准紀錄] --> B
    B --> F[統一日誌平台]
    F --> G[防竄改儲存]
    F --> H[即時監控]
    F --> I[稽核查詢介面]

10.2 組織級最佳實踐

10.2.1 建立組織標準

標準項目說明
平行測試 SOP組織統一的標準作業程序
計劃書範本統一的計劃書格式
差異分類標準統一的差異嚴重等級定義
退場標準統一的 Exit Criteria 框架
報表格式統一的每日報表、結案報表格式
工具套件共用的比對工具與報表平台

10.2.2 知識管理

活動說明頻率
案例庫建立收集歷次平行測試的經驗與教訓每次結案後
範本更新依據經驗持續改善範本每年
培訓課程新人平行測試培訓每季
經驗分享跨專案經驗交流每月

10.2.3 成熟度模型

graph TD
    A["Level 1<br/>初始級<br/>─────<br/>無標準流程<br/>人工比對為主"] --> B["Level 2<br/>管理級<br/>─────<br/>有基本 SOP<br/>部分自動化"]
    B --> C["Level 3<br/>定義級<br/>─────<br/>標準化流程<br/>自動化 ≥ 60%"]
    C --> D["Level 4<br/>量化級<br/>─────<br/>量化管理<br/>自動化 ≥ 90%"]
    D --> E["Level 5<br/>優化級<br/>─────<br/>持續改善<br/>AI 輔助分析"]
    
    style A fill:#f44336,color:#fff
    style B fill:#FF9800,color:#fff
    style C fill:#FFC107,color:#000
    style D fill:#4CAF50,color:#fff
    style E fill:#2196F3,color:#fff

10.3 技術最佳實踐

10.3.1 比對工具設計原則

原則說明
外部化設定比對規則放在 YAML/JSON 設定檔
插件式架構支援擴充新的比對類型
增量比對支援只比對變更部分
平行處理大量資料使用多執行緒比對
結果可視化差異報告支援圖表展示

10.3.2 資料處理最佳實踐

/**
 * 資料標準化處理 - 在比對前統一資料格式
 * 
 * <p>避免因格式差異產生假性差異</p>
 */
public class DataNormalizer {

    /**
     * 標準化金額欄位
     * - 統一使用 BigDecimal
     * - 統一小數位數
     * - 去除千分位符號
     */
    public BigDecimal normalizeAmount(String value, int scale) {
        if (value == null || value.isBlank()) {
            return BigDecimal.ZERO;
        }
        String cleaned = value.replaceAll("[,$\\s]", "");
        return new BigDecimal(cleaned)
            .setScale(scale, RoundingMode.HALF_UP);
    }

    /**
     * 標準化日期欄位
     * - 統一為 ISO 格式 (yyyy-MM-dd)
     * - 處理不同分隔符號
     */
    public String normalizeDate(String value) {
        if (value == null || value.isBlank()) return "";
        // 處理常見格式:2026/02/12、20260212、2026-02-12
        String cleaned = value.replaceAll("[/\\-.]", "");
        if (cleaned.length() == 8) {
            return cleaned.substring(0, 4) + "-" 
                 + cleaned.substring(4, 6) + "-" 
                 + cleaned.substring(6, 8);
        }
        return value;
    }

    /**
     * 標準化文字欄位
     * - 去除前後空白
     * - 統一全形/半形
     * - 統一大小寫(依設定)
     */
    public String normalizeText(String value, boolean caseInsensitive) {
        if (value == null) return "";
        String result = value.trim();
        if (caseInsensitive) {
            result = result.toUpperCase();
        }
        return result;
    }
}

實務提醒

  1. 平行測試是投資,不是成本——它避免的損失遠大於投入
  2. 寧可延後上線,也不要帶著 S1 差異硬上
  3. 自動化是長期投資,第一次建立的工具可以在日後的專案重複使用
  4. 每次平行測試結束後都要做 Post-mortem,持續改善流程

附錄 檢查清單(Checklist)

A. 平行測試啟動前檢查清單

#檢查項目確認備註
1平行測試計劃書已經各方簽核
2測試範圍(In-Scope / Out-of-Scope)已明確定義
3成功標準(Exit Criteria)已各方確認
4容忍誤差標準已定義並經風控確認
5RACI 矩陣已確認,角色責任已分配
6測試環境已建置並通過驗證
7資料同步機制已測試成功
8比對工具已部署並經過測試
9測試資料已準備完成(含脫敏處理)
10回滾計畫已撰寫
11通報機制已建立(S1 差異即時通知)
12日誌記錄機制已建立

B. 每日執行檢查清單

#檢查項目確認備註
1新舊系統都正常運行
2輸入資料已同步完成
3批次任務已全數執行完成
4自動化比對已執行
5差異報告已產出並檢視
6新增 S1/S2 差異已即時通報
7每日報表已發送
8執行日誌已保存

C. 結案前檢查清單

#檢查項目確認備註
1所有 S1 差異已歸零
2所有 S2 差異已歸零
3連續 N 天無新增 S1/S2 差異(N ≥ 3)
4所有差異都有處理紀錄(修正或標註已知)
5效能指標符合 SLA
6結案報告已完成
7稽核軌跡完整
8各單位已簽核

D. 上線前檢查清單

#檢查項目確認備註
1平行測試已通過結案判定
2上線核准單已經全員簽核
3回滾計畫已至少演練 1 次
4運維團隊已完成培訓
5監控告警機制已設定
6值班人員已安排
7緊急聯絡人清單已更新
8上線步驟已經 Dry Run
9舊系統保留可回切狀態
10通知相關上下游系統

E. 上線後監控檢查清單

#檢查項目確認備註
1新系統第一筆交易成功
2批次日結正常完成
3上線後仍持續執行比對(至少 30 天)
4效能指標在預期範圍內
5異常通報機制正常運作
6使用者回饋已收集並追蹤
7上線後問題已記錄並處理

重點摘要

章節核心要點
第一章平行測試是上線前的「最後防線」,必須使用真實資料、涵蓋完整業務週期
第二章10 個階段標準流程,RACI 矩陣明確分工
第三章計劃書範本必須包含範圍、策略、容差、風險、Exit Criteria、溝通計畫
第四章數值比對需注意浮點精度、四捨五入方式、BigDecimal 使用
第五章自動化比對 ≥ 90%,整合 CI/CD,報表自動產出
第六章金融系統需跨月結/季結驗證,回滾計畫不可缺
第七章分權、雙人覆核、日誌防竄改、ISO 27001 對應
第八章最常見失敗原因:時間不足、資料不完整、容差未定義
第九章標準表單確保流程一致性與可追溯性,含回滾計畫範本
第十章組織級標準化、成熟度提升、知識管理

本文件為內部標準作業文件。
最後更新:2026-02-13