@elf-express/admin-net-mcp 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.js +83 -88
  2. package/knowledge/Furion_Teaching_Manual/04-1-/351/205/215/347/275/256.md +442 -0
  3. package/knowledge/Furion_Teaching_Manual/04-2-/351/201/270/351/240/205.md +363 -0
  4. package/knowledge/Furion_Teaching_Manual/05-1-/345/213/225/346/205/213WebAPI.md +825 -0
  5. package/knowledge/Furion_Teaching_Manual/05-2-HttpContext.md +217 -0
  6. package/knowledge/Furion_Teaching_Manual/05-3-/347/257/251/351/201/270/345/231/250/346/224/224/346/210/252/345/231/250AOP.md +581 -0
  7. package/knowledge/Furion_Teaching_Manual/05-4-/350/253/213/346/261/202/347/250/275/346/240/270/346/227/245/350/252/214.md +129 -0
  8. package/knowledge/Furion_Teaching_Manual/05-5-/344/270/255/344/273/213/350/273/237/351/253/224Middleware.md +328 -0
  9. package/knowledge/Furion_Teaching_Manual/05-6-Vue-React-Angular/344/273/213/351/235/242/344/273/243/347/220/206.md +317 -0
  10. package/knowledge/Furion_Teaching_Manual/06-1/350/246/217/347/257/204/345/214/226/346/216/245/345/217/243.md +1458 -0
  11. package/knowledge/Furion_Teaching_Manual/06-2/347/254/254/344/270/211/346/226/271API_Scalar.md +91 -0
  12. package/knowledge/Furion_Teaching_Manual/07-/345/217/213/345/245/275/344/276/213/345/244/226/350/231/225/347/220/206.md +511 -0
  13. package/knowledge/Furion_Teaching_Manual/08-1-/350/263/207/346/226/231/351/251/227/350/255/211/345/237/272/347/244/216/344/275/277/347/224/250.md +587 -0
  14. package/knowledge/Furion_Teaching_Manual/10-1-SqlSugar/346/225/264/345/220/210.md +336 -0
  15. package/knowledge/Furion_Teaching_Manual/11-SaaS /345/244/232/347/247/237/346/210/266/347/255/206/350/250/230.md" +271 -0
  16. package/knowledge/Furion_Teaching_Manual/12-furion-dependency-injection.md +408 -0
  17. package/knowledge/Furion_Teaching_Manual/13-/347/211/251/344/273/266/350/263/207/346/226/231/346/230/240/345/260/204/357/274/210Mapster/357/274/211.md +162 -0
  18. package/knowledge/Furion_Teaching_Manual/14-/345/210/206/345/270/203/345/274/217/347/274/223/345/255/230.md +311 -0
  19. package/knowledge/Furion_Teaching_Manual/15-/345/256/211/345/205/250/351/211/264/346/235/203.md +832 -0
  20. package/knowledge/Furion_Teaching_Manual/17-/346/252/242/350/246/226/347/257/204/346/234/254/345/274/225/346/223/216.md +327 -0
  21. package/knowledge/Furion_Teaching_Manual/18-/346/227/245/350/252/214/350/250/230/351/214/204.md +639 -0
  22. package/knowledge/Furion_Teaching_Manual/19-1-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/345/237/272/347/244/216/344/275/277/347/224/250.md +621 -0
  23. package/knowledge/Furion_Teaching_Manual/19-2-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/351/200/262/351/232/216/346/214/207/345/215/227.md +928 -0
  24. package/knowledge/Furion_Teaching_Manual/19-3-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/345/270/270/350/246/213/345/225/217/351/241/214.md +362 -0
  25. package/knowledge/Furion_Teaching_Manual/20-/350/263/207/346/226/231/345/212/240/350/247/243/345/257/206.md +286 -0
  26. package/knowledge/Furion_Teaching_Manual/20-/351/231/204/351/214/204-KSort/350/263/207/346/226/231/347/260/275/345/220/215/345/256/214/346/225/264/345/216/237/345/247/213/347/242/274.md +305 -0
  27. package/knowledge/Furion_Teaching_Manual/21-/345/205/250/347/220/203/345/214/226/345/222/214/346/234/254/345/234/260/345/214/226.md +342 -0
  28. package/knowledge/Furion_Teaching_Manual/22-/344/272/213/344/273/266/345/214/257/346/265/201/346/216/222EventBus.md +448 -0
  29. package/knowledge/Furion_Teaching_Manual/23-JSON/345/272/217/345/210/227/345/214/226.md +340 -0
  30. package/knowledge/Furion_Teaching_Manual/24-/345/215/263/346/231/202/351/200/232/350/250/212SignalR.md +247 -0
  31. package/knowledge/Furion_Teaching_Manual/25-/350/274/224/345/212/251/350/247/222/350/211/262/346/234/215/345/213/231WorkerService.md +295 -0
  32. package/knowledge/Furion_Teaching_Manual/26-1-/346/216/222/347/250/213/344/275/234/346/245/255/345/256/232/346/231/202/344/273/273/345/213/231.md +631 -0
  33. package/knowledge/Furion_Teaching_Manual/26-2-Cron/350/241/250/351/201/224/345/274/217.md +203 -0
  34. package/knowledge/Furion_Teaching_Manual/26-3-/344/273/273/345/213/231/344/275/207/345/210/227TaskQueue.md +215 -0
  35. package/knowledge/Furion_Teaching_Manual/27-/345/210/206/346/225/243/345/274/217ID/347/224/237/346/210/220.md +86 -0
  36. package/knowledge/Furion_Teaching_Manual/28-/346/250/241/347/265/204/345/214/226/351/226/213/347/231/274.md +68 -0
  37. package/knowledge/Furion_Teaching_Manual/29-/346/265/201/350/256/212/347/211/251/344/273/266Clay.md +313 -0
  38. package/knowledge/Furion_Teaching_Manual/30-/350/204/253/346/225/217/350/231/225/347/220/206/357/274/210Sensitive Detection).md" +168 -0
  39. package/knowledge/Furion_Teaching_Manual/32-/346/234/203/350/251/261/345/222/214/347/213/200/346/205/213/347/256/241/347/220/206.md +147 -0
  40. package/knowledge/Furion_Teaching_Manual/33-IPC/347/250/213/345/272/217/351/200/232/350/250/212.md +98 -0
  41. package/knowledge/Furion_Teaching_Manual/34-2-Docker/351/203/250/347/275/262.md +313 -0
  42. package/knowledge/Furion_Teaching_Manual/34-3-Nginx/351/203/250/347/275/262.md +157 -0
  43. package/knowledge/Furion_Teaching_Manual/34-8-WindowsService/351/203/250/347/275/262.md +112 -0
  44. package/knowledge/Furion_Teaching_Manual/35-1-Docker/347/222/260/345/242/203/346/214/201/347/272/214/351/203/250/347/275/262Jenkins.md +169 -0
  45. package/knowledge/Furion_Teaching_Manual/36-1-/345/226/256/345/205/203/346/270/254/350/251/246/346/225/264/345/220/210/346/270/254/350/251/246.md +275 -0
  46. package/knowledge/Furion_Teaching_Manual/36-3-/345/237/272/346/272/226/346/270/254/350/251/246Benchmarking.md +80 -0
  47. package/knowledge/attributes.md +153 -0
  48. package/knowledge/config.md +147 -0
  49. package/knowledge/entity.md +115 -0
  50. package/knowledge/event.md +124 -0
  51. package/knowledge/plugin.md +136 -0
  52. package/knowledge/service.md +146 -0
  53. package/knowledge/sqlsugar.md +172 -0
  54. package/knowledge/vue-typescript.md +338 -0
  55. package/package.json +3 -2
@@ -0,0 +1,631 @@
1
+ # 26.1 排程作業(定時任務)
2
+
3
+ > **版本說明**:僅適用於 Furion 4.8.0+(Sundial)。Furion 已內建此功能;非 Furion 框架可安裝 `Sundial` 或 `Sundial.Dashboard`。
4
+
5
+ ---
6
+
7
+ ## 26.1.1 概述
8
+
9
+ 排程作業(定時任務)是指在預設時間點或按設定週期自動觸發執行的任務。應用場景包括:資料備份、定時發送郵件/簡訊、報表生成、資料採集、DevOps 定時發佈等。
10
+
11
+ ---
12
+
13
+ ## 26.1.2 快速入門
14
+
15
+ ### 定義作業處理程式
16
+
17
+ ```csharp
18
+ public class MyJob : IJob
19
+ {
20
+ private readonly ILogger<MyJob> _logger;
21
+ public MyJob(ILogger<MyJob> logger) => _logger = logger;
22
+
23
+ public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
24
+ {
25
+ _logger.LogInformation($"{context}");
26
+ return Task.CompletedTask;
27
+ }
28
+ }
29
+ ```
30
+
31
+ ### 註冊 Schedule 服務
32
+
33
+ ```csharp
34
+ services.AddSchedule(options =>
35
+ {
36
+ options.AddJob<MyJob>(Triggers.Secondly()); // 每秒執行
37
+ options.AddJob<MyJob>("myjob", Triggers.Secondly()); // 指定作業 Id
38
+ });
39
+ ```
40
+
41
+ ### 多個觸發器
42
+
43
+ ```csharp
44
+ options.AddJob<MyJob>(
45
+ Triggers.Minutely(),
46
+ Triggers.PeriodSeconds(5),
47
+ Triggers.Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds));
48
+ ```
49
+
50
+ ### 串行執行
51
+
52
+ ```csharp
53
+ options.AddJob<MyJob>(concurrent: false, Triggers.Secondly());
54
+ ```
55
+
56
+ > 串行模式下,若上次未完成則跳過本次觸發。可透過 `options.LogEnabled = false` 關閉跳過警告日誌。
57
+
58
+ ### HTTP 請求作業(4.8.7.7+)
59
+
60
+ ```csharp
61
+ options.AddHttpJob(request =>
62
+ {
63
+ request.RequestUri = "https://www.chinadot.net";
64
+ request.HttpMethod = HttpMethod.Get;
65
+ // request.Body = "{}";
66
+ // request.Headers.Add("framework", "Furion");
67
+ // request.Timeout = 100_000;
68
+ // request.PrintResponseContent = true;
69
+ }, Triggers.PeriodSeconds(5));
70
+ ```
71
+
72
+ ### 委託方式(動態)作業
73
+
74
+ ```csharp
75
+ options.AddJob((context, stoppingToken) =>
76
+ {
77
+ context.ServiceProvider.GetLogger().LogInformation($"{context}");
78
+ return Task.CompletedTask;
79
+ }, Triggers.PeriodSeconds(5));
80
+ ```
81
+
82
+ ### 臨時性作業(4.9.7.67+)
83
+
84
+ ```csharp
85
+ options.AddJob<MyJob>(builder => builder.SetTemporary(), Triggers.PeriodMinutes(5));
86
+ ```
87
+
88
+ ### 設定作業組名稱(4.9.2.9+)
89
+
90
+ ```csharp
91
+ options.GroupSet("group1", () =>
92
+ {
93
+ options.AddJob<MyJob>(Triggers.Secondly());
94
+ options.AddJob<MyJob>(Triggers.Hourly());
95
+ });
96
+ ```
97
+
98
+ ### 觸發器特性掃描
99
+
100
+ ```csharp
101
+ options.AddJob(JobBuilder.Create<MyJob>().SetIncludeAnnotations(true));
102
+ // 或批次掃描
103
+ options.AddJob(App.EffectiveTypes.ScanToBuilders());
104
+ ```
105
+
106
+ ```csharp
107
+ [Minutely]
108
+ [PeriodSeconds(5)]
109
+ [Cron("*/3 * * * * *", CronStringFormat.WithSeconds)]
110
+ public class MyJob : IJob { }
111
+ ```
112
+
113
+ ### 執行階段動態操作
114
+
115
+ ```csharp
116
+ // 注入 ISchedulerFactory
117
+ _schedulerFactory.AddJob<MyJob>("動態作業 Id", Triggers.Secondly());
118
+ ```
119
+
120
+ ### 非 DI 環境使用(4.8.8.5+)
121
+
122
+ ```csharp
123
+ await new ServiceCollection()
124
+ .AddSchedule(options => options.AddJob<MyJob>(Triggers.Period(5000)))
125
+ .GetScheduleHostedService()
126
+ .StartAsync(new CancellationTokenSource().Token);
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 26.1.3 作業資訊 JobDetail
132
+
133
+ ### 屬性
134
+
135
+ | 屬性 | 型別 | 預設值 | 說明 |
136
+ |------|------|--------|------|
137
+ | `JobId` | `string` | — | 作業 Id |
138
+ | `GroupName` | `string` | — | 作業組名稱 |
139
+ | `JobType` | `string` | — | 處理程式型別 FullName |
140
+ | `AssemblyName` | `string` | — | 程式集 Name |
141
+ | `Description` | `string` | — | 描述 |
142
+ | `Concurrent` | `bool` | `true` | 並行(true)/ 串行(false) |
143
+ | `IncludeAnnotations` | `bool` | `false` | 是否掃描觸發器特性 |
144
+ | `Properties` | `string` | `"{}"` | 額外資料 |
145
+ | `UpdatedTime` | `DateTime?` | — | 更新時間 |
146
+
147
+ ### JobBuilder 建立方式
148
+
149
+ ```csharp
150
+ JobBuilder.Create<MyJob>();
151
+ JobBuilder.Create("job1");
152
+ JobBuilder.Create("YourProject", "YourProject.MyJob");
153
+ JobBuilder.Create((context, stoppingToken) => { return Task.CompletedTask; });
154
+ JobBuilder.From(jobDetail); // 從執行階段建立
155
+ JobBuilder.From(jsonString); // 從 JSON 建立
156
+ JobBuilder.Clone(fromJobBuilder); // 克隆
157
+ ```
158
+
159
+ ### JobBuilder 設定方法
160
+
161
+ `.SetJobId()` / `.SetGroupName()` / `.SetJobType<T>()` / `.SetDescription()` / `.SetConcurrent()` / `.SetIncludeAnnotations()` / `.SetProperties()` / `.SetDynamicExecuteAsync()` / `.SetTemporary()` / `.LoadFrom()`
162
+
163
+ ### 額外資料操作
164
+
165
+ ```csharp
166
+ jobDetail.GetProperty<int>("count");
167
+ jobDetail.AddOrUpdateProperty("count", count + 1);
168
+ jobDetail.RemoveProperty("key");
169
+ jobDetail.ClearProperties();
170
+ jobDetail.ContainsProperty("key"); // 4.9.2.32+
171
+ ```
172
+
173
+ > 額外資料值僅支援 `int32`、`int64`、`string`、`bool`、`null` 或其陣列。
174
+
175
+ ### 輸出格式
176
+
177
+ ```csharp
178
+ jobDetail.ConvertToJSON();
179
+ jobDetail.ConvertToSQL("tableName", PersistenceBehavior.Appended, NamingConventions.CamelCase);
180
+ jobDetail.ConvertToMonitor();
181
+ jobDetail.ToString();
182
+ ```
183
+
184
+ ### 啟用自動日誌(4.8.3.7+)
185
+
186
+ ```csharp
187
+ options.JobDetail.LogEnabled = true;
188
+ ```
189
+
190
+ ---
191
+
192
+ ## 26.1.4 作業處理程式 IJob
193
+
194
+ ### JobExecutingContext 上下文
195
+
196
+ | 屬性 | 說明 |
197
+ |------|------|
198
+ | `JobId` | 作業 Id |
199
+ | `TriggerId` | 觸發器 Id |
200
+ | `JobDetail` | 作業資訊 |
201
+ | `Trigger` | 觸發器 |
202
+ | `OccurrenceTime` | 計畫觸發時間(最準確) |
203
+ | `ExecutingTime` | 實際執行時間 |
204
+ | `RunId` | 本次執行唯一 Id(4.8.5.1+) |
205
+ | `Result` | 設定/讀取執行結果(4.8.7.7+) |
206
+ | `ServiceProvider` | 服務提供器(4.8.7.10+) |
207
+ | `Mode` | 觸發方式:定時/手動(4.9.3.1+) |
208
+
209
+ ### 作業處理程式實例(4.8.8.13+)
210
+
211
+ 預設每次觸發都建立新實例。推薦高頻作業註冊為**單例**:
212
+
213
+ ```csharp
214
+ services.AddSingleton<MyJob>();
215
+ ```
216
+
217
+ 自訂工廠:
218
+
219
+ ```csharp
220
+ public class JobFactory : IJobFactory
221
+ {
222
+ public IJob CreateJob(IServiceProvider sp, JobFactoryContext ctx)
223
+ => ActivatorUtilities.GetServiceOrCreateInstance(sp, ctx.JobType) as IJob;
224
+ }
225
+
226
+ options.AddJobFactory<JobFactory>();
227
+ ```
228
+
229
+ ### 異常處理與重試
230
+
231
+ ```csharp
232
+ options.AddJob<MyJob>(Triggers.PeriodSeconds(3).SetNumRetries(3));
233
+ ```
234
+
235
+ > 異常僅影響當次觸發,不影響下次執行。推薦使用作業執行器 `IJobExecutor` 設定全域重試策略。
236
+
237
+ ### 異常回退策略(4.8.8.6+)
238
+
239
+ ```csharp
240
+ public class TestJob : IJob
241
+ {
242
+ public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) { throw new Exception(); }
243
+ public Task FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken)
244
+ {
245
+ Console.WriteLine("回退被呼叫");
246
+ return Task.CompletedTask;
247
+ }
248
+ }
249
+ ```
250
+
251
+ ### 上下文資料(4.9.7.82+)
252
+
253
+ ```csharp
254
+ public IDictionary<string, object> GetContextData()
255
+ => new Dictionary<string, object> { { "db", _dbContext } };
256
+
257
+ // 在監視器/執行器中取得
258
+ var db = context.GetItem<IDbContext>("db");
259
+ ```
260
+
261
+ ### Roslyn 動態建立(4.8.8.7+)
262
+
263
+ ```csharp
264
+ var jobAssembly = Schedular.CompileCSharpClassCode(@"/* C# 程式碼 */");
265
+ var jobType = jobAssembly.GetType("YourProject.MyJob");
266
+ _schedulerFactory.AddJob(jobType, Triggers.PeriodSeconds(5));
267
+ ```
268
+
269
+ ---
270
+
271
+ ## 26.1.5 作業觸發器 Trigger
272
+
273
+ ### 屬性
274
+
275
+ | 屬性 | 型別 | 預設值 | 說明 |
276
+ |------|------|--------|------|
277
+ | `TriggerId` | `string` | — | 觸發器 Id |
278
+ | `JobId` | `string` | — | 作業 Id |
279
+ | `TriggerType` / `AssemblyName` | `string` | — | 觸發器型別 |
280
+ | `Args` | `string` | — | 初始化參數(`object[]` 序列化) |
281
+ | `Status` | `TriggerStatus` | `Ready` | 觸發器狀態 |
282
+ | `StartTime` / `EndTime` | `DateTime?` | — | 起止時間 |
283
+ | `LastRunTime` / `NextRunTime` | `DateTime?` | — | 上次/下次執行時間 |
284
+ | `NumberOfRuns` / `MaxNumberOfRuns` | `long` | `0` | 觸發次數 / 最大次數(0=不限) |
285
+ | `NumberOfErrors` / `MaxNumberOfErrors` | `long` | `0` | 出錯次數 / 最大出錯(0=不限) |
286
+ | `NumRetries` | `int` | `0` | 重試次數 |
287
+ | `RetryTimeout` | `int` | `1000` | 重試間隔(ms) |
288
+ | `StartNow` | `bool` | `true` | 是否立即啟動 |
289
+ | `RunOnStart` | `bool` | `false` | 啟動時執行一次 |
290
+ | `ResetOnlyOnce` | `bool` | `true` | 重啟時重置僅一次的作業 |
291
+ | `Result` | `string` | — | 執行結果(4.8.7.7+) |
292
+ | `ElapsedTime` | `long` | `0` | 執行耗時 ms(4.8.7.7+) |
293
+
294
+ ### TriggerStatus 列舉
295
+
296
+ `Backlog(0)` / `Ready(1)` / `Running(2)` / `Pause(3)` / `Blocked(4)` / `ErrorToReady(5)` / `Archived(6)` / `Panic(7)` / `Overrun(8)` / `Unoccupied(9)` / `NotStart(10)` / `Unknown(11)` / `Unhandled(12)`
297
+
298
+ ### 內建觸發器速查(Triggers 靜態類)
299
+
300
+ | 方法 | 說明 |
301
+ |------|------|
302
+ | `Period(ms)` / `PeriodSeconds(n)` / `PeriodMinutes(n)` / `PeriodHours(n)` | 間隔觸發 |
303
+ | `Cron(expr, format)` | Cron 表達式 |
304
+ | `Secondly()` / `Minutely()` / `Hourly()` / `Daily()` / `Monthly()` / `Weekly()` / `Yearly()` / `Workday()` | 週期觸發 |
305
+ | `Secondly(n)` / `Minutely(n)` / `Hourly(n)` | 間隔 n 秒/分/時(4.9.7.224+) |
306
+ | `SecondlyAt(3,5,6)` / `MinutelyAt(3)` / `HourlyAt(3)` / `DailyAt(3)` / `MonthlyAt(3)` / `WeeklyAt(3,"FRI")` / `YearlyAt(3,"MAR")` | Macro At 方式 |
307
+ | `At("2025-11-26 13:20:13")` | 指定時間一次性觸發(4.9.7.214+) |
308
+
309
+ ### TriggerBuilder 建立方式
310
+
311
+ ```csharp
312
+ Triggers.PeriodSeconds(5);
313
+ Triggers.Cron("* * * * *");
314
+ TriggerBuilder.Create<CronTrigger>("* * * * *", CronStringFormat.Default);
315
+ TriggerBuilder.From(trigger); // 從執行階段
316
+ TriggerBuilder.From(jsonString); // 從 JSON
317
+ TriggerBuilder.Clone(from); // 克隆
318
+ ```
319
+
320
+ ### TriggerBuilder 設定方法
321
+
322
+ `.SetTriggerId()` / `.SetDescription()` / `.SetStatus()` / `.SetStartTime()` / `.SetEndTime()` / `.SetMaxNumberOfRuns()` / `.SetNumRetries()` / `.SetRetryTimeout()` / `.SetStartNow()` / `.SetRunOnStart()` / `.SetResult()` 等。
323
+
324
+ ### 更改觸發時間(AlterTo)(4.8.7.31+)
325
+
326
+ ```csharp
327
+ triggerBuilder.AlterToPeriodSeconds(5);
328
+ triggerBuilder.AlterToCron("* * * * *");
329
+ triggerBuilder.AlterToSecondly();
330
+ triggerBuilder.AlterToMinutelyAt(3, 5, 6);
331
+ triggerBuilder.AlterTo<CustomTrigger>(30);
332
+ ```
333
+
334
+ ### 自訂觸發器
335
+
336
+ ```csharp
337
+ public class CustomTrigger : Trigger
338
+ {
339
+ public CustomTrigger(int seconds) => Seconds = seconds;
340
+ private int Seconds { get; set; }
341
+ public override DateTime? GetNextOccurrence(DateTime startAt) => startAt.AddSeconds(Seconds);
342
+ }
343
+
344
+ options.AddJob<MyJob>(Triggers.Create<CustomTrigger>(3));
345
+ ```
346
+
347
+ ### 觸發器特性
348
+
349
+ `[Period]` / `[PeriodSeconds]` / `[PeriodMinutes]` / `[PeriodHours]` / `[Cron]` / `[Secondly]` / `[Minutely]` / `[Hourly]` / `[Daily]` / `[Monthly]` / `[Weekly]` / `[Yearly]` / `[Workday]` / `[SecondlyAt]` / `[MinutelyAt]` / `[HourlyAt]` / `[DailyAt]` / `[MonthlyAt]` / `[WeeklyAt]` / `[YearlyAt]` / `[At]`
350
+
351
+ ### 查看最近運行記錄(4.8.4.3+)
352
+
353
+ ```csharp
354
+ var timelines = trigger.GetTimelines(); // 最近 5 條
355
+ ```
356
+
357
+ ---
358
+
359
+ ## 26.1.6 作業計畫 Scheduler
360
+
361
+ ### SchedulerBuilder 建立
362
+
363
+ ```csharp
364
+ SchedulerBuilder.Create<MyJob>(Triggers.PeriodSeconds(5), Triggers.Minutely());
365
+ SchedulerBuilder.Create(jobBuilder, triggerBuilder1, triggerBuilder2);
366
+ SchedulerBuilder.From(scheduler); // 從 IScheduler
367
+ SchedulerBuilder.From(jsonString); // 從 JSON
368
+ SchedulerBuilder.Clone(from);
369
+ ```
370
+
371
+ ### SchedulerBuilder 操作
372
+
373
+ ```csharp
374
+ schedulerBuilder.GetJobBuilder();
375
+ schedulerBuilder.GetTriggerBuilders();
376
+ schedulerBuilder.AddTriggerBuilder(triggerBuilder);
377
+ schedulerBuilder.UpdateTriggerBuilder(triggerBuilder);
378
+ schedulerBuilder.RemoveTriggerBuilder("triggerId");
379
+ schedulerBuilder.ConvertToJSON();
380
+ ```
381
+
382
+ ### 持久化標記
383
+
384
+ `.Appended()` → INSERT / `.Updated()` → UPDATE / `.Removed()` → DELETE
385
+
386
+ ---
387
+
388
+ ## 26.1.7 ScheduleOptionsBuilder 設定
389
+
390
+ ```csharp
391
+ services.AddSchedule(options =>
392
+ {
393
+ options.UseUtcTimestamp = false;
394
+ options.LogEnabled = true;
395
+ options.ClusterId = "cluster1";
396
+ options.BuildSqlType = SqlTypes.SqlServer;
397
+ options.JobDetail.LogEnabled = true;
398
+ options.JobDetail.ConvertToSQL = (tableName, cols, detail, behavior, naming) => { };
399
+ options.Trigger.ConvertToSQL = (tableName, cols, trigger, behavior, naming) => { };
400
+ options.UnobservedTaskExceptionHandler = (obj, args) => { };
401
+ options.RunOnStartProvider = (trigger, checkTime) => { };
402
+
403
+ // 作業註冊
404
+ options.AddJob<MyJob>(Triggers.Secondly());
405
+ options.AddHttpJob(request => { }, Triggers.PeriodSeconds(5));
406
+ options.AddMonitor<YourJobMonitor>();
407
+ options.AddExecutor<YourJobExecutor>();
408
+ options.AddPersistence<DbJobPersistence>();
409
+ options.AddClusterServer<JobClusterServer>();
410
+ });
411
+ ```
412
+
413
+ ---
414
+
415
+ ## 26.1.8 作業監視器 IJobMonitor
416
+
417
+ ```csharp
418
+ public class YourJobMonitor : IJobMonitor
419
+ {
420
+ public Task OnExecutingAsync(JobExecutingContext context, CancellationToken ct)
421
+ {
422
+ // 執行之前
423
+ return Task.CompletedTask;
424
+ }
425
+
426
+ public Task OnExecutedAsync(JobExecutedContext context, CancellationToken ct)
427
+ {
428
+ // 執行之後(context.Exception 可取得異常)
429
+ return Task.CompletedTask;
430
+ }
431
+ }
432
+
433
+ options.AddMonitor<YourJobMonitor>();
434
+ ```
435
+
436
+ ---
437
+
438
+ ## 26.1.9 作業執行器 IJobExecutor
439
+
440
+ ### 重試策略
441
+
442
+ ```csharp
443
+ public class YourJobExecutor : IJobExecutor
444
+ {
445
+ public async Task ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken ct)
446
+ {
447
+ await Retry.InvokeAsync(async () => await jobHandler.ExecuteAsync(context, ct), 3, 1000);
448
+ }
449
+ }
450
+ ```
451
+
452
+ ### 逾時控制
453
+
454
+ ```csharp
455
+ public async Task ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken ct)
456
+ {
457
+ await jobHandler.ExecuteAsync(context, ct).WaitAsync(TimeSpan.FromMilliseconds(3000));
458
+ }
459
+ ```
460
+
461
+ ---
462
+
463
+ ## 26.1.10 作業計畫工廠 ISchedulerFactory
464
+
465
+ 單例服務,提供執行階段操作作業的完整 API。
466
+
467
+ ### 常用方法速查
468
+
469
+ | 方法 | 說明 |
470
+ |------|------|
471
+ | `GetJobs()` / `GetJobs("group")` | 查詢所有/分組作業 |
472
+ | `GetNextRunJobs(DateTime.Now)` | 下一批觸發的作業 |
473
+ | `TryGetJob("jobId", out scheduler)` / `GetJob("jobId")` | 取得單個作業 |
474
+ | `TrySaveJob(builder, out scheduler)` | 儲存(新增/更新/刪除) |
475
+ | `TryAddJob<T>(triggers, out scheduler)` | 新增作業 |
476
+ | `TryUpdateJob(builder, out scheduler)` | 更新作業 |
477
+ | `TryRemoveJob("jobId", out scheduler)` | 刪除作業 |
478
+ | `ContainsJob("jobId")` | 檢查是否存在 |
479
+ | `StartAll()` / `PauseAll()` / `RemoveAll()` | 全部啟動/暫停/刪除 |
480
+ | `TryRunJob("jobId", out scheduler)` | 手動執行(4.8.7.11+) |
481
+ | `TryCancelJob("jobId", out scheduler)` | 取消執行(4.9.1.9+) |
482
+ | `TryStartJob("jobId")` / `TryPauseJob("jobId")` | 啟停單個作業(4.9.2.16+) |
483
+ | `CancelSleep()` | 強制喚醒排程器 |
484
+ | `GCCollect()` | 觸發 GC 回收(4.9.1.23+) |
485
+ | `OnChanged` | 作業變更事件(4.8.8.29+) |
486
+ | `OnExecutionRecord` | 觸發記錄通知(4.9.1.16+) |
487
+
488
+ ---
489
+
490
+ ## 26.1.11 作業計畫 IScheduler
491
+
492
+ 單個作業操作介面。
493
+
494
+ ```csharp
495
+ scheduler.GetJobDetail();
496
+ scheduler.GetTriggers();
497
+ scheduler.GetTriggerBuilder("triggerId");
498
+ scheduler.TryAddTrigger(triggerBuilder, out trigger);
499
+ scheduler.TryUpdateTrigger(triggerBuilder, out trigger);
500
+ scheduler.TryRemoveTrigger("triggerId", out trigger);
501
+ scheduler.UpdateDetail(jobBuilder);
502
+ scheduler.Start() / .Pause() / .Remove();
503
+ scheduler.Run() / .Run("triggerId"); // 手動執行
504
+ scheduler.Cancel() / .Cancel("triggerId"); // 取消執行
505
+ scheduler.Reload(); // 強制重新整理
506
+ scheduler.ConvertToJSON();
507
+ scheduler.GetEnumerable(); // 轉可枚舉字典
508
+ ```
509
+
510
+ ---
511
+
512
+ ## 26.1.12 作業持久化器 IJobPersistence
513
+
514
+ ```csharp
515
+ public class DbJobPersistence : IJobPersistence
516
+ {
517
+ public Task<IEnumerable<SchedulerBuilder>> PreloadAsync(CancellationToken ct)
518
+ {
519
+ // 啟動時從資料庫載入作業
520
+ return Task.FromResult(Enumerable.Empty<SchedulerBuilder>());
521
+ }
522
+
523
+ public Task<SchedulerBuilder> OnLoadingAsync(SchedulerBuilder builder, CancellationToken ct)
524
+ {
525
+ // 初始化時進一步修改(可標記 .Updated() / .Appended() / .Removed())
526
+ return Task.FromResult(builder);
527
+ }
528
+
529
+ public Task OnChangedAsync(PersistenceContext context)
530
+ {
531
+ var sql = context.ConvertToSQL("job_detail"); // 執行 SQL 即可
532
+ return Task.CompletedTask;
533
+ }
534
+
535
+ public Task OnTriggerChangedAsync(PersistenceTriggerContext context)
536
+ {
537
+ var sql = context.ConvertToSQL("job_trigger");
538
+ return Task.CompletedTask;
539
+ }
540
+
541
+ public Task OnExecutionRecordAsync(PersistenceExecutionRecordContext context)
542
+ {
543
+ // 寫入運行記錄表
544
+ return Task.CompletedTask;
545
+ }
546
+ }
547
+
548
+ options.AddPersistence<DbJobPersistence>();
549
+ ```
550
+
551
+ > 框架自動生成標準 SQL(支援 `SqlTypes.SqlServer` / `MySQL` / `PostgreSQL` / `Sqlite` / `Oracle` / `Firebird`)。
552
+
553
+ ---
554
+
555
+ ## 26.1.13 作業叢集控制
556
+
557
+ 提供簡單的**故障轉移**功能(非負載均衡)。
558
+
559
+ ```csharp
560
+ public class JobClusterServer : IJobClusterServer
561
+ {
562
+ public void Start(JobClusterContext context) { /* 寫入 Waiting 狀態 */ }
563
+ public async Task WaitingForAsync(JobClusterContext context) { /* 輪詢檢查,條件滿足時呼叫 WorkNow */ }
564
+ public void Stop(JobClusterContext context) { /* 更新為 Crashed */ }
565
+ public void Crash(JobClusterContext context) { /* 更新為 Crashed */ }
566
+ }
567
+
568
+ options.ClusterId = "cluster1";
569
+ options.AddClusterServer<JobClusterServer>();
570
+ ```
571
+
572
+ `ClusterStatus` 列舉:`Crashed` / `Working` / `Waiting`(預設)。
573
+
574
+ ---
575
+
576
+ ## 26.1.16 Dashboard 看板(4.8.4+)
577
+
578
+ ```csharp
579
+ app.UseStaticFiles();
580
+ app.UseScheduleUI(); // 必須在 UseStaticFiles 之後
581
+ ```
582
+
583
+ > 預設入口:`/schedule`,預設帳號:`schedule`,密碼為空。
584
+
585
+ ### 設定選項
586
+
587
+ ```csharp
588
+ app.UseScheduleUI(options =>
589
+ {
590
+ options.RequestPath = "/custom-job";
591
+ options.DisableOnProduction = true;
592
+ options.DisplayEmptyTriggerJobs = true;
593
+ options.DefaultExpandAllJobs = false;
594
+ options.Title = "定時任務看板";
595
+
596
+ options.LoginConfig.OnLoging = async (username, password, httpContext) =>
597
+ await Task.FromResult(username == "furion" && password == "furion");
598
+ options.LoginConfig.DefaultUsername = "";
599
+ options.LoginConfig.DefaultPassword = "";
600
+ });
601
+ ```
602
+
603
+ ### Worker Service 中啟用看板
604
+
605
+ ```csharp
606
+ IHost host = Host.CreateDefaultBuilder(args)
607
+ .ConfigureServices(services => { services.AddSchedule(); })
608
+ .ConfigureWebHostDefaults(builder => builder.Configure(app =>
609
+ {
610
+ app.UseStaticFiles();
611
+ app.UseScheduleUI();
612
+ }))
613
+ .Build();
614
+ host.Run();
615
+ ```
616
+
617
+ ---
618
+
619
+ ## 26.1.15 部署建議
620
+
621
+ - 部署到 IIS 需停用應用程式集區回收
622
+ - **強烈建議**採用 Worker Service 獨立部署
623
+ - 確保伺服器不會進入休眠
624
+
625
+ ---
626
+
627
+ ## 26.1.17 常見問題
628
+
629
+ - **時間偏差**:使用 `context.OccurrenceTime` 而非 `DateTime.Now`
630
+ - **參數序列化**:使用 `Schedular.Serialize()` / `Schedular.Deserialize<T>()`
631
+ - **延遲操作**:使用 `Task.Delay` 而非 `Thread.Sleep`