<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>沒墨的筆記本</title><description>把路上的阻礙，堆成這裡的筆記</description><link>https://joyceowo.github.io/</link><language>zh_TW</language><item><title>關閉快速啟動與 PCIe 省電，解決筆電黑屏喚醒失敗</title><link>https://joyceowo.github.io/posts/3aba78ea09fa80f2a65ced87e238b80c/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/3aba78ea09fa80f2a65ced87e238b80c/</guid><pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;Windows 筆電鎖定後若黑屏無法喚醒，可先關閉「快速啟動」與「PCIe 連結狀態電源管理」，目前實測可恢復正常。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;鎖定螢幕或待機後，畫面無法再次亮起。&lt;/li&gt;
&lt;li&gt;鍵盤、滑鼠仍可能有反應，但只能強制重新開機。&lt;/li&gt;
&lt;li&gt;Windows 筆電出現不固定的休眠或喚醒異常。&lt;/li&gt;
&lt;li&gt;想先從系統電源設定排除問題，不立即重灌或送修。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 關閉 Windows 快速啟動&lt;/h3&gt;
&lt;p&gt;快速啟動會保留部分系統狀態，加快下次開機速度，但有時可能干擾休眠、關機與硬體重新初始化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按下 &lt;code&gt;Win + R&lt;/code&gt;，輸入 &lt;code&gt;control&lt;/code&gt; 開啟控制台。&lt;/li&gt;
&lt;li&gt;將「檢視方式」切換為「大圖示」。&lt;/li&gt;
&lt;li&gt;進入「電源選項」。&lt;/li&gt;
&lt;li&gt;點擊「選擇按下電源按鈕時的行為」。&lt;/li&gt;
&lt;li&gt;點擊「變更目前無法使用的設定」。&lt;/li&gt;
&lt;li&gt;取消勾選「開啟快速啟動（建議選項）」。&lt;/li&gt;
&lt;li&gt;儲存變更後重新開機。
這個設定通常是優先排查項目。若問題來自關機狀態或驅動程式沒有完整重新載入，關閉後可能改善喚醒失敗。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 關閉 PCIe 連結狀態電源管理&lt;/h3&gt;
&lt;p&gt;PCIe 連結狀態電源管理會降低顯示卡或其他 PCIe 裝置的耗電，但部分硬體可能無法從深度省電狀態正常恢復。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;進入控制台的「電源選項」。&lt;/li&gt;
&lt;li&gt;在目前使用的電源計畫旁，點擊「變更計畫設定」。&lt;/li&gt;
&lt;li&gt;點擊「變更進階電源設定」。&lt;/li&gt;
&lt;li&gt;展開「PCI Express」。&lt;/li&gt;
&lt;li&gt;展開「連結狀態電源管理」。&lt;/li&gt;
&lt;li&gt;將設定調整為「關閉」。&lt;/li&gt;
&lt;li&gt;點擊「套用」並確認。
若筆電分成「使用電池」與「一般電源」兩個選項，建議先全部設為關閉，再觀察黑屏問題是否仍會發生。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;關閉 PCIe 省電可能稍微增加耗電量，確認問題解決後，可再測試只關閉插電模式。&lt;/li&gt;
&lt;li&gt;修改設定後務必重新開機，避免舊的電源狀態仍被保留。&lt;/li&gt;
&lt;li&gt;若問題持續，下一步應檢查顯示卡驅動程式、BIOS 與 Windows 更新，不要一直靠強制關機硬扛。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;開啟控制台：
Win + R
輸入：control

快速啟動：
控制台
→ 電源選項
→ 選擇按下電源按鈕時的行為
→ 變更目前無法使用的設定
→ 取消「開啟快速啟動」

PCIe 電源管理：
控制台
→ 電源選項
→ 變更計畫設定
→ 變更進階電源設定
→ PCI Express
→ 連結狀態電源管理
→ 關閉
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;先關閉快速啟動，再停用 PCIe 連結狀態電源管理，是排查 Windows 鎖定黑屏問題成本最低的做法；若仍無效，再往驅動程式與 BIOS 查，莫與黑屏鬥法到天明。&lt;/p&gt;
</content:encoded></item><item><title>SQL 即時監看筆記：Profiler 與 Azure SQL XEvent</title><link>https://joyceowo.github.io/posts/34ca78ea09fa8099a9d9c84c70d5b503/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/34ca78ea09fa8099a9d9c84c70d5b503/</guid><pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;SQL Server 可直接用 SQL Server Profiler 即時監看 SQL，但 Azure SQL Database 不支援 Profiler，需要改用 Extended Events（XEvent）搭配 TSQL 查詢事件資料。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;查詢「誰正在打 SQL」&lt;/li&gt;
&lt;li&gt;分析 API / 系統即時 SQL&lt;/li&gt;
&lt;li&gt;排查慢查詢與高 CPU SQL&lt;/li&gt;
&lt;li&gt;Azure SQL 無法使用 Profiler 時&lt;/li&gt;
&lt;li&gt;DBA / 後端工程師效能分析&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 一般 SQL Server：直接使用 SQL Server Profiler&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SQL Server 傳統最常見做法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可即時看到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;執行中的 SQL&lt;/li&gt;
&lt;li&gt;Duration&lt;/li&gt;
&lt;li&gt;CPU&lt;/li&gt;
&lt;li&gt;Login User&lt;/li&gt;
&lt;li&gt;Application Name&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;適合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On-Prem SQL Server&lt;/li&gt;
&lt;li&gt;VM SQL Server&lt;/li&gt;
&lt;li&gt;短時間即時除錯
操作位置：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SSMS → Tools → SQL Server Profiler
&lt;img src=&quot;../../assets/notion-images/34ca78ea09fa8099a9d9c84c70d5b503/34ca78ea09fa8099a9d9c84c70d5b503-ee5ab4cf2b7d.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用事件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RPC:Completed&lt;/li&gt;
&lt;li&gt;SQL:BatchCompleted&lt;/li&gt;
&lt;li&gt;SP:StmtCompleted
常用 Filter：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ApplicationName
LoginName
DatabaseName
Duration
TextData
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;實務上通常一定要加 Filter，不然大型系統會像瀑布一樣噴 SQL，眼睛直接工傷 (´-ω-`)&lt;/p&gt;
&lt;h3&gt;2. Azure SQL Database：改用 Extended Events（XEvent）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Azure SQL Database 不支援 SQL Server Profiler&lt;/li&gt;
&lt;li&gt;官方也逐漸建議改用 XEvent&lt;/li&gt;
&lt;li&gt;效能比 Profiler 更好&lt;/li&gt;
&lt;li&gt;可自訂追蹤條件
建立 XEvent Session：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;CREATE EVENTSESSION MonitorSQL
ON DATABASE
ADD EVENT sqlserver.sql_batch_completed(
ACTION (
        sqlserver.client_app_name,
        sqlserver.client_hostname,
        sqlserver.username
    )
)
ADD TARGET package0.ring_buffer;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;啟動 Session：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER EVENTSESSION MonitorSQL
ON DATABASE
STATE=START;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;操作位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSMS → XEvent 分析工具 → TSQL
&lt;img src=&quot;../../assets/notion-images/34ca78ea09fa8099a9d9c84c70d5b503/34ca78ea09fa8099a9d9c84c70d5b503-0808ca07d587.png&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;這邊使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sql_batch_completed&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;監看 SQL 執行完成&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ring_buffer&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;暫存記憶體事件&lt;/li&gt;
&lt;li&gt;不需額外檔案權限
Azure SQL 很多環境沒有檔案系統權限，因此通常會優先用 ring_buffer。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 使用 TSQL 查詢即時 SQL 內容&lt;/h3&gt;
&lt;p&gt;建立完成後，可直接查詢 XEvent 內容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
    event_data.value(&apos;(event/@name)[1]&apos;,&apos;varchar(50)&apos;)AS EventName,
    event_data.value(&apos;(event/data[@name=&quot;duration&quot;]/value)[1]&apos;,&apos;bigint&apos;)/1000AS DurationMs,
    event_data.value(&apos;(event/action[@name=&quot;client_app_name&quot;]/value)[1]&apos;,&apos;nvarchar(256)&apos;)AS AppName,
    event_data.value(&apos;(event/action[@name=&quot;username&quot;]/value)[1]&apos;,&apos;nvarchar(256)&apos;)AS UserName,
    event_data.value(&apos;(event/data[@name=&quot;batch_text&quot;]/value)[1]&apos;,&apos;nvarchar(max)&apos;)AS SQLText
FROM (
SELECTCAST(xet.target_dataAS XML)AS target_data
FROM sys.dm_xe_database_sessions s
JOIN sys.dm_xe_database_session_targets xet
ON s.address= xet.event_session_address
WHERE s.name=&apos;MonitorSQL&apos;
) t
CROSS APPLY target_data.nodes(&apos;//RingBufferTarget/event&apos;)AS x(event_data);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可看到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SQL 內容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;執行時間&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用者&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;呼叫 App&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Host Name
對 API 系統非常好用，尤其查：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;哪支 API 打爆 DB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;EF Core 產生什麼 SQL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;哪個排程在狂跑&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Profiler 長時間開啟效能較差，不建議正式機長期使用&lt;/li&gt;
&lt;li&gt;Azure SQL 建議搭配 Query Store 一起分析慢 SQL&lt;/li&gt;
&lt;li&gt;XEvent 若沒停止，事件資料會持續累積&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 建立 XEvent
CREATE EVENTSESSION MonitorSQL
ON DATABASE
ADD EVENT sqlserver.sql_batch_completed(
ACTION (
        sqlserver.client_app_name,
        sqlserver.client_hostname,
        sqlserver.username
    )
)
ADD TARGET package0.ring_buffer;
GO

-- 啟動 XEvent
ALTER EVENTSESSION MonitorSQL
ON DATABASE
STATE=START;
GO

-- 查詢 XEvent
SELECT
    event_data.value(&apos;(event/@name)[1]&apos;,&apos;varchar(50)&apos;)AS EventName,
    event_data.value(&apos;(event/data[@name=&quot;duration&quot;]/value)[1]&apos;,&apos;bigint&apos;)/1000AS DurationMs,
    event_data.value(&apos;(event/action[@name=&quot;client_app_name&quot;]/value)[1]&apos;,&apos;nvarchar(256)&apos;)AS AppName,
    event_data.value(&apos;(event/action[@name=&quot;username&quot;]/value)[1]&apos;,&apos;nvarchar(256)&apos;)AS UserName,
    event_data.value(&apos;(event/data[@name=&quot;batch_text&quot;]/value)[1]&apos;,&apos;nvarchar(max)&apos;)AS SQLText
FROM (
SELECTCAST(xet.target_dataAS XML)AS target_data
FROM sys.dm_xe_database_sessions s
JOIN sys.dm_xe_database_session_targets xet
ON s.address= xet.event_session_address
WHERE s.name=&apos;MonitorSQL&apos;
) t
CROSS APPLY target_data.nodes(&apos;//RingBufferTarget/event&apos;)AS x(event_data);

-- 停止 XEvent
ALTER EVENTSESSION MonitorSQL
ON DATABASE
STATE= STOP;
GO

-- 刪除 XEvent
DROP EVENTSESSION MonitorSQLON DATABASE;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;以前抓 SQL 靠 Profiler，現在 Azure SQL 時代，XEvent 幾乎已經是標配了。&lt;/p&gt;
&lt;p&gt;會寫一份 XEvent TSQL，很多效能問題就不用靠運氣通靈。&lt;/p&gt;
</content:encoded></item><item><title>MSSQL RESTORE DATABASE 實戰：用 FILE + MOVE 還原成新 DB</title><link>https://joyceowo.github.io/posts/345a78ea09fa808a8d27cf9bb32b8b92/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/345a78ea09fa808a8d27cf9bb32b8b92/</guid><pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;RESTORE FILELISTONLY + MOVE&lt;/code&gt; 可把備份檔還原成另一個新資料庫名稱，避免覆蓋原 DB，也能順便重新指定 MDF / LDF 路徑。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;還原正式環境備份到測試環境&lt;/li&gt;
&lt;li&gt;複製一份資料庫做查詢分析&lt;/li&gt;
&lt;li&gt;上版前先建立測試副本&lt;/li&gt;
&lt;li&gt;原 DB 不能動，但需要資料驗證&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 先確認備份檔內容與版本&lt;/h3&gt;
&lt;p&gt;先查看 &lt;code&gt;.bak&lt;/code&gt; 是否可用、備份類型是否正確。若是多組備份，也能確認要還原哪一份。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESTORE HEADERONLY
FROM DISK = N&apos;C:\\SampleBackup\\DemoDb_backup_2026_04_17_163005.bak&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若看到 &lt;code&gt;BackupType = 1&lt;/code&gt; 通常代表完整備份，可直接還原。&lt;/p&gt;
&lt;h3&gt;2. 查詢資料檔邏輯名稱&lt;/h3&gt;
&lt;p&gt;還原時 &lt;code&gt;MOVE&lt;/code&gt; 用的不是實體檔名，而是備份內的&lt;strong&gt;邏輯名稱&lt;/strong&gt;，這一步必查，不然常報錯。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESTORE FILELISTONLY
FROM DISK = N&apos;C:\\SampleBackup\\DemoDb_backup_2026_04_17_163005.bak&apos;;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你會看到類似：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;LogicalName&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DemoDb&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DemoDb_log&lt;/td&gt;
&lt;td&gt;L&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;之後 &lt;code&gt;MOVE&lt;/code&gt; 就使用這兩個名稱。&lt;/p&gt;
&lt;h3&gt;3. 還原成新的資料庫名稱&lt;/h3&gt;
&lt;p&gt;建立新資料庫 &lt;code&gt;DemoDb_Copy260417&lt;/code&gt;，並把資料檔放到新路徑，避免與原本 DB 衝突。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESTORE DATABASE [DemoDb_Copy260417]
FROM DISK = N&apos;C:\\SampleBackup\\DemoDb_backup_2026_04_17_163005.bak&apos;
WITH
    FILE = 1,
    MOVE N&apos;DemoDb&apos; TO N&apos;D:\\MSSQL\\Data\\DemoDb_Copy260417.mdf&apos;,
    MOVE N&apos;DemoDb_log&apos; TO N&apos;D:\\MSSQL\\Log\\DemoDb_Copy260417.ldf&apos;,
    RECOVERY,
    STATS = 10;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;STATS = 10&lt;/code&gt; 代表每 10% 顯示一次進度，做人要有回報，SQL 也是。&lt;/p&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FILE = 1&lt;/code&gt; 通常是第一組備份；若同一 bak 含多份備份，需先用 &lt;code&gt;HEADERONLY&lt;/code&gt; 確認 Position。&lt;/li&gt;
&lt;li&gt;若 MDF / LDF 路徑不存在，還原會失敗，先建資料夾。&lt;/li&gt;
&lt;li&gt;若目標 DB 已存在，可先刪除或加 &lt;code&gt;REPLACE&lt;/code&gt;（正式環境慎用）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESTORE HEADERONLY
FROM DISK = N&apos;C:\\SampleBackup\\DemoDb_backup_2026_04_17_163005.bak&apos;;

RESTORE FILELISTONLY
FROM DISK = N&apos;C:\\SampleBackup\\DemoDb_backup_2026_04_17_163005.bak&apos;;
GO

RESTORE DATABASE [DemoDb_Copy260417]
FROM DISK = N&apos;C:\\SampleBackup\\DemoDb_backup_2026_04_17_163005.bak&apos;
WITH
    FILE = 1,
    MOVE N&apos;DemoDb&apos; TO N&apos;D:\\MSSQL\\Data\\DemoDb_Copy260417.mdf&apos;,
    MOVE N&apos;DemoDb_log&apos; TO N&apos;D:\\MSSQL\\Log\\DemoDb_Copy260417.ldf&apos;,
    RECOVERY,
    STATS = 10;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;還原資料庫不難，難的是搞錯邏輯檔名與路徑。先查清楚再下手，能少踩很多雷。&lt;/p&gt;
</content:encoded></item><item><title>SQL Server OLE DB Driver 19 Token 登入錯誤解法（降版 18.7.5）</title><link>https://joyceowo.github.io/posts/343a78ea09fa8035994bf279bab00694/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/343a78ea09fa8035994bf279bab00694/</guid><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OLE DB Driver 19 可能導致 Token 登入失敗，直接降版到 18.7.5 可解&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用 OLE DB 連線 SQL Server（含 Azure SQL）&lt;/li&gt;
&lt;li&gt;使用 Token / AAD 驗證&lt;/li&gt;
&lt;li&gt;發生 &lt;code&gt;&amp;lt;token-identified principal&amp;gt;&lt;/code&gt; 登入錯誤&lt;/li&gt;
&lt;li&gt;SSIS / Linked Server / 外部系統整合&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 確認錯誤訊息&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;出現以下錯誤：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Login failed for user &apos;&amp;lt;token-identified principal&amp;gt;&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;The server is not currently configured to accept this token&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通常發生在：
&lt;ul&gt;
&lt;li&gt;使用 OLE DB Driver 19&lt;/li&gt;
&lt;li&gt;搭配 Azure AD / Token 驗證&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;這不是 SQL 權限問題，而是 Driver 相容性問題&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 移除 OLE DB Driver 19 (選擇性)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;到「控制台 → 程式與功能」&lt;/li&gt;
&lt;li&gt;找到：
&lt;ul&gt;
&lt;li&gt;Microsoft OLE DB Driver 19 for SQL Server&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;進行解除安裝&lt;/li&gt;
&lt;li&gt;避免多版本混用（可能導致連線仍抓到 19）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 安裝指定版本 18.7.5&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;下載舊版 Driver（官方）：
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/sql/connect/oledb/release-notes-for-oledb-driver-for-sql-server?view=sql-server-ver17#1875&quot;&gt;https://learn.microsoft.com/en-us/sql/connect/oledb/release-notes-for-oledb-driver-for-sql-server?view=sql-server-ver17#1875&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;安裝：
&lt;ul&gt;
&lt;li&gt;Microsoft OLE DB Driver 18.7.5&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;安裝完成後重新啟動服務（如 IIS / SSIS / App）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Driver 19 在某些 Token 驗證場景有相容性問題（特別是舊系統）&lt;/li&gt;
&lt;li&gt;若使用 Linked Server，也會影響連線測試&lt;/li&gt;
&lt;li&gt;不建議混裝 18 / 19，避免 DLL 載入混亂&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;有些錯不是你權限問題，是版本在搞你，降版反而最快解&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Web App DNS 解析異常紀錄（Public / Private 切換）</title><link>https://joyceowo.github.io/posts/343a78ea09fa809f97d2c221d3e00135/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/343a78ea09fa809f97d2c221d3e00135/</guid><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;同一個 Azure Web App 會解析到不同 IP，是因為「是否走 Private Link DNS」決定的。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Azure Web App 無法連線時排查&lt;/li&gt;
&lt;li&gt;Private Endpoint / Private Link 設定驗證&lt;/li&gt;
&lt;li&gt;判斷流量是走內網還是外網&lt;/li&gt;
&lt;li&gt;DevOps / Infra troubleshooting&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 使用 nameresolver 驗證 DNS 解析結果&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 Kudu 或 Web App console 執行 nameresolver.exe 查詢 domain&lt;/li&gt;
&lt;li&gt;觀察是否解析到 public IP（13.x.x.x）或 private IP（10.x.x.x）&lt;/li&gt;
&lt;li&gt;這一步是最快確認「流量路徑」的方式&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;nameresolver.exe website.azurewebsites.net
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 判斷 Public vs Private DNS 行為&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;解析到 public：
&lt;ul&gt;
&lt;li&gt;domain → *.azurewebsites.net&lt;/li&gt;
&lt;li&gt;IP 為 13.x.x.x&lt;/li&gt;
&lt;li&gt;表示走 Azure 公網&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;解析到 private：
&lt;ul&gt;
&lt;li&gt;domain → *.privatelink.azurewebsites.net&lt;/li&gt;
&lt;li&gt;IP 為 10.x.x.x&lt;/li&gt;
&lt;li&gt;表示走 Private Endpoint
關鍵差異在於 DNS 是否被「Private DNS Zone 覆蓋」&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 確認 Private Link / DNS Zone 設定&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若有建立 Private Endpoint：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Azure 會建立 privatelink DNS 記錄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若 VNet 有綁 Private DNS Zone：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;會強制解析到內網 IP&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若沒有綁定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仍會走 public IP
簡單判斷：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同一台機器解析結果不同 → DNS 路徑不同&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同一 domain 出現兩種結果 → DNS override 發生&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Private DNS Zone 綁在 VNet 上才會生效（常見漏設定）&lt;/li&gt;
&lt;li&gt;Azure Web App 沒有 VNet Integration 也可能被 DNS 影響&lt;/li&gt;
&lt;li&gt;同一 domain 在不同環境解析不同是正常現象，不是 bug&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;看 IP 就知道你走哪條路，別再猜網路了，DNS 才是關鍵。&lt;/p&gt;
</content:encoded></item><item><title>如何禁止特定帳號使用 LinkServer？用假帳號直接封鎖</title><link>https://joyceowo.github.io/posts/33ca78ea09fa8096b44ccfaf8486be0c/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/33ca78ea09fa8096b44ccfaf8486be0c/</guid><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;用「假帳號 mapping」覆蓋 LinkServer 登入，讓指定使用者永遠連線失敗，是最簡單可控的封鎖方式。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;想限制「特定帳號」不能用 LinkServer&lt;/li&gt;
&lt;li&gt;不想影響其他使用者（維持 public 可用）&lt;/li&gt;
&lt;li&gt;無法用 DENY 精準控管 LinkServer 時&lt;/li&gt;
&lt;li&gt;權限已混亂，需要快速止血&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 確認目前 LinkServer 登入設定&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;sp_helplinkedsrvlogin&lt;/code&gt; 檢視 mapping&lt;/li&gt;
&lt;li&gt;可以看到誰對應哪個遠端帳號&lt;/li&gt;
&lt;li&gt;判斷是否有 &lt;code&gt;NULL&lt;/code&gt;（代表所有人共用）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;EXEC sp_helplinkedsrvlogin;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 移除目標使用者的既有 mapping&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;針對所有 LinkServer&lt;/li&gt;
&lt;li&gt;刪除該 User 的登入對應&lt;/li&gt;
&lt;li&gt;避免舊設定干擾&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;EXEC sp_droplinkedsrvlogin
    @rmtsrvname = N&apos;LinkServerName&apos;,
    @locallogin = N&apos;UserA&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 建立「假帳號」強制登入失敗&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;核心做法：指定一組不存在的帳號&lt;/li&gt;
&lt;li&gt;當 User 呼叫 LinkServer 時 → 一定登入失敗&lt;/li&gt;
&lt;li&gt;等同「軟性 DENY」&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;EXEC sp_addlinkedsrvlogin
    @rmtsrvname = N&apos;LinkServerName&apos;,
    @useself = N&apos;False&apos;,
    @locallogin = N&apos;UserA&apos;,
    @rmtuser = N&apos;FakeDenyUser&apos;,
    @rmtpassword = N&apos;WrongPassword&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;沒有 &lt;code&gt;DENY USE LINKSERVER&lt;/code&gt; 這種權限，只能用 mapping 控制&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@locallogin = NULL&lt;/code&gt; 代表所有人共用，需特別注意覆蓋順序&lt;/li&gt;
&lt;li&gt;此方法不影響其他使用者（只針對指定帳號）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 檢視權限
EXEC sp_helplinkedsrvlogin;

-- 動態產生：刪除 + 建立 Fake Mapping
DECLARE @UserName sysname = N&apos;UserA&apos;;
DECLARE @FakeUser sysname = N&apos;FakeDenyUser&apos;;
DECLARE @FakePassword nvarchar(200) = N&apos;WrongPassword&apos;;

DECLARE @SQL nvarchar(max) = N&apos;&apos;;

-- 1. 刪除 mapping
SELECT
    @SQL = @SQL +
    N&apos;BEGIN TRY
        EXEC sp_droplinkedsrvlogin
            @rmtsrvname = N&apos;&apos;&apos; + REPLACE(name, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + &apos;&apos;&apos;,
            @locallogin = N&apos;&apos;&apos; + REPLACE(@UserName, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + &apos;&apos;&apos;;
    END TRY
    BEGIN CATCH
    END CATCH;&apos; + CHAR(13) + CHAR(10)
FROM sys.servers
WHERE is_linked = 1;

-- 2. 建立 Fake mapping
SELECT
    @SQL = @SQL +
    N&apos;EXEC sp_addlinkedsrvlogin
        @rmtsrvname = N&apos;&apos;&apos; + REPLACE(name, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + &apos;&apos;&apos;,
        @useself = N&apos;&apos;False&apos;&apos;,
        @locallogin = N&apos;&apos;&apos; + REPLACE(@UserName, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + &apos;&apos;&apos;,
        @rmtuser = N&apos;&apos;&apos; + REPLACE(@FakeUser, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + &apos;&apos;&apos;,
        @rmtpassword = N&apos;&apos;&apos; + REPLACE(@FakePassword, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + &apos;&apos;&apos;;&apos;
    + CHAR(13) + CHAR(10)
FROM sys.servers
WHERE is_linked = 1;

-- Debug
PRINT @SQL;

-- 執行
EXEC sp_executesql @SQL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;LinkServer 沒有權限控管？那就用「讓他永遠連不上」這招，乾脆俐落。
實務建議：權限設計一開始就分帳號，不然後面都只能補破網。&lt;/p&gt;
</content:encoded></item><item><title>Azure SQL 怎麼一次備份全部 DB？PowerShell 自動匯出 bacpac</title><link>https://joyceowo.github.io/posts/33ca78ea09fa80d9bce3ec7e370c08b7/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/33ca78ea09fa80d9bce3ec7e370c08b7/</guid><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;這支腳本透過 &lt;code&gt;DatabaseName&lt;/code&gt; 參數控制匯出範圍：有填值時只匯出指定 DB，空值時自動匯出整台 Azure SQL Server 下所有使用者 DB。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;想先測試單一 DB 匯出是否正常&lt;/li&gt;
&lt;li&gt;想一次備份整台 Azure SQL Server 的所有使用者 DB&lt;/li&gt;
&lt;li&gt;想把備份流程做成可重複執行的 PowerShell 腳本&lt;/li&gt;
&lt;li&gt;想保留每次匯出結果與錯誤訊息，方便後續追查&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 先定義參數與登入資訊&lt;/h3&gt;
&lt;p&gt;先準備 Azure 資源名稱、Storage 位置、SQL Admin 帳號，並加入 &lt;code&gt;DatabaseName&lt;/code&gt; 參數。這個參數是整篇重點：空值代表全部 DB，有值代表單一指定 DB。接著輸入 SQL Admin 密碼，並抓出 Storage Account Key，後面匯出 bacpac 會用到。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ===== 基本參數 =====
$ResourceGroupName   = &quot;your-rg&quot;
$ServerName          = &quot;your-sql-server&quot;
$DatabaseName        = &quot;&quot;   # 空值 = 全部 DB；有值 = 指定單一 DB
$StorageAccountName  = &quot;yourstorageacct&quot;
$ContainerName       = &quot;sql-backup&quot;
$SqlAdminLogin       = &quot;your_sql_admin&quot;

# 會跳出輸入框，請輸入 Azure SQL admin 密碼
$SqlAdminPassword = Read-Host &quot;Enter Azure SQL admin password&quot; -AsSecureString

# 取得 Storage Account Key
$StorageKey = (Get-AzStorageAccountKey `
    -ResourceGroupName $ResourceGroupName `
    -Name $StorageAccountName)[0].Value
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 依 DatabaseName 決定要跑哪些 DB&lt;/h3&gt;
&lt;p&gt;這一步只處理清單，不做匯出。若 &lt;code&gt;DatabaseName&lt;/code&gt; 是空值，就抓整台 logical server 底下所有資料庫，並排除 &lt;code&gt;master&lt;/code&gt;；若有值，就只建立單一 DB 清單。這樣後面的匯出流程可共用，不必寫兩份腳本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ([string]::IsNullOrWhiteSpace($DatabaseName)) {
    # 抓出此 logical server 底下的資料庫，排除 master
    $databases = Get-AzSqlDatabase `
        -ResourceGroupName $ResourceGroupName `
        -ServerName $ServerName |
        Where-Object { $_.DatabaseName -ne &quot;master&quot; }
}
else {
    # 若有指定 DB，則只建立單一清單
    $databases = @(
        [PSCustomObject]@{
            DatabaseName = $DatabaseName
        }
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 逐一匯出 bacpac 到 Blob Storage&lt;/h3&gt;
&lt;p&gt;每個 DB 都加上時間戳記，避免同名檔被覆蓋。接著呼叫 &lt;code&gt;New-AzSqlDatabaseExport&lt;/code&gt; 送出匯出請求，把 bacpac 寫到指定的 Blob Container。這段是核心，也是最實用的一段。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 建立結果紀錄
$results = @()

foreach ($db in $databases) {
    $CurrentDatabaseName = $db.DatabaseName
    $TimeStamp = Get-Date -Format &quot;yyyyMMdd-HHmmss&quot;
    $BacpacFileName = &quot;$CurrentDatabaseName-$TimeStamp.bacpac&quot;
    $StorageUri = &quot;&amp;lt;https://$StorageAccountName.blob.core.windows.net/$ContainerName/$BacpacFileName&amp;gt;&quot;

    Write-Host &quot;Starting export for $CurrentDatabaseName ...&quot;

    try {
        $exportRequest = New-AzSqlDatabaseExport `
            -ResourceGroupName $ResourceGroupName `
            -ServerName $ServerName `
            -DatabaseName $CurrentDatabaseName `
            -StorageKeyType &quot;StorageAccessKey&quot; `
            -StorageKey $StorageKey `
            -StorageUri $StorageUri `
            -AdministratorLogin $SqlAdminLogin `
            -AdministratorLoginPassword $SqlAdminPassword
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 持續查詢狀態並整理結果&lt;/h3&gt;
&lt;p&gt;匯出不是同步完成，所以要用 &lt;code&gt;Get-AzSqlDatabaseImportExportStatus&lt;/code&gt; 輪詢狀態。完成後把每個 DB 的狀態、輸出位置與錯誤訊息收進結果表，最後統一輸出。這樣就不會只看到某個 DB 卡住，卻不知道其他 DB 的情況。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # 輪詢匯出狀態
        do {
            Start-Sleep -Seconds 15
            $status = Get-AzSqlDatabaseImportExportStatus `
                -OperationStatusLink $exportRequest.OperationStatusLink

            Write-Host &quot;$CurrentDatabaseName =&amp;gt; $($status.Status)&quot;
        } while ($status.Status -eq &quot;InProgress&quot;)

        $results += [PSCustomObject]@{
            DatabaseName = $CurrentDatabaseName
            Status       = $status.Status
            StorageUri   = $StorageUri
            ErrorMessage = $status.ErrorMessage
        }
    }
    catch {
        $results += [PSCustomObject]@{
            DatabaseName = $CurrentDatabaseName
            Status       = &quot;Failed&quot;
            StorageUri   = $StorageUri
            ErrorMessage = $_.Exception.Message
        }
    }
}

$results | Format-Table -AutoSize
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;master&lt;/code&gt; 建議直接排除，通常沒有備份成 bacpac 的實際價值&lt;/li&gt;
&lt;li&gt;建議用 &lt;code&gt;$CurrentDatabaseName&lt;/code&gt;，不要在 &lt;code&gt;foreach&lt;/code&gt; 裡覆蓋原始 &lt;code&gt;$DatabaseName&lt;/code&gt; 參數，後續比較不會亂&lt;/li&gt;
&lt;li&gt;若遇到 schema 不支援、跨 DB 參照、外部物件等問題，匯出失敗多半是 bacpac 限制，不是 PowerShell 語法錯&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安裝 Az PowerShell 模組
Install-Module -Name Az -Scope CurrentUser

# 匯入 Az 模組
Import-Module Az

# 登入 Azure
Connect-AzAccount

# ===== 基本參數 =====
$ResourceGroupName   = &quot;your-rg&quot;
$ServerName          = &quot;your-sql-server&quot;
$DatabaseName        = &quot;&quot;   # 空值 = 全部 DB；有值 = 指定單一 DB
$StorageAccountName  = &quot;yourstorageacct&quot;
$ContainerName       = &quot;sql-backup&quot;
$SqlAdminLogin       = &quot;your_sql_admin&quot;

# 會跳出輸入框，請輸入 Azure SQL admin 密碼
$SqlAdminPassword = Read-Host &quot;Enter Azure SQL admin password&quot; -AsSecureString

# 取得 Storage Account Key
$StorageKey = (Get-AzStorageAccountKey `
    -ResourceGroupName $ResourceGroupName `
    -Name $StorageAccountName)[0].Value

# 抓取 DB 清單：空值 = 全部 DB；有值 = 單一 DB
if ([string]::IsNullOrWhiteSpace($DatabaseName)) {
    $databases = Get-AzSqlDatabase `
        -ResourceGroupName $ResourceGroupName `
        -ServerName $ServerName |
        Where-Object { $_.DatabaseName -ne &quot;master&quot; }
}
else {
    $databases = @(
        [PSCustomObject]@{
            DatabaseName = $DatabaseName
        }
    )
}

# 建立結果紀錄
$results = @()

foreach ($db in $databases) {
    $CurrentDatabaseName = $db.DatabaseName
    $TimeStamp = Get-Date -Format &quot;yyyyMMdd-HHmmss&quot;
    $BacpacFileName = &quot;$CurrentDatabaseName-$TimeStamp.bacpac&quot;
    $StorageUri = &quot;&amp;lt;https://$StorageAccountName.blob.core.windows.net/$ContainerName/$BacpacFileName&amp;gt;&quot;

    Write-Host &quot;Starting export for $CurrentDatabaseName ...&quot;

    try {
        $exportRequest = New-AzSqlDatabaseExport `
            -ResourceGroupName $ResourceGroupName `
            -ServerName $ServerName `
            -DatabaseName $CurrentDatabaseName `
            -StorageKeyType &quot;StorageAccessKey&quot; `
            -StorageKey $StorageKey `
            -StorageUri $StorageUri `
            -AdministratorLogin $SqlAdminLogin `
            -AdministratorLoginPassword $SqlAdminPassword

        # 輪詢匯出狀態
        do {
            Start-Sleep -Seconds 15
            $status = Get-AzSqlDatabaseImportExportStatus `
                -OperationStatusLink $exportRequest.OperationStatusLink

            Write-Host &quot;$CurrentDatabaseName =&amp;gt; $($status.Status)&quot;
        } while ($status.Status -eq &quot;InProgress&quot;)

        $results += [PSCustomObject]@{
            DatabaseName = $CurrentDatabaseName
            Status       = $status.Status
            StorageUri   = $StorageUri
            ErrorMessage = $status.ErrorMessage
        }
    }
    catch {
        $results += [PSCustomObject]@{
            DatabaseName = $CurrentDatabaseName
            Status       = &quot;Failed&quot;
            StorageUri   = $StorageUri
            ErrorMessage = $_.Exception.Message
        }
    }
}

# 顯示結果
$results | Format-Table -AutoSize

# ===== 補充查詢 / 驗證指令 =====

# 查詢某台 SQL Server 底下所有 DB
Get-AzSqlDatabase `
    -ResourceGroupName &quot;your-rg&quot; `
    -ServerName &quot;your-sql-server&quot;

# 查詢指定 DB
Get-AzSqlDatabase `
    -ResourceGroupName &quot;your-rg&quot; `
    -ServerName &quot;your-sql-server&quot; `
    -DatabaseName &quot;your-db-name&quot;

# 查詢 Storage Account Key
Get-AzStorageAccountKey `
    -ResourceGroupName &quot;your-rg&quot; `
    -Name &quot;yourstorageacct&quot;

# 建立 Storage Context
$ctx = New-AzStorageContext `
    -StorageAccountName &quot;yourstorageacct&quot; `
    -StorageAccountKey &quot;your-storage-key&quot;

# 查詢 Blob Container 是否存在
Get-AzStorageContainer `
    -Name &quot;sql-backup&quot; `
    -Context $ctx

# 建立 Blob Container
New-AzStorageContainer `
    -Name &quot;sql-backup&quot; `
    -Context $ctx

# 手動測試單一 DB 匯出
$DatabaseName = &quot;appdb&quot;

# 手動切換成匯出全部 DB
$DatabaseName = &quot;&quot;

# 手動查某次匯出狀態
Get-AzSqlDatabaseImportExportStatus `
    -OperationStatusLink &quot;貼上 OperationStatusLink&quot;

# 查看匯出後的 bacpac 檔案
Get-AzStorageBlob `
    -Container &quot;sql-backup&quot; `
    -Context $ctx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;這版不是多高深，就是把該放一起的指令一次收齊；之後你要單檔測試、全站備份、接排程，都比較不會再來回補洞。&lt;/p&gt;
</content:encoded></item><item><title>Git fetch/pull 爆錯？大小寫分支衝突完整解法</title><link>https://joyceowo.github.io/posts/334a78ea09fa80a889fbf092a1576c5e/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/334a78ea09fa80a889fbf092a1576c5e/</guid><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用指令找出大小寫衝突 → 保留一個 → 刪除多餘分支，就能解掉 Git fetch/pull 卡死問題&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows / Mac 開發者遇到 Git pull 失敗&lt;/li&gt;
&lt;li&gt;團隊有 Linux + Windows 混用&lt;/li&gt;
&lt;li&gt;分支命名沒有統一規範&lt;/li&gt;
&lt;li&gt;CI/CD 突然抓不到分支&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 找出大小寫衝突分支&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;git ls-remote&lt;/code&gt; 抓遠端 branch，轉小寫後分組比對&lt;/li&gt;
&lt;li&gt;核心是找「名稱不同但小寫後相同」的分支&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;git ls-remote --heads origin |
ForEach-Object { ($_ -split &quot;`t&quot;)[1] } |
Group-Object { $_.ToLower() } |
Where-Object { $_.Count -gt 1 } |
Select-Object -ExpandProperty Group
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 這步是關鍵，沒找出衝突就無法處理&lt;/p&gt;
&lt;h3&gt;2. 決定保留分支&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;通常統一為小寫（如 &lt;code&gt;feature/login&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;或依團隊規範決定
👉 Git 在 Windows 上不分大小寫，所以只能留一個&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 刪除多餘分支&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;刪除遠端衝突分支&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;git push origin --delete Feature/Login
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;刪除後記得同步本地&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;git fetch --prune
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 prune 很重要，不然本地還會殘留舊資料&lt;/p&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows / Mac 預設是不分大小寫檔案系統，Linux 是分大小寫 → 衝突來源&lt;/li&gt;
&lt;li&gt;不建議用 &lt;code&gt;reftable&lt;/code&gt; 解法，工具支援還不穩&lt;/li&gt;
&lt;li&gt;CI/CD 若綁錯分支，刪除前要先確認 pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 找出大小寫衝突
git ls-remote --heads origin |
ForEach-Object { ($_ -split &quot;`t&quot;)[1] } |
Group-Object { $_.ToLower() } |
Where-Object { $_.Count -gt 1 } |
Select-Object -ExpandProperty Group
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 自訂 function
function Find-GitCaseConflict {
    git ls-remote --heads origin |
    ForEach-Object { ($_ -split &quot;`t&quot;)[1] } |
    Group-Object { $_.ToLower() } |
    Where-Object { $_.Count -gt 1 } |
    Select-Object -ExpandProperty Group
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 刪除遠端分支
git push origin --delete Feature/Login

# 清理本地
git fetch --prune
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Git 分支大小寫不統一，就是定時炸彈&lt;/li&gt;
&lt;li&gt;建議一開始就規範命名（全小寫），後面會少很多坑&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;/h2&gt;
</content:encoded></item><item><title>別再手動給權限了！用這套 SQL 角色設計讓管理變簡單</title><link>https://joyceowo.github.io/posts/32fa78ea09fa800285daf3ee84dd0fc4/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/32fa78ea09fa800285daf3ee84dd0fc4/</guid><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用「自訂角色 + 內建角色」組合管理 SQL Server 權限，比直接對使用者 &lt;code&gt;GRANT&lt;/code&gt; 更穩，也更容易維護。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;資料庫使用者越來越多，權限開始失控&lt;/li&gt;
&lt;li&gt;新人、廠商、支援人員需要不同層級的 DB 權限&lt;/li&gt;
&lt;li&gt;想把權限管理做成固定模板，而不是每次手動重配&lt;/li&gt;
&lt;li&gt;希望降低直接對 User 給權限造成的追蹤困難&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 先把角色模型定下來&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;先不要急著對人給權限，先定義三種標準角色：唯讀、一般開發、可改 Schema 的進階開發。這樣做的重點不是「分很多角色」，而是把常見需求收斂成固定模板，之後新增人員只要加到對應角色就好。實務上我會把命名統一成 &lt;code&gt;role_db_*&lt;/code&gt;，一眼就知道這是自訂角色，不會跟內建角色混在一起。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = &apos;role_db_readonly&apos;)
    CREATE ROLE role_db_readonly;

IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = &apos;role_db_developer&apos;)
    CREATE ROLE role_db_developer;

IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = &apos;role_db_schema_developer&apos;)
    CREATE ROLE role_db_schema_developer;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 用內建角色拼出權限模板&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;建完自訂角色後，再把內建角色的能力掛進去。&lt;code&gt;role_db_readonly&lt;/code&gt; 給查詢與檢視定義，適合報表或查詢人員；&lt;code&gt;role_db_developer&lt;/code&gt; 再加上寫入與執行 &lt;code&gt;SP&lt;/code&gt; 的能力，適合一般後端工程師。這樣做的好處是層級很清楚，權限差異也看得出來，不會每個人都一套特規。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ALTER ROLE db_datareader ADD MEMBER role_db_readonly;
GRANT VIEW DEFINITION TO role_db_readonly;
GO

ALTER ROLE db_datareader ADD MEMBER role_db_developer;
ALTER ROLE db_datawriter ADD MEMBER role_db_developer;
GRANT EXECUTE ON SCHEMA::dbo TO role_db_developer;
GRANT VIEW DEFINITION TO role_db_developer;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 進階角色只留給真的需要的人&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;role_db_schema_developer&lt;/code&gt; 可以再加上 &lt;code&gt;db_ddladmin&lt;/code&gt;，讓角色具備 &lt;code&gt;CREATE / ALTER / DROP&lt;/code&gt; 這類 DDL 能力。這類權限很方便，但也很容易手滑出事，所以不要因為「比較省事」就亂發。通常只建議給 Tech Lead、DBA，或真的需要調整物件結構的人。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ALTER ROLE db_datareader ADD MEMBER role_db_schema_developer;
ALTER ROLE db_datawriter ADD MEMBER role_db_schema_developer;
ALTER ROLE db_ddladmin ADD MEMBER role_db_schema_developer;

GRANT EXECUTE ON SCHEMA::dbo TO role_db_schema_developer;
GRANT VIEW DEFINITION TO role_db_schema_developer;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 最後只管角色，不直接管人&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;權限模型定好後，使用者只要加入角色即可。關鍵原則很簡單：&lt;strong&gt;不要直接對 User 發權限&lt;/strong&gt;。你今天直接 &lt;code&gt;GRANT SELECT TO user1&lt;/code&gt;，明天就很難知道這個人到底為什麼有權限；但如果是加進 &lt;code&gt;role_db_readonly&lt;/code&gt;，整個權限來源就很清楚。權限管理本質上不是發散，而是收斂，這句話不是裝深沉，是真的能救命 =_=&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 將使用者加入對應角色
-- ALTER ROLE role_db_readonly ADD MEMBER [user1];
-- ALTER ROLE role_db_developer ADD MEMBER [user2];
-- ALTER ROLE role_db_schema_developer ADD MEMBER [user3];
GO

-- 不建議：直接對使用者給權限
GRANT SELECT TO user1;

-- 建議：把使用者加進角色
ALTER ROLE role_db_readonly ADD MEMBER user1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;h3&gt;角色與權限對照表&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色名稱&lt;/th&gt;
&lt;th&gt;權限內容&lt;/th&gt;
&lt;th&gt;適用對象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;role_db_readonly&lt;/td&gt;
&lt;td&gt;查詢、檢視定義&lt;/td&gt;
&lt;td&gt;報表、查詢、支援&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;role_db_developer&lt;/td&gt;
&lt;td&gt;查詢、寫入、執行、檢視定義&lt;/td&gt;
&lt;td&gt;後端工程師、API 開發&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;role_db_schema_developer&lt;/td&gt;
&lt;td&gt;查詢、寫入、執行、DDL、檢視定義&lt;/td&gt;
&lt;td&gt;Tech Lead、DBA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;SQL Agent 補充權限&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;很多人以為加了資料庫權限就能看 SQL Agent Job，結果打開一片空白，然後開始懷疑人生。這不是 SQL Server 壞掉，而是 **SQL Agent 權限在 **&lt;code&gt;**msdb**&lt;/code&gt;，要另外補。
| 需求 | msdb 角色 |
| --- | --- |
| 只看自己的 Job | SQLAgentUserRole |
| 看全部 Job / History | SQLAgentReaderRole |
| 看全部且可操作 Job | SQLAgentOperatorRole |&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;USE msdb;
GO
ALTER ROLE SQLAgentUserRole ADD MEMBER [YourLogin];
GO

USE msdb;
GO
ALTER ROLE SQLAgentReaderRole ADD MEMBER [YourLogin];
GO

USE msdb;
GO
ALTER ROLE SQLAgentOperatorRole ADD MEMBER [YourLogin];
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GRANT EXECUTE ON SCHEMA::dbo&lt;/code&gt; 只涵蓋 &lt;code&gt;dbo&lt;/code&gt; schema；若你的 &lt;code&gt;SP&lt;/code&gt; 不在 &lt;code&gt;dbo&lt;/code&gt;，要另外補授權。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;db_ddladmin&lt;/code&gt; 很強，但不等於完整 DBA；它偏向物件結構調整，不是所有管理權限都包含。&lt;/li&gt;
&lt;li&gt;若系統已經存在大量直接對 User 的授權，建議先盤點再慢慢收斂，不要一次硬切，否則很容易炸。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;把權限做成角色模板，之後維護的就不是一堆人，而是一套規則；規則穩了，系統才不會天天鬧脾氣。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>VPN 連上卻連不到內網？其實是 Route 把你帶錯路</title><link>https://joyceowo.github.io/posts/32ea78ea09fa80b2ac57fb26e2eb69cb/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/32ea78ea09fa80b2ac57fb26e2eb69cb/</guid><pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;VPN 連不上內網，多半不是壞掉，而是 &lt;strong&gt;route 導錯路，刪掉錯誤路由即可恢復&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;VPN 已連線但無法連 DB / 內網主機&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ping&lt;/code&gt; 不通但 DNS 正常解析&lt;/li&gt;
&lt;li&gt;Azure / 公司內網混合環境&lt;/li&gt;
&lt;li&gt;Windows 環境排查網路問題&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 確認目前路由狀態&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;執行 &lt;code&gt;route print&lt;/code&gt; 查看 routing table&lt;/li&gt;
&lt;li&gt;找出目標 IP 是否被導向 VPN gateway&lt;/li&gt;
&lt;li&gt;如果看到 &lt;code&gt;/32&lt;/code&gt; 單一 IP 指向 VPN，多半就是問題來源&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;route print
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 重點不是「有沒有 route」，而是「走去哪」&lt;/p&gt;
&lt;h3&gt;2. 判斷是否被 VPN 導錯&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;確認目標 IP（例如 10.x.x.x）是否本來應走 LAN&lt;/li&gt;
&lt;li&gt;若 route 指向 VPN gateway，代表流量被強制帶錯路&lt;/li&gt;
&lt;li&gt;常見情境：split tunnel 設錯或 VPN 推送錯誤 route
👉 DNS 正常但連不到，通常就是這種問題&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 刪除錯誤 route（快速解法）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;直接刪掉指定 IP route&lt;/li&gt;
&lt;li&gt;Windows 會重新選擇最佳路徑（通常回到 LAN）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;route delete 10.1.1.10
route delete 10.1.1.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 刪掉後重新 ping / 連線，多半立即恢復&lt;/p&gt;
&lt;h3&gt;4.（選用）手動修正路由&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;若 VPN 會一直覆蓋，可手動加正確 route&lt;/li&gt;
&lt;li&gt;指定走本地 gateway，避免再被搶走&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;route add 10.1.1.10 mask 255.255.255.255 192.168.1.1 metric 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 metric 越低優先權越高，用來「壓過 VPN」&lt;/p&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nslookup 正常 ≠ 連線正常&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VPN 常會推 &lt;code&gt;/32&lt;/code&gt; 精準路由，很容易踩雷&lt;/li&gt;
&lt;li&gt;重開 VPN 可能會把錯誤 route 加回來&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 查看路由
route print

# 刪除錯誤 route
route delete 10.1.1.10

# 手動指定正確路由
route add 10.1.1.10 mask 255.255.255.255 192.168.1.1 metric 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;網路不通，很多時候不是壞，是走錯路&lt;/li&gt;
&lt;li&gt;先看 route，再懷疑世界，效率會高很多&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>SQL Agent 多 Job 匯出怎麼做：一支腳本重建多個排程與 Step</title><link>https://joyceowo.github.io/posts/32da78ea09fa8006894dd4c158a631fb/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/32da78ea09fa8006894dd4c158a631fb/</guid><pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;用一支腳本批次匯出並重建 SQL Agent Job，連 step、schedule、通知一起帶走，省時間又避免漏設定。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;DB 搬機 / 環境切換（DEV → UAT → PROD）&lt;/li&gt;
&lt;li&gt;多個 SQL Agent Job 要同步重建&lt;/li&gt;
&lt;li&gt;想避免手動建立 Job 遺漏 step 或 schedule&lt;/li&gt;
&lt;li&gt;DBA / 後端工程師做維運自動化&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 定義參數與 Job 清單&lt;/h3&gt;
&lt;p&gt;先集中管理會變動的設定（DB、Owner、是否覆蓋），再用表變數定義要匯出的 Job 名單，支援改名。這樣做可以一次處理多個 Job，不用重複操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DECLARE @OverrideDatabaseName SYSNAME = N&apos;你的新DB&apos;; -- 指定新 DB
DECLARE @OverrideOwnerLoginName SYSNAME = N&apos;sa&apos;;     -- 指定 owner

INSERT INTO @Jobs (SourceJobName, TargetJobName)
VALUES
    (N&apos;Job_A&apos;, N&apos;Job_A_New&apos;),
    (N&apos;Job_B&apos;, NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 先做檢查，避免匯出錯誤&lt;/h3&gt;
&lt;p&gt;在產生腳本前先驗證來源 Job 是否存在，以及目標名稱是否重複。這能避免搬到一半才發現錯誤。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IF EXISTS (
    SELECT 1
    FROM @Jobs j
    LEFT JOIN msdb.dbo.sysjobs sj
        ON sj.name = j.SourceJobName
    WHERE sj.job_id IS NULL
)
BEGIN
    RAISERROR(N&apos;Job 不存在&apos;, 16, 1);
    RETURN;
END;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 動態組出 Job / Step / Schedule&lt;/h3&gt;
&lt;p&gt;透過系統表 &lt;code&gt;msdb.dbo.sysjobs&lt;/code&gt;、&lt;code&gt;sysjobsteps&lt;/code&gt;、&lt;code&gt;sysschedules&lt;/code&gt;，逐一組出完整腳本。這樣重建後的 Job 行為會與原本一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXEC msdb.dbo.sp_add_job
    @job_name = N&apos;Job_A_New&apos;,
    @enabled = 1;

EXEC msdb.dbo.sp_add_jobstep
    @job_id = @jobId,
    @step_name = N&apos;Step1&apos;,
    @command = N&apos;SELECT 1;&apos;,
    @database_name = N&apos;你的新DB&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 輸出 Warning 與檢查資訊&lt;/h3&gt;
&lt;p&gt;腳本會額外標示風險，例如非 TSQL step、Proxy、寫死 DB 名稱等，讓你在部署前能先檢查。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- WARNING: 非 TSQL step
-- WARNING: 使用 Proxy
-- WARNING: command 可能寫死 DB
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;command 內若寫死 DB 名稱，腳本不會自動改，需手動調整&lt;/li&gt;
&lt;li&gt;Proxy / SSIS / PowerShell step 要先在新環境建立對應設定&lt;/li&gt;
&lt;li&gt;正式環境建議不要直接覆蓋 Job，先確認再執行&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE msdb;
GO

SET NOCOUNT ON;

-------------------------------------------------------------------------------
-- 參數區
-------------------------------------------------------------------------------
DECLARE @OverrideDatabaseName SYSNAME = N&apos;你的新DB&apos;;   -- NULL = 不覆蓋 step database_name
DECLARE @OverrideOwnerLoginName SYSNAME = N&apos;sa&apos;;       -- NULL = 沿用原 owner
DECLARE @OverrideOperatorName SYSNAME = NULL;          -- NULL = 沿用原 operator
DECLARE @DropIfExists BIT = 1;                         -- 1=若同名 Job 已存在，先刪除再重建
DECLARE @AttachToLocalServer BIT = 1;                  -- 1=最後執行 sp_add_jobserver 綁到 (LOCAL)

DECLARE @CRLF NVARCHAR(10) = CHAR(13) + CHAR(10) + &apos;/**/&apos;;
DECLARE @FinalSQL NVARCHAR(MAX) = N&apos;&apos;;
DECLARE @Header NVARCHAR(MAX) = N&apos;&apos;;

-------------------------------------------------------------------------------
-- 指定要匯出的 Jobs
-------------------------------------------------------------------------------
DECLARE @Jobs TABLE
(
    RowNum INT IDENTITY(1,1) PRIMARY KEY,
    SourceJobName SYSNAME NOT NULL,
    TargetJobName SYSNAME NULL
);

-- ===== 在這裡填你要匯出的 Jobs =====
INSERT INTO @Jobs (SourceJobName, TargetJobName)
VALUES
    (N&apos;Job_A&apos;, N&apos;Job_A_New&apos;),
    (N&apos;Job_B&apos;, NULL),
    (N&apos;Job_C&apos;, N&apos;Job_C_New&apos;);
-- ==================================

-------------------------------------------------------------------------------
-- 基本檢查：來源 Job 是否存在
-------------------------------------------------------------------------------
IF EXISTS
(
    SELECT 1
    FROM @Jobs j
    LEFT JOIN msdb.dbo.sysjobs sj
        ON sj.name = j.SourceJobName
    WHERE sj.job_id IS NULL
)
BEGIN
    SELECT
        j.SourceJobName
    FROM @Jobs j
    LEFT JOIN msdb.dbo.sysjobs sj
        ON sj.name = j.SourceJobName
    WHERE sj.job_id IS NULL;

    RAISERROR(N&apos;上面列出的 SourceJobName 在 msdb.dbo.sysjobs 中找不到，請先修正。&apos;, 16, 1);
    RETURN;
END;

-------------------------------------------------------------------------------
-- 檢查 TargetJobName 是否重複
-------------------------------------------------------------------------------
IF EXISTS
(
    SELECT 1
    FROM
    (
        SELECT ISNULL(TargetJobName, SourceJobName) AS FinalJobName
        FROM @Jobs
    ) x
    GROUP BY x.FinalJobName
    HAVING COUNT(*) &amp;gt; 1
)
BEGIN
    SELECT
        x.FinalJobName,
        COUNT(*) AS Cnt
    FROM
    (
        SELECT ISNULL(TargetJobName, SourceJobName) AS FinalJobName
        FROM @Jobs
    ) x
    GROUP BY x.FinalJobName
    HAVING COUNT(*) &amp;gt; 1;

    RAISERROR(N&apos;上面列出的 TargetJobName / FinalJobName 有重複，會互相覆蓋，請先修正。&apos;, 16, 1);
    RETURN;
END;

-------------------------------------------------------------------------------
-- Header
-------------------------------------------------------------------------------
SET @Header =
N&apos;/* =====================================================================&apos; + @CRLF +
N&apos;   SQL Agent Multi-Job Export Script&apos; + @CRLF +
N&apos;   Generated  : &apos; + CONVERT(NVARCHAR(19), GETDATE(), 120) + @CRLF +
N&apos;   Notes:&apos; + @CRLF +
N&apos;   1. 請先確認新環境是否存在對應 Login / Operator / Proxy / Database。&apos; + @CRLF +
N&apos;   2. 若 step command 內文有寫死舊 DB，需人工調整。&apos; + @CRLF +
N&apos;   3. 建議先建立後停用 Job，手動測試 step 無誤再啟用 schedule。&apos; + @CRLF +
N&apos;===================================================================== */&apos; + @CRLF + @CRLF;

SET @FinalSQL = @Header;

-------------------------------------------------------------------------------
-- 先準備待處理 Job 清單
-------------------------------------------------------------------------------
DECLARE @JobList TABLE
(
    RowNum INT PRIMARY KEY,
    SourceJobName SYSNAME NOT NULL,
    TargetJobName SYSNAME NOT NULL,
    JobId UNIQUEIDENTIFIER NOT NULL
);

INSERT INTO @JobList (RowNum, SourceJobName, TargetJobName, JobId)
SELECT
    j.RowNum,
    j.SourceJobName,
    ISNULL(j.TargetJobName, j.SourceJobName) AS TargetJobName,
    sj.job_id
FROM @Jobs j
JOIN msdb.dbo.sysjobs sj
    ON sj.name = j.SourceJobName;

-------------------------------------------------------------------------------
-- 逐一組 Script
-------------------------------------------------------------------------------
DECLARE
    @RowNum INT,
    @SourceJobName SYSNAME,
    @TargetJobName SYSNAME,
    @JobId UNIQUEIDENTIFIER,
    @JobSQL NVARCHAR(MAX),
    @Warnings NVARCHAR(MAX),
    @Tmp NVARCHAR(MAX);

DECLARE JobCursor CURSOR FAST_FORWARD FOR
SELECT RowNum, SourceJobName, TargetJobName, JobId
FROM @JobList
ORDER BY RowNum;

OPEN JobCursor;
FETCH NEXT FROM JobCursor INTO @RowNum, @SourceJobName, @TargetJobName, @JobId;

WHILE @@FETCH_STATUS = 0
BEGIN
    SET @JobSQL = N&apos;&apos;;
    SET @Warnings = N&apos;&apos;;
    SET @Tmp = N&apos;&apos;;

    ---------------------------------------------------------------------------
    -- Job 區塊標頭
    ---------------------------------------------------------------------------
    SET @JobSQL = @JobSQL +
    N&apos;/* =====================================================================&apos; + @CRLF +
    N&apos;   Job #&apos; + CAST(@RowNum AS NVARCHAR(10)) + N&apos;&apos; + @CRLF +
    N&apos;   Source Job : &apos; + REPLACE(@SourceJobName, &apos;*/&apos;, &apos;* /&apos;) + @CRLF +
    N&apos;   Target Job : &apos; + REPLACE(@TargetJobName, &apos;*/&apos;, &apos;* /&apos;) + @CRLF +
    N&apos;===================================================================== */&apos; + @CRLF + @CRLF;

    ---------------------------------------------------------------------------
    -- Warning 區
    ---------------------------------------------------------------------------
    IF EXISTS (
        SELECT 1
        FROM msdb.dbo.sysjobsteps
        WHERE job_id = @JobId
        GROUP BY job_id
        HAVING COUNT(*) &amp;gt; 1
    )
    BEGIN
        SET @Warnings = @Warnings + N&apos;-- WARNING: 此 Job 含多個 steps，請逐一確認 step 間跳轉。&apos; + @CRLF;
    END;

    IF EXISTS (
        SELECT 1
        FROM msdb.dbo.sysjobsteps
        WHERE job_id = @JobId
          AND subsystem &amp;lt;&amp;gt; N&apos;TSQL&apos;
    )
    BEGIN
        SET @Warnings = @Warnings + N&apos;-- WARNING: 含非 TSQL steps（如 CmdExec / PowerShell / SSIS），新環境需確認元件與權限。&apos; + @CRLF;
    END;

    IF EXISTS (
        SELECT 1
        FROM msdb.dbo.sysjobsteps
        WHERE job_id = @JobId
          AND proxy_id IS NOT NULL
          AND proxy_id &amp;lt;&amp;gt; 0
    )
    BEGIN
        SET @Warnings = @Warnings + N&apos;-- WARNING: 此 Job 使用 Proxy，新環境必須先建立對應 Proxy/Credential。&apos; + @CRLF;
    END;

    IF EXISTS (
        SELECT 1
        FROM msdb.dbo.sysjobsteps
        WHERE job_id = @JobId
          AND (
                command LIKE N&apos;%USE [%]%&apos;
             OR command LIKE N&apos;%].dbo.%&apos;
             OR command LIKE N&apos;%.dbo.%&apos;
          )
    )
    BEGIN
        SET @Warnings = @Warnings + N&apos;-- WARNING: 某些 step command 可能寫死資料庫名稱，請人工檢查 command 內容。&apos; + @CRLF;
    END;

    IF @Warnings &amp;lt;&amp;gt; N&apos;&apos;
    BEGIN
        SET @JobSQL = @JobSQL +
        N&apos;/* Pre-check warnings */&apos; + @CRLF +
        @Warnings + @CRLF;
    END;

    ---------------------------------------------------------------------------
    -- 刪除同名 Job（可選）
    ---------------------------------------------------------------------------
    SET @JobSQL = @JobSQL +
    N&apos;USE msdb;&apos; + @CRLF +
    N&apos;GO&apos; + @CRLF;

    IF @DropIfExists = 1
    BEGIN
        SET @JobSQL = @JobSQL +
        N&apos;IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N&apos;&apos;&apos; + REPLACE(@TargetJobName, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;)&apos; + @CRLF +
        N&apos;BEGIN&apos; + @CRLF +
        N&apos;    EXEC msdb.dbo.sp_delete_job @job_name = N&apos;&apos;&apos; + REPLACE(@TargetJobName, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;;&apos; + @CRLF +
        N&apos;END&apos; + @CRLF +
        N&apos;GO&apos; + @CRLF;
    END;

    SET @JobSQL = @JobSQL +
    N&apos;DECLARE @jobId BINARY(16);&apos; + @CRLF + @CRLF;

    ---------------------------------------------------------------------------
    -- sp_add_job
    ---------------------------------------------------------------------------
    SELECT @Tmp =
    N&apos;EXEC msdb.dbo.sp_add_job&apos; + @CRLF +
    N&apos;    @job_name = N&apos;&apos;&apos; + REPLACE(@TargetJobName, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF +
    N&apos;    @enabled = &apos; + CAST(j.enabled AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @notify_level_eventlog = &apos; + CAST(j.notify_level_eventlog AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @notify_level_email = &apos; + CAST(j.notify_level_email AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @notify_level_netsend = &apos; + CAST(j.notify_level_netsend AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @notify_level_page = &apos; + CAST(j.notify_level_page AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @delete_level = &apos; + CAST(j.delete_level AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @description = N&apos;&apos;&apos; + REPLACE(ISNULL(j.description, N&apos;&apos;), &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF +
    N&apos;    @category_name = N&apos;&apos;&apos; + REPLACE(ISNULL(c.name, N&apos;[Uncategorized (Local)]&apos;), &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF +
    N&apos;    @owner_login_name = N&apos;&apos;&apos; + REPLACE(ISNULL(@OverrideOwnerLoginName, SUSER_SNAME(j.owner_sid)), &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF +
    N&apos;    @job_id = @jobId OUTPUT;&apos; + @CRLF + @CRLF
    FROM msdb.dbo.sysjobs j
    LEFT JOIN msdb.dbo.syscategories c
        ON j.category_id = c.category_id
    WHERE j.job_id = @JobId;

    SET @JobSQL = @JobSQL + ISNULL(@Tmp, N&apos;&apos;);

    ---------------------------------------------------------------------------
    -- Steps
    ---------------------------------------------------------------------------
    SET @Tmp = N&apos;&apos;;

    SELECT @Tmp = @Tmp +
    N&apos;EXEC msdb.dbo.sp_add_jobstep&apos; + @CRLF +
    N&apos;    @job_id = @jobId,&apos; + @CRLF +
    N&apos;    @step_id = &apos; + CAST(s.step_id AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @step_name = N&apos;&apos;&apos; + REPLACE(s.step_name, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF +
    N&apos;    @subsystem = N&apos;&apos;&apos; + REPLACE(s.subsystem, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF +
    N&apos;    @command = N&apos;&apos;&apos; + REPLACE(ISNULL(s.command, N&apos;&apos;), &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF +
    CASE
        WHEN s.additional_parameters IS NULL THEN N&apos;&apos;
        ELSE N&apos;    @additional_parameters = N&apos;&apos;&apos; + REPLACE(s.additional_parameters, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF
    END +
    N&apos;    @cmdexec_success_code = &apos; + CAST(ISNULL(s.cmdexec_success_code, 0) AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @on_success_action = &apos; + CAST(s.on_success_action AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @on_success_step_id = &apos; + CAST(s.on_success_step_id AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @on_fail_action = &apos; + CAST(s.on_fail_action AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @on_fail_step_id = &apos; + CAST(s.on_fail_step_id AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    CASE
        WHEN s.server IS NULL THEN N&apos;&apos;
        ELSE N&apos;    @server = N&apos;&apos;&apos; + REPLACE(s.server, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF
    END +
    N&apos;    @database_name = &apos; +
        CASE
            WHEN ISNULL(@OverrideDatabaseName, s.database_name) IS NULL THEN N&apos;NULL&apos;
            ELSE N&apos;N&apos;&apos;&apos; + REPLACE(ISNULL(@OverrideDatabaseName, s.database_name), &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;&apos;
        END + N&apos;,&apos; + @CRLF +
    N&apos;    @database_user_name = &apos; +
        CASE
            WHEN s.database_user_name IS NULL THEN N&apos;NULL&apos;
            ELSE N&apos;N&apos;&apos;&apos; + REPLACE(s.database_user_name, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;&apos;
        END + N&apos;,&apos; + @CRLF +
    N&apos;    @retry_attempts = &apos; + CAST(ISNULL(s.retry_attempts, 0) AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @retry_interval = &apos; + CAST(ISNULL(s.retry_interval, 0) AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @os_run_priority = &apos; + CAST(ISNULL(s.os_run_priority, 0) AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @output_file_name = &apos; +
        CASE
            WHEN s.output_file_name IS NULL THEN N&apos;NULL&apos;
            ELSE N&apos;N&apos;&apos;&apos; + REPLACE(s.output_file_name, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;&apos;
        END + N&apos;,&apos; + @CRLF +
    CASE
        WHEN p.name IS NULL OR p.name = N&apos;&apos; THEN N&apos;&apos;
        ELSE N&apos;    @proxy_name = N&apos;&apos;&apos; + REPLACE(p.name, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF
    END +
    N&apos;    @flags = &apos; + CAST(ISNULL(s.flags, 0) AS NVARCHAR(10)) + N&apos;;&apos; + @CRLF + @CRLF
    FROM msdb.dbo.sysjobsteps s
    LEFT JOIN msdb.dbo.sysproxies p
        ON s.proxy_id = p.proxy_id
    WHERE s.job_id = @JobId
    ORDER BY s.step_id;

    SET @JobSQL = @JobSQL + ISNULL(@Tmp, N&apos;&apos;);

    ---------------------------------------------------------------------------
    -- start_step_id
    ---------------------------------------------------------------------------
    SELECT @Tmp =
    N&apos;EXEC msdb.dbo.sp_update_job&apos; + @CRLF +
    N&apos;    @job_id = @jobId,&apos; + @CRLF +
    N&apos;    @start_step_id = &apos; + CAST(j.start_step_id AS NVARCHAR(10)) + N&apos;;&apos; + @CRLF + @CRLF
    FROM msdb.dbo.sysjobs j
    WHERE j.job_id = @JobId;

    SET @JobSQL = @JobSQL + ISNULL(@Tmp, N&apos;&apos;);

    ---------------------------------------------------------------------------
    -- schedules
    ---------------------------------------------------------------------------
    SET @Tmp = N&apos;&apos;;

    SELECT @Tmp = @Tmp +
    N&apos;EXEC msdb.dbo.sp_add_jobschedule&apos; + @CRLF +
    N&apos;    @job_id = @jobId,&apos; + @CRLF +
    N&apos;    @name = N&apos;&apos;&apos; + REPLACE(sch.name, &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;,&apos; + @CRLF +
    N&apos;    @enabled = &apos; + CAST(sch.enabled AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @freq_type = &apos; + CAST(sch.freq_type AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @freq_interval = &apos; + CAST(sch.freq_interval AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @freq_subday_type = &apos; + CAST(sch.freq_subday_type AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @freq_subday_interval = &apos; + CAST(sch.freq_subday_interval AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @freq_relative_interval = &apos; + CAST(sch.freq_relative_interval AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @freq_recurrence_factor = &apos; + CAST(sch.freq_recurrence_factor AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @active_start_date = &apos; + CAST(sch.active_start_date AS NVARCHAR(20)) + N&apos;,&apos; + @CRLF +
    N&apos;    @active_end_date = &apos; + CAST(sch.active_end_date AS NVARCHAR(20)) + N&apos;,&apos; + @CRLF +
    N&apos;    @active_start_time = &apos; + CAST(sch.active_start_time AS NVARCHAR(20)) + N&apos;,&apos; + @CRLF +
    N&apos;    @active_end_time = &apos; + CAST(sch.active_end_time AS NVARCHAR(20)) + N&apos;;&apos; + @CRLF + @CRLF
    FROM
    (
        SELECT DISTINCT
            sch.name,
            sch.enabled,
            sch.freq_type,
            sch.freq_interval,
            sch.freq_subday_type,
            sch.freq_subday_interval,
            sch.freq_relative_interval,
            sch.freq_recurrence_factor,
            sch.active_start_date,
            sch.active_end_date,
            sch.active_start_time,
            sch.active_end_time
        FROM msdb.dbo.sysjobschedules js
        JOIN msdb.dbo.sysschedules sch
            ON js.schedule_id = sch.schedule_id
        WHERE js.job_id = @JobId
    ) sch
    ORDER BY sch.name;

    SET @JobSQL = @JobSQL + ISNULL(@Tmp, N&apos;&apos;);

    ---------------------------------------------------------------------------
    -- operator / notify
    ---------------------------------------------------------------------------
    SELECT @Tmp =
    N&apos;EXEC msdb.dbo.sp_update_job&apos; + @CRLF +
    N&apos;    @job_id = @jobId,&apos; + @CRLF +
    N&apos;    @notify_level_email = &apos; + CAST(j.notify_level_email AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @notify_level_netsend = &apos; + CAST(j.notify_level_netsend AS NVARCHAR(10)) + N&apos;,&apos; + @CRLF +
    N&apos;    @notify_level_page = &apos; + CAST(j.notify_level_page AS NVARCHAR(10)) +
    CASE
        WHEN ISNULL(@OverrideOperatorName, op_email.name) IS NOT NULL
            THEN N&apos;,&apos; + @CRLF + N&apos;    @notify_email_operator_name = N&apos;&apos;&apos; + REPLACE(ISNULL(@OverrideOperatorName, op_email.name), &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;&apos;
        ELSE N&apos;&apos;
    END +
    CASE
        WHEN ISNULL(@OverrideOperatorName, op_netsend.name) IS NOT NULL
            THEN N&apos;,&apos; + @CRLF + N&apos;    @notify_netsend_operator_name = N&apos;&apos;&apos; + REPLACE(ISNULL(@OverrideOperatorName, op_netsend.name), &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;&apos;
        ELSE N&apos;&apos;
    END +
    CASE
        WHEN ISNULL(@OverrideOperatorName, op_page.name) IS NOT NULL
            THEN N&apos;,&apos; + @CRLF + N&apos;    @notify_page_operator_name = N&apos;&apos;&apos; + REPLACE(ISNULL(@OverrideOperatorName, op_page.name), &apos;&apos;&apos;&apos;, &apos;&apos;&apos;&apos;&apos;&apos;) + N&apos;&apos;&apos;&apos;
        ELSE N&apos;&apos;
    END +
    N&apos;;&apos; + @CRLF + @CRLF
    FROM msdb.dbo.sysjobs j
    LEFT JOIN msdb.dbo.sysoperators op_email
        ON j.notify_email_operator_id = op_email.id
    LEFT JOIN msdb.dbo.sysoperators op_netsend
        ON j.notify_netsend_operator_id = op_netsend.id
    LEFT JOIN msdb.dbo.sysoperators op_page
        ON j.notify_page_operator_id = op_page.id
    WHERE j.job_id = @JobId;

    SET @JobSQL = @JobSQL + ISNULL(@Tmp, N&apos;&apos;);

    ---------------------------------------------------------------------------
    -- job server
    ---------------------------------------------------------------------------
    IF @AttachToLocalServer = 1
    BEGIN
        SET @JobSQL = @JobSQL +
        N&apos;EXEC msdb.dbo.sp_add_jobserver&apos; + @CRLF +
        N&apos;    @job_id = @jobId,&apos; + @CRLF +
        N&apos;    @server_name = N&apos;&apos;(LOCAL)&apos;&apos;;&apos; + @CRLF + @CRLF;
    END;

    ---------------------------------------------------------------------------
    -- step review
    ---------------------------------------------------------------------------
    SET @JobSQL = @JobSQL +
    N&apos;/* Step Review&apos; + @CRLF;

    SET @Tmp = N&apos;&apos;;

    SELECT @Tmp = @Tmp +
    N&apos;-- Step &apos; + CAST(s.step_id AS NVARCHAR(10)) +
    N&apos; | Name: &apos; + REPLACE(REPLACE(s.step_name, CHAR(13), N&apos; &apos;), CHAR(10), N&apos; &apos;) +
    N&apos; | Subsystem: &apos; + s.subsystem +
    N&apos; | DB: &apos; + ISNULL(s.database_name, N&apos;(NULL)&apos;) +
    N&apos; | Proxy: &apos; + ISNULL(p.name, N&apos;(NULL)&apos;) +
    CASE
        WHEN s.command LIKE N&apos;%USE [%]%&apos;
          OR s.command LIKE N&apos;%].dbo.%&apos;
          OR s.command LIKE N&apos;%.dbo.%&apos;
        THEN N&apos; | CHECK_COMMAND_FOR_DB_NAME=YES&apos;
        ELSE N&apos;&apos;
    END +
    @CRLF
    FROM msdb.dbo.sysjobsteps s
    LEFT JOIN msdb.dbo.sysproxies p
        ON s.proxy_id = p.proxy_id
    WHERE s.job_id = @JobId
    ORDER BY s.step_id;

    SET @JobSQL = @JobSQL + ISNULL(@Tmp, N&apos;&apos;) +
    N&apos;*/&apos; + @CRLF +
    N&apos;GO&apos; + @CRLF + @CRLF;

    ---------------------------------------------------------------------------
    -- 加到總腳本
    ---------------------------------------------------------------------------
    SET @FinalSQL = @FinalSQL + @JobSQL;

    FETCH NEXT FROM JobCursor INTO @RowNum, @SourceJobName, @TargetJobName, @JobId;
END

CLOSE JobCursor;
DEALLOCATE JobCursor;

-------------------------------------------------------------------------------
-- 最終輸出
-------------------------------------------------------------------------------
SELECT @FinalSQL AS DeployScript;
GO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;這種腳本重點不是匯出，而是讓 Job 搬遷變成「可重複、可驗證」的流程。&lt;/p&gt;
</content:encoded></item><item><title>SSMS 每次寫 SQL 都在補註解？用 Code Snippet 一鍵生成</title><link>https://joyceowo.github.io/posts/32da78ea09fa80e095dff8f2f76f31f7/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/32da78ea09fa80e095dff8f2f76f31f7/</guid><pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 SSMS Code Snippet 建立註解模板，輸入 &lt;code&gt;cmt&lt;/code&gt; 就能快速產生完整 SQL 註解結構。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;團隊需要統一 SQL 註解格式&lt;/li&gt;
&lt;li&gt;常寫 Stored Procedure / Script&lt;/li&gt;
&lt;li&gt;想減少重複貼模板的時間&lt;/li&gt;
&lt;li&gt;想提升 SQL 可讀性與維護性&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 建立 Snippet 檔案&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;將 XML 存成 &lt;code&gt;.snippet&lt;/code&gt; 檔，放到指定路徑&lt;/li&gt;
&lt;li&gt;路徑：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;C:\\Users\\@User\\Documents\\SQL Server Management Studio 21\\Code Snippets\\SQL\\My Code Snippets
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;SSMS 會自動讀取這個資料夾內的 snippet（重開 SSMS 才會生效）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 設定 Snippet 結構&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定義標題、快捷鍵（Shortcut）、變數（Literal）&lt;/li&gt;
&lt;li&gt;Shortcut 設為 &lt;code&gt;cmt&lt;/code&gt;，之後輸入 cmt + Tab 直接展開&lt;/li&gt;
&lt;li&gt;變數像 &lt;code&gt;$author$&lt;/code&gt;、&lt;code&gt;$date$&lt;/code&gt; 可快速填值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;CodeSnippets xmlns=&quot;&amp;lt;http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet&amp;gt;&quot;&amp;gt;
  &amp;lt;CodeSnippet Format=&quot;1.0.0&quot;&amp;gt;
    &amp;lt;Header&amp;gt;
      &amp;lt;Title&amp;gt;SQL-Comment&amp;lt;/Title&amp;gt;
      &amp;lt;Shortcut&amp;gt;cmt&amp;lt;/Shortcut&amp;gt;
      &amp;lt;Description&amp;gt;SQL-Comment&amp;lt;/Description&amp;gt;
      &amp;lt;Author&amp;gt;J&amp;lt;/Author&amp;gt;
    &amp;lt;/Header&amp;gt;

    &amp;lt;Snippet&amp;gt;
      &amp;lt;Code Language=&quot;SQL&quot;&amp;gt;&amp;lt;![CDATA[
-- =============================================
-- Author:      $author$
-- Create Date: $date$
-- Description: $desc$
-- =============================================
/*  Parameter:
        $param$

    SQL:
        $sql$

    Log:
        $log$
*/
]]&amp;gt;&amp;lt;/Code&amp;gt;

      &amp;lt;Declarations&amp;gt;
        &amp;lt;Literal&amp;gt;
          &amp;lt;ID&amp;gt;author&amp;lt;/ID&amp;gt;
          &amp;lt;Default&amp;gt;YourName&amp;lt;/Default&amp;gt;
        &amp;lt;/Literal&amp;gt;

        &amp;lt;Literal&amp;gt;
          &amp;lt;ID&amp;gt;date&amp;lt;/ID&amp;gt;
          &amp;lt;Default&amp;gt;yyyy/MM/dd&amp;lt;/Default&amp;gt;
        &amp;lt;/Literal&amp;gt;

        &amp;lt;Literal&amp;gt;
          &amp;lt;ID&amp;gt;desc&amp;lt;/ID&amp;gt;
          &amp;lt;Default&amp;gt;Description&amp;lt;/Default&amp;gt;
        &amp;lt;/Literal&amp;gt;

        &amp;lt;Literal&amp;gt;
          &amp;lt;ID&amp;gt;param&amp;lt;/ID&amp;gt;
          &amp;lt;Default&amp;gt;@Param Comment&amp;lt;/Default&amp;gt;
        &amp;lt;/Literal&amp;gt;

        &amp;lt;Literal&amp;gt;
          &amp;lt;ID&amp;gt;sql&amp;lt;/ID&amp;gt;
          &amp;lt;Default&amp;gt;SQL Sample&amp;lt;/Default&amp;gt;
        &amp;lt;/Literal&amp;gt;

        &amp;lt;Literal&amp;gt;
          &amp;lt;ID&amp;gt;log&amp;lt;/ID&amp;gt;
          &amp;lt;Default&amp;gt;yyyyMMdd by YourName - adjustment notes&amp;lt;/Default&amp;gt;
        &amp;lt;/Literal&amp;gt;
      &amp;lt;/Declarations&amp;gt;
    &amp;lt;/Snippet&amp;gt;
  &amp;lt;/CodeSnippet&amp;gt;
&amp;lt;/CodeSnippets&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 使用 Snippet&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 SSMS Query 視窗輸入：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;cmt
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;按 &lt;code&gt;Tab&lt;/code&gt; → 自動展開模板&lt;/li&gt;
&lt;li&gt;再用 &lt;code&gt;Tab&lt;/code&gt; 在各欄位間切換填值&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SSMS 不會即時 reload Snippet → 一定要重開&lt;/li&gt;
&lt;li&gt;Shortcut 衝突時會無法展開（建議用簡短但不常見的名稱）&lt;/li&gt;
&lt;li&gt;可建立多個 snippet（例如：sp、select、log 模板）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expend&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;展開 snippet：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;cmt + Tab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;懶不是問題，重複寫才是，Snippet 幫你把時間偷回來。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>SSMS 匯入/匯出資料按鈕反灰？其實你少裝一個東西</title><link>https://joyceowo.github.io/posts/295a78ea09fa80e28e34da278854af77/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/295a78ea09fa80e28e34da278854af77/</guid><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SSMS 匯入/匯出功能是靠 SSIS，沒裝就會直接鎖死&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SSMS 出現「匯入/匯出資料」反灰&lt;/li&gt;
&lt;li&gt;點擊時提示版本不支援&lt;/li&gt;
&lt;li&gt;新環境或重灌 SSMS 後功能消失&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 確認問題現象&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 SSMS 中對資料庫右鍵 → 匯入資料 / 匯出資料&lt;/li&gt;
&lt;li&gt;發現按鈕反灰或顯示不支援&lt;/li&gt;
&lt;li&gt;這不是權限問題，而是工具缺失（常見誤判）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 安裝 SSIS（核心解法）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;安裝 SQL Server Integration Services（SSIS）&lt;/li&gt;
&lt;li&gt;SSMS 本身只是 UI，實際執行是 SSIS 在跑&lt;/li&gt;
&lt;li&gt;沒裝 SSIS，就像有按鈕但沒引擎&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 驗證功能恢復&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;重開 SSMS&lt;/li&gt;
&lt;li&gt;再次右鍵 DB → 匯入/匯出資料&lt;/li&gt;
&lt;li&gt;若可點擊，代表環境已完整&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SSMS 與 SSIS 是分開安裝的（很多人會以為內建）&lt;/li&gt;
&lt;li&gt;安裝 SQL Server 時，需勾選 Integration Services 才會有&lt;/li&gt;
&lt;li&gt;有些精簡版本（例如 Developer Tools Only）不會包含 SSIS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;這問題不難，但很容易卡住半天，少裝就是少靈魂&lt;/li&gt;
&lt;li&gt;新環境記得先確認 SSIS，有些坑踩一次就好&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>從 APK 到 EXE：我怎麼用 JADX 和 ChatGPT 把反編譯結果變成可執行工具</title><link>https://joyceowo.github.io/posts/264a78ea09fa80208b6ae2a1734a2a83/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/264a78ea09fa80208b6ae2a1734a2a83/</guid><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;這套流程很適合拿來快速理解 APK 內部邏輯、抽出可測試的程式片段，並逐步整理成可執行的 Python 工具。&lt;/p&gt;
&lt;p&gt;流程很直白：&lt;strong&gt;先反編譯、再分析、持續測試、最後打包。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;想研究 APK 的程式邏輯&lt;/li&gt;
&lt;li&gt;想把某段功能抽出來做本機驗證&lt;/li&gt;
&lt;li&gt;想快速做成可執行工具給自己或同事使用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 使用 JADX 反編譯 APK 檔&lt;/h3&gt;
&lt;p&gt;JADX 可以把 APK 內容反編譯成較容易閱讀的 Java 原始碼，拿來理解程式流程很方便。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工具網址：&lt;a href=&quot;https://github.com/skylot/jadx&quot;&gt;https://github.com/skylot/jadx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;常見用途：
&lt;ul&gt;
&lt;li&gt;查看 class 結構&lt;/li&gt;
&lt;li&gt;找 API 呼叫流程&lt;/li&gt;
&lt;li&gt;找加解密、參數組裝、驗證邏輯&lt;/li&gt;
&lt;li&gt;追 UI 與業務邏輯的對應關係&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 將相關檔案丟給 ChatGPT，產生 Python 檔案進行測試&lt;/h3&gt;
&lt;p&gt;從 JADX 找到需要的邏輯後，可以把關鍵程式碼、設定檔、流程片段整理出來，交給 ChatGPT 協助轉成 Python 測試版本。&lt;/p&gt;
&lt;p&gt;這一步通常會做的事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽出核心邏輯&lt;/li&gt;
&lt;li&gt;補齊缺少的參數&lt;/li&gt;
&lt;li&gt;改寫成可直接執行的 Python 程式&lt;/li&gt;
&lt;li&gt;降低原始專案環境依賴，方便單獨驗證&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 不斷執行 → 測試 → 修改，直到測試成功&lt;/h3&gt;
&lt;p&gt;這段其實最花時間，但也是最重要的一段。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python your_file.py arg1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;建議做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先讓程式能跑起來&lt;/li&gt;
&lt;li&gt;再補齊缺少的邏輯&lt;/li&gt;
&lt;li&gt;遇到錯誤就看 traceback 修正&lt;/li&gt;
&lt;li&gt;每次只改一小段，比較好追問題&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 打包成 &lt;code&gt;.exe&lt;/code&gt; 檔&lt;/h2&gt;
&lt;p&gt;當 Python 測試完成後，就可以用 PyInstaller 打包成 EXE。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pyinstaller
pyinstaller --onefile your_file.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;說明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-onefile&lt;/code&gt; → 打包成單一 EXE 檔&lt;/li&gt;
&lt;li&gt;輸出位置：&lt;code&gt;dist/your_file.exe&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 執行 EXE 檔&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;dist\\your_file.exe arg1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注意事項&lt;/h2&gt;
&lt;h3&gt;1. 不要一開始就急著打包&lt;/h3&gt;
&lt;p&gt;先把 Python 版本測通，再進入打包。&lt;/p&gt;
&lt;h3&gt;2. 反編譯後的程式不一定完整&lt;/h3&gt;
&lt;p&gt;可能有混淆或缺少部分邏輯，需要自行補齊。&lt;/p&gt;
&lt;h3&gt;3. ChatGPT 適合做輔助&lt;/h3&gt;
&lt;p&gt;用來整理、改寫、補框架很快，但最終還是要自己測。&lt;/p&gt;
&lt;h2&gt;重點整理&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;階段&lt;/th&gt;
&lt;th&gt;工具 / 做法&lt;/th&gt;
&lt;th&gt;目的&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;反編譯 APK&lt;/td&gt;
&lt;td&gt;JADX&lt;/td&gt;
&lt;td&gt;讀取原始邏輯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邏輯整理&lt;/td&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;轉成測試程式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;執行驗證&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;確認功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打包發佈&lt;/td&gt;
&lt;td&gt;PyInstaller&lt;/td&gt;
&lt;td&gt;產出 EXE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最終執行&lt;/td&gt;
&lt;td&gt;EXE&lt;/td&gt;
&lt;td&gt;實際使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;指令總整理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;python your_file.py arg1
pip install pyinstaller
pyinstaller --onefile your_file.py
dist\\your_file.exe arg1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;這流程本質是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拆 → 測 → 修 → 打包&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前面像考古，後面像馴獸。&lt;/p&gt;
&lt;p&gt;能跑起來的那一刻，會爽一下。&lt;/p&gt;
</content:encoded></item><item><title>Google App Script 上架卡關？完整流程＋OAuth設定一次搞懂 </title><link>https://joyceowo.github.io/posts/23ba78ea09fa8197a481f7c38016388e/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa8197a481f7c38016388e/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;你如果寫了 Google App Script，想做成 Add-on 上架到 Google Workspace Marketplace，最容易卡的不是程式，而是 OAuth、部署 ID、審核流程。&lt;/p&gt;
&lt;p&gt;這些設定分散又繁瑣，一不注意就被退件。&lt;/p&gt;
&lt;p&gt;這篇整理完整 Google App Script 上架流程與 OAuth 設定，讓你一次走完，不再卡關。&lt;/p&gt;
&lt;h2&gt;🎯 目標說明&lt;/h2&gt;
&lt;p&gt;本篇將帶你完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;建立 App Script 專案&lt;/li&gt;
&lt;li&gt;設定 OAuth 同意畫面&lt;/li&gt;
&lt;li&gt;測試 Add-on&lt;/li&gt;
&lt;li&gt;發布至 Google Workspace Marketplace&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心流程（步驟拆解）&lt;/h2&gt;
&lt;h3&gt;步驟一：建立 App Script 與部署&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;建立 App Script
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-5aa68c9ccbdf.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;開啟 appscript.json &amp;amp; 指定 GCP 專案編號
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-a2b0810fab70.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;部署→新增部署
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-4ee97182d5ab.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;部署→測試部署 (開發測試)
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-362d22785683.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;步驟二：設定 OAuth 權限&lt;/h3&gt;
&lt;h3&gt;設定 Branding（OAuth 同意畫面）&lt;/h3&gt;
&lt;p&gt;需填寫：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;App Name：需與 Add-On 套件一致&lt;/li&gt;
&lt;li&gt;App domain：需架設一個介紹網站，內容需包含隱私條件、服務條款 (可用自己的 domain / 放在 github.io)&lt;/li&gt;
&lt;li&gt;Authorized domains：放 App domain 使用的網域
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-0c4fd9230d58.png&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;背後原理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google 透過 OAuth 畫面驗證應用可信度&lt;/li&gt;
&lt;li&gt;審核重點在這裡&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步驟三：Marketplace 上架設定&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;啟用「&lt;strong&gt;Google Workspace Marketplace SDK&lt;/strong&gt;」服務
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-ab23b503ef44.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;兩個分頁內都需填寫：設定 App Scripts的部署ID、開發者資訊、商品資訊
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-275c7e8e4acd.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;商店資訊→瀏覽 MARKETPLACE：上架前測試。該頁填寫「草稿測試人員」的Mail
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-61aad0f23ff0.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;想測試還需要設定 OAuth 的測試：OAuth 同意畫面→目標對象→測試使用者 + Add
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-531d06ae4e71.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;測試完回到「商店資訊」：送出審核
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-3ffb5a14756c.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OAUTH 切換至 Production
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8197a481f7c38016388e/23ba78ea09fa8197a481f7c38016388e-436c8e234262.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最後~ 就等 GOOGLE 審核結果&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;⚠️ 常見問題 / 踩坑&lt;/h2&gt;
&lt;h3&gt;OAuth 被退件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;沒有隱私權政策&lt;/li&gt;
&lt;li&gt;Domain 不一致&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;無法測試 Add-on&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;未加入測試使用者&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;上架失敗&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Deployment ID 錯誤&lt;/li&gt;
&lt;li&gt;OAuth 尚未 Production&lt;/li&gt;
&lt;li&gt;文字使用到 GOOGLE的產品需加™
(e.g. Google Sheets™、Google Workspace™)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;總結&lt;/h2&gt;
&lt;p&gt;這篇帶你走完：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;App Script 建立與部署&lt;/li&gt;
&lt;li&gt;OAuth 設定&lt;/li&gt;
&lt;li&gt;Marketplace 上架流程
關鍵重點：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;OAuth 設定是最大關鍵&lt;/li&gt;
&lt;li&gt;測試流程不能省&lt;/li&gt;
&lt;li&gt;Marketplace 資訊要完整&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Error: invalid version 16？SMO 抓不到語法直接炸掉</title><link>https://joyceowo.github.io/posts/23ba78ea09fa8185bbdedd283180ed59/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa8185bbdedd283180ed59/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SMO 版本過舊無法解析新版 SQL Server（v16），更新 SSMS 就能解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用 SMO 取得 Table / View Script&lt;/li&gt;
&lt;li&gt;自動化產生 SQL Schema&lt;/li&gt;
&lt;li&gt;使用舊版 SSMS / 舊 SDK 的環境&lt;/li&gt;
&lt;li&gt;出現 version 16 / 版本不支援錯誤&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 確認錯誤來源（SMO 版本不支援）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;執行取得 Create Table / View 語法時出現 &lt;code&gt;invalid version 16&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;代表 SMO 無法識別 SQL Server 2022（v16）&lt;/li&gt;
&lt;li&gt;常見於：
&lt;ul&gt;
&lt;li&gt;舊版 SSMS&lt;/li&gt;
&lt;li&gt;舊版 NuGet：Microsoft.SqlServer.Smo
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa8185bbdedd283180ed59/23ba78ea09fa8185bbdedd283180ed59-7e6d5d8af3d2.png&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 更新 SSMS（最快解）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;到微軟官方下載最新版 SSMS&lt;/li&gt;
&lt;li&gt;安裝後會自動更新 SMO 相關元件&lt;/li&gt;
&lt;li&gt;為什麼：
&lt;ul&gt;
&lt;li&gt;SMO 版本跟 SSMS 綁在一起&lt;/li&gt;
&lt;li&gt;新版 SSMS 才支援 SQL Server 2022+&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. （程式端）更新 NuGet 套件（如果有用 SMO）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;更新 SMO 套件版本&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 更新 SMO 套件
dotnet add package Microsoft.SqlServer.SqlManagementObjects
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;確保版本 &amp;gt;= 支援 SQL Server 2022&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SSMS 跟 SQL Server 版本&lt;strong&gt;不需要一致，但不能太舊&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;SMO 很吃版本，不是 DB 問題，是 client 問題&lt;/li&gt;
&lt;li&gt;若 CI/CD 有用 SMO，也要同步升級套件&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;看到 version 16 別懷疑人生，九成是工具太老，更新就好。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Azure Hybrid Connection 實戰：讓雲端安全連回地端服務</title><link>https://joyceowo.github.io/posts/23ba78ea09fa815faea3c2deb4cdcc98/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa815faea3c2deb4cdcc98/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 Hybrid Connection 可以讓 Azure Web App &lt;strong&gt;不開 VPN、不曝露 IP&lt;/strong&gt;，直接安全連回地端服務。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Web App 需要連地端 DB / API&lt;/li&gt;
&lt;li&gt;不想開 VPN 或 Public IP&lt;/li&gt;
&lt;li&gt;公司內網服務不能對外開放&lt;/li&gt;
&lt;li&gt;想快速打通雲地連線（低維運成本）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 建立 Hybrid Connection（雲端端）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 Azure Portal 建立 Hybrid Connection&lt;/li&gt;
&lt;li&gt;設定目標：
&lt;ul&gt;
&lt;li&gt;Host（地端服務 IP / hostname）&lt;/li&gt;
&lt;li&gt;Port（例如 1433 / 80 / 443）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;本質是 Azure 幫你建立一條「轉送通道」，不是直接連線
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa815faea3c2deb4cdcc98/23ba78ea09fa815faea3c2deb4cdcc98-7255d168e1c2.png&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Web App 綁定 Hybrid Connection&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;到 Web App → Networking → Hybrid Connections&lt;/li&gt;
&lt;li&gt;加入剛剛建立的 Hybrid Connection&lt;/li&gt;
&lt;li&gt;綁定後，Web App 存取該 Host:Port 時，會自動走這條通道
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa815faea3c2deb4cdcc98/23ba78ea09fa815faea3c2deb4cdcc98-a6659663d789.png&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 地端安裝 Connection Manager&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;安裝 Hybrid Connection Manager（HCM）&lt;/li&gt;
&lt;li&gt;登入 Azure 帳號並選擇該 Hybrid Connection&lt;/li&gt;
&lt;li&gt;HCM 會主動「向 Azure 建立 outbound 連線」（重點）&lt;/li&gt;
&lt;li&gt;不需要開 inbound port（防火牆壓力小）
&lt;img src=&quot;../../assets/notion-images/23ba78ea09fa815faea3c2deb4cdcc98/23ba78ea09fa815faea3c2deb4cdcc98-86380e5ff177.png&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 驗證連線&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Web App 直接用：
&lt;ul&gt;
&lt;li&gt;Host（設定的名稱）&lt;/li&gt;
&lt;li&gt;Port&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不需改 DNS、不需改程式架構&lt;/li&gt;
&lt;li&gt;成功即代表雲端 → 地端通道已建立&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HCM 是 &lt;strong&gt;主動連出去&lt;/strong&gt;，所以不用開防火牆 inbound（常見誤解）&lt;/li&gt;
&lt;li&gt;只支援 TCP，不支援 UDP&lt;/li&gt;
&lt;li&gt;若連不到，先確認：
&lt;ul&gt;
&lt;li&gt;HCM 是否 Online&lt;/li&gt;
&lt;li&gt;Port 是否正確&lt;/li&gt;
&lt;li&gt;地端服務是否允許本機連線&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 測試連線（Web App Console）
tcpping your-host 1433

# 常見 DB 連線字串（範例）
Server=your-host,1433;Database=DB;User Id=xxx;Password=xxx;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Hybrid Connection 就像一條「偷偷打洞的安全管道」，不用 VPN 也能穩穩通；先確認 HCM 在線，再談其他問題。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;參考來源&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/azure/app-service/app-service-hybrid-connections&quot;&gt;https://learn.microsoft.com/azure/app-service/app-service-hybrid-connections&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>NuGet 套件發布與本地測試實戰：從推送到驗證一次搞懂</title><link>https://joyceowo.github.io/posts/23ba78ea09fa815bab31cd64e4ab15c2/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa815bab31cd64e4ab15c2/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;發佈後直接用「專案參考」測試，比重新下載套件更快、更可控。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;自建 NuGet 私服 / 公司內部套件&lt;/li&gt;
&lt;li&gt;開發共用工具（Logging / Utils / SDK）&lt;/li&gt;
&lt;li&gt;發佈後想快速驗證功能是否正常&lt;/li&gt;
&lt;li&gt;避免反覆 push / install 浪費時間&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 推送 NuGet 套件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;將打包好的 &lt;code&gt;.nupkg&lt;/code&gt; 上傳到 NuGet Server&lt;/li&gt;
&lt;li&gt;確認 API Key 與 Server URL 正確&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 推送套件
dotnet nuget push -s &amp;lt;https://xxx.yyy.zzz/v3/index.json&amp;gt; -k KEYXXX1234 AppLogger.1.0.0.nupkg
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;為什麼：
&lt;ul&gt;
&lt;li&gt;讓其他專案可透過 NuGet 安裝&lt;/li&gt;
&lt;li&gt;作為正式版本發佈來源&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 使用「專案參考」進行測試&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不直接安裝 NuGet 套件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;改用本地專案測試
操作：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方案右鍵 → 加入現有專案（套件專案）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;專案 → 相依性 → 新增專案參考&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;選擇你的套件專案&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;為什麼：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改後立即生效（不用重包）&lt;/li&gt;
&lt;li&gt;Debug 更方便（可直接追進套件）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 驗證功能是否正常&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;執行主專案&lt;/li&gt;
&lt;li&gt;確認：
&lt;ul&gt;
&lt;li&gt;API / Method 是否正常&lt;/li&gt;
&lt;li&gt;DI / 設定是否正確&lt;/li&gt;
&lt;li&gt;相依套件是否缺漏&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 範例：測試 Logger 套件
var logger = new AppLogger();
logger.Log(&quot;測試訊息&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;不要每次測試都 push 套件 → 會拖慢開發節奏&lt;/li&gt;
&lt;li&gt;專案參考測試 OK 再重新打包發布&lt;/li&gt;
&lt;li&gt;注意版本號，不然 NuGet 可能抓舊版本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expend&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 推送 NuGet 套件
dotnet nuget push -s &amp;lt;https://xxx.yyy.zzz/v3/index.json&amp;gt; -k KEYXXX1234 AppLogger.1.0.0.nupkg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;測試階段用專案參考，發佈再用 NuGet，節奏會乾淨很多。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>連不到 DB？用 Hosts 強制綁 IP 解決 DNS 問題</title><link>https://joyceowo.github.io/posts/23ba78ea09fa81299cfad8077223fb1e/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa81299cfad8077223fb1e/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 Hosts 直接指定「IP → 網址」，可繞過 DNS，快速驗證或修正連線問題&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;測試環境 DNS 尚未設定完成&lt;/li&gt;
&lt;li&gt;DB / API 網址解析錯誤&lt;/li&gt;
&lt;li&gt;VPN / Private Link 導致 DNS 指錯 IP&lt;/li&gt;
&lt;li&gt;想強制連到指定機器（除錯用）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 修改 Hosts 檔案&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;到路徑 &lt;code&gt;C:\\Windows\\System32\\drivers\\etc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用「系統管理員」開啟 &lt;code&gt;hosts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新增一行 IP 對應網址，直接覆蓋 DNS 解析&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;10.10.0.1 test.database.windows.net
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;本機會優先使用這筆設定，不再查 DNS&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 套用設定&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;儲存後不一定立即生效，需刷新 DNS 快取&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ipconfig /flushdns
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;避免還在用舊的 DNS 結果&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 驗證是否生效&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;ping&lt;/code&gt; 或 &lt;code&gt;nslookup&lt;/code&gt; 確認解析結果&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ping test.database.windows.net
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;若 IP 顯示為 &lt;code&gt;10.10.0.1&lt;/code&gt;，代表設定成功&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Hosts 只影響「本機」，不會影響其他人&lt;/li&gt;
&lt;li&gt;若連線異常，優先檢查是否有舊 Hosts 殘留&lt;/li&gt;
&lt;li&gt;Azure / Private Endpoint 常會被 Hosts 覆蓋導致誤判&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Hosts 設定
10.10.0.1 test.database.windows.net

# 清除 DNS
ipconfig /flushdns

# 驗證
ping test.database.windows.net
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Hosts 是最快的「強制導流工具」，但記得用完要清，別害未來的你&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>GitHub 不接受 100MB 以上檔案？使用 Git LFS 解決大檔案 Commit 問題</title><link>https://joyceowo.github.io/posts/23ba78ea09fa81a49dead618ac617265/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa81a49dead618ac617265/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;當 GitHub 因檔案超過 100MB 而拒絕 Push 時，可透過 Git LFS（Large File Storage）管理大型檔案，即使使用 TortoiseGit 也能正常 Commit 與 Push。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用 GitHub 管理大型檔案&lt;/li&gt;
&lt;li&gt;專案包含壓縮檔、安裝包、模型檔案等大容量檔案&lt;/li&gt;
&lt;li&gt;使用 TortoiseGit 進行版本控制&lt;/li&gt;
&lt;li&gt;遇到 GitHub 100MB 檔案限制&lt;/li&gt;
&lt;li&gt;需要將既有大型檔案轉換為 Git LFS 管理&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 安裝 Git LFS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;從 Git LFS 官網下載並安裝&lt;/li&gt;
&lt;li&gt;安裝完成後只需初始化一次&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 初始化 Git LFS
git lfs install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GitHub 雖然支援 Git LFS，但本機必須先安裝並啟用，否則無法正常追蹤大型檔案。&lt;/p&gt;
&lt;h3&gt;2. 將既有大檔案轉換為 Git LFS&lt;/h3&gt;
&lt;p&gt;如果專案中已經存在超過 100MB 的檔案，建議直接使用 Git LFS Migration 功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 將所有超過 100MB 的檔案轉換為 LFS
git lfs migrate import--everything--above=100MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此指令會掃描整個 Repository 歷史紀錄，並將符合條件的大檔案改為 Git LFS 管理，不需指定副檔名。&lt;/p&gt;
&lt;p&gt;適合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;舊專案&lt;/li&gt;
&lt;li&gt;已經 Commit 過大型檔案&lt;/li&gt;
&lt;li&gt;不確定有哪些檔案超過限制&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 指定特定副檔名使用 Git LFS&lt;/h3&gt;
&lt;p&gt;若專案固定會放入特定格式的大檔案，也可以直接設定追蹤規則。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 追蹤 7z 檔案
git lfs track&quot;*.7z&quot;

# 追蹤 tar.gz 檔案
git lfs track&quot;*.tar.gz&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行後會自動更新 &lt;code&gt;.gitattributes&lt;/code&gt;，後續新增的檔案都會自動使用 Git LFS 儲存。&lt;/p&gt;
&lt;h3&gt;4. 使用 TortoiseGit Commit 與 Push&lt;/h3&gt;
&lt;p&gt;完成 Git LFS 設定後，即可回到 TortoiseGit 操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;右鍵 → TortoiseGit → Commit&lt;/li&gt;
&lt;li&gt;確認 &lt;code&gt;.gitattributes&lt;/code&gt; 已加入版本控制&lt;/li&gt;
&lt;li&gt;Commit&lt;/li&gt;
&lt;li&gt;Push 到 GitHub
此時大型檔案實際上會存放於 Git LFS Storage，而 Git Repository 僅保留指標檔，因此不會再遇到 GitHub 的 100MB 限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;h3&gt;常見錯誤&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;錯誤訊息&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File exceeds GitHub&apos;s file size limit of 100 MB&lt;/td&gt;
&lt;td&gt;檔案未使用 Git LFS 管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git LFS not installed&lt;/td&gt;
&lt;td&gt;本機尚未安裝 Git LFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push 被拒絕&lt;/td&gt;
&lt;td&gt;Repository 歷史紀錄中仍存在超大檔案&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;實務建議&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新專案可直接設定常用副檔名追蹤規則&lt;/li&gt;
&lt;li&gt;舊專案建議使用 &lt;code&gt;git lfs migrate import&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若已 Push 過超大檔案，需重新改寫歷史紀錄後再 Push&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安裝後初始化
git lfs install

# 將所有超過 100MB 檔案轉換為 LFS
git lfs migrate import--everything--above=100MB

# 追蹤指定副檔名
git lfs track&quot;*.7z&quot;
git lfs track&quot;*.tar.gz&quot;

# 查看目前追蹤規則
git lfs track

# 查看 LFS 管理檔案
git lfs ls-files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;GitHub 的 100MB 限制並不代表大型檔案無法納入版本控制，透過 Git LFS 搭配 TortoiseGit，即可用原本的操作流程管理大型檔案。&lt;/p&gt;
</content:encoded></item><item><title>使用 IdentityServer4 + ASP.Net Identity 初始化專案流程</title><link>https://joyceowo.github.io/posts/23ba78ea09fa81dc9fe8e63d47cb55ad/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa81dc9fe8e63d47cb55ad/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;is4aspid&lt;/code&gt; 範本 + Migration + Seed，一次完成 IdentityServer4 + 使用者系統初始化。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;需要快速建立 OAuth / OpenID Connect Server&lt;/li&gt;
&lt;li&gt;想整合 &lt;a href=&quot;http://asp.net/&quot;&gt;ASP.NET&lt;/a&gt; Identity（帳密登入）&lt;/li&gt;
&lt;li&gt;建立內部驗證服務或 SSO 系統&lt;/li&gt;
&lt;li&gt;測試或學習 IdentityServer 架構&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 安裝範本並建立專案&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;dotnet new&lt;/code&gt; 安裝 IdentityServer4 範本&lt;/li&gt;
&lt;li&gt;建立整合 &lt;a href=&quot;http://asp.net/&quot;&gt;ASP.NET&lt;/a&gt; Identity 的專案（is4aspid）&lt;/li&gt;
&lt;li&gt;為什麼：這個範本已內建登入、使用者管理，不用自己刻&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 安裝範本
dotnet new -i identityserver4.templates

# 建立專案（含 ASP.NET Identity）
dotnet new is4aspid -n IdentityServerAspNetIdentity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用範本簡單對照：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;範本&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;is4empty&lt;/td&gt;
&lt;td&gt;空專案&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is4inmem&lt;/td&gt;
&lt;td&gt;記憶體測試&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is4ef&lt;/td&gt;
&lt;td&gt;DB 儲存設定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is4aspid&lt;/td&gt;
&lt;td&gt;整合帳密登入（推薦）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;2. 建立資料庫（Migration）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新增 Migration（產生資料表結構）&lt;/li&gt;
&lt;li&gt;更新資料庫（建立 Identity + IdentityServer Tables）&lt;/li&gt;
&lt;li&gt;為什麼：IdentityServer 需要 DB 存 client / user / config&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 建立 Migration
dotnet ef migrations add InitialCreate

# 建立資料表
dotnet ef database update
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;3. 初始化 Seed 資料&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;啟動專案時加 &lt;code&gt;/seed&lt;/code&gt; 參數&lt;/li&gt;
&lt;li&gt;自動寫入測試使用者、Client 設定&lt;/li&gt;
&lt;li&gt;為什麼：避免手動建資料，直接可測試登入流程&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dotnet run /seed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程式碼核心：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var seed = args.Contains(&quot;/seed&quot;);

if (seed)
{
    args = args.Except(new[] { &quot;/seed&quot; }).ToArray();
}

var host = CreateHostBuilder(args).Build();

if (seed)
{
    // 初始化資料
    var config = host.Services.GetRequiredService&amp;lt;IConfiguration&amp;gt;();
    var connectionString = config.GetConnectionString(&quot;DefaultConnection&quot;);

    SeedData.EnsureSeedData(connectionString);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;IdentityServer4 已停止維護，正式專案建議評估 Duende（需付費）&lt;/li&gt;
&lt;li&gt;Seed 只適合開發環境，正式環境請用 Migration + Script 控管&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/seed&lt;/code&gt; 設計很方便，但要避免誤用在 Production&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;三步驟搞定驗證系統初始化，重點不是寫程式，是選對範本。
工程上：先能跑，再優化。&lt;/p&gt;
</content:encoded></item><item><title>Outlook 信件關鍵字觸發 Windows 通知？用 Snoretoast 一次搞定</title><link>https://joyceowo.github.io/posts/23ba78ea09fa81a28219e45692344815/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa81a28219e45692344815/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 Outlook VBA + Snoretoast，可針對信件內容/主旨自動觸發 Windows 通知&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;需要即時關注特定信件（報名 / 告警 / 任務通知）&lt;/li&gt;
&lt;li&gt;不想一直盯 Outlook&lt;/li&gt;
&lt;li&gt;有簡單 VBA 經驗的工程師&lt;/li&gt;
&lt;li&gt;想做輕量級通知機制（不引入外部服務）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 設定 Outlook 監聽收件匣&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;綁定 Inbox Items，監聽新信件進來&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;Items_ItemAdd&lt;/code&gt; 事件觸發&lt;/li&gt;
&lt;li&gt;這是整個流程的入口，沒這段就不會自動執行&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Private WithEvents Items As Outlook.Items

Private Sub Application_Startup()
    Dim Ns As Outlook.NameSpace
    Set Ns = Application.GetNamespace(&quot;MAPI&quot;)
    Set Items = Ns.GetDefaultFolder(olFolderInbox).Items
End Sub
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 判斷信件條件 + 抓關鍵內容&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;檢查 Subject / Body 是否包含指定關鍵字&lt;/li&gt;
&lt;li&gt;從信件內抓指定資訊（例如：報名時間）&lt;/li&gt;
&lt;li&gt;避免抓到轉寄內容（直接中斷）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Private Sub Items_ItemAdd(ByVal Item As Object)
    On Error Resume Next
    If TypeOf Item Is Outlook.MailItem Then
        Dim mail As Outlook.MailItem
        Set mail = Item

        Dim signUpLine As String
        signUpLine = GetLineWithKeyword(mail.Body, &quot;報名時間&quot;)

        If InStr(LCase(mail.Subject), &quot;testa&quot;) &amp;gt; 0 And _
           InStr(LCase(mail.Body), &quot;testb&quot;) &amp;gt; 0 Then

            Call ShowToast(Replace(mail.Subject, &quot; &quot;, &quot;&quot;), signUpLine)
        End If
    End If
End Sub
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 呼叫 Snoretoast 顯示通知&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;Shell&lt;/code&gt; 執行外部 exe&lt;/li&gt;
&lt;li&gt;設定標題、內容、顯示時間、圖片&lt;/li&gt;
&lt;li&gt;Snoretoast 是核心推播工具 (&lt;a href=&quot;https://github.com/KDE/snoretoast&quot;&gt;https://github.com/KDE/snoretoast&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Private Sub ShowToast(title As String, message As String)
    Dim cmd As String
    cmd = &quot;D:\\Snoretoast\\snoretoast.exe -appID &quot;&quot;OutlookNotify&quot;&quot; -t &quot;&quot;&quot; &amp;amp; title &amp;amp; &quot;&quot;&quot; -m &quot;&quot;&quot; &amp;amp; message &amp;amp; &quot;&quot;&quot; -d long -p D:\\Snoretoast\\01.jpg &quot;
    Shell cmd, vbHide
End Sub
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 抽取信件關鍵行（避免抓錯內容）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;將信件拆行後逐行搜尋&lt;/li&gt;
&lt;li&gt;遇到轉寄分隔直接停止&lt;/li&gt;
&lt;li&gt;清除空白 / Tab 避免格式干擾&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Private Function GetLineWithKeyword(bodyText As String, keyword As String) As String
    Dim lines() As String
    Dim i As Long
    lines = Split(bodyText, vbCrLf)

    For i = LBound(lines) To UBound(lines)
        If InStr(lines(i), &quot;-----Original Message-----&quot;) &amp;gt; 0 Or _
           InStr(lines(i), &quot;-----轉寄郵件-----&quot;) &amp;gt; 0 Then
            Exit For
        End If

        If InStr(lines(i), keyword) &amp;gt; 0 Then
            GetLineWithKeyword = Trim(lines(i))
            GetLineWithKeyword = Replace(GetLineWithKeyword, &quot; &quot;, &quot;&quot;)
            GetLineWithKeyword = Replace(GetLineWithKeyword, vbTab, &quot;&quot;)
            Exit Function
        End If
    Next i

    GetLineWithKeyword = &quot;(找不到報名時間)&quot;
End Function
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Snoretoast 路徑需正確，否則不會跳通知&lt;/li&gt;
&lt;li&gt;Outlook 需開啟且 VBA 有啟用&lt;/li&gt;
&lt;li&gt;關鍵字建議轉小寫比對，避免大小寫問題&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expend&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;snoretoast.exe -appID &quot;OutlookNotify&quot; -t &quot;Title&quot; -m &quot;Message&quot; -d long -p image.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;這做法很土，但穩、可控，適合內部自動通知場景&lt;/li&gt;
&lt;li&gt;建議把關鍵字抽成設定檔，會更好維護&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>PowerShell 美化教學：oh-my-posh 主題設定快速上手</title><link>https://joyceowo.github.io/posts/23ba78ea09fa813393c1ea6288eb5afa/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa813393c1ea6288eb5afa/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 oh-my-posh + 主題設定，一行指令直接讓 PowerShell 變好看又好用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;想提升 CLI 使用體驗的工程師&lt;/li&gt;
&lt;li&gt;長時間使用 PowerShell / Terminal 的人&lt;/li&gt;
&lt;li&gt;想快速辨識 Git / 路徑 / 狀態資訊&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 安裝 oh-my-posh&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;安裝工具（略過已安裝）&lt;/li&gt;
&lt;li&gt;確認指令可用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;oh-my-posh --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 查詢主題&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;查看內建主題清單&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Get-PoshThemes
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;用來挑喜歡的樣式（建議先用預設測試）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 套用主題&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;初始化 PowerShell + 指定主題&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;oh-my-posh init pwsh --config &quot;$env:POSH_THEMES_PATH\\jandedobbeleer.omp.json&quot; | Invoke-Expression
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;會立即套用樣式（當前 session）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 設定永久生效&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;開啟 Profile&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;notepad $PROFILE
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;加入以下內容&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;oh-my-posh init pwsh --config &quot;$env:POSH_THEMES_PATH\\jandedobbeleer.omp.json&quot; | Invoke-Expression
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;字體需 Nerd Font（否則圖示會亂碼）&lt;/li&gt;
&lt;li&gt;Profile 找不到 → 先執行 &lt;code&gt;$PROFILE&lt;/code&gt; 建立&lt;/li&gt;
&lt;li&gt;主題可自由換（json 檔直接改）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expend&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查詢主題
Get-PoshThemes

# 套用主題
oh-my-posh init pwsh --config &quot;$env:POSH_THEMES_PATH\\jandedobbeleer.omp.json&quot; | Invoke-Expression

# 編輯 Profile
notepad $PROFILE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一行指令，讓你的 CLI 從工地風變成精品店，順眼真的會讓人多寫幾行 code&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>EF Core 查詢忽快忽慢？用 RECOMPILE 解掉 Parameter Sniffing</title><link>https://joyceowo.github.io/posts/23ba78ea09fa8171ba34eeb03351b2a5/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa8171ba34eeb03351b2a5/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;DbCommandInterceptor + TagWith&lt;/code&gt;，即可在 EF Core 查詢加上 &lt;code&gt;OPTION(RECOMPILE)&lt;/code&gt;，快速避開 Parameter Sniffing。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;查詢「同一支 SQL，但速度不穩」的場景&lt;/li&gt;
&lt;li&gt;EF Core 查詢 timeout，但 SSMS 正常&lt;/li&gt;
&lt;li&gt;條件差異大（例如：熱門 vs 冷門資料）&lt;/li&gt;
&lt;li&gt;不想改 SQL，但需要快速解法&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 註冊 Interceptor&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 DbContext 注入自訂 Interceptor&lt;/li&gt;
&lt;li&gt;讓 EF Core 有機會攔截 SQL 並改寫&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;builder.UseSqlServer(connectionString)
    .UseRecompileExtensions(); // 註冊攔截器
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 在查詢標記 RECOMPILE&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;TagWith&lt;/code&gt; 做標記（EF Core 內建）&lt;/li&gt;
&lt;li&gt;讓 Interceptor 知道哪段 SQL 要處理&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var name = &quot;SomeName&quot;;
var result = context.SomeItems
    .Where(x =&amp;gt; x.Name == name)
    .WithRecompile() // 加上標記
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;實際 SQL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT [s].[Id], [s].[Name]
FROM [SomeItems] AS [s]
WHERE [s].[Name] = @__name_0
OPTION(RECOMPILE)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 實作 Interceptor 改寫 SQL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;偵測 Tag&lt;/li&gt;
&lt;li&gt;移除 comment 並補上 &lt;code&gt;OPTION(RECOMPILE)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static class RecompileExtensions
{
    private const string RecompileTag = &quot;recompile_query_tag&quot;;
    private const string RecompileComment = &quot;-- &quot; + RecompileTag + &quot;\\r\\n&quot;;

    public static DbContextOptionsBuilder UseRecompileExtensions(this DbContextOptionsBuilder builder)
    {
        return builder.AddInterceptors(RecompileInterceptor.Instance);
    }

    public static IQueryable&amp;lt;T&amp;gt; WithRecompile&amp;lt;T&amp;gt;(this IQueryable&amp;lt;T&amp;gt; query)
    {
        return query.TagWith(RecompileTag);
    }

    private class RecompileInterceptor : DbCommandInterceptor
    {
        public static RecompileInterceptor Instance = new();

        public override InterceptionResult&amp;lt;DbDataReader&amp;gt; ReaderExecuting(
            DbCommand command,
            CommandEventData eventData,
            InterceptionResult&amp;lt;DbDataReader&amp;gt; result)
        {
            CorrectCommand(command);
            return base.ReaderExecuting(command, eventData, result);
        }

        private static void CorrectCommand(DbCommand command)
        {
            var newQuery = command.CommandText.Replace(RecompileComment, &quot;&quot;);

            if (!ReferenceEquals(newQuery, command.CommandText))
            {
                if (newQuery.StartsWith(&quot;\\r\\n&quot;))
                    newQuery = newQuery.Substring(2);

                newQuery += &quot;\\r\\nOPTION(RECOMPILE)&quot;;
                command.CommandText = newQuery;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RECOMPILE&lt;/code&gt; 每次都重新產生執行計畫，CPU 成本會增加，不適合高頻查詢&lt;/li&gt;
&lt;li&gt;適合「查詢不穩」而不是「查詢本身慢」&lt;/li&gt;
&lt;li&gt;若是全域問題，可考慮 DB 層級關閉 Parameter Sniffing&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ALTER DATABASE SCOPED CONFIGURATION SET PARAMETER_SNIFFING = OFF;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 註冊
builder.UseRecompileExtensions();

// 使用
query.WithRecompile().ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- DB 層級關閉 Parameter Sniffing
ALTER DATABASE SCOPED CONFIGURATION
SET PARAMETER_SNIFFING = OFF;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;當 EF 跟 SQL 表現不一致時，這招通常能快速止血；但長期還是要回頭優化索引與查詢。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;參考來源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/73073436/ef-core-takes-a-lot-of-time-sometimes-to-perform-select-query&quot;&gt;https://stackoverflow.com/questions/73073436/ef-core-takes-a-lot-of-time-sometimes-to-perform-select-query&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ASP.NET Core + OpenIddict 實作筆記：Password Flow 與 Refresh Token</title><link>https://joyceowo.github.io/posts/23ba78ea09fa81dcb655f1a2f1693b47/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa81dcb655f1a2f1693b47/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;這篇整理的是 &lt;strong&gt;OpenIddict 在 &lt;strong&gt;&lt;a href=&quot;http://asp.net/&quot;&gt;&lt;strong&gt;ASP.NET&lt;/strong&gt;&lt;/a&gt;&lt;/strong&gt; Core 專案中的最小可用建置流程&lt;/strong&gt;，包含套件安裝、資料表建立、&lt;code&gt;/connect/token&lt;/code&gt; 實作、Refresh Token 流程，以及 Azure 上常見的憑證權限坑。&lt;/p&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;要自己做登入授權中心的 .NET 專案&lt;/li&gt;
&lt;li&gt;需要發 &lt;code&gt;access token&lt;/code&gt; / &lt;code&gt;refresh token&lt;/code&gt; 的 API 系統&lt;/li&gt;
&lt;li&gt;已有 &lt;a href=&quot;http://asp.net/&quot;&gt;ASP.NET&lt;/a&gt; Core Identity，想接 OpenIddict&lt;/li&gt;
&lt;li&gt;部署到 Azure App Service，遇到憑證或權限問題的人&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 先把資料層建起來&lt;/h3&gt;
&lt;p&gt;先安裝需要的套件，最少會用到 &lt;code&gt;Microsoft.EntityFrameworkCore.SqlServer&lt;/code&gt;、&lt;code&gt;Microsoft.EntityFrameworkCore.Tools&lt;/code&gt;、&lt;code&gt;OpenIddict.AspNetCore&lt;/code&gt;、&lt;code&gt;OpenIddict.EntityFrameworkCore&lt;/code&gt;。接著用 Migration 產生 Schema，這一步的重點不是花俏，而是先讓 OpenIddict 需要的資料表真的落地，不然後面 Token 流程再漂亮也只是空中樓閣。實作上可直接跑 EF Migration，若想先看 SQL，也能先輸出 script 再交給 DBA 檢查。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Visual Studio Package Manager Console
Add-Migration InitialCreate
Update-Database

# DotNet CLI
dotnet ef migrations add InitialCreate
dotnet ef database update

# 產生 SQL 腳本
dotnet ef migrations script InitialCreate --context DefaultDbContext
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 在 Startup 註冊 OpenIddict 與 Identity&lt;/h3&gt;
&lt;p&gt;這一步要做的是把 &lt;code&gt;DbContext&lt;/code&gt;、Identity、OpenIddict Core / Server / Validation 串起來。重點有三個：一是 &lt;code&gt;options.UseOpenIddict()&lt;/code&gt; 要掛進 &lt;code&gt;DbContext&lt;/code&gt;；二是 Server 端要開 &lt;code&gt;/connect/token&lt;/code&gt; 並允許 &lt;code&gt;PasswordFlow&lt;/code&gt;、&lt;code&gt;RefreshTokenFlow&lt;/code&gt;；三是 Token 的壽命、Scope、簽章憑證要先定清楚。若你只是先在開發機測通流程，可以先用 &lt;code&gt;AddDevelopmentEncryptionCertificate()&lt;/code&gt; 與 &lt;code&gt;AddDevelopmentSigningCertificate()&lt;/code&gt;，但正式環境別偷懶，這種東西混過去，日後都會回來找你 (´-ω-`)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddDbContext&amp;lt;DefaultDbContext&amp;gt;(options =&amp;gt;
{
    options.UseSqlServer(Configuration.GetConnectionString(&quot;DefaultConnection&quot;));
    options.UseOpenIddict(); // 讓 OpenIddict 使用 EF Core
});

services.AddOpenIddict()
    .AddCore(options =&amp;gt;
    {
        options.UseEntityFrameworkCore()
            .UseDbContext&amp;lt;DefaultDbContext&amp;gt;();
    })
    .AddServer(options =&amp;gt;
    {
        options.SetTokenEndpointUris(&quot;/connect/token&quot;);
        options.SetUserinfoEndpointUris(&quot;/connect/userinfo&quot;);

        options.AllowPasswordFlow();
        options.AllowRefreshTokenFlow();
        options.AllowCustomFlow(&quot;custom_flow_name&quot;);

        options.UseReferenceAccessTokens();
        options.UseReferenceRefreshTokens();

        options.RegisterScopes(
            OpenIddictConstants.Permissions.Scopes.Email,
            OpenIddictConstants.Permissions.Scopes.Profile,
            OpenIddictConstants.Permissions.Scopes.Roles);

        options.SetAccessTokenLifetime(TimeSpan.FromMinutes(30));
        options.SetRefreshTokenLifetime(TimeSpan.FromDays(7));

        options.AddDevelopmentEncryptionCertificate()
               .AddDevelopmentSigningCertificate();

        options.UseAspNetCore()
               .EnableTokenEndpointPassthrough();
    })
    .AddValidation(options =&amp;gt;
    {
        options.UseLocalServer();
        options.UseAspNetCore();
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 建立測試 Client 與使用者，先把登入鏈路打通&lt;/h3&gt;
&lt;p&gt;在開發環境先自動建立一組 Client 與管理者帳號，能大幅減少初始化時的手工操作。這段的目的很單純：確認你的資料表、Identity、OpenIddict 三者真的接起來，而不是每次測試都在懷疑到底是帳號錯、Client 沒建、還是授權配置有洞。這裡會建立 &lt;code&gt;default-client&lt;/code&gt;，並產出一個 admin 使用者，密碼先用 &lt;code&gt;PasswordHasher&lt;/code&gt; 轉成雜湊後存入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#if DEBUG
CreateCientAndUser(app);
#endif

private static void CreateCientAndUser(IApplicationBuilder app)
{
    using var scope = app.ApplicationServices
        .GetRequiredService&amp;lt;IServiceScopeFactory&amp;gt;()
        .CreateScope();

    var context = scope.ServiceProvider.GetRequiredService&amp;lt;DefaultDbContext&amp;gt;();
    context.Database.EnsureCreated();

    var manager = scope.ServiceProvider.GetRequiredService&amp;lt;IOpenIddictApplicationManager&amp;gt;();
    var existingClientApp = manager.FindByClientIdAsync(&quot;default-client&quot;).GetAwaiter().GetResult();

    if (existingClientApp == null)
    {
        manager.CreateAsync(new OpenIddictApplicationDescriptor
        {
            ClientId = &quot;default-client&quot;,
            ClientSecret = &quot;your-client-secret&quot;,
            DisplayName = &quot;Default client application&quot;,
            Permissions =
            {
                OpenIddictConstants.Permissions.Endpoints.Token,
                OpenIddictConstants.Permissions.GrantTypes.Password,
                OpenIddictConstants.Permissions.GrantTypes.RefreshToken
            }
        }).GetAwaiter().GetResult();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 實作 &lt;code&gt;/connect/token&lt;/code&gt;，把 Password 與 Refresh Token 都接上&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/connect/token&lt;/code&gt; 是整個流程的核心入口，收到請求後先判斷 Grant Type，再分流到不同處理方法。Password Grant 的做法是：找到使用者、驗證密碼、建立 &lt;code&gt;ClaimsIdentity&lt;/code&gt;、加上角色與必要 Scope，最後回傳 &lt;code&gt;SignIn(...)&lt;/code&gt; 讓 OpenIddict 幫你發 Token。Refresh Token 則簡單很多，直接從目前驗證內容取出 &lt;code&gt;ClaimsPrincipal&lt;/code&gt; 再重新簽發即可。白話講，前者是「靠帳密換票」，後者是「拿舊票換新票」。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[HttpPost(&quot;~/connect/token&quot;)]
[AllowAnonymous]
public async Task&amp;lt;IActionResult&amp;gt; Exchange()
{
    var oidcRequest = HttpContext.GetOpenIddictServerRequest();

    if (oidcRequest.IsPasswordGrantType())
        return await TokensForPasswordGrantType(oidcRequest);

    if (oidcRequest.IsRefreshTokenGrantType())
        return await TokenForRefreshGrantType();

    return BadRequest(new OpenIddictResponse
    {
        Error = OpenIddictConstants.Errors.UnsupportedGrantType
    });
}

private async Task&amp;lt;IActionResult&amp;gt; TokensForPasswordGrantType(OpenIddictRequest request)
{
    var user = await _userManager.FindByNameAsync(request.Username);
    if (user == null)
        return Unauthorized();

    var signInResult = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
    if (!signInResult.Succeeded)
        return Unauthorized();

    var identity = new ClaimsIdentity(
        TokenValidationParameters.DefaultAuthenticationType,
        OpenIddictConstants.Claims.Name,
        OpenIddictConstants.Claims.Role);

    identity.AddClaim(OpenIddictConstants.Claims.Subject, user.Id.ToString(), OpenIddictConstants.Destinations.AccessToken);
    identity.AddClaim(OpenIddictConstants.Claims.Username, user.Username, OpenIddictConstants.Destinations.AccessToken);

    foreach (var userRole in user.UserRoles)
    {
        identity.AddClaim(OpenIddictConstants.Claims.Role, userRole.Role.NormalizedName, OpenIddictConstants.Destinations.AccessToken);
    }

    var principal = new ClaimsPrincipal(identity);
    principal.SetScopes(new[]
    {
        OpenIddictConstants.Scopes.Roles,
        OpenIddictConstants.Scopes.OfflineAccess,
        OpenIddictConstants.Scopes.Email,
        OpenIddictConstants.Scopes.Profile
    });

    return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}

private async Task&amp;lt;IActionResult&amp;gt; TokenForRefreshGrantType()
{
    var principal = (await HttpContext.AuthenticateAsync(
        OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;

    return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Azure App Service 若遇到 &lt;code&gt;WindowsCryptographicException: Access is denied.&lt;/code&gt;，可先補上 &lt;code&gt;WEBSITE_LOAD_USER_PROFILE=1&lt;/code&gt;，這通常是在載入開發憑證時踩到的坑。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UseReferenceAccessTokens()&lt;/code&gt; / &lt;code&gt;UseReferenceRefreshTokens()&lt;/code&gt; 代表實際 Token 內容存在資料庫，外部拿到的是參照值，方便控管，但也更依賴資料庫穩定性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Password Flow&lt;/code&gt; 雖然實作快，但通常只適合受控系統或內部整合；若是對外產品，授權流程要再多想一步，別只求先能跑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expand&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安裝後執行 Migration
Add-Migration InitialCreate
Update-Database

dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet ef migrations script InitialCreate --context DefaultDbContext
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;POST /connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=password
client_id=default-client
client_secret=your-client-secret
username=admin
password=your-password
scope=offline_access profile email roles
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;POST /connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
client_id=default-client
client_secret=your-client-secret
refresh_token=your-refresh-token
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;OpenIddict 真的不難，難的是一開始把資料表、授權設定、Token 流程一次接對；先求最小可用，再慢慢補安全與正式環境配置，這才是正常活路。&lt;/p&gt;
&lt;h2&gt;參考來源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OpenIddict官方文件&lt;/strong&gt;
&lt;a href=&quot;https://github.com/openiddict/openiddict-core&quot;&gt;https://github.com/openiddict/openiddict-core&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在 .NET 5 中使用 OpenIddict 設置令牌身份驗證&lt;/strong&gt;
&lt;a href=&quot;https://nwb.one/blog/openid-connect-dotnet-5&quot;&gt;https://nwb.one/blog/openid-connect-dotnet-5&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用 OpenIddict 建立一個支援 Client Credentials Grant 的 Authentication Server 來保護你的 Web API&lt;/strong&gt;
&lt;a href=&quot;https://wezmag.github.io/posts/protect-api-with-client-credentials-grant-using-openiddict-part-1/&quot;&gt;https://wezmag.github.io/posts/protect-api-with-client-credentials-grant-using-openiddict-part-1/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用 OpenIddict 設置授權服務器 - 第 VI 部分 - 刷新令牌&lt;/strong&gt;
&lt;a href=&quot;https://dev.to/robinvanderknaap/setting-up-an-authorization-server-with-openiddict-part-vi-refresh-tokens-5669&quot;&gt;https://dev.to/robinvanderknaap/setting-up-an-authorization-server-with-openiddict-part-vi-refresh-tokens-5669&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OpenIddict詳細流程&lt;/strong&gt;
&lt;a href=&quot;https://www.cnblogs.com/liyouming/p/15772403.html&quot;&gt;https://www.cnblogs.com/liyouming/p/15772403.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Robocopy 教學：強大又穩定的備份神器</title><link>https://joyceowo.github.io/posts/23ba78ea09fa81e7a124eac97b80afce/</link><guid isPermaLink="true">https://joyceowo.github.io/posts/23ba78ea09fa81e7a124eac97b80afce/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用一條 Robocopy 指令即可完成「鏡像備份＋排除＋Log＋加速」，適合日常自動化備份。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;適合用在哪裡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;定期備份（每日 / CI 前備份）&lt;/li&gt;
&lt;li&gt;大量檔案同步（專案資料夾）&lt;/li&gt;
&lt;li&gt;需要排除 node_modules / bin 等暫存資料&lt;/li&gt;
&lt;li&gt;需要保留權限與時間戳的情境&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程步驟&lt;/h2&gt;
&lt;h3&gt;1. 建立基礎備份指令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 robocopy 指定來源與目的地，並加上 mirror 模式同步整體資料&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mir&lt;/code&gt; 會讓目的地完全等同來源（包含刪除），適合「真正備份」而非單純 copy&lt;/li&gt;
&lt;li&gt;指令如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;robocopy &quot;來源資料夾&quot; &quot;目的地資料夾&quot; /mir
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 加入排除與效能優化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;排除不需要的檔案（如 .bak、.scc）與資料夾（bin、obj、node_modules）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/MT:100&lt;/code&gt; 開啟多執行緒加速，大量檔案時差很多&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/xj&lt;/code&gt; 避免 Junction 無限遞迴（常見在 node_modules）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;robocopy &quot;來源資料夾&quot; &quot;目的地資料夾&quot; /mir /xj /MT:100 /xf *.scc *.bak ^
  /xd &quot;packages&quot; &quot;bin&quot; &quot;obj&quot; &quot;node_modules&quot; &quot;.vs&quot; &quot;TempFile&quot; &quot;Data&quot; &quot;history&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 控制同步範圍＋輸出 Log&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/maxage:7&lt;/code&gt; 限制只同步最近 7 天變更，減少不必要 I/O&lt;/li&gt;
&lt;li&gt;將結果輸出到 log，方便排錯或稽核&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;robocopy &quot;來源資料夾&quot; &quot;目的地資料夾&quot; /mir /xj /MT:100 /xf *.scc *.bak ^
  /xd &quot;packages&quot; &quot;bin&quot; &quot;obj&quot; &quot;node_modules&quot; &quot;.vs&quot; &quot;TempFile&quot; &quot;Data&quot; &quot;history&quot; ^
  /maxage:7 &amp;gt; .\\Log\\%dd%.log
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;補充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/mir&lt;/code&gt; 會刪除目的地多餘檔案，誤用會直接「幫你清空」&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/MT&lt;/code&gt; 雖然快，但過高可能吃滿 CPU 或 IO，建議 16～64 起測&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%DATE%&lt;/code&gt; 格式會受系統語系影響，跨機器可能要調整&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指令 / 範例整理&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Click to expend&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:: 建立 Log 資料夾
if not exist &quot;.\\Log&quot; mkdir &quot;.\\Log&quot;

:: 產生日期（YYYYMMDD）
set dd=%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%

:: 完整備份指令
robocopy &quot;來源資料夾&quot; &quot;目的地資料夾&quot; /mir /xj /MT:100 /xf *.scc *.bak ^
  /xd &quot;packages&quot; &quot;bin&quot; &quot;obj&quot; &quot;node_modules&quot; &quot;.vs&quot; &quot;TempFile&quot; &quot;Data&quot; &quot;history&quot; ^
  /maxage:7 &amp;gt; .\\Log\\%dd%.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Robocopy 不難，難的是「參數組合」，這套基本夠打 90% 備份場景。&lt;/li&gt;
&lt;li&gt;建議先在測試資料夾跑一次，確認 &lt;code&gt;/mir&lt;/code&gt; 行為再上正式環境。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>