0%

[DotnetCore]排程神器-Hangfire:動態排程篇2

前情提要

前篇[DotnetCore]排程神器-Hangfire:動態排程篇1有提到共用類Job的設計,這篇主要講解各個Job種類的實作細節,再複習一下上篇內容,筆者這邊動態排程主要是著墨在排程的資訊設定於資料庫中,透過一支Job Initial的API,可以重新AddOrUpdate來達成動態排程的效果。若所有的Job是宣告於程式端中,要新增類似的Job時著實麻煩,失去其原有的設計理念。因此筆者這邊統計歸納出四種類型:

  • SP執行類: 可以進階設定是否要將SP執行結果打包成檔案上傳至指定路徑

  • Bat檔案執行類: 主要是因為與其他系統串接,主要的前台部分,需要由執行期bat檔案做驅動
  • 下載類: 主要與其他系統交換檔案,由檔案內容塞到固定的資料表中,透過一支SP做後置處理
  • Normal類: 有一些排程無法歸納為上述三種,即便前面執行其中一項,但後續有額外的處理,筆者這邊就不另外規劃做處理,保留其彈性

內容

接下來就以四種類型中的SP執行類介紹其實作內容,大致上分為SPExecuteJobService以及SPExecuteJob,其中日期參數取得比較複雜,獨立設計其作法。

日期參數置換Service

以上篇介紹的CommCode結構來說,筆者就拿某一個Column當作設定SP的語法,會牽涉到參數置換的問題,共同參數大概就是營業日了,因為各個作業可能還是會有不同的需求,可能要減一天,或者要用實際日期,或者不同時區的日期等等,筆者這邊也因應這些需求有撰寫一隻置換日期的Service。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// interface
public interface IBusinessDateService
{
string GetBusinessDateWithParameter((string source, string format) conditions);
}

// Service
public class BusinessDateService : IBusinessDateService
{
private readonly ILoggerManager _logger;
private readonly IConfiguration _config;
// 筆者這邊取得資料庫連線工廠,可以任何形式,只要達成取得資料庫連線即可
private readonly SqlConnectionFactory _factory;
public BusinessDateService(IConfiguration config, SqlConnectionFactory factory, ILoggerManager logger)
{
_config = config;
_factory = factory;
_logger = logger;
}
public string GetBusinessDateWithParameter((string source, string format) conditions)
{
if (string.IsNullOrEmpty(conditions.source)) return conditions.source;

var result = conditions.source;
// 符合{0}、{-1}、{2}、{DateTime}、{DateTime:US Eastern Standard Time}皆適用
var regex = new Regex(@"(\{-?[0-9a-zA-Z]+\})|(\{([0-9a-zA-Z]+):([0-9a-zA-Z\s]+)\}){1}");
foreach (Match match in regex.Matches(result))
{
var parameter = match.Value.Trim('{').Trim('}');
// 分為DataTime及營業日;DataTime則以實際日期為準,若有多設定時區則以該時區時間為準
if (match.Value.Contains("DateTime"))
{
var datetimeResult = new DateTime();
var keywordList = parameter.Split(":");
if (keywordList.Length > 1)
{
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(keywordList[1]);
datetimeResult = TimeZoneInfo.ConvertTime(DateTime.Now, timeZoneInfo);
}
else
{
datetimeResult = DateTime.Now;
}
result = result.Replace(match.Value, datetimeResult.ToString(conditions.format));
}
else
{
if (double.TryParse(parameter, out var period))
{
var datePara = "";
// 取得營業日邏輯(實作忽略,畢竟每一家的邏輯都不一樣,筆者不另外貼出來)
result = result.Replace(match.Value, datePara);
}
}
}
return result;
}
}

SPExecuteJobService

筆者這邊都會使用Service的形式,不直接把執行邏輯寫在Job中,於Job中注入Job對應的Service做事

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class SPExecuteJobService : ISPExecuteJobService
{
private readonly ILoggerManager _logger;
private readonly FtpServiceResolver _ftpServiceResolver;
private IFtpService _ftpService;
private readonly HttpClient _client;
private readonly IBusinessDateService _businessDateService;
private readonly SqlConnectionFactory _factory;
public SPExecuteJobService(FtpServiceResolver ftpServiceResolver
, HttpClient client
, IBusinessDateService businessDateService, ILoggerManager logger, SqlConnectionFactory factory)
{
_client = client;
_businessDateService = businessDateService;
_ftpServiceResolver = ftpServiceResolver;
_logger = logger;
_factory = factory;
}
public void SyncProcess(CommCode jobSetting)
{
// Step0: Data Initial
// CodeVal2置放主要SP語法
var spExecuteSql = _businessDateService.GetBusinessDateWithParameter(
(jobSetting.CodeVal2, "yyyy/MM/dd"));
// Step1: Execute SP and Check Upload
if (string.IsNullOrEmpty(jobSetting.CodeVal4) && string.IsNullOrEmpty(jobSetting.CodeVal6))
{
using (var _conn = _factory.GetConnection())
_conn.Execute(spExecuteSql);
}
else
{
// 若CodeVal4有值表示,執行完SP後須上傳
if (string.IsNullOrEmpty(jobSetting.CodeVal4) == false)
{
var ftpConfigSetting = jobSetting.CodeVal4.Split(";");
// 這邊就用到筆者寫的FTPServiceResolver,筆者這邊使用英文字母F或S來區分使用FtpService/SFtpService
_ftpService = _ftpServiceResolver(ftpConfigSetting[0]);
var result = new List<FileUploadResponseModel>();
using (var _conn = _factory.GetConnection())
{
result = _conn.Query<FileUploadResponseModel>(spExecuteSql).ToList();
}
var ftpConfigBaseModel = new FTPConfigBaseModel()
{
Server = ftpConfigSetting[1],
Account = ftpConfigSetting[2],
Password = ftpConfigSetting[3],
RemotePath = ftpConfigSetting[4],
LocalPath = ftpConfigSetting[5]
};
_ftpService.UploadData(ftpConfigBaseModel, result);
}
}
}

public void SyncProcess()
{
throw new NotImplementedException();
}
}

SPExecuteJob

我們的重頭戲,SP執行類Job,不過筆者設計都是肥在ServiceJob會單純許多,就像MVC中的C要輕一樣,盡量一行內就解決,主要邏輯都塞在Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SPExecuteJob : TimerJob<SPExecuteJob, CommCode>
, ITimerJob<CommCode>
{
private readonly ISPExecuteJobService _spExecuteJobService;
private string _jobId;
public SPExecuteJob(IConfiguration config
, ISPExecuteJobService spExecuteJobService
, List<SchedulerLog> schedulerLogs
, SchedulerLog schedulerLog
, ILoggerManager loggerManager)
: base(config, schedulerLogs, schedulerLog, loggerManager)
{
_spExecuteJobService = spExecuteJobService;
}
public override string JobId => "SPExecuteJob";

protected override void PerformJobTasks(List<CommCode> parameters)
{
_spExecuteJobService.SyncProcess(parameters.FirstOrDefault());
}
}

註冊Service

筆者這邊就列出SP執行類相關的Service註冊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void ConfigureServices(IServiceCollection services)
{
// 以上省略
services.AddTransient<SPExecuteJob>();
services.AddTransient<ISPExecuteJobService, SPExecuteJobService>();
services.AddTransient<IBusinessDateService, BusinessDateService>();
services.AddTransient<FtpServiceResolver>(serviceProvider => key =>
{
switch (key)
{
case "F":
return serviceProvider.GetService<FtpService>();
case "S":
return serviceProvider.GetService<SFtpService>();
default:
return null;
}
});
// 以下省略
}

結論

此篇介紹SPExecuteJob,可以回頭看筆者的上篇文章,就會比較清楚了,筆者原本一篇就塞四種類型Job介紹,但篇幅會過長,只好切割成不同的三篇文章,加上第一篇動態排程就完整了筆者客製版的動態排程。

參考