前言
在上一篇文章【基于ASP.NET ZERO,開發(fā)SaaS版供應(yīng)鏈管理系統(tǒng)】中有提到對(duì)Webhook功能的擴(kuò)展改造,本文詳細(xì)介紹一下具體過程。
Webhook功能操作說明,請(qǐng)參見此文檔鏈接:Webhook數(shù)據(jù)推送
Webhook功能發(fā)布日期:
- ASP.NET Boilerplate(以下簡(jiǎn)稱ABP)在v5.2(2020-02-18)版本中發(fā)布了Webhook功能,詳細(xì)說明,請(qǐng)參見:官方幫助鏈接;
- ASP.NET ZERO(以下簡(jiǎn)稱ZERO)在v8.2.0(2020-02-20)版本中發(fā)布了Webhook功能;
- 我們系統(tǒng)是在2021年4月完成了對(duì)Webhook功能的改造:內(nèi)部接口(用戶自行設(shè)定接口地址的)、第三方接口(微信內(nèi)部群、釘釘群、聚水潭API等)。
1、Webhook定義
- 為了區(qū)分內(nèi)部接口與第三方接口,在第三方接口名稱前統(tǒng)一附加特定前綴,如:Third.WX.XXX、Third.DD.XXX等;
- 添加定義條目時(shí)候設(shè)定對(duì)應(yīng)的特性(featureDependency),基于特性功能對(duì)不同租戶顯示或者隱藏定義的條目。
public class AppWebhookDefinitionProvider : WebhookDefinitionProvider
{
public override void SetWebhooks(IWebhookDefinitionContext context)
{
//物料檔案 - 全部可見
context.Manager.Add(new WebhookDefinition(name: AppWebHookNames.T11071001_Created));
context.Manager.Add(new WebhookDefinition(name: AppWebHookNames.T11071001_Updated));
context.Manager.Add(new WebhookDefinition(name: AppWebHookNames.T11071001_Deleted));
//生產(chǎn)訂單 - 生產(chǎn)管理可見
var featureC = new SimpleFeatureDependency("SCM.C");
context.Manager.Add(new WebhookDefinition(name: AppWebHookNames.T13041001_Created, featureDependency: featureC));
context.Manager.Add(new WebhookDefinition(name: AppWebHookNames.T13041001_Updated, featureDependency: featureC));
context.Manager.Add(new WebhookDefinition(name: AppWebHookNames.T13041001_Deleted, featureDependency: featureC));
context.Manager.Add(new WebhookDefinition(name: AppWebHookNames.T13041001_MRP_Data, featureDependency: featureC));
//...
}
}
- 在CoreModule中添加Webhook定義,并設(shè)定參數(shù)選項(xiàng):
public class SCMCoreModule : AbpModule
{
public override void PreInitialize()
{
Configuration.Webhooks.Providers.Add<AppWebhookDefinitionProvider>();
Configuration.Webhooks.TimeoutDuration = TimeSpan.FromMinutes(1);
Configuration.Webhooks.IsAutomaticSubscriptionDeactivationEnabled = true;
Configuration.Webhooks.MaxSendAttemptCount = 3;
Configuration.Webhooks.MaxConsecutiveFailCountBeforeDeactivateSubscription = 10;
//...
}
//...
}
2、Webhook訂閱
- 前端用戶創(chuàng)建Webhook訂閱記錄(WebhookUri、Webhooks、Headers等),之后傳遞到后端API;
- 后端API通過WebhookSubscriptionManager添加保存WebhookSubscription(Webhook訂閱):
[AbpAuthorize(AppPermissions.Pages_Administration_WebhookSubscription)]
public class WebhookSubscriptionAppService : SCMAppServiceBase, IWebhookSubscriptionAppService
{
//...
[AbpAuthorize(AppPermissions.Pages_Administration_WebhookSubscription_Create)]
public async Task AddSubscription(WebhookSubscription subscription)
{
subscription.TenantId = AbpSession.TenantId;
await _webHookSubscriptionManager.AddOrUpdateSubscriptionAsync(subscription);
}
//...
}
3、Webhook發(fā)布(數(shù)據(jù)推送)
監(jiān)測(cè)實(shí)體事件(CreatedEvent、UpdatedEvent、DeletedEvent)數(shù)據(jù),按租戶用戶創(chuàng)建的Webhook訂閱,推送數(shù)據(jù):
public class T11071001Syncronizer :
IEventHandler<EntityCreatedEventData<T11071001>>,
IEventHandler<EntityUpdatedEventData<T11071001>>,
IEventHandler<EntityDeletedEventData<T11071001>>,
ITransientDependency
{
private readonly IAppWebhookPublisher _appWebhookPublisher;
public T11071001Syncronizer(IAppWebhookPublisher appWebhookPublisher)
{
_appWebhookPublisher = appWebhookPublisher;
}
public void HandleEvent(EntityCreatedEventData<T11071001> eventData)
{
DoWebhook("N", eventData.Entity);
}
public void HandleEvent(EntityUpdatedEventData<T11071001> eventData)
{
DoWebhook("U", eventData.Entity);
}
public void HandleEvent(EntityDeletedEventData<T11071001> eventData)
{
int? tenantId = eventData.Entity.TenantId;
string whName = AppWebHookNames.T11071001_Deleted;
var subscriptions = _appWebhookPublisher.GetSubscriptions(tenantId, whName);
if (subscriptions == null) { return; }
_appWebhookPublisher.PublishWebhookUOW(whName, eventData.Entity, tenantId, subscriptions);
}
}
- DoWebhook()方法:基于具體的訂閱(內(nèi)部接口、第三方接口等)推送對(duì)應(yīng)的內(nèi)容:
private void DoWebhook(string nu, T11071001 entity)
{
int? tenantId = entity.TenantId;
var whCache = _appWebhookPublisher.GetWebhookCache(tenantId); if (whCache.Count == 0) { return; }
string whName = nu == "N" ? AppWebHookNames.T11071001_Created : AppWebHookNames.T11071001_Updated;
string whNameWX = AppWebHookNames.WX_T11071001_Created;
string whNameDD = AppWebHookNames.DD_T11071001_Created;
bool isWH = whCache.Names.ContainsKey(whName);
bool isWX = whCache.Names.ContainsKey(whNameWX);
bool isDD = whCache.Names.ContainsKey(whNameDD);
if (!(isWH || isWX || isDD)) { return; }
var data = ObjectMapper.Map<T11071001WebhookDto>(entity);
//內(nèi)部接口
if (isWH)
{
_appWebhookPublisher.PublishWebhookUOW(whName, data, tenantId, whCache.Names[whName], false);
}
//企業(yè)微信內(nèi)部群
if (isWX)
{
var wxData = new WxTCardWebhookDto { template_card = GetWxTCard(data, tenantId, nu) };
_appWebhookPublisher.PublishWebhookUOW(whNameWX, wxData, tenantId, whCache.Names[whNameWX], true);
}
//釘釘內(nèi)部群
if (isDD)
{
var title = GetNUTitle(nu, L(T));
var mdText = GetNewMarkdown(data, title);
var ddData = new DdMarkdownWebhookDto { markdown = new DdMarkdownContentDto { title = title, text = mdText } };
_appWebhookPublisher.PublishWebhookUOW(whNameDD, ddData, tenantId, whCache.Names[whNameDD], true);
}
}
- GetWebhookCache()方法:實(shí)現(xiàn)按租戶緩存Webhook訂閱的數(shù)據(jù):
public SCMWebhookCacheItem GetWebhookCache(int? tenantId)
{
return SetAndGetCache(tenantId);
}
private SCMWebhookCacheItem SetAndGetCache(int? tenantId, string keyName = "SubscriptionCount")
{
int tid = tenantId ?? 0; var cacheKey = $"{keyName}-{tid}";
return _cacheManager.GetSCMWebhookCache().Get(cacheKey, () =>
{
int count = 0;
var names = new Dictionary<string, List<WebhookSubscription>>();
UnitOfWorkManager.WithUnitOfWork(() =>
{
using (UnitOfWorkManager.Current.SetTenantId(tenantId))
{
if (_featureChecker.IsEnabled(tid, "SCM.H")) //Feature核查
{
var items = _webhookSubscriptionRepository.GetAllList(e => e.TenantId == tenantId && e.IsActive == true);
count = items.Count;
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.Webhooks)) { continue; }
var whNames = JsonHelper.DeserializeObject<string[]>(item.Webhooks); if (whNames == null) { continue; }
foreach (string whName in whNames)
{
if (names.ContainsKey(whName))
{
names[whName].Add(item.ToWebhookSubscription());
}
else
{
names.Add(whName, new List<WebhookSubscription> { item.ToWebhookSubscription() });
}
}
}
}
}
});
return new SCMWebhookCacheItem(count, names);
});
}
- PublishWebhookUOW()方法:替換ABP中WebHookPublisher的默認(rèn)實(shí)現(xiàn),直接按傳入的訂閱,通過WebhookSenderJob推送數(shù)據(jù):
public void PublishWebhookUOW(string webHookName, object data, int? tenantId, List<WebhookSubscription> webhookSubscriptions = null, bool sendExactSameData = false)
{
UnitOfWorkManager.WithUnitOfWork(() =>
{
using (UnitOfWorkManager.Current.SetTenantId(tenantId))
{
Publish(webHookName, data, tenantId, webhookSubscriptions, sendExactSameData);
}
});
}
private void Publish(string webhookName, object data, int? tenantId, List<WebhookSubscription> webhookSubscriptions, bool sendExactSameData = false)
{
if (string.IsNullOrWhiteSpace(webhookName)) { return; }
//若無直接傳入訂閱則按webhookName查詢
webhookSubscriptions ??= _webhookSubscriptionRepository.GetAllList(subscriptionInfo =>
subscriptionInfo.TenantId == tenantId &&
subscriptionInfo.IsActive &&
subscriptionInfo.Webhooks.Contains("\"" + webhookName + "\"")
).Select(subscriptionInfo => subscriptionInfo.ToWebhookSubscription()).ToList();
if (webhookSubscriptions.IsNullOrEmpty()) { return; }
var webhookInfo = SaveAndGetWebhookEvent(tenantId, webhookName, data);
foreach (var webhookSubscription in webhookSubscriptions)
{
var jobArgs = new WebhookSenderArgs
{
TenantId = webhookSubscription.TenantId,
WebhookEventId = webhookInfo.Id,
Data = webhookInfo.Data,
WebhookName = webhookInfo.WebhookName,
WebhookSubscriptionId = webhookSubscription.Id,
Headers = webhookSubscription.Headers,
Secret = webhookSubscription.Secret,
WebhookUri = webhookSubscription.WebhookUri,
SendExactSameData = sendExactSameData
};
//指定隊(duì)列執(zhí)行任務(wù),由觸發(fā)事件的server執(zhí)行
IBackgroundJobClient hangFireClient = new BackgroundJobClient();
hangFireClient.Create<WebhookSenderJob>(x => x.ExecuteAsync(jobArgs), new EnqueuedState(AppVersionHelper.MachineName));
}
}
- WebhookSenderJob:重寫WebhookManager的SignWebhookRequest方法,對(duì)于第三方接口,不添加簽名的Header:
public override void SignWebhookRequest(HttpRequestMessage request, string serializedBody, string secret)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
//第三方接口,不添加簽名Header
if (IsThirdAPI(request))
{
return;
}
if (string.IsNullOrWhiteSpace(serializedBody))
{
throw new ArgumentNullException(nameof(serializedBody));
}
var secretBytes = Encoding.UTF8.GetBytes(secret);
using (var hasher = new HMACSHA256(secretBytes))
{
request.Content = new StringContent(serializedBody, Encoding.UTF8, "application/json");
var data = Encoding.UTF8.GetBytes(serializedBody);
var sha256 = hasher.ComputeHash(data);
var headerValue = string.Format(CultureInfo.InvariantCulture, SignatureHeaderValueTemplate, BitConverter.ToString(sha256));
request.Headers.Add(SignatureHeaderName, headerValue);
}
}
- WebhookSenderJob:重寫WebhookSender的CreateWebhookRequestMessage方法,對(duì)于第三方接口,進(jìn)行特殊處理:
protected override HttpRequestMessage CreateWebhookRequestMessage(WebhookSenderArgs webhookSenderArgs)
{
return webhookSenderArgs.WebhookName switch
{
AppWebHookNames.JST_supplier_upload => JSTHttpRequestMessage(webhookSenderArgs), //聚水潭 - 供應(yīng)商上傳
//...
_ => new HttpRequestMessage(HttpMethod.Post, webhookSenderArgs.WebhookUri)
};
}
- WebhookSenderJob:重寫WebhookSender的AddAdditionalHeaders方法, 對(duì)于第三方接口,不添加Headers:
protected override void AddAdditionalHeaders(HttpRequestMessage request, WebhookSenderArgs webhookSenderArgs)
{
//第三方接口,不添加Header
if (IsThirdAPI(request))
{
return;
}
foreach (var header in webhookSenderArgs.Headers)
{
if (request.Headers.TryAddWithoutValidation(header.Key, header.Value))
{
continue;
}
if (request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value))
{
continue;
}
throw new Exception($"Invalid Header. SubscriptionId:{webhookSenderArgs.WebhookSubscriptionId},Header: {header.Key}:{header.Value}");
}
}
- WebhookSenderJob:重寫WebhookSender的SendHttpRequest方法,處理第三方接口的回傳數(shù)據(jù):
protected override async Task<(bool isSucceed, HttpStatusCode statusCode, string content)> SendHttpRequest(HttpRequestMessage request)
{
using var client = _httpClientFactory.CreateClient(); //避免使用 new HttpClient()方式
client.Timeout = _webhooksConfiguration.TimeoutDuration;
var response = await client.SendAsync(request);
var isSucceed = response.IsSuccessStatusCode;
var statusCode = response.StatusCode;
var content = await response.Content.ReadAsStringAsync();
//第三方接口,需要處理回傳的數(shù)據(jù)
if (IsThirdAPI(request))
{
string method = TryGetHeader(request.Headers, "ThirdAPI1");
int tenantId = Convert.ToInt32(TryGetHeader(request.Headers, "ThirdAPI2"));
switch (method)
{
case AppWebHookNames.JST_supplier_upload: await JSTSupplierUploadResponse(method, content, tenantId); break;
//...
default: break;
}
}
return (isSucceed, statusCode, content);
}
總結(jié)
基于ABP/ZERO的Webhook功能實(shí)現(xiàn),進(jìn)行一些擴(kuò)展改造,可以實(shí)現(xiàn)業(yè)務(wù)數(shù)據(jù)按用戶訂閱進(jìn)行推送,包括推送到第三方接口(企業(yè)微信群、釘釘?shù)龋?,在很大程度上提升了業(yè)務(wù)系統(tǒng)的靈活性與實(shí)用性。文章來源地址http://www.zghlxwxcb.cn/news/detail-698806.html
文章來源:http://www.zghlxwxcb.cn/news/detail-698806.html
到了這里,關(guān)于擴(kuò)展ABP的Webhook功能,推送數(shù)據(jù)到第三方接口(企業(yè)微信群、釘釘群等)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!