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),這在實際多客戶部署情境下有兩個問題:
- 新客戶機器拿到的是錯誤的預設連線、不會中止啟動,使用者反而誤判「程式好像跑得起來」直到連線失敗才察覺
- 客戶之間需要不同
DataSource/DataBase,但 ClickOnce / Velopack 不會更新此檔(在預期之內),導致部署人員要手動編輯 XML — 易出錯、沒有審計痕跡
評估過幾條路:
- 多 channel 發布(每家公司一個 blob container)— 維護成本乘 N,每次發布要簽 N 次、上傳 N 次
- 首次啟動 setup wizard — 要寫 UI、處理密碼加密、scope 變大
- 單一發布包 + 部署腳本 — 既有
appsettings.local.json機制(App.xaml.cs已支援)配合腳本化即可達成
Decision¶
採「單一發布包 + 部署時腳本產生客戶設定」路線:
appsettings.json新增DatabaseListSourcePath欄位(與DatabaseListPath並列)DatabaseListPath:目的地(沿用,空字串走預設c:/TEMPS/SqlLocation/)DatabaseListSourcePath:客戶專屬DatabaseList.xml的來源完整路徑-
repo 內兩個都留空,由客戶機器的
%ProgramData%\TsERP\appsettings.local.json覆寫 -
XmlSqlLocation.EnsureDatabaseList()在 App 啟動時呼叫 - 若
SourcePath非空 + 來源存在:當目的地不存在或來源較新(LastWriteTimeUtc比對)→ 覆寫目的地 - 來源設了但檔案不存在 → throw(典型 IT 設錯路徑情境)
- 同步後目的地仍不存在 → throw
-
任一例外在
App.xaml.cs用MessageBox提示 +Shutdown(1) -
拿掉
CreateList()/EditToNewDatabase()/GetData()自動產生預設清單的邏輯 -
該功能是早期開發機 bootstrap 用,多客戶部署情境會誤導使用者,不留情面砍掉
-
新增
deploy/deploy-customer.ps1部署腳本 - 參數:
-Customer / -DbServer / -Database必填,-CompanyName / -IsAzure / -SourcePath / -ConfigPath / -Force選填 - 在客戶機器產生兩個檔案:
C:\ProgramData\TsERP\source\DatabaseList.xml(客戶連線資訊)C:\ProgramData\TsERP\appsettings.local.json(指向上述來源)
-
用
XmlWriter+ConvertTo-Json避免字串拼接造成跳脫問題 -
TsERP.SchedulerWorker同步處理 - 它有獨立
appsettings.json與獨立啟動流程,一併支援DatabaseListSourcePath - 在
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=False走IsAzure旗標(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.json設DatabaseListSourcePath指向 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架構衝突,且DataBasePoco有IsAzure/IsEnabled等欄位 XML 表達較直觀。改 JSON 影響面太大 - (拒絕) 把目的地路徑也改成
%ProgramData%:使用者希望「保留原本路徑」,且c:/TEMPS/SqlLocation/已是 deployed 客戶的既定路徑,動了會破壞既有部署 - (拒絕) 用 hash 比對而非 LastWriteTime:UNC 路徑的 LastWriteTime 可靠、hash 多一次 I/O,沒必要