「我們有套 Web ERP,B2B 開票想自動化 — 員工現在每天手動匯出再丟 Turnkey。」
這幾乎是每個導入 ERP 自動化的客戶都會丟過來的需求。
聽起來簡單:把 XML 寫到 Turnkey 的 SRC 目錄、Turnkey 自己會排程上傳、結束。但實際做下去就會發現 — 「寫到 SRC」只是第一步,做完才知道平台收到沒有、有沒有錯、什麼時候才會送、出錯了拿不拿得到中文錯誤訊息。
這篇把我們做了幾套 B2B 電子發票整合(攤位 POS、總部後台、Electron 桌面 App)踩過的 4 個坑寫出來。寫給準備自己 hack Turnkey 整合的工程師看 — 少踩兩個就值。
先 1 分鐘理解 Turnkey 是什麼 #
Turnkey 是財政部給 B2B / B2C 開立電子發票的標準上傳工具,目前主流版本是 MIG 4.1:
- 跑在 Windows,是 Java 程式(
java.exe跑TurnkeyCmd) - 設定存在 MSSQL(資料庫名通常叫
EInvoiceTurnKey) - 你寫 XML 到指定目錄 → 它按排程 Pack 成 zip → 上傳到財政部 → 平台回 ProcessResult → 它再下載解開
四種主要 XML 訊息類型(B2B 部分):
| Type | 用途 |
|---|---|
| F0401 | 一般稅額發票 |
| F0501 | 一般稅額發票作廢 |
| G0401 | 折讓單 |
| G0501 | 折讓單作廢 |
| E0402 | 空白未使用發票(期末必交) |
OK 背景清楚了。接下來進坑。
坑 1:寫到 SRC 子目錄不夠 — 要走 B2SSTORAGE 完整 4 層 #
最常見的天真做法:
E:\EINVTurnkey\XML\UpCast\F0401\SRC\F0401_xxx.xml
寫進去等 Turnkey 排程 Pack。但這條路有時候會卡住 — 因為 MIG 4.1 的 Pack 邏輯預期你的 XML 是先放到「B2SSTORAGE 完整分層」下:
E:\EINVTurnkey\XML\UpCast\B2SSTORAGE\F0401\SRC\F0401_xxx.xml
E:\EINVTurnkey\XML\UpCast\B2SSTORAGE\F0501\SRC\F0501_xxx.xml
E:\EINVTurnkey\XML\UpCast\B2SSTORAGE\G0401\SRC\G0401_xxx.xml
E:\EINVTurnkey\XML\UpCast\B2SSTORAGE\G0501\SRC\G0501_xxx.xml
注意這層 B2SSTORAGE(B2B 存證類型),跟 B2BEXCHANGE(買賣方訊息交換)、B2PMESSAGE(平台訊息)是平行的三個 CATEGORY。只有 B2SSTORAGE 是「開票實際上傳」用的,其他兩個是訊息交換、不要混。
寫對位置之後,Pack 才會把它包進去。
坑 2:監控 BAK 目錄只能知道「送出去」— 看不到「平台回什麼」 #
第二個常見天真做法:用 chokidar / FileSystemWatcher 監控輸出目錄:
SendFile\B2SSTORAGE\BAK\... → 看到檔案 = 上傳成功
問題:這只代表「Turnkey 把檔案送出去了」,不代表「財政部平台收下了」,更不代表「平台檢查通過、發票合法」。
實際的完整 pipeline 是這樣:
寫入 SRC
↓ UpCast 處理
├─→ ERR → local_error(XML 格式錯,財政部沒看過)
└─→ ↓ Pack
├─→ ERR → local_error(打包失敗)
└─→ ↓ SendFile
├─→ ERR → local_error
└─→ BAK → uploaded_pending(平台收到,等下回 ProcessResult)
下載
ReceiveFile → Unpack → DownCast → RecvTarget(平台 ProcessResult)
ProcessResult 才是真相:發票通過 → processSuccess;失敗 → 有 ERRORCODE + MESSAGE1~6 中文錯誤敘述(例如「買方統編檢核碼錯誤」)。
要拿到這層資訊,光看 BAK 不夠 — 你得直連 Turnkey 的 MSSQL。
坑 3:直連 Turnkey MSSQL 才能看到完整 4 桶狀態 #
我們最後的做法:用 mssql driver 連到 EInvoiceTurnKey 資料庫,撈 TURNKEY_MESSAGE_LOG 跟 TURNKEY_SYSEVENT_LOG:
SELECT
m.SEQNO,
m.INVOICE_IDENTIFIER, -- F0401BA80015223 + 20260508 (xml_type + invoice_no + date)
m.STATUS,
m.IN_OUT_BOUND, -- O = 我們送出 / I = 平台回應
m.UUID, -- 同批次共用
d.TASK, -- UpCast / Pack / SendFile / Unpack / DownCast
d.FILENAME,
d.STATUS AS DETAIL_STATUS,
e.ERRORCODE,
e.MESSAGE1 + e.MESSAGE2 + e.MESSAGE3 + e.MESSAGE4 + e.MESSAGE5 + e.MESSAGE6 AS ERROR_TEXT
FROM TURNKEY_MESSAGE_LOG m
LEFT JOIN TURNKEY_MESSAGE_LOG_DETAIL d ON d.SEQNO = m.SEQNO
LEFT JOIN TURNKEY_SYSEVENT_LOG e ON e.SEQNO = m.SEQNO
WHERE m.INVOICE_IDENTIFIER LIKE @prefix
ORDER BY m.SEQNO DESC;
一筆 SEQNO 同時會有 UpCast 進 BAK(我們送)+ Unpack ProcessResult(平台回)的紀錄,整個 pipeline 跟平台回應全串起來。
拿到資料後,我們在 UI 把所有發票分成 4 桶:
| 桶 | 對應狀態 | UI 怎麼處理 |
|---|---|---|
| 待上傳 | 還沒寫到 SRC | 列表 + checkbox + 「上傳」按鈕 |
| 已匯出尚未上傳 | 在 SRC / Pack 中 | 顯示卡在哪個 task |
| 已上傳 | 平台 ProcessResult 為 success | 綠燈,可開電子發票證明聯 |
| 異常 | 任何階段 ERR | 紅色 badge + ERRORCODE + 中文錯誤 |
這比「狀態欄位寫個 enum 5 值」實用太多 — 使用者一眼看到的就是「我現在要處理什麼」,不是抽象狀態碼。
坑 4:等排程太慢 — spawn run_start.cmd + 暫改 SCHEDULE_CONFIG 強制觸發 #
預設 Turnkey 排程是 1 小時跑一次。對員工開完一張 B2B 發票要等 1 小時才上傳 — 不能接受。
但直接改 Turnkey 排程設定會破壞它,業主下次裝更新也會被覆蓋。我們的做法:暫時改 + 用完還原。
完整流程(在 driver 層做 try/finally 包好):
// 1. 偵測 Turnkey 是否在跑(避免重複 spawn)
const isRunning = await detectTurnkeyProcess(); // tasklist | grep java.exe + TurnkeyCmd
if (!isRunning) {
// 2. 備份原始排程
const originalSchedule = await mssql.query('SELECT * FROM SCHEDULE_CONFIG WHERE TASK = @t');
try {
// 3. 改成 FIX TIME (馬上跑一次)
await mssql.query(`
UPDATE SCHEDULE_CONFIG
SET TYPE = 'FIX', FIX_TIME = @now
WHERE TASK = @t
`);
// 4. spawn 排程程式 detached
spawn('E:\\EINVTurnkey\\run_start.cmd', [], { detached: true });
// 5. tail log 等 "Job end" marker
await waitForJobEnd('E:\\EINVTurnkey\\logs\\turnkey.log', { timeout: 60_000 });
} finally {
// 6. 不管成功失敗,都還原原始排程
await mssql.query('UPDATE SCHEDULE_CONFIG ... SET ... WHERE TASK = @t', originalSchedule);
// 7. 停 Turnkey
spawn('E:\\EINVTurnkey\\run_stop.cmd');
}
}
幾個關鍵點:
try/finally是必要的。如果中途崩潰沒還原,下次正常排程會跑錯時間- 偵測 java.exe + TurnkeyCmd,不只看 process name — 那台機器可能還跑別的 Java 程式
run_start.cmd要 detached spawn,不然你的程式結束 Turnkey 也會被連帶 kill- tail log 看
Job end,不要靠 sleep N 秒 — 慢機可能跑 30s、快機 5s
做完這套,員工開完發票 5 秒內就上傳,跟「即時開立電子發票」感覺一樣。
沒裝 Turnkey 的情況:CSV fallback 模式 #
不是每個客戶都裝 Turnkey。有的就 1 個月開 20-30 張 B2B 發票,裝一套 Java Turnkey + 後續維護不划算。
這時做第三條路:CSV 匯出 + 開啟財政部上傳頁。
// 規格:BTB207W H/M/D 三層 + E0402,UTF-8 with BOM
exportCsv({
format: 'BTB207W',
invoices: pendingInvoices,
output: `${appDataDir}/csv_export/F0401_${yyyymmdd}_${timestamp}.csv`,
});
// 開啟瀏覽器到財政部上傳頁
shell.openExternal('https://einv-platform.einvoice.nat.gov.tw/');
員工只要:點「匯出 CSV」→ 開瀏覽器 → 拖檔進去 → 完成。比印單據掃描上傳省 90% 時間,零 Turnkey 維護成本。
整套系統內部我們稱為「三模式上傳」:本機 Turnkey / 遠端 Turnkey(UNC + FTP)/ CSV 匯出。客戶想用哪種他自己選。
重點整理 #
| # | 坑 | 解法 |
|---|---|---|
| 1 | XML 寫到 UpCast\F0401\SRC\ |
應該寫到 UpCast\B2SSTORAGE\F0401\SRC\ 完整 4 層 |
| 2 | 監控 BAK 以為上傳成功 | BAK 只代表送出去,要看 ProcessResult 才知道平台收下 |
| 3 | 用 chokidar 監控檔案系統 | 直連 MSSQL TURNKEY_MESSAGE_LOG 才能拿完整 4 桶狀態 |
| 4 | 等排程 1 小時太慢 | spawn run_start.cmd + 暫改 SCHEDULE_CONFIG,5 秒內上傳 |
客戶想評估 #
如果你正在做的 ERP 或 POS 要串電子發票,先別自己從零 hack Turnkey 整合 — 上面這些坑都會碰到,每個坑都吃掉 2-3 天 debug。
我們做過 Electron POS、Web ERP、總部後台 3 種不同型態的 Turnkey 整合。可以聊聊:
- 評估你現在的系統適合本機 Turnkey / 遠端 Turnkey / CSV 哪一種
- 直接接案做整合(依工時報價,諮詢免費)
- 賣現成的 emobile-b2b 桌面 App(B2B 開票 + Turnkey 整合一條龍)
聯絡:0912852835 / henryccy@icloud.com / LINE @3q3tw / 線上表單
延伸閱讀: