排程作業(TsERP.SchedulerWorker)¶
TsERP 排程作業是一支獨立的 Windows Console 程式(
TsERP.SchedulerWorker.exe),由 Windows 工作排程器每 5 分鐘觸發一次,負責行事曆事件 Email 通知與AutoPilot 自動排程。本頁是給系統管理員 / DBA 看的部署與維運手冊。
功能簡介¶
舊版的 ERP 系統把這兩件工作放在 LogicBll.Timer.TimerBll 內的 DispatcherTimer,只有當有人開著 ERP 主程式時才會跑。實務上這帶來幾個痛點:
- 下班後沒人開 ERP,行事曆通知信不會寄出
- AutoPilot 自動排程必須仰賴某個使用者「掛著」ERP
- 多人同時開 ERP 時,每台都會跑同一份排程,造成重複通知
新版把排程邏輯抽成獨立 TsERP.SchedulerWorker.exe,部署到 server,由 Windows 工作排程器(schtasks)每 5 分鐘觸發一次。優點:
- 不需要使用者開 ERP,server 上自動跑
- 單一執行點,靠 Mutex 保證不重疊
- 可獨立升級,與 ERP 主程式解耦
- 失敗會留下 'F' 狀態 + log,方便管理員追查
與舊版 TimerBll 的差異
舊版 ERP 主程式仍保留 TimerBll 相關程式碼,但 MainWindowBtnTimerViewModel 已不再實際啟動該 timer(避免與本排程作業衝突)。請務必確認 server 上只有排程作業在跑,使用者端 ERP 不會重複觸發通知。
排程 proc 與資料表位置(ADR 0065)¶
自 2026-06-28 起(NOGI),排程/自動化的預存程序與其產出的資料表集中在
集中自動化庫 darbnogiauto1,不再放公司庫 darbnogicsa1:
| 物件 | 位置 |
|---|---|
TWORK_SKD_GET_PENDING_NOTIFY / _AUTOEXEC |
darbnogiauto1(讀 csa 行事曆 view,三段式) |
TWORK_SKD_MAILQUEUE 表 / _DEQUEUE / _UPDATE |
darbnogiauto1 |
TWORK_AUTO_SAFETYSTOCK + twork_auto_safetystockhistory 表 |
darbnogiauto1(讀 csa 庫存,三段式) |
DARB_GETDATA4(結果檢視用) |
darbnogiauto1(另保留 csa 既有版) |
TWORK_SKD_GETEVENT / TWORK_SKD_UPDATE_EVENT_STATUS |
仍在 darbnogicsa1(行事曆 UI 共用) |
執行不再走共用派發器,而是 log 庫的排程專屬派發器 DARB_LOG_SYS_SKD_EXEC(稽核寫
darbnogilog1.dbo.log_sys_skd、目標一律導 darbnogiauto1)。
- 部署:在目標 SQL 執行個體跑
sql/deploy-darbnogiauto1.sql(sqlcmd -f 65001或 SSMS)。 - 安全庫存:行事曆事件
參數需設Dispatcher=SKD、Procedure=TWORK_AUTO_SAFETYSTOCK、結果表=twork_auto_safetystockhistory,「執行結果」檢視才會經 SKD 派發器讀新庫。 - 稽核查詢:排程歷程看
darbnogilog1.dbo.log_sys_skd(與主流程log_sys、AI 的log_sys_ai分離)。 - 多客戶:其他客戶需各自
darbXXauto1變體+在該客戶 log 庫佈DARB_LOG_SYS_SKD_EXEC/log_sys_skd。
詳見 ADR 0065。
前置需求¶
| 項目 | 需求 |
|---|---|
| 作業系統 | Windows Server 2019 / 2022(或 Windows 10 / 11 Pro) |
| .NET Runtime | .NET 8 Desktop Runtime(x64) |
| SQL Server | 可從排程主機連線到 ERP 各公司資料庫 |
| Mailgun | 可從排程主機對外發出 HTTPS request 到 Mailgun API |
DatabaseList.xml |
路徑 c:/TEMPS/SqlLocation/DatabaseList.xml,包含目標 DB 連線資訊 |
| 服務帳號 | 一個專用本機 / 網域帳號,能讀 XML、寫 log、連 SQL、連網 |
資料庫變更(DBA 必做)¶
排程作業靠兩支單一職責 SP 撈出待處理事件——dbo.TWORK_SKD_GET_PENDING_NOTIFY(待 Email 通知)與 dbo.TWORK_SKD_GET_PENDING_AUTOEXEC(待 AutoPilot 自動執行)。需要 DBA 先在每個目標公司資料庫做下列改動。
歷史對照
這兩支 SP 取代了原 TWORK_SKD_NOTIFYEVENT(@QueryType=1 / =2 兩個分支),詳細決策見 ADR 0015。舊 SP 仍可保留 30 天再 drop。
1. 行事曆 base table 加欄位¶
行事曆事件的底層 table(即 Twork_skd_vw_eventitem view 對應的 base table)需要新增「執行開始時間」欄位,作為殭屍判定基準。
-- 範例 DDL,實際 table 名請依貴公司 schema 調整
ALTER TABLE [dbo].[行事曆事件 base table]
ADD [執行開始時間] datetime NULL;
GO
欄位型別必須是 datetime
CalendarBll.Update已通知 / Update已完成 多載會把 DateTime.Now 透過 yyyy/MM/dd HH:mm:ss 格式字串傳給 SP,欄位型別必須能容納此值。
2. 建立兩支撈待辦事件 SP¶
完整 DDL 在 SqlBI/TWORK_SKD_GET_PENDING_NOTIFY.sql 與 SqlBI/TWORK_SKD_GET_PENDING_AUTOEXEC.sql,DBA 直接執行即可。摘要:
TWORK_SKD_GET_PENDING_NOTIFY(@datetime) — 待 Email 通知¶
CREATE PROCEDURE [dbo].[TWORK_SKD_GET_PENDING_NOTIFY]
(@datetime datetime)
AS
BEGIN
SET NOCOUNT ON;
SELECT *
FROM dbo.twork_vw_calendarevent
WHERE 已審核 = 'Y'
AND 是否停用 <> 'Y'
AND 通知 = 'Y'
AND 通知帳號 <> ''
AND 已通知 <> 'Y'
AND @datetime >= 通知時間;
END
TWORK_SKD_GET_PENDING_AUTOEXEC(@datetime) — 待 AutoPilot 自動執行¶
CREATE PROCEDURE [dbo].[TWORK_SKD_GET_PENDING_AUTOEXEC]
(@datetime datetime)
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP 20 *
FROM twork_vw_calendarevent
WHERE 已完成 NOT IN ('Y', 'F')
AND (已完成 <> 'R' OR 執行開始時間 < DATEADD(MINUTE, -10, @datetime))
AND 已審核 = 'Y'
AND 是否停用 <> 'Y'
AND 是否自動執行 = 'Y'
ORDER BY 預定時間 ASC;
END
關鍵點:
TOP 20對應Scheduler.BatchSize,每次最多 20 筆ORDER BY 預定時間 ASC依時間先進先出已完成 NOT IN ('Y','F'):已完成的 Y 與失敗的 F 都不再撈- 殭屍回收條件
(已完成 <> 'R' OR 執行開始時間 < DATEADD(MINUTE, -10, @datetime)):非執行中(不是 R),或雖是 R 但 10 分鐘前就開始 → 視為前一輪 crash 重撈 @datetime是殭屍判定基準時間,由 caller 傳當下時間,便於外部測試
3. 新增 SP dbo.TWORK_SKD_UPDATE_EVENT_STATUS¶
CalendarBll.Update已通知(pkid, status, time) 與 Update已完成(pkid, status, time) 多載會呼叫這支 SP 一次更新「狀態 + 執行開始時間」。請建立:
| 參數 | 對應 C# 端 | 說明 |
|---|---|---|
LP_P1 |
type | '1'=已通知、'2'=已完成 |
LP_P2 |
pkid | 事件主鍵 |
LP_P3 |
status | 'R'=執行中(Running) / 'Y'=完成 / 'F'=失敗 / ''=重設 |
LP_P4 |
timestamp | yyyy/MM/dd HH:mm:ss 字串;空字串代表不更新時間 |
當 LP_P1='2'(已完成分支)且 LP_P3='Y' 時,SP 會自動把 [完成時間] 設為 GETDATE();其他狀態(R/F/'')不動 完成時間。
CREATE PROCEDURE [dbo].[TWORK_SKD_UPDATE_EVENT_STATUS]
@LP_P1 varchar(1), -- 1 = 已通知, 2 = 已完成
@LP_P2 varchar(50), -- pkid
@LP_P3 varchar(1), -- 'R' / 'Y' / 'F' / ''
@LP_P4 varchar(20) -- yyyy/MM/dd HH:mm:ss 或 ''
AS
BEGIN
SET NOCOUNT ON;
DECLARE @StartTime datetime = NULL;
IF @LP_P4 <> ''
SET @StartTime = CONVERT(datetime, @LP_P4, 111);
IF @LP_P1 = '1'
UPDATE [行事曆事件 base table]
SET [已通知] = @LP_P3,
[執行開始時間] = CASE WHEN @LP_P4 = '' THEN [執行開始時間] ELSE @StartTime END
WHERE [pkid] = @LP_P2;
ELSE IF @LP_P1 = '2'
UPDATE [行事曆事件 base table]
SET [已完成] = @LP_P3,
[執行開始時間] = CASE WHEN @LP_P4 = '' THEN [執行開始時間] ELSE @StartTime END,
[完成時間] = CASE WHEN @LP_P3 = 'Y' THEN GETDATE() ELSE [完成時間] END,
[是否停用] = CASE WHEN @LP_P3 = 'Y' THEN 'Y' ELSE '' END
WHERE [pkid] = @LP_P2;
END
GO
SP 名稱
請依貴公司命名慣例調整 TWORK_SKD_UPDATE_EVENT_STATUS。Common 端是用 Procedure.TWORK_SKD_UPDATE_EVENT_STATUS.Name 取的常數,DBA 改完 SP 後若名稱有異,請同步通知開發端調整 Procedure 列舉。
部署步驟¶
1. 發佈 Console 程式¶
在開發機把 TsERP.SchedulerWorker 專案 publish 為 framework-dependent:
dotnet publish TsERP.SchedulerWorker/TsERP.SchedulerWorker.csproj `
-c Release `
-r win-x64 `
--self-contained false `
-o C:\Publish\Scheduler
或 self-contained(檔案大但不需在 server 裝 .NET):
dotnet publish TsERP.SchedulerWorker/TsERP.SchedulerWorker.csproj `
-c Release `
-r win-x64 `
--self-contained true `
-o C:\Publish\Scheduler
2. 複製到 server¶
把 C:\Publish\Scheduler\ 整個資料夾複製到 server,建議放:
C:\ERPDeploy\Scheduler\
├── TsERP.SchedulerWorker.exe
├── TsERP.SchedulerWorker.dll
├── appsettings.json
└── ...其他依賴 dll...
3. 建立 log 目錄¶
排程作業透過 SchedulerLog 把所有歷程(INFO 與 ERROR)寫到 C:\temps\scheduler\yyyy-MM-dd.txt,每天一個檔。首次執行時若資料夾不存在會自動建立,但建議事先建好並設好權限:
New-Item -Path "C:\temps\scheduler" -ItemType Directory -Force
icacls "C:\temps\scheduler" /grant "<服務帳號>:(OI)(CI)M"
指令說明¶
New-Item:
| 參數 | 意思 |
|---|---|
-Path "C:\temps\scheduler" |
建在這個路徑 |
-ItemType Directory |
建的是資料夾(不是檔案) |
-Force |
父資料夾 C:\temps\ 不存在時自動建出來;目標已存在時不報錯 |
icacls:Windows 內建的 ACL(檔案權限)編輯工具。
| 參數 | 意思 |
|---|---|
/grant |
授予權限(不是拒絕、不是取代既有權限) |
<服務帳號> |
佔位符,替換成實際帳號(見下方範例) |
(OI) |
Object Inherit — 套用到資料夾內所有檔案 |
(CI) |
Container Inherit — 套用到所有子資料夾 |
M |
Modify — 可讀 / 可寫 / 可刪檔,不能改權限或拿走所有權 |
服務帳號範例¶
依你工作排程器的「執行此工作時,請使用下列使用者帳戶」設定替換 <服務帳號>:
# 本機專屬服務帳號(手冊推薦做法)
icacls "C:\temps\scheduler" /grant "tsERPSched:(OI)(CI)M"
# 用 SYSTEM 帳號跑
icacls "C:\temps\scheduler" /grant "SYSTEM:(OI)(CI)M"
# 網域帳號(公司網域 DARBWARE,使用者 erpsvc)
icacls "C:\temps\scheduler" /grant "DARBWARE\erpsvc:(OI)(CI)M"
不知道目前用哪個帳號?開「工作排程器」 → 找到 TsERP Scheduler → 點選 → 看下方「一般」分頁的「執行此工作時,請使用下列使用者帳戶」欄位。
驗證¶
icacls "C:\temps\scheduler"
輸出應包含類似 tsERPSched:(OI)(CI)(M) 的行,代表已套用成功。
什麼情況可以省略 icacls?
若排程是用 Administrator 或具有同等權限的帳號跑(例如個人開發機自己當管理員),對 C:\temps\ 預設就有完整權限,第二條可跳過。正式環境若用低權限服務帳號則必設 — 不設會導致 SchedulerLog 寫檔失敗(但因為 try/catch 吞例外,會靜默失敗,磁碟上完全看不到 log 檔)。
4. 確認 DatabaseList.xml¶
排程作業共用 ERP 主程式的 SQL 連線設定,會讀取:
c:/TEMPS/SqlLocation/DatabaseList.xml
請確認此檔案於 server 存在,且含有 appsettings.json 中 Scheduler.Databases[*].Name 對應的 <DataBase> 條目。
5. 編輯 appsettings.json¶
打開 C:\ERPDeploy\Scheduler\appsettings.json,依環境調整(詳見配置檔說明)。
6. 手動跑一次驗證¶
以服務帳號身份開啟 PowerShell:
cd C:\ERPDeploy\Scheduler
.\TsERP.SchedulerWorker.exe
預期 console 輸出:
[SchedulerJob] === DB darbcylog1 on TWORKSQL ===
[SchedulerJob] Notify: no rows.
[SchedulerJob] AutoPilot: no rows.
[Scheduler] Done. exitCode=0
如果出現 Fatal error 或 exitCode 為 2,請先處理疑難排解再進入下一步。
服務帳號設定¶
建議不要用 Administrator 或一般使用者帳號跑排程作業,請建立專屬本機帳號:
1. 建立本機帳號¶
$pw = Read-Host -AsSecureString "Set password"
New-LocalUser -Name "tsERPSched" -Password $pw -PasswordNeverExpires -UserMayNotChangePassword
或在「電腦管理 → 本機使用者及群組 → 使用者」手動新增。
2. 賦予最小權限¶
| 權限 | 設定方式 |
|---|---|
以批次工作登入 (SeBatchLogonRight) |
本機安全性原則 → 使用者權限指派 → 「以批次工作登入」加入 tsERPSched |
讀取 c:\TEMPS\SqlLocation\DatabaseList.xml |
在檔案內容 → 安全性 → 加入 tsERPSched 給「讀取」 |
修改 C:\ERPDeploy\Scheduler\ |
同上,給「修改」 |
修改 C:\temps\scheduler\ |
SchedulerLog 寫入路徑(每日一檔) |
| SQL Server 連線 | 在 SQL Server 加入 Login,授予 ERP 各公司資料庫對應 SP 的 EXECUTE 權限 |
| 網路 | 確認防火牆允許對 api.mailgun.net HTTPS(443)出站連線 |
無互動登入即可
服務帳號不需要「允許本機登入」權限,建議於本機安全性原則「拒絕從本機登入」加入此帳號,降低被當作互動帳號濫用的風險。
Windows 工作排程器設定¶
1. 建立排程¶
開啟「工作排程器」 → 「建立工作」(不是簡單建立工作):
一般¶
- 名稱:
TsERP Scheduler - 使用者帳戶:
tsERPSched - 不論使用者是否登入皆執行:勾
- 以最高權限執行:依需求(通常不需勾)
- 設定:Windows Server 2019 / 2022 / 11
觸發程序¶
- 新增觸發程序,每日
- 開始:選擇生效日,時間 00:00:00
- 重複工作每:5 分鐘
- 持續時間:無限期
- 已啟用:勾
動作¶
- 動作:啟動程式
- 程式或指令碼:
C:\ERPDeploy\Scheduler\TsERP.SchedulerWorker.exe - 開始於(選擇性):
C:\ERPDeploy\Scheduler\
條件¶
- 只有在電腦使用 AC 電源時才啟動工作:取消勾選(server 不需要)
- 只有在下列網路連線可用時才啟動:依環境
設定¶
| 選項 | 建議值 |
|---|---|
| 允許隨選執行此工作 | 勾 |
| 如果排定的開始時間錯過則盡快執行此工作 | 勾 |
| 如果工作失敗,每隔重新啟動 | 不勾(讓下一個 5 分鐘自然觸發即可) |
| 停止超過下列時間的工作 | 勾,4 分鐘(hard timeout,防止某次 hang 卡住下次觸發) |
| 如果執行中的工作未在要求時結束,將強制停止 | 勾 |
| 如果工作未排定再次執行則在以下時間之後刪除 | 不勾 |
| 如果工作已在執行則使用下列規則 | 不要啟動新的執行個體 |
務必設「不要啟動新的執行個體」
雖然程式內部已有 Mutex 保護,但工作排程器層級也要設成「不重疊」,避免同時噴出多個 process 競爭 log 檔案 handle。
2. 用 schtasks CLI 建立(替代方式)¶
schtasks /Create ^
/TN "TsERP Scheduler" ^
/TR "C:\ERPDeploy\Scheduler\TsERP.SchedulerWorker.exe" ^
/SC MINUTE /MO 5 ^
/RU "tsERPSched" /RP "***" ^
/RL LIMITED ^
/F
CLI 設定完仍建議在 GUI 確認「設定」分頁的「不要啟動新的執行個體」與「4 分鐘 timeout」是否正確。
配置檔說明¶
appsettings.json 結構:
{
"MailGun": {
"ApiKey": "<your-mailgun-api-key>",
"Domain": "mg.darbware.com",
"Sender": "@\"TWork ERP Mail Service <tworkservice@mg.darbware.com>\""
},
"Scheduler": {
"Databases": [
{ "Name": "darbzdcsa1", "ConnectionString": "" }
],
"AlertRecipients": ["admin@example.com"],
"ZombieTimeoutMinutes": 10,
"BatchSize": 20,
"DefaultSrvdbid": 1
}
}
MailGun 區段¶
| 欄位 | 說明 |
|---|---|
| ApiKey | Mailgun API Key(私鑰,請勿外流) |
| Domain | Mailgun 域名 |
| Sender | 寄件人字串,含格式化引號 |
Scheduler 區段¶
| 欄位 | 必填 | 預設 | 說明 |
|---|---|---|---|
| Databases | 是 | [] |
要處理的公司資料庫清單,留空陣列代表跑 DatabaseList.xml 內全部 DB。Name 對應 XML 內 <DataBase> 名稱,須完全一致(不分大小寫) |
| Databases[*].ConnectionString | 否 | "" |
預留欄位;目前實際連線仍以 DatabaseList.xml 為準,這裡填了不會生效 |
| AlertRecipients | 否 | [] |
致命錯誤時的告警收件人(保留欄位,目前由 SchedulerLog 處理;未來可擴充用) |
| ZombieTimeoutMinutes | 否 | 10 |
殭屍回收門檻(分鐘)。注意:實際判定寫死在 SP 內的 DATEADD(MINUTE, -10, ...),此欄位目前是給 SP 端參考的契約值,調動時請同步改 SP |
| BatchSize | 否 | 20 |
每次最多處理筆數。注意:實際 TOP 20 寫死在 SP 內,調動時請同步改 SP |
| DefaultSrvdbid | 否 | 1 |
SchedulerUser.Srvdbid 預設值,影響 LoggingId / log 路由 |
ZombieTimeoutMinutes / BatchSize 是契約值
這兩個欄位目前不會主動傳到 SQL 端,SP 內的 TOP 20 與 DATEADD(MINUTE, -10) 是寫死的。把它放在 config 是為了讓管理員一眼看到「殭屍 = 10 分鐘」「批次 = 20 筆」這兩個約定,真正要改值請改 SP。
多公司支援¶
跑單一公司¶
"Databases": [
{ "Name": "darbcylog1" }
]
跑多公司(依序,同一 process 內逐個切 connection)¶
"Databases": [
{ "Name": "darbcylog1" },
{ "Name": "darbzdcsa1" },
{ "Name": "darbtest" }
]
排程作業會依序處理每個 DB(呼叫 IDatabaseConnection.ChangeParameter),每個 DB 各自跑一輪 Notify + AutoPilot。某個 DB 失敗不影響其他 DB。
跑全部 DB¶
"Databases": []
留空陣列時,排程作業會把 DatabaseList.xml 內的所有 DB 都掃過一遍。
多公司 + 4 分鐘 timeout 的取捨
如果有 5 個以上公司、每家都有大量待處理事件,5 分鐘觸發 + 4 分鐘 hard timeout 可能不夠。建議:
- 拆成多支排程作業(複製目錄、各自
appsettings.json跑不同 DB) - 或調高
BatchSize並把 timeout 拉到 8 分鐘,但觸發改成每 10 分鐘
警示通知¶
自 2026-07-13 起,排程作業除了行事曆 Email 通知與安全庫存警示外,再新增三種主動警示信:應收逾期、票據到期、金流預測跌破警戒。它們與安全庫存警示一樣,共用既有的 Mailgun 通道與 twork_skd_mailqueue 郵件佇列——警示源只負責「判定 + 組信 + 入列」,實際寄送仍統一回排程作業。門檻、收件人、啟用開關由使用者端的警示規則維護頁(twork_sys_alertrule 表)設定,DBA 只需部署 SP/表並掛好排程事件。
規則設定 vs 排程掛法
「發不發、發給誰、門檻多少」由業務端在警示規則維護設定;「什麼時候跑」由本節的排程事件觸發。兩者缺一不可:規則啟用但沒掛排程事件不會發,掛了事件但規則未啟用也不會發。
資料庫部署(DBA 必做)¶
| 物件 | 位置 | 部署腳本 |
|---|---|---|
twork_sys_alertrule(警示規則表) |
各客戶公司庫(csa) | sql\alertrule-deploy.sql |
twork_auto_alertlog(抑制指紋紀錄表) |
集中自動化庫(auto) | sql\alertlog-deploy.sql |
TWORK_AUTO_AR_OVERDUE(應收逾期 SP) |
auto 庫 | sql\ar-overdue-deploy.sql |
TWORK_AUTO_CHECK_DUE(票據到期 SP) |
auto 庫 | sql\check-due-deploy.sql |
正式機 proc_param 為 IDENTITY
部署腳本檔頭已註明兩環境差異:正式機 proc_param 為 IDENTITY,請用不帶 pkid 的版本。跑 sqlcmd 時務必加 -f 65001(含中文),且注意 QUOTED_IDENTIFIER 需為 ON(預設 OFF 建含 JSON 索引的 SP/DML 會炸 Msg 1934),部署後實查物件存在再收工。
三支警示怎麼掛排程¶
警示與安全庫存一樣透過行事曆事件觸發,依警示源型態分兩種掛法:
SP 型:應收逾期、票據到期(走 SKD 派發器,零 C#)¶
在行事曆事件的「參數」欄設定,比照安全庫存慣例:
| 警示 | 參數設定 |
|---|---|
| 應收逾期 | Dispatcher=SKD、Procedure=TWORK_AUTO_AR_OVERDUE |
| 票據到期 | Dispatcher=SKD、Procedure=TWORK_AUTO_CHECK_DUE |
事件到觸發時間後,經 log 庫的排程專屬派發器 DARB_LOG_SYS_SKD_EXEC 執行對應 SP,SP 內部自行讀規則表門檻、判定、組信、入列。
C# 型:金流預測跌破警戒(走 AutoPilot task)¶
金流跌破的判定重用金流預測的 C# 純函數(不搬進 SQL),因此走 AutoPilot 而非 SKD 派發器。在行事曆事件設定為自動執行、對應 AutoPilot 動作 Flowcast_BaselineAlert;task 內部讀規則表的 警戒線 門檻、比對金流預測累計淨額,未被抑制時經 MailQueueEnqueuer 入列。
金流跌破警示 runtime 尚未驗收
Flowcast_BaselineAlert 的排程實跑(需登入 context 的行事曆事件觸發)截至 2026-07-13 尚未完整 runtime 驗收,正式啟用前請先在測試事件實跑一輪確認。
抑制視窗行為(避免同一警示轟炸)¶
三支警示在入列前都會對「警示內容」計算指紋(twork_auto_alertlog),比對抑制視窗(規則表 抑制視窗小時,預設 24h)內的近期紀錄:
- 同內容、視窗內已發過 → 靜音,不重寄
- 內容有變(逾期名單/票據清單增減)→ 指紋改變 → 照常再通知
- 金流跌破只通知首次跌破:指紋維度 =(首次跌破月, 警戒線);只要這兩者沒變就不重寄,跌破月移動或警戒線調整才會再發
缺 auto 庫的靜默 no-rows
SKD 派發一律導向對應的 auto 庫。若某客戶 log 庫缺對應 auto 庫,派發會靜默回無資料(no-rows)不報錯——多客戶部署時請逐一確認 auto 庫與 SP 都已建立(可 EXEC 一次 SP 看有無執行紀錄)。
詳見 ADR 0092。收件人兩層 fallback(規則表有值用之、否則退回事件「通知帳號」)、通道限制(僅 Email)與規則欄位意義見警示規則維護頁。
排程報表自動寄送(ScheduledDeck)¶
自 2026-07-13 起,排程作業可依 CUSTOMDASH 自訂查詢配置無人值守產出報表並以附件寄出。走 AutoPilot task(Customdash_ScheduledDeck):到觸發時間 → 子行程 headless 產檔(EXCEL/HTML/HTMLMOBILE/PPT)→ 經 SKD 派發器入列 twork_skd_mailqueue(附件多檔以 ; 串接)→ 同鏈 Mailgun 寄出;行動版另可推上受控網址(見發布到受控網址)。
怎麼掛排程¶
在行事曆事件設定為自動執行、單據別 = CUSTOMDASH、執行方法 = ScheduledDeck,並在「參數」填 JSON:
| 參數 | 必填 | 說明 |
|---|---|---|
配置pkid |
✔ | 要產出的 CUSTOMDASH 配置主鍵 |
格式 |
— | EXCEL,HTML,HTMLMOBILE,PPT 多選逗號分隔(預設 EXCEL,HTML;HTMLMOBILE=手機直立版、別名 MOBILE,見行動版格式;PDF 目前不支援) |
收件人 |
✔ | Email,逗號分隔 |
輸出目錄 |
— | 預設 C:\temps\排程報表\;產出檔留存供稽核 |
月份 |
— | 空=當期;支援 {-d1}(上月)或 yyyy/MM、yyyy/MM/dd |
保留天數 |
— | 預設 30;每次寄送成功後清該目錄下本配置超過天數的舊產出檔(≤0=不清理) |
發布網址 |
— | 預設空=不發布;設 Y 時寄信成功後把最新行動版推上受控網址(見發布到受控網址),主管手機開網址即看 |
事件的「參數顯示」會帶出人話,例:排程報表:配置#3(當期)→ 收件人 2 人,格式 EXCEL,HTML。
六個是事件參數、使用者代號是登入帳號
上表六欄是掛在行事曆事件上的參數 JSON。「使用者代號」不在事件參數裡——它是排程作業產檔時要用的 headless 登入帳號,設在 worker 設定檔(見下方headless 登入帳號),全公司共用一組、不隨事件個別設定。
月報每月自動產上個月報表
把事件觸發時間設在每月 1 號、月份 填 {-d1},排程作業每月 1 號會自動產出「上個月」的報表寄出——月報從此無人值守。{-d1} 由 BuildScheduledParameter 於觸發當下展開成實際年月。
產出檔名含期間段
自 2026-07-14 起檔名加入期間段:<配置名>_<期間>_<產出日>(期間=目標月 yyyyMM 或「當期」),行動版再加尾碼 _mobile,例 財務報表_202606_20260714_mobile.html。原因:同配置的日報(當期)與週報(上月)若同日產出,舊命名會互相覆蓋。保留天數清理同時認得新舊兩種檔名,既有舊檔照常清理。
保留天數清理的安全界線
清理只刪符合本功能命名模式(<配置名>_<期間>_yyyyMMdd[_mobile].xlsx/.html/.pptx)且屬該配置的過期檔,目錄下任何其他檔案一律不碰;且只在寄送入列成功之後執行,逐檔失敗記 log 不影響主流程。
PDF 格式本期未支援
格式 可填 EXCEL/HTML/HTMLMOBILE/PPT(可多選逗號分隔)。PDF 目前不支援——指定 PDF 會記一筆警告後略過,不會中止其他格式。需要 PDF 請改用面板內的「彙整列印」互動操作。
行動版格式(手機直立閱讀)¶
格式 加入 HTMLMOBILE(別名 MOBILE)後,排程作業會額外產出一份專為手機直立畫面設計的 HTML,讓主管不必開電腦、直接在手機上看報表。它與桌機版 HTML 各自獨立,兩者可同時勾選並存、互不影響。
產出檔名:桌機版 HTML 的檔名尾加上 _mobile,例:財務報表_202606_20260714_mobile.html(期間段規則見上方「產出檔名含期間段」)。
信件裡怎麼認:信件內文會把行動版附件明確標為「(行動版)」,並提示「請用手機瀏覽器開啟」;桌機版附件則不加標記。主管在手機收件匣直接點行動版附件即可。
手機上的閱讀體驗:
- 直立卡片流:內容改成由上而下、一張張卡片堆疊,不再是桌機版那種需要左右縮放的寬版面。
- 關鍵指標(KPI)置頂:最重要的數字放在最上方,一開啟就看得到。
- 表格可橫向捲動:欄位多的明細表在手機上以橫捲呈現,不會被壓扁到看不清。
- 可加到主畫面:用手機瀏覽器開啟後,可「加入主畫面」,之後像 App 一樣一點就開。
行動版會隱藏零值,桌機版不變
為了讓手機小螢幕聚焦在有數字的資料上,行動版只在此格式做兩件事:
- 整列全部為 0 的資料列不顯示(例如當月完全沒有異動的科目就不佔版面)。
- 數值欄的 0 顯示為空白(右對齊的金額/數量欄),讓非零數字更醒目。
代號類欄位(如「0001」這種左對齊代號)不受影響、照常顯示。桌機版 HTML 與 EXCEL 維持原樣、不隱藏任何列或值。
行動版是「快照」,不是即時更新
附件與受控網址上的內容都是排程當下產出的快照,反映的是那個時間點的資料,開啟後不會自動更新。要看更新後的數字,需等下一次排程產出(頻率由行事曆事件的觸發時間決定),或改在電腦上開 ERP 面板即時查詢。
發布到受控網址(選用)¶
除了寄附件(人人收件匣各一份),排程還能把最新一份行動版推到一個受控網址,主管手機開網址即看、不必翻信找附件。啟用方式:在行事曆事件的「參數」把 發布網址 設為 Y(預設空=不發布)。
啟用後的效果:
- 寄信成功後,排程作業會自動把該次產出的最新行動版推上受控網址;主管手機開同一個網址永遠看到最新一份。
- 網址可「加入主畫面」,之後一點就開,體驗接近 App。
- 內容同樣是快照(見上方警告),非即時。
前置需求:需 IT 先完成一次性設定
受控網址由 IT 以 Cloudflare Access(Email 白名單驗證)保護——只有白名單內的 Email、通過驗證後才進得去,不是公開網頁。首次啟用前,需 IT 完成 Cloudflare Access 政策與 worker 環境變數設定(含一項「已確認 Access 生效」的明示旗標)。這屬 IT 部署範疇,細節見 IT 部署文件,管理員這邊只要把 發布網址 設 Y 即可。
安全閘:IT 未確認完成前,系統會拒絕上傳真實數據
為避免財報在未受保護的狀態下外流,系統採技術閘把關:IT 未明示確認「Access 已啟用且驗證有效」之前,帶真實數據的行動版一律拒絕上傳(只會記 log,不會硬送出去)。因此若 發布網址=Y 但網址上遲遲沒有最新內容,多半是 IT 端的確認尚未完成——請聯繫 IT,而不是反覆重掛排程。
發布失敗不影響寄信
發布到受控網址與寄信是兩條獨立的路:就算發布失敗(例如網路暫時不通、IT 設定未就緒),也只會記 log,不會影響附件正常寄出。主管仍收得到信件附件。
headless 登入帳號(使用者代號)¶
排程作業要在無人登入的情況下把配置整頁跑一遍、產出檔案,需要一組 headless 登入帳號(使用者代號+密碼)。這組帳號設在 worker 設定檔 appsettings.json 的 ScheduledDeck 節(與 ERP 使用者登入同一組帳號體系):
{
"ScheduledDeck": {
"User": "",
"Password": ""
}
}
| 欄位 | 說明 |
|---|---|
| User | headless 登入用的使用者代號(同 ERP 登入帳號);排程作業以此身份跑查詢與匯出 |
| Password | 對應密碼 |
帳密嚴禁寫進版控的 appsettings.json
原始 appsettings.json 的 User/Password 一律留空,帳密只在部署現場覆寫:本機以 appsettings.local.json(gitignored)或 %ProgramData% 設定檔覆寫,正式環境建議走 Azure Key Vault。憑證未設定(User/Password 為空)時,排程報表任務會明確失敗並記 log,不會 fallback 到任何寫死帳密。
使用者代號決定看得到哪些配置
排程作業以這組帳號的身份載入 CUSTOMDASH 配置,因此該帳號要對目標配置有可見權限(配置的分享可見範圍見自訂查詢面板的「儲存/載入設置」)。建議用一個具足夠可見範圍的專用帳號。
方法字典登錄(選用)¶
排程能執行不靠字典——引擎用 單據別 + 執行方法 反查程式 AutoPilotRegistry(ScheduledDeck 已內建註冊)。字典僅為自動執行方法維護畫面的 discoverability:
- 顯示名(必要,已內建):中文名走多語系字典
AUTOPILOTMETHOD.ScheduledDeck(Common/Lang/Xaml/AutopilotmethodMethod.json),排程/事件畫面即顯示「排程報表自動寄送」。 - 字典列(選用):若要在「自動執行方法」表單看到此方法,於各公司庫跑
sql/autopilotmethod-register-deploy.sql(冪等、僅在該庫已建twork_sys_autopilotmethod時登錄;含中文須sqlcmd -f 65001,部署後實查該列)。
DBA 需部署的物件¶
正式環境需在對應資料庫執行下列四支腳本(含中文一律 sqlcmd -f 65001,部署後實查物件存在再收工):
| 部署腳本 | 位置 | 用途 |
|---|---|---|
sql/mailqueue-attachment-deploy.sql |
各 auto 庫 | 郵件佇列 twork_skd_mailqueue 加附件路徑欄 + 重建 TWORK_SKD_MAILQUEUE_DEQUEUE(讓寄送端撈得到附件) |
sql/mailqueue-enqueue-deploy.sql |
各 auto 庫 + log 庫 proc_param | 入列 SP TWORK_SKD_MAILQUEUE_ENQUEUE(產檔後把信+附件放進佇列) |
sql/alertlog-skd-deploy.sql |
各 auto 庫 + log 庫 proc_param | 排程資料存取改走 SKD 派發器(連帶關閉上節「金流預測跌破警戒」警示在排程環境無法入列的缺口) |
sql/autopilotmethod-register-deploy.sql(選用) |
各公司庫(csa) | ScheduledDeck 方法字典列(僅為維護畫面 discoverability,冪等) |
正式機 proc_param 為 IDENTITY
與其他排程腳本相同:正式機 proc_param 為 IDENTITY,請用腳本檔頭註明的不帶 pkid 版本;QUOTED_IDENTIFIER 須為 ON。多客戶部署時每個客戶的 auto/log/csa 庫都要各跑一次。
headless 登入憑證 / runtime 驗收
子行程 headless 產檔需登入憑證(worker appsettings 的 ScheduledDeck 節,見headless 登入帳號;缺則任務明確失敗,嚴禁寫入版控,正式建議走 Key Vault)。端到端真寄已於 dev 驗過;行事曆 UI 選用與字典顯示的 runtime 觀察,正式啟用前建議再走一輪。
失敗處理 / 重設 'F' 狀態¶
'F' 狀態怎麼來¶
當 MailGun 寄信失敗、AutoPilot 動作 throw exception、或回傳 IsSuccess = false 時,排程作業會把該事件的 已通知 或 已完成 標為 'F',並在 log 留下原因。
'F' 狀態不會被自動重撈(SP 過濾 NOT IN ('Y', 'F')),需要管理員手動處理。
重設讓事件再跑一次¶
在 SQL Management Studio 執行:
-- 把單一事件重新放回待處理
UPDATE [行事曆事件 base table]
SET [已通知] = '', -- 或 'N',依貴司 default
[執行開始時間] = NULL
WHERE [pkid] = '<目標 pkid>';
-- AutoPilot 同理
UPDATE [行事曆事件 base table]
SET [已完成] = '',
[執行開始時間] = NULL
WHERE [pkid] = '<目標 pkid>';
批次重設今天所有失敗事件¶
UPDATE [行事曆事件 base table]
SET [已通知] = '',
[執行開始時間] = NULL
WHERE [已通知] = 'F'
AND CAST([預定時間] AS date) = CAST(GETDATE() AS date);
先確認失敗原因
重設前請先看 log(C:\temps\scheduler\yyyy-MM-dd.txt)確認失敗原因。如果是 Mailgun 帳號爆量、收件人 email 格式錯誤,重設只會再失敗一次。先修根因再重設。
殭屍 'X' 狀態¶
什麼是殭屍¶
排程作業在處理一筆事件時,會先把狀態標為 'X'(執行中)並寫入 執行開始時間 = DateTime.Now。正常情況下幾秒內就會更新成 'Y'(成功)或 'F'(失敗)。
但如果 process 在這當中被強制終止(OOM、Windows 關機、被 timeout 殺掉、藍屏),事件會卡在 'X' 永遠不會被更新 → 殭屍。
自動回收¶
SP 的查詢條件包含:
[已完成] <> 'X' OR [執行開始時間] < DATEADD(MINUTE, -10, GETDATE())
意思是:'X' 狀態超過 10 分鐘的事件會被當成殭屍重新撈出。下次觸發排程作業時,這些殭屍會被重跑。
手動處理¶
若要立刻把殭屍復活(不等 10 分鐘):
UPDATE [行事曆事件 base table]
SET [已通知] = '',
[執行開始時間] = NULL
WHERE [已通知] = 'X';
為什麼是 10 分鐘¶
排程是 5 分鐘觸發 + 4 分鐘 timeout,正常一次跑滿不會超過 4 分鐘。10 分鐘給予 1 個完整週期的緩衝,避免「上一輪還沒跑完」就被當殭屍。
疑難排解 (FAQ)¶
信沒寄出去怎麼辦?
依序檢查:
- 手動跑一次(
TsERP.SchedulerWorker.exe),看 console 輸出。如果出現Notify pkid=... -> F (...),看括號內錯誤訊息 - 看 log:
C:\temps\scheduler\yyyy-MM-dd.txt,找最近的[ERROR] [SchedulerJob] Notify pkid=... -> F (...)行 - 確認 Mailgun:API Key 是否過期、Domain 是否被停用、額度是否爆
- 確認收件人:行事曆事件的
通知帳號欄位是否有效 email - 確認 SP:手動
EXEC dbo.TWORK_SKD_GET_PENDING_NOTIFY @datetime = GETDATE()看有沒有撈到 row。若沒有,問題在 SP 的過濾條件
AutoPilot 沒跑怎麼辦?
- 確認該事件的
通知方式不是 Email(AutoPilot 走的是LP_P1='2'流程,跟通知信不同) - 看 log 是否有
AutoPilot pkid=... -> F - 確認
IAutoPilotHelper.ExecuteMethod對該事件回傳的Message - AutoPilot 動作本身可能依賴主程式的某些 plugin / DLL,請確認
TsERP.SchedulerWorker部署目錄含有對應 DLL
log 看哪裡?
SchedulerLog(主要 log):C:\temps\scheduler\yyyy-MM-dd.txt,每天一個檔,含完整 INFO + ERROR 歷程- stdout / stderr:手動執行
.exe時可在 console 看到;工作排程器啟動時 stdout 會被丟棄(已不需要靠 stdout 看歷程,SchedulerLog已涵蓋) - Mailgun 端:Mailgun Logs
- 行格式:
yyyy-MM-dd HH:mm:ss [LEVEL] 訊息,例:2026-05-05 02:30:00 [INFO] [SchedulerJob] === DB darbnogilog1 on localhost\t2022bin ===
exit code 1 跟 2 有什麼差別?
| exit code | 含義 | 處理方式 |
|---|---|---|
| 0 | 全部成功(含「沒有資料要處理」) | 無需處理 |
| 1 | 部分失敗:某個 DB 的 NotifyAsync 或 AutoPilotAsync 拋例外,但其他 DB 仍跑完 | 看 log 找哪個 DB 失敗,多半是 SP 沒改、權限不足、SQL 連線問題 |
| 2 | 致命錯誤:根本沒進入主流程(DI 註冊失敗、appsettings 解析失敗、Mutex 競爭以外的問題) | 看 stderr 第一行 Fatal error: ...,通常是設定檔錯誤或缺 DLL |
排程作業會跟正在開 ERP 的使用者衝突嗎?
不會。新版 ERP 主程式的 MainWindowBtnTimerViewModel 已移除 TimerBll 欄位,使用者端不會再啟動本機 timer。所有通知/AutoPilot 都由 server 上的排程作業統一處理,避免重複。
舊版 client 升級前的過渡期
若 server 已啟用排程作業、但部分使用者還在用舊版 ERP client(仍含 TimerBll 啟動邏輯),會出現重複通知。請務必同步升級所有 client,或暫時停用 server 排程作業。
前一次還沒跑完,下一次又被觸發了,怎麼辦?
程式內以 Global\TsERP_Scheduler Mutex 保護單一執行;後到的 process 會印 Another scheduler instance is running. Exiting. 後 exit code 0 結束。工作排程器層級也設了「不要啟動新的執行個體」雙保險。
若想驗證,可手動連續執行兩次 TsERP.SchedulerWorker.exe,第二次應立即結束。
排程作業要怎麼停?
工作排程器 → 找到 TsERP Scheduler → 停用。已在執行中的 process 會跑完當下這輪,不會被中斷(除非超過 hard timeout)。
若要立即終止 process:在工作管理員 kill TsERP.SchedulerWorker.exe。下次觸發前殭屍 'X' 狀態會自動回收。
可以調整觸發間隔嗎?
可以。修改工作排程器觸發程序的「重複工作每 5 分鐘」為其他值即可。但請注意:
- 間隔太短(< 2 分鐘)會頻繁敲 SQL,影響效能
- 間隔太長(> 30 分鐘)通知信延遲體感很差
- 改間隔後請同步調整 hard timeout(建議 = 觸發間隔 - 1 分鐘)與 SP 的殭屍判定門檻(建議 = 觸發間隔 × 2)
截圖補充清單¶
以下截圖建議補上,可放於 docs/admin/images/:
- [需補:scheduler-task-general.png] — 工作排程器「一般」分頁完成設定畫面
- [需補:scheduler-task-trigger.png] — 工作排程器「觸發程序」分頁,5 分鐘重複設定畫面
- [需補:scheduler-task-action.png] — 工作排程器「動作」分頁,指向 exe 的設定畫面
- [需補:scheduler-task-settings.png] — 工作排程器「設定」分頁,4 分鐘 timeout 與「不要啟動新的執行個體」畫面
- [需補:scheduler-console-output.png] — 手動執行 exe 的 console 成功輸出畫面
- [需補:scheduler-log-folder.png] —
C:\temps\scheduler\內的 log 範例 - [需補:scheduled-deck-event.png] — 行事曆事件掛「排程報表自動寄送」:
單據別=CUSTOMDASH、執行方法=ScheduledDeck、參數 JSON 與人話「參數顯示」 - [需補:scheduled-deck-appsettings.png] — worker 設定檔
ScheduledDeck節(headless 使用者代號/密碼;示範用測試帳號) - [需補:scheduled-deck-mail.png] — 收件匣收到的排程報表信(主旨「【排程報表】…」與 Excel/HTML 附件)
- [需補:scheduled-deck-event-mobile.png] — 行事曆事件參數:
格式含HTMLMOBILE、發布網址=Y的設定畫面 - [需補:scheduled-deck-mobile-view.png] — 手機瀏覽器開啟行動版附件的畫面(直立卡片流、KPI 置頂、零值隱藏效果)
- [需補:scheduled-deck-mail-mobile-attach.png] — 收件匣信件內文把行動版附件標為「(行動版)」的畫面