扩展ABP的Webhook功能,推送数据到第三方接口(企业微信群、钉钉群等)
前言
在上一篇文章【基于ASP.NET ZERO,开发SaaS版供应链管理系统】中有提到对Webhook功能的扩展改造,本文详细介绍一下具体过程。
Webhook功能操作说明,请参见此文档链接:Webhook数据推送
Webhook功能发布日期:
- ASP.NET Boilerplate(以下简称ABP)在v5.2(2020-02-18)版本中发布了Webhook功能,详细说明,请参见:官方帮助链接;
- ASP.NET ZERO(以下简称ZERO)在v8.2.0(2020-02-20)版本中发布了Webhook功能;
- 我们系统是在2021年4月完成了对Webhook功能的改造:内部接口(用户自行设定接口地址的)、第三方接口(微信内部群、钉钉群、聚水潭API等)。
1、Webhook定义
- 为了区分内部接口与第三方接口,在第三方接口名称前统一附加特定前缀,如:Third.WX.XXX、Third.DD.XXX等;
- 添加定义条目时候设定对应的特性(featureDependency),基于特性功能对不同租户显示或者隐藏定义的条目。
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));
//生产订单 - 生产管理可见
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定义,并设定参数选项:
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订阅
- 前端用户创建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发布(数据推送)
监测实体事件(CreatedEvent、UpdatedEvent、DeletedEvent)数据,按租户用户创建的Webhook订阅,推送数据:
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()方法:基于具体的订阅(内部接口、第三方接口等)推送对应的内容:
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);
//内部接口
if (isWH)
{
_appWebhookPublisher.PublishWebhookUOW(whName, data, tenantId, whCache.Names[whName], false);
}
//企业微信内部群
if (isWX)
{
var wxData = new WxTCardWebhookDto { template_card = GetWxTCard(data, tenantId, nu) };
_appWebhookPublisher.PublishWebhookUOW(whNameWX, wxData, tenantId, whCache.Names[whNameWX], true);
}
//钉钉内部群
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()方法:实现按租户缓存Webhook订阅的数据:
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的默认实现,直接按传入的订阅,通过WebhookSenderJob推送数据:
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
};
//指定队列执行任务,由触发事件的server执行
IBackgroundJobClient hangFireClient = new BackgroundJobClient();
hangFireClient.Create<WebhookSenderJob>(x => x.ExecuteAsync(jobArgs), new EnqueuedState(AppVersionHelper.MachineName));
}
}
- WebhookSenderJob:重写WebhookManager的SignWebhookRequest方法,对于第三方接口,不添加签名的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方法,对于第三方接口,进行特殊处理:
protected override HttpRequestMessage CreateWebhookRequestMessage(WebhookSenderArgs webhookSenderArgs)
{
return webhookSenderArgs.WebhookName switch
{
AppWebHookNames.JST_supplier_upload => JSTHttpRequestMessage(webhookSenderArgs), //聚水潭 - 供应商上传
//...
_ => new HttpRequestMessage(HttpMethod.Post, webhookSenderArgs.WebhookUri)
};
}
- WebhookSenderJob:重写WebhookSender的AddAdditionalHeaders方法, 对于第三方接口,不添加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方法,处理第三方接口的回传数据:
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();
//第三方接口,需要处理回传的数据
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);
}
总结
基于ABP/ZERO的Webhook功能实现,进行一些扩展改造,可以实现业务数据按用户订阅进行推送,包括推送到第三方接口(企业微信群、钉钉等),在很大程度上提升了业务系统的灵活性与实用性。
扩展ABP的Webhook功能,推送数据到第三方接口(企业微信群、钉钉群等)的更多相关文章
- 【转载】利用tasker推送手机短信到企业微信(App或者微信公众号)
本随笔转载自:https://www.52pojie.cn/thread-804477-1-1.html 转载声明:本文转自酷安tasker评论区 ID:随风荡 的评论,我只是按照我的需求进行了一次修 ...
- 使用SignalR ASP.NET Core来简单实现一个后台实时推送数据给Echarts展示图表的功能
什么是 SignalR ASP.NET Core ASP.NET Core SignalR 是一种开放源代码库,可简化将实时 web 功能添加到应用程序的功能. 实时 web 功能使服务器端代码可以立 ...
- Asp.net Core3.1+Vue 使用SignalR推送数据
本文就简单使用 往前端页面推送消息 SignalR 是什么 SignalR是一个.NET Core/.NET Framework的开源实时框架. SignalR的可使用Web Socket, Serv ...
- java接口对接——调用别人接口推送数据
实际开发中经常会遇到要和其他平台或系统对接的情况,实际操作就是互相调用别人的接口获取或者推送数据, 当我们调用别人接口推送数据时,需要对方给一个接口地址以及接口的规范文档,规范中要包括接口的明确入参及 ...
- SQL Server 2000向SQL Server 2008 R2推送数据
[文章摘要]最近做的一个项目要获取存在于其他服务器的一些数据,为了安全起见,采用由其他“服务器”向我们服务器推送的方式实现.我们服务器使用的是SQL Server 2008 R2,其他“服务器”使用的 ...
- WebService推送数据,数据结构应该怎样定义?
存放在Session有一些弊端,不能实时更新.server压力增大等... 要求:将从BO拿回来的数据存放在UI Cache里面,数据库更新了就通过RemoveCallback "告诉&qu ...
- Flume推送数据到SparkStreaming案例实战和内幕源码解密
本期内容: 1. Flume on HDFS案例回顾 2. Flume推送数据到Spark Streaming实战 3. 原理绘图剖析 1. Flume on HDFS案例回顾 上节课要求大家自己安装 ...
- SuperSocket主动从服务器端推送数据到客户端
关键字: 主动推送, 推送数据, 客户端推送, 获取Session, 发送数据, 回话快照 通过Session对象发送数据到客户端 前面已经说过,AppSession 代表了一个逻辑的 socke ...
- httpclient post推送数据
客户端代码 /** * 从接口获取数据 * @param url 服务器接口地址 * @param json 传入的参数 若获取全部,此项为空 * @return 返回查询到的数据 * @throws ...
- hive向es推送数据
第一步:首先要保证网络是通的,很多公司里子网遍布,要和运维和工程侧同事确认好网络是通的,es的地址可以通过curl es地址的方式测试一下. 第二步:下载需要的jar包,必须的是es-hadoop的包 ...
随机推荐
- 渗透测试-struts2攻防环境搭建拿shell
一.下载Jspstudy 打开目录D:\JspStudy\tomcat\webapps 二.打开struts2并进行拿shell 1.打开struts2 在浏览器中输入网址http://localho ...
- 近期SQL优化实战分享
分享一下本周SQL优化的两个场景. 如果能对读者有一定的启发,共同探讨,不胜荣幸. 版本信息:mysql,5.7.19 引擎: innodb 场景1 我们有一张常口表,里面的数据由各种数据源合并而来, ...
- 文心一言 VS 讯飞星火 VS chatgpt (27)-- 算法导论5.1 2题
二.请描述 RANDOM(a,b)过程的一种实现,它只调用 RANDOM(0,1).作为a和b的函数,你的过程的期望运行时间是多少? 文心一言: 假设我们有一个伪随机数生成器,可以生成在[0,1)范围 ...
- Java中读取用户输入的是谁?Scanner类
前言 我们在初学 Java 编程的时候,总是感觉很枯燥乏味,想着做点可以交互的小系统,可以让用户自由输入,系统可以接收做出反映.这就要介绍一下 Java 中的 Scanner 类了. 一.Scanne ...
- CF1810G The Maximum Prefix
经典套路. 题意 你将随机生成一个长度为 \(k\) 的数组 \(a\),其中 \(a_i\) 有 \(p_i\) 概率为 \(1\),否则为 \(-1\).定义其前缀和数组 \(s_i = \sum ...
- 洛谷 P9047 [PA2021] Poborcy podatkowi
题意 给一棵有边权的树,从中选出若干条长度为 4 的路径,要求边不交,求最大权值和. 数据范围:\(1\le n\le 2\times 10^5, -10^9\le w\le 10^9\). 题解 考 ...
- Dash应用页面整体布局技巧
本文示例代码已上传至我的Github仓库:https://github.com/CNFeffery/dash-master 大家好我是费老师,对于刚上手dash应用开发的新手朋友来说,如何进行合理且美 ...
- 如何通过AWS的云安全服务保护企业数据
目录 随着企业数字化程度的不断加深,数据安全和隐私保护成为了企业面临的新的挑战.在数字化转型的过程中,企业需要处理大量的数据,这些数据的安全性和隐私保护的重要性不言而喻. AWS 云安全服务是Amaz ...
- FPGA加速技术:在数据中心和云计算中的应用
目录 1. 引言 2. 技术原理及概念 3. 实现步骤与流程 3.1 准备工作:环境配置与依赖安装 3.2 核心模块实现 3.3 集成与测试 4. 应用示例与代码实现讲解 4.1. 应用场景介绍 4. ...
- 如何构建您的第一部AWS数据库服务
目录 2.1. 基本概念解释 2.2. 技术原理介绍 2.3. 相关技术比较 实现步骤与流程 2.3.1 准备工作:环境配置与依赖安装 2.3.2 核心模块实现 2.3.3 集成与测试 4. 应用示例 ...