0%

[DotnetCore]排程神器-Hangfire:動台排程篇4

前情提要

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

  • SP執行類: 可以進階設定是否要將SP執行結果打包成檔案上傳至指定路徑
  • Bat檔案執行類: 主要是因為與其他系統串接,主要的前台部分,需要由執行期bat檔案做驅動
  • 下載類: 主要與其他系統交換檔案,由檔案內容塞到固定的資料表中,透過一支SP做後置處理

  • Normal類: 有一些排程無法歸納為上述三種,即便前面執行其中一項,但後續有額外的處理,筆者這邊就不另外規劃做處理,保留其彈性

內容

講解一下下載類主要的職責,下載會分單方面取得資料應用,或取得資料後融合自己這邊的資料,再轉拋到另一個系統中等等,大概就跑不掉這兩種,因此主要做的事情

  • 下載檔案
  • 執行SP(資料應用、融合資料)
  • 上傳檔案

筆者為這類的Job特性,設計並定義好哪些Column該放甚麼值,使共用Job可以統一做事,若有其他需求,也是屬這類的話,就照規定放資料,就是一個新的排程了。

設計下載檔案中的資料存放結構

主要設計是這樣,筆者這邊寫的是一個共用類,各個排程中下載檔案格式各不相同,若由程式解析,恐怕擴充不完,筆者這邊環境比較傾向是把解析邏輯寫在SP中,以該情況來說,SP不用重新編譯,上版時比程式來得快,這樣一來,共用類的解析來說只剩下把每一列資料讀取後,原封不動的將資料列存放到共同資料結構中,由各自不同SP去解析對應的檔案內容,才得以讓共用類順利執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 依照該Model建一個對應的資料表
public partial class FileInput
{
// 流水序號
public long SeqNo { get; set; }
// 檔案名稱
public string Fname { get; set; }
// 資料日期
public string DataDate { get; set; }
// 每列資料
public string DataStr { get; set; }
public string AuditUser { get; set; }
public DateTime? AuditTime { get; set; }
}

以上設計僅供參考,這個完全可以照各位的環境遇到的情境,再做一個調整,筆者這邊環境需要這些欄位這樣而已。

設計Service

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Interface
public interface IDownloadJobService : ISyncJobService
{
void SyncProcess(CommCode jobSetting);
}

// Service
public class DownloadJobService : IDownloadJobService
{
private FtpServiceResolver _ftpServiceResolver;
private IFtpService _ftpService;
private readonly IFilePathService _filePathService;
private string _localPath;
private readonly IBusinessDateService _businessDateService;
private readonly SqlConnectionFactory _factory;
public DownloadJobService(FtpServiceResolver ftpServiceResolver
, IFilePathService filePathService
, IBusinessDateService businessDateService, SqlConnectionFactory factory)
{
_ftpServiceResolver = ftpServiceResolver;
_filePathService = filePathService;
_businessDateService = businessDateService;
_factory = factory;
}
public void SyncProcess(CommCode jobSetting)
{
// 參考筆者動態排程1文章中的CommCode的設計,筆者這邊利用CodeVal3這個欄位做一個Config設定
// 利用「;」這個符號將每個設定隔開
/*
0:S or F
1:Server
2:Account
3:Password
4:RemotePath
5:LocalPath
6:FileName
7:DataDate
8:FName
9:StoreProcedure
10:Encoding
*/
// Step1: Config Data Arrangement
var ftpDownloadConfigSetting = jobSetting.CodeVal3.Split(";");
_ftpService = _ftpServiceResolver(ftpDownloadConfigSetting[0]);
var ftpDownloadConfigBaseModel = new FTPConfigBaseModel()
{
Server = ftpDownloadConfigSetting[1],
Account = ftpDownloadConfigSetting[2],
Password = ftpDownloadConfigSetting[3],
RemotePath = ftpDownloadConfigSetting[4],
LocalPath = ftpDownloadConfigSetting[5]
};
_localPath = _filePathService.GetDirectoryPath(ftpDownloadConfigBaseModel.LocalPath);
var fileName = _businessDateService.GetBusinessDateWithParameter((ftpDownloadConfigSetting[6], "yyyyMMdd"));

// Step2: Fileinput Data Arrangement
if (string.IsNullOrEmpty(ftpDownloadConfigSetting[8]) == false)
{
var dataDate = _businessDateService.GetBusinessDateWithParameter((ftpDownloadConfigSetting[7], "yyyy/MM/dd"));

var fileInputs = new List<FileInput>();
var fName = ftpDownloadConfigSetting[8];
var encodingName = ftpDownloadConfigSetting[10];
var encoding = string.IsNullOrEmpty(encodingName) ? Encoding.UTF8
: Encoding.GetEncoding(encodingName);

using (var reader = new StreamReader(_filePathService.GetFile(_localPath, fileName), encoding))
{
while (reader.Peek() >= 0)
{
var content = reader.ReadLine().Trim();
if (string.IsNullOrEmpty(content)) continue;
fileInputs.Add(new FileInput()
{
Fname = fName,
DataDate = dataDate,
DataStr = content,
AuditUser = "SYS",
AuditTime = DateTime.Now
});
}
}

using (var _transaction = new TransactionScope())
{
using (var _conn = _factory.GetConnection())
{
_conn.Open();
_conn.Execute(@"DELETE FileInput WHERE DataDate = @DataDate AND FName = @FName"
, new
{
DataDate = dataDate,
FName = fName
});

using (SqlBulkCopy bulkCopy = new SqlBulkCopy((SqlConnection)(_conn as SKBSqlConnection)))
{
using (var reader = ObjectReader.Create(fileInputs
, new string[] { "SeqNo", "Fname", "DataDate", "DataStr", "AuditUser", "AuditTime" }))
{
bulkCopy.DestinationTableName = "dbo.FileInput";
bulkCopy.WriteToServer(reader);
}
}
_transaction.Complete();
}
}
}

// Step3: Execute logic Store Procedure and Upload file
if (string.IsNullOrEmpty(ftpDownloadConfigSetting[9]) == false)
{
using (var _conn = _factory.GetConnection())
{
// Step3-1: Data Initial
var spExecuteSql = _businessDateService.GetBusinessDateWithParameter(
(ftpDownloadConfigSetting[9], "yyyy/MM/dd"));
// Step3-2: Execute SP and Check Upload
if (string.IsNullOrEmpty(jobSetting.CodeVal4))
{
_conn.Execute(spExecuteSql);
}
else
{
var result = _conn.Query<FileUploadResponseModel>(spExecuteSql).ToList();
var ftpUploadConfigSetting = jobSetting.CodeVal4.Split(";");
var ftpUploadConfigBaseModel = new FTPConfigBaseModel()
{
Server = ftpUploadConfigSetting[1],
Account = ftpUploadConfigSetting[2],
Password = ftpUploadConfigSetting[3],
RemotePath = ftpUploadConfigSetting[4],
LocalPath = ftpUploadConfigSetting[5]
};
_ftpService.UploadData(ftpUploadConfigBaseModel, result);
}
}
}
}
}

設計Job

Job就沒甚麼好解釋了,基本上就是執行Job Service而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DownloadJob : TimerJob<DownloadJob, CommCode>, ITimerJob<CommCode>
{
private readonly IDownloadJobService _downloadJobService;
public DownloadJob(IConfiguration config
, IDownloadJobService downloadJobService
, List<SchedulerLog> schedulerLogs
, SchedulerLog schedulerLog
, ILoggerManager loggerManager)
: base(config, schedulerLogs, schedulerLog, loggerManager)
{
_downloadJobService = downloadJobService;
}

public override string JobId => "DownloadJob";

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

註冊相關Service

這邊就列出該共用類Job相關的Service註冊作業

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void ConfigureServices(IServiceCollection services)
{
// 以上省略
services.AddTransient<DownloadJob>();
services.AddTransient<IFilePathService, FilePathService>();
services.AddTransient<IDownloadJobService, DownloadJobService>();
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;
}
});
// 以下省略
}

結論

下載類的Job介紹到這邊,雖然思路都有提到,筆者這邊要提醒就是上述講的那些設計是完全是照著筆者上班環境中的情境來設計,原則就是要將你自己的系統需求,要先歸納統整出幾類共用的Job類型,以至於新增同類型的Job時,輕而易舉,希望這幾篇有幫助到你的思考。

參考