跳轉到

ADR 0019 — 一個發布包 + 客戶端設定檔覆寫的部署機制

  • 日期:2026-05-13
  • 狀態:Accepted
  • 決策者:使用者
  • 相關ADR 0003

Context

TsERP 走 Velopack 單一 channel 發布(darb-vpk),原本所有客戶共用一份 appsettings.json,連線資訊來自客戶機器的 c:/TEMPS/SqlLocation/DatabaseList.xml

XmlSqlLocation.CreateList() 在檔案不存在時會自動產生一筆寫死的預設朝陽 / darbcylog1 / 192.168.0.10),這在實際多客戶部署情境下有兩個問題:

  1. 新客戶機器拿到的是錯誤的預設連線、不會中止啟動,使用者反而誤判「程式好像跑得起來」直到連線失敗才察覺
  2. 客戶之間需要不同 DataSource / DataBase,但 ClickOnce / Velopack 不會更新此檔(在預期之內),導致部署人員要手動編輯 XML — 易出錯、沒有審計痕跡

評估過幾條路:

  • 多 channel 發布(每家公司一個 blob container)— 維護成本乘 N,每次發布要簽 N 次、上傳 N 次
  • 首次啟動 setup wizard — 要寫 UI、處理密碼加密、scope 變大
  • 單一發布包 + 部署腳本 — 既有 appsettings.local.json 機制(App.xaml.cs 已支援)配合腳本化即可達成

Decision

採「單一發布包 + 部署時腳本產生客戶設定」路線:

  1. appsettings.json 新增 DatabaseListSourcePath 欄位(與 DatabaseListPath 並列)
  2. DatabaseListPath:目的地(沿用,空字串走預設 c:/TEMPS/SqlLocation/
  3. DatabaseListSourcePath:客戶專屬 DatabaseList.xml 的來源完整路徑
  4. repo 內兩個都留空,由客戶機器的 %ProgramData%\TsERP\appsettings.local.json 覆寫

  5. XmlSqlLocation.EnsureDatabaseList() 在 App 啟動時呼叫

  6. SourcePath 非空 + 來源存在:當目的地不存在或來源較新(LastWriteTimeUtc 比對)→ 覆寫目的地
  7. 來源設了但檔案不存在 → throw(典型 IT 設錯路徑情境)
  8. 同步後目的地仍不存在 → throw
  9. 任一例外在 App.xaml.csMessageBox 提示 + Shutdown(1)

  10. 拿掉 CreateList() / EditToNewDatabase() / GetData() 自動產生預設清單的邏輯

  11. 該功能是早期開發機 bootstrap 用,多客戶部署情境會誤導使用者,不留情面砍掉

  12. 新增 deploy/deploy-customer.ps1 部署腳本

  13. 參數:-Customer / -DbServer / -Database 必填,-CompanyName / -IsAzure / -SourcePath / -ConfigPath / -Force 選填
  14. 在客戶機器產生兩個檔案:
    • C:\ProgramData\TsERP\source\DatabaseList.xml(客戶連線資訊)
    • C:\ProgramData\TsERP\appsettings.local.json(指向上述來源)
  15. XmlWriter + ConvertTo-Json 避免字串拼接造成跳脫問題

  16. TsERP.SchedulerWorker 同步處理

  17. 它有獨立 appsettings.json 與獨立啟動流程,一併支援 DatabaseListSourcePath
  18. Program.cs 的 DI 設定前呼叫 EnsureDatabaseList()(失敗會讓 worker 拋例外 + 進入 fatal log)

Consequences

正面

  • 一次發布、所有客戶共用安裝包,Velopack 更新流程不變
  • 客戶差異集中在 appsettings.local.json 與來源 DatabaseList.xml,不會被 Velopack 更新覆蓋(兩個都在 %ProgramData%,不在安裝目錄)
  • 設定錯誤(路徑寫錯、來源檔不存在)會在啟動時明確報錯,不會默默用錯誤預設連線
  • LastWriteTime 比對讓 IT 可在中央位置(網路磁碟)更新 DatabaseList.xml,所有客戶下次啟動自動拿到新版
  • 部署腳本參數化,可組合成批次部署(多台一次跑)

負面 / 風險

  • 既有開發機若沒手動準備 DatabaseList.xml:升級到新版會直接以 MessageBox 拒絕啟動。需在 c:/TEMPS/SqlLocation/ 手動放一份,或設 DatabaseListSourcePath 指向自己的開發 XML
  • 部署腳本不會自動把 TsERP 本體裝起來:IT 仍需另跑 Velopack Setup.exe,二步驟流程
  • appsettings.local.json 寫死路徑 %ProgramData%\TsERP\(在 App.xaml.cs 內):客戶若用非標準路徑要改 code,現階段不暴露為可設定值(YAGNI)
  • 共用帳密維持現狀DataBaseDal.Properties.Settings 仍是寫死 foxlog / qwertgbnm,./,這次不擴大 scope,未來若有 per-customer 帳密需求再另開 ADR
  • Encrypt=FalseIsAzure 旗標(ADR 0016):腳本支援 -IsAzure 參數,IT 部署 Azure 客戶時務必加上

Migration

客戶端

  • 已部署客戶:若 c:/TEMPS/SqlLocation/DatabaseList.xml 已存在 → 程式啟動行為無變化(沒設 DatabaseListSourcePath 時只驗證目的地存在)
  • 新客戶:跑 deploy/deploy-customer.ps1 後再裝 TsERP

開發端

  • 開發機若依賴自動產生的預設 XML,第一次跑新版會 fail。處理擇一:
  • 手動在 c:/TEMPS/SqlLocation/DatabaseList.xml 放開發環境清單
  • appsettings.local.jsonDatabaseListSourcePath 指向 dev XML
  • 跑一次 deploy/deploy-customer.ps1 把開發環境當「客戶」處理

Alternatives Considered

  • (拒絕) 多 channel 發布:N 倍維護成本、N 次簽章、N 次上傳。客戶數少時不划算,多時更不划算(建議改 setup wizard)
  • (拒絕) 首次啟動 setup wizard:需寫 UI、處理本機密碼加密(DPAPI)、scope 變大。本次需求是「IT 部署」而非「使用者自助」,腳本化更合適
  • (拒絕) 保留自動產生預設 XML 作為 fallback:誤導使用者,違反「fail loud」原則
  • (拒絕) 把連線資訊直接放 appsettings.json / appsettings.local.json:與既有 XmlSqlLocation + DataBaseParameter + Sql 架構衝突,且 DataBasePocoIsAzure / IsEnabled 等欄位 XML 表達較直觀。改 JSON 影響面太大
  • (拒絕) 把目的地路徑也改成 %ProgramData%:使用者希望「保留原本路徑」,且 c:/TEMPS/SqlLocation/ 已是 deployed 客戶的既定路徑,動了會破壞既有部署
  • (拒絕) 用 hash 比對而非 LastWriteTime:UNC 路徑的 LastWriteTime 可靠、hash 多一次 I/O,沒必要