生产环境配置完生产通道之后,请务必使用测试人群包创建任务进行至少一次全链路性能测试,确保当前的QPS、batchSize等设置合理,避免生产过程中出现打爆下游接口的情况。
当前通用webhook的能力边界:(2022-08-19)
支持发送请求体为任意json结构的http请求
支持发表单,允许表单中字段值为json结构体
支持接受并解析任意json回包/回执,并支持批量回执
支持下发流水号/消息ID,但是格式固定,长度小于20字节
支持解析客户回包中的流水号/消息ID
尽管我们支持客户自定义消息id回传给我们,但我们强烈建议客户使用gmp下发的消息id,以便保证webhook调用的幂等性,避免因失败重试等导致用户重复触达等客情问题
支持被动接受json回执,但是是基于流水号/消息ID的单个回执
支持主动轮询json回执,但是是基于流水号/消息ID的单个查询
支持批量发送与批量响应
支持kafka/rmq的发送与接收
如何判断gmpWebhook是否可以承载客户业务?
gmpWebhook本质是通过产品化配置直接构造http请求访问客户接口,因此需要客户接口请求响应的数据结构可以直接给出,或者可以直接给出示例curl命令或示例报文数据,而不是只能给出黑盒sdk或代码实现。
客户接口请求参数全部都可以从cdp取出、通过触达配置进行配置、从webhook模板结构中抽取,或者经过简单纯计算过程转换得到,如字符串拼接、值映射等操作。
推送过程必须是一次性完成的,即调用一次接口就可以完成对于一个人(或多个人)的触达,而没有任何前置或后置接口调用操作。
客户自定义接口示例可参考文档 webhook接口示例
视频版配合使用:播放视频
通用对接能力示例:GMP通用对接能力示例
4.3版本diff:
抽象整合出上行通道配置,初步支持gmp与外部系统的双向可配置化数据流动
webhook回执配置从webhook通道配置中析出,成为独立配置,webhook通道配置可以选择对应的回执配置
webhook回执新增支持定时批量查询回执(无游标),初步支持定时批量查询机制
新增webhook上行消息通道,支持通过可配置的方式接收用户上行消息(4.3版本该功能尚未完全成熟,存在性能问题,预计在4.9版本完成优化)
webhook之外的业务初步接入通用对接能力
如上图所示,调用外部接口时,可以对请求地址、鉴权方式、HTTP方法、完整的请求参数以及请求响应解析规则进行配置。除此之外,为了进一步提高通用性,GMP还允许根据客户接口实际情况输入自定义请求处理脚本和自定义响应处理脚本,分别对组装的请求和收到的响应进行处理。
外部接口的调用流程如下所示:
暂时无法在飞书文档外展示此内容
以下分五个步骤讲解如上配置:
含义:客户侧HTTP接口url:可以是一个完整的url,如果客户接口url中带有query参数变量,则对应参数可以先不写在这里,而是在下文的query配置中进行配置。
示例:
需要注意的是,如果选择了GET方法,则不可以设置请求体,即下述的
Content-Type
和body配置
这两项配置都会隐藏。
Content-Type含义:会影响组装出的请求的Header中的Content-Type
值,也会影响请求体的组装逻辑;当前支持application/json
和multipart/form-data
两种。
application/json
:请求Header中将包含Content-Type:application/json
;请求体也会是一段json字符串。此时body配置会首选允许客户根据自身接口请求输入一段对应的json,并按照实际需要将其中的一些字段的值替换为对应的占位符,从而解析出对应的参数。
例如,假设某客户需要通过webhook调用其自有的消息平台对用户进行触达,其接口需要接收如下请求:
{ "recipient": "xxxx", // 触达目标id "recipientType": "xxx", // 触达目标id类型 "msg": "xxxx", // 触达消息内容 }
则该客户可在此输入如下jsonBody:
点击解析
之后,GMP会将jsonBody中的解析出来,成为对应的可设置的参数:
可以基于客户接口的实际情况,选择这些参数的类型和取值等,这里支持的参数类型取决于实际业务场景,将在后文对应处讲解。
在最终发送时,请求体就是上文中配置的jsonBody,展示其中的占位符均被替换成了对应的参数内容。可以放心的是,尽管jsonBody占位符的类型都是字符串,但是在发送时会被整个替换成对应的类型和值,可能是数字,也可能是数组、对象等。
参数的类型的可选范围与当前业务场景有关,后文详述。
multipart/form-data
: 请求Header中将包含Content-Type:multipart/form-data;boundary=xxxx
;请求体也会是由配置的body参数组成的表单。
contentType未来可基于客户实际需求进行扩展。如有客户需要支持xml等其他格式,可以与对应GMP侧PM及RD沟通。
不排除客户侧接口需要携带某些header参数或者query参数,可以在此设置,设置方式与设置body参数一致。
以上配置产生的请求可能最终仍然不能匹配上客户的接口,则还可通过自定义请求处理脚本再进行一轮处理。
自定义请求处理脚本接受固定原型的JavaScript函数,其中对象参数request包含如下字段,可供脚本处理:
type GlueInput struct { Method string // http请求方法,取值为 "POST" 或 "GET" Header map[string]string // 请求组装得到的请求Header Scheme string // 请求协议,取值为 "http" 或 "https" Host string // 请求Host Path string // 请求路径 QueryParams map[string]string // 请求中携带的query参数 Body string // 请求体字符串 }
自定义请求处理脚本的输出应当也包含上述字段,GMP将基于脚本输出组装出下一步的请求。
示例一: 假设经过第一步组装请求,获得如下请求:
// POST https://example.com/touch?code=1001&action=send // Body: { "recipientId": "13422145048", "recipientType": "mobile", "deviceType": "android" }
然而客户侧接口要求,如果recipientType的取值为"mobile",则改为"phone";如果deviceType取值为"ios",则改为"xxx_ios";如果取值为"andriod"或"harmony",则改为"xxx_andriod"。则可以写入如下脚本:
function process(ctx, request) { let jsonBody = JSON.parse(request.Body) if (jsonBody.recipientType === "mobile") { jsonBody.recipientType = "phone" } if (jsonBody.deviceType === "ios") { jsonBody.deviceType = "xxx_ios" } else if (jsonBody.deviceType === "andriod" || jsonBody.deviceType === "harmony") { jsonBody.deviceType = "xxx_andriod" } request.Body = JSON.stringify(jsonBody) return request }
示例二:
假设经过第一步得到如下请求体:
{ "send_id": "niezhicheng@bytedance.com", "code": "10065" "content": { "param1": "zvip.cn/Suc2", "param2": "bytedance" } }
然而客户侧接口希望将对象content编码成字符串,因此可以写如下的脚本:
function process(ctx, request) { jsonBody = JSON.parse(request.Body) jsonBody.content = JSON.stringify(jsonBody.content) request.Body = JSON.stringify(jsonBody) return request }
示例三: 假设经过第一步生成如下请求:
{ "send_id": "niezhicheng@bytedance.com", "code": "10065" "params": { "param1": "zvip.cn/Suc2", "param2": "bytedance" } }
然而客户侧接口希望我们的最终请求体是按照如下方式编码得到的字符串:{send_id};{code};{paramlist}
;其中,paramlist
表示将params中的value按照key进行排序并使用分号进行拼接得到的字符串。则可以写入如下脚本:
function process(ctx, request) { var jsonBody = JSON.parse(request.Body) var res = jsonBody.send_id + ";" + jsonBody.code var keys = Object.keys(jsonBody.params) keys.sort() for (var i in keys) { res = res + ";" + jsonBody.params[keys[i]] } request.Body = res return request }
为降低脚本代码编写工作量,我们在所有需要编写脚本的地方都预置了ctx函数,目前主要有两类,如需添加其他预置函数欢迎找GMP产研提需求,我们也会不断着眼实际使用需求添加通用可用的内置函数。
提供一些签名/编码算法
如下示例:对当前的请求体使用hmac-sha1算法计算签名并写入header的signature字段中
function process(ctx, data) { data.Header['signature'] = ctx.getSignHandler().calculate(data.Body, "hmac-sha1", "9x8U2J13") return data } function process(ctx, request) { request.Header["aes"] = ctx.getSignHandler().calculate( request.Body, "aes/gcm/noPadding/base64", "1ca9dfa37f6d422d81a4f9a6832299b5", "1ca9dfa37f6d422d") return request }
calculate参数说明:
参数一 | 签名算法输入 |
---|---|
参数二 | 签名算法,当前支持: |
参数三 | 可选参数,部分算法可能会需要密钥,就可以填到参数三 |
参数四 | 可选参数,算法aes/gcm/noPadding/base64需要设置盐值,通过该参数传入 |
用于做数据格式转换,目前支持json和urlEncoded之间的相互转换
使用场景:客户系统的请求响应数据可能都是urlEncoded格式的,但gmp只能输出/理解json格式数据。则此时可以在请求处理脚本中将gmp的json请求体转成urlEncoded(注意记得同时改Header哦~);以及在响应处理脚本中将客户的urlEncoded数据转成json
如果需要添加其他格式转换机制,欢迎联系gmp产研
示例:客户发来的数据是urlEncoded格式的,我们需要转成json格式以进行后续解析(urlEncoded转json)
function process(ctx, data) { var convRes = ctx.getDataConverter().bodyConv(data.Body, 'url_encoded', 'json') if (convRes.ErrMsg !== '') { // 转换失败,报错返回 data.ErrMsg = convRes.ErrMsg return data } var jsonBodyStr = convRes.Output var jsonBody = JSON.parse(jsonBodyStr) // ... 其他逻辑 data.Body = jsonBodyStr return data }
bodyConv参数说明
参数一 | 用于进行转换的数据 |
---|---|
参数二 | 数据的当前格式(url_encoded、json) |
参数三 | 数据的目标格式(url_encoded、json) |
效果:
GMP当前支持两种鉴权方式,密钥鉴权、oauth鉴权(basic auth设计歪了,先忽略...)
密钥鉴权即通过密钥计算一个签名放在请求中的某处。我们默认密钥鉴权是选用某种算法对请求体进行计算,因此当客户侧接口使用这类鉴权方式时,可以在此选择使用的算法,并配置签名在请求中的位置:
除此之外,我们支持客户自定义密钥算法。当客户选择“自定义算法”时,可以输入自定义鉴权脚本,并无需再设置签名的位置、key、密钥等参数。自定义鉴权脚本的函数原型与前文所述的自定义请求处理脚本一致。
由于在这里可能需要引用一些库函数进行计算,我们在服务端准备好了crypto-js库,可以正常使用如下命令进行引用,该库详情可见链接https://www.npmjs.com/package/crypto-js
let crypto = require('crypto-js')
如果在对接过程中发现需要依赖其他js库或者一些通过简单的JS脚本无法实现的能力,可以联系GMP研发@聂志成 进行添加支持的js es5库或者经过抽象处理后注入一些可以通用的功能函数。但是加库或者加函数必须要考虑安全性问题,避免允许写一些危险脚本,如访问本地文件、访问http等。
仅支持密码模式的oauth2.0鉴权类似机制:
配置页面如图所示,其配置可以视为一个无需鉴权的基础通用配置。token地址就是令牌请求地址;请求方法、header/query配置、content-type、自定义请求/响应处理脚本、成功/失败响应配置与前文一致。
自定义响应处理脚本、成功/失败响应配置的介绍详见后文。
除了通用的外部接口调用配置之外,此处还有三个配置:
access_token的jsonPath:oauth令牌在该接口响应中的位置。GMP只有在接口响应判定为成功时才会尝试从中解析出令牌。
如假设响应为如下json,其中token的值就是访问令牌,那么该处应该填入$.data.token
。
{ "status": 0, "data":{ "token": "asfklghasericbajHIYUGcuie" } }
access_token的有效时长:顾名思义,单位为秒。需要简单介绍一下GMP的token刷新规则:GMP会每半小时轮询检查一次本地配置,如果某个token的上次刷新时间距今已超过其有效期的1/3,则会基于其oauth配置,请求并刷新这个token。
access_token的使用配置:GMP拿到令牌后,会在每次调用客户接口时使用这个令牌。客户系统可能会要求这个令牌放在请求头部或者请求参数中(应该不会要求放在请求体里面吧...),并且有一个指定的key。因此可以在此指定令牌在请求中的位置和字段名。Value则是对应的值,因为考虑到客户接口要求的可能不只是一个token,而是要做一些组装,加个前缀后缀啥的。因此需要在Value中填入一个带占位符${gmp_k_oauth_token}
的表达式。该占位符会在实际调用接口时被替换成令牌。
假设客户接口的要求如下:
http://xxx.com/xx/xx?access_token=[访问令牌]
则做如下配置:
也有客户侧接口做如下要求:
http://xxx.com/xx/xx --header 'Authorization: Basic [访问令牌]'
则应当做如下配置:
点击保存时,GMP便会基于当前配置访问令牌接口并按照配置尝试从中获取令牌,只有成功获取到一个非空的令牌,GMP才会保存当前的oauth配置,否则会报错返回。
响应判定即基于响应判断本次请求是否成功。其配置界面如下所示。可以在这里配置一系列的成功/失败响应判定规则。每条规则都可以指定jsonPath、比较符、比较值、http状态码,失败响应规则还能配置该失败情况对应的提示语,用以优化结果可读性。
规则匹配与最终判定伪代码:
func matchRule(rule, resp) bool { if rule.statusCode != resp.statusCode {return false} if rule.jsonPath == nil && rule.operator == nil && rule.value == nil {return true} val, err = jsonPathLookUp(resp.body, rule.jsonPath) if err != nil {return false} return compare (val, rule.operator, rule.value) } func judgeResp(config, resp) bool { if config.successRules == nil && config.failRules == nil {return true} for rule in config.successRule { if matchRule(rule, resp) {return true} } for rule in config.failRules { if matchRule(rule, resp) {return false} } return config.successRules == nil }
存在一些可能,客户直接返回的响应比较...刁钻,需要进行简单的转换才能进行成功与否的判断,因此可以先试用自定义响应处理脚本进行处理,再进行响应判定。
{ Body string StatusCode int64 }
例如,有客户的接口在收到请求后会进行多个子通道的下发,并发挥其在每个通道的下发登记,其响应可以抽象成如下json:
{ "result":[ {"status": 1}, // status == 1 表示成功 {"status": 2}, // status != 1 表示失败 ] }
客户认为,只要有一个子通道成功就算成功,这一结果判定难以直接使用jsonPath表达(其实也可以,只是会表麻烦)。此时可以引入自定义响应处理脚本:
function process(ctx, response) { const jsonBody = JSON.parse(response.Body) jsonBody["IsSuccess"] = 0 for (let i = 0 ; i < jsonBody.result.length; i ++ ) { if ( "status" in jsonBody.result[i] && jsonBody.result[i]["status"] === 1) { jsonBody["IsSuccess"] = 1 } } response.Body = JSON.stringify(jsonBody) return response }
以上脚本会在客户响应中塞入一个字段IsSuccess
,从而使得成功/失败响应配置可如下:
选择“自定义接口接入”
webhook通道配置分四块:
基础配置
外接模板配置
webhook请求响应配置
回执配置
配置webhook名称和webhook类型
同一GMP项目下,webhook名称不允许重复
webhook类型用于智能触达等功能模块,暂时可以不用关注
允许在客户侧拉取触达模板,实现更加灵活的触达配置。所谓触达模板,就是一段带占位符的字符串,如下所示。对接触达模板可以使webhook通道获得动态参数的能力,即可以基于触达模板中的参数动态产生多个通道参数。使得一个通道可以对接多个模板,而无需为每个模板配置一个通道,可以极大地减少重复配置和机械劳动。
// 模板示例 【xx打车】司机已接单,${car},请尽快赶往上车点 尊敬的客户${username},您的礼包${gift}将于${expire_date}过期
外界模板配置界面:请求地址、鉴权方式、请求方法、Content-type、body/header/query配置、自定义请求/响应处理脚本、成功/失败响应配置等含义可见上文。
需要注意的是,对于body/header/query参数配置,其参数类型可选项如图所示:
常量:顾名思义,就是个常量参数
GMP保留字:支持在请求中插入当前页码和页规格,以支持分页接口
字符串:将成为模板搜索页面的一个文本搜索项
下拉单选框:将成为模板搜索页面的一个下拉选择框
参数配置需要按照客户接口实际情况完成。
模板数组的jsonPath:我们默认客户侧提供的接口都会返回一个触达模板列表,因此需要提供这个模板数组在客户接口响应中的位置
模板数量的jsonPath:我们希望客户接口返回当前搜索条件下的模板数量,以方便前端进行分页展示,因此需要提供这个数量在响应中的位置
模板内容的相对路径:我们理解的模板至少会有两个字段:模板id、模板内容,因此必定是一个json对象。我们需要客户提供模板内容在模板内容在模板中的路径,从而找出这个模板进行解析
占位符的正则表达式:客户模板千奇百怪,不同客户的模板中参数的占位符也可能不同,我们将基于客户配置的这个正则表达式对模板进行解析。
列表展示字段:为了方便客户在配置触达任务时选择正确的模板,必须在模板列表中国展示模板中的一些信息,我们支持客户配置这样的key-value对,用以决定模板列表将展示那些字段,以及每个字段的值如何获取。
模板内容字典:有时模板中有些字段可能很长,不方便在列表中展示,可以在此设置,以单独查看
客户侧接口如文档所示:webhook接口示例
其配置如图所示:
最终该通道的模板搜索页面如图所示:
可查看的详细内容:
请求响应配置如下。除了基础通用配置的配置项之外,有几点需要额外介绍。
可以选择本通道是单次发送还是批量发送,如果选择批量发送,则会出现如下额外配置:
批量发送处理脚本的函数原型与前文的自定义请求处理脚本一致。需要注意的是,这个request是由本次发送的多个子请求组合而来,其中每个子请求都是经过请求组装和自定义请求处理脚本得到的。其组合方式为:请求头部和请求参数取自请求列表中的某一个子请求,而请求体是一个字符串数组的json编码,其中每个字符串都是一个子请求的请求体字符串。
暂时无法在飞书文档外展示此内容
假设客户接口请求体规范如下所示:
{ "msg_count": 1, // 本次请求的消息数量 "app_id": 1234, // 常量 "time_stamp": 1660825544000, // 请求发出的毫秒时间戳 "msg_list": [ // 子请求列表 { // ... } ] }
假设基础配置中得到的请求体是:
{ "target_id": "xxx", "messsage": "yyy" }
则批量脚本的输入中 request.Body为(假设每次发送id数为2):
[\"{\\\"target_id\\\":\\\"xx1\\\",\\\"messsage\\\":\\\"yyy1\\\"}\",\"{\\\"target_id\\\":\\\"xx2\\\",\\\"messsage\\\":\\\"yyy2\\\"}\"]
则其批量处理脚本:
function process (ctx, req) { let jsonBody = JSON.parse(req.Body) let bodyList = new Array() for (let i = 0; i < jsonBody.length; i ++) { // 输入的body是一个字符串数组,还需对每个字符串进行json解码,才能得到每个单独的请求 bodyList[i] = JSON.parse(jsonBody[i]) } let newBody = new Object() newBody.msg_count = bodyList.length newBody.app_id = 1234 newBody.msg_list = bodyList newBody.time_stamp = Date.parse(new Date()) req.Body = JSON.stringify(newBody) return req }
需要指出的是,如果不设置批量发送胶水层,则将发出的请求体是一个由所有子请求的请求体组成的json对象数组,如下所示:
[ { "targetId":"user1", "content": "content1" }, { "targetId":"user2", "content": "content2" } //... ]
类型 | 写入内容 | 样式 |
---|---|---|
字符串 | 字符串 | |
文本 | 可以插入用户id,用户属性,用户标签,短链 | |
数值 | 整数 | |
小数 | 小数 | |
数组 | 支持非对象 | |
对象 | 可以多层嵌套 | |
结构体数据 | map格式,value只支持字符串或者文本,支持在任务触达配置中手动动态添加字段。 | |
日期 | 前端传当前日期 | |
日期时分秒 | 前端传当前日期时分秒 | |
图片 | 将文件上传到minio,webhook字段中存储minio的链接 | |
单选下拉选择框 | 需要配置的时候添加选项,支持文本上传配置选项 | |
多选下拉选择框 | 需要配置的时候添加选项,支持文本上传配置选项 | |
单选下拉输入框 | 需要配置的时候添加选项,支持文本上传配置选项。可以在任务配置的时候新增选项 | |
多选下拉输入框 | 需要配置的时候添加选项,支持文本上传配置选项。可以在任务配置的时候新增选项 | |
动态参数 | 仅能同时选择用户id/用户属性/用户标签 |
除此之外,还额外添加了一些参数类型:
如果客户在触达配置中选择了webhook模板,则该参数的值将会由客户选择的模板参数及其值组成,至于如何组成取决于第三项下拉框的选择。假设客户接口请求体如上图中json配置所示,其选择的模板及填写的值如下图所示。
则在不同选项下产生的请求体如下:
结构体:
{ "params": { "address": "xx小区菜鸟驿站", "express": "顺丰" } }
编码:
{ "params": "{\"address\":\"xx小区菜鸟驿站\",\"express\":\"顺丰\"}" }
展开:原字段params被删除,取而代之的是一组模板占位符参数。(不是很建议用这个,因为可能会有key冲突,但是如果客户的存量接口就是这么设计的话...我们也能支持
{ "address": "xx小区菜鸟驿站", "express": "顺丰" }
当客户使用webhook模板时,可能会希望GMP在进行webhook发送时除了带上模板占位符参数,还会希望带上当前使用的模板自身的一些属性。此时可以使用外接模板参数满足客户需求,只需要在配置上所需参数在模板中的jsonPath便可。比如,假设客户除了外界模板占位符,还需要我们传模板的code字段的值,则可如上图进行配置。
GMP保留字机制为webhook发送一些特殊的值预留了扩展空间,当前支持以下保留字。该列表可以持续扩展,如需扩展,请及时联系@刘泽宇 @聂志成 加需求。
消息id | GMP会基于snowflake算法为每一次webhook调用生成一个消息id,该消息id长度14字节。 |
---|---|
发送id | 发送目标在当前通道的发送id类型下的id |
发送id类型 | 当前通道的发送id类型 |
任务创建人 | 当前触达任务/流程画布的任务创建人 |
任务所在资源组 | 当前触达任务/流程画布所在的资源组 |
可以理解为一种在通道配置中配置好的动态参数,设置好后将不出现在触达配置中,减少客户运营配置工作,避免客户运营出错
3.12新增消息队列配置。支持gmp通用webhook通过kafka/rocketMq进行发送;也支持通过kafka/rocketMq接受回执。
设置消息队列配置名称
设置配置类型:
请求配置:gmp通用webhook可以给予该配置将数据塞入指定的消息队列
回执配置:gmp将监听并消费该消息队列,从中得到通用webhook的回执
配置代码:特定于具体的消息队列类型与配置类型,输入一段json,表明当前消息队列的一些参数:
务必不要创建两个完全一样的消息队列配置,否则无法创建producer,或无法启动consumer;即相同配置只能启动一个producer或consumer
mq_type:消息队列类型,必填。当前支持的枚举值:rocketMq,kafka。
rocketMq相关配置:
{ "mq_type": "rocketMq", "name_svr_addr":[ "192.168.2.143:9876", "192.168.2.200:9876", "192.168.2.74:9876" ], "topic":"gmp_webhookb_rmq", "tag":"tag1" // 有些客户会给,没给也可以不填 }
{ "mq_type": "rocketMq", "name_svr_addr":[ "192.168.2.143:9876", "192.168.2.200:9876", "192.168.2.74:9876" ], "topic":"gmp_webhookb_rmq_callback", "MessageSelector":{ // 有些客户会要求只消费带有某些tag的消息,此时可以在此如此配置。 "enable":true, "type":"TAG", "expression":"tag4 || tag5 || tag6"// 表示可以消费tag4或者tag5或者tag6, 根据实际要求填写,多tag可以使用||连接,表示“或” }, "group_name": "" // 消费组 }
kafka相关配置:
{ "mq_type": "kafka", "send":{ "brokers":[ "192.168.2.137:9192", "192.168.2.143:9192", "192.168.2.200:9192", "192.168.2.213:9192", "192.168.2.65:9192", "192.168.2.74:9192" ], "topic":"gmp_webhookb_send" }, "version":"2.1.0" // kafka版本,可以先默认这个 }
{ "mq_type": "kafka", "listen":{ "brokers":[ "192.168.2.137:9192", "192.168.2.143:9192", "192.168.2.200:9192", "192.168.2.213:9192", "192.168.2.65:9192", "192.168.2.74:9192" ], "topics":["gmp_webhookb_callback"], "group": "" }, "version":"2.1.0" // kafka版本 }
在webhook请求/响应配置中选择发送方式,然后选择对应的请求消息队列配置:
在webhook回执中选择对应的回执消息配置:
gmp当前支持两种上行方向,四种上行机制:
上行方向 | 自定义回执(客户系统=>gmp) | gmp轮询(gmp=>客户系统) | ||
---|---|---|---|---|
上行机制 | http推送 | 消息队列推送 | 消息id轮询 | 定时轮询 |
http推送方式配置:
消息队列推送方式:
客户系统主动将回执消息推送给gmp,可以选择通过http推送或者通过消息队列推送。两者配置内容类似。如果选择消息队列推送方式,需要额外选择该通道所消费的消息队列的配置id。
客户系统主动推送消息时,可能会存在通道复用的情况。简言之,如多个不同的上行通道的消息共用一个「物理通道」进入gmp。此时就需要为每一个通道配置一个「消息处理与判断脚本」来判断当前消息是否应当由当前上行通道进行处理
共用物理通道在此指经由同一个http接口进入gmp,或者通过同一个消息队列consumer消费进入gmp(topic、tag、consumerGroup等均相同)
举例而言,假设客户企业存在一个消息中台,gmp只能向其注册一个回执地址。该消息中台同时拥有站内信、短信、push、权益等多种用户触达能力,且均与gmp完成了webhook对接或权益对接。在gmp通过webhook通道或权益通道对用户完成任何一种触达、或者消息中台收到用户短信回复之后,消息中台均可能会向gmp主动推送对应回执,且这些回执的处理机制各不相同。但是这些回执都只能通过同一个http接口进入gmp。因此gmp需要一种可配置低代码的方式来表达如何对这些消息进行正确的分流。
暂时无法在飞书文档外展示此内容
基于以上例子,当客户消息中台向gmp发送一条push的回执之后,gmp会找到所有「http上行通道」(如果是通过消息队列发送,则会找到所有使用该消息队列配置的上行通道)。遍历这些通道,以当前消息为输入,逐个运行上行通道的「消息处理与通道判定脚本」,当某个通道的脚本返回true时,停止遍历,并由该上行通道的上层业务接收并处理该消息。
实际编写脚本逻辑如下:
函数参数data中包含以下字段:
字段名 | 数据类型 | 含义 |
---|---|---|
Method | 字符串 | 请求方法 (Post 或 Get 等) |
Header | 字典 | 请求头 |
Scheme | 字符串 | url协议(http或https等) |
Host | 字符串 | 请求域名 |
Path | 字符串 | 请求url的path |
QueryParams | 字典 | 请求url中的参数 |
Body | 字符串**(utf-8)** | 原始请求体 |
编写该脚本时,需要基于所有可能共用物理通道的请求各方的特性进行判断,如一些关键字段的取值等。如果确认当前消息应当由当前通道处理,则为data.Hit赋值为true,在处理结束之后返回data即可。
举例而言,假设客户的回执消息结构如下所示:
{ "msg_type": "send_res" // 消息类型:send_res(发送结果,用于表征消息实际达到);user_reply(用户回复消息);coupon_write_off(权益核销) "send_channel": "sms" // 假如是消息类型是send_res,则该字段非空,可能是 sms\batch_sms\push\inbox // ... 其他字段 }
则对于push回执的上行通道,其脚本可做如下编写
function process(ctx, data) { var jsonBody = JSON.parse(data.Body) if (jsonBody.msg_type == 'send_res' && jsonBody.send_channel = 'push') { data.Hit = true } return data }
由于自定义响应生成脚本是与上行通道强绑定的,因此其只能作用于独立api;对于统一api,由于会存在多个上行通道,且所有消息处理逻辑,包括通道判定逻辑均是异步处理的,因此不支持自定义响应,此时gmp只会返回标准响应。
gmp收到客户系统的http请求后的http响应,在默认情况下,其状态码为200,其规范格式为:
{ "code": 0, // 0代表成功,其他代表失败 "extra_msg": "xxx" // code非零时,该字段说明详细错误原因 }
由于上行消息都采用异步处理,即简单进行消息封装后便塞入消息队列等待处理,因此绝大多数情况下响应都是成功的
然而在很多情况下,客户系统可能对gmp的响应存在要求,需要gmp接口的响应也符合客户系统的规范,为此,我们提供自定义响应生成脚本,支持客户基于gmp的标准响应和当前原始请求生成客户系统需要的自定义http响应。
暂时无法在飞书文档外展示此内容
该脚本的编写逻辑与其他脚本有所不同,其data数据组成如下:
{ "Header": { "xxx": "yyy" } // 当前响应header,也是标准响应header入参。 "StatusCode": 200, //当前响应http状态码,也是标准响应http状态码入参 "Body": "xxx", // 当前响应体,也是标准响应体入参 "RawReq": { "Method": "GET", //原始请求的方法 GET/POST "Header": {}, // 原始请求的header map "Scheme": "", // 原始请求的协议(http/https) "Host": "", // 原始请求的url host "Path": "", // 原始请求的url path "QueryParams": {}, // 原始请求中的请求参数map "Body": "" // 原始请求体 }// 原始请求信息 }
示例:
假设客户主动上行数据请求体为:
userid=J10003&pwd=26dad7f364507df18f3841cc9c4ff94d×tamp=0803192020&cmd=MO_REQ&seqid=1003
要求gmp的响应格式为:
// 成功: { "cmd":"MO_RESP", "seqid":1003, "result":0 } // 失败: { "cmd":"MO_RESP", "seqid":1003, "result":-100999 }
则脚本可做如下编写:
function process(ctx, data) { // 详见dataConverter一节 var convRes = ctx.getDataConverter().bodyConv(data.RawReq.Body, 'url_encoded', 'json') if (convRes.ErrMsg !== '') { data.ErrMsg = convRes.ErrMsg return data } var jsonBodyStr = convRes.Output // 还原原始请求,取出其中用于构造自定义响应的数据 var jsonBody = JSON.parse(jsonBodyStr) // 构造新的自定义响应 var newBody = new Object() newBody['cmd'] = 'MO_RESP' newBody['seqid'] = jsonBody.seqid // 还原标准响应,基于标准响应了解当前是否处理成功 var standardJsonBody = JSON.parse(data.Body) if (standardJsonBody.code === 0) { newBody['result'] = 0 } else { newBody['result'] = -100999 } data.Body = JSON.stringify(newBody) return data }
对于http推送回执,我们提供两种接口:
统一api /gmp/webhook
:所有http推送回执消息都可以通过该api进入gmp,但必须要能够通过「消息处理与通道判定脚本」确认之后,才能被相应通道及业务逻辑处理
独立api(推荐使用):每个http上行通道都++同时拥有自己的独立api,即消息既可以通过统一api进入,也可以通过独立api进入。由于独立api的消息在被gmp业务处理前不会检查data.Hit
,因此如果确认消息只会通过独立api进入,请勿在「消息处理与通道判定脚本」中对该字段赋true,以确保该通道屏蔽来自统一api的所有消息。可想而知,由于不需要遍历++执行检查脚本进行确认,使用独立api能够得到更好的处理性能。能用独立api则尽量使用独立api
暂时无法在飞书文档外展示此内容
两种gmp轮询机制的基本配置类似,都是需要一个外部接口调用基础通用配置,可通过阅读前文复习此处如何进行配置
适用于针对消息id进行单个查找的回执接口。查询重试机制:
命中某个失败响应规则,且该规则的被设置为可重试
判定为失败,且未命中任何失败响应规则
每个消息id会在两天内不断重试,直至判定为查询成功,其重试规律与时间间隔参照RocketMQ的重试消费机制
适用于流式批量查询客户接口获取回执的场景。当前固定每分钟调用一次。gmp不维护当前游标,需要由客户接口维护。
对于更复杂的定时轮询机制欢迎与gmp产研进行探讨。
过往设计中,每个自定义webhook通道都要配置一套回执配置,但是实际应用场景中,很可能多个自定义webhook对接的是客户的同一个系统或同一个接口,这些通道的回执机制可能完全相同,甚至可能会混在一起发给gmp。因此在4.3版本中,gmp将自定义webhook通道与回执通道进行解耦,从一对一的关系变为多对一的关系。以提高处理性能、支持更灵活的业务场景并降低通道配置工作量
专注于webhook回执消息的解析与判定,与此前版本的回执配置基本一致。
有效性判断:
每条规则可以选择判断方式:前缀判断或者正则表达式判断。继而设置对应的表达式与命中该规则的消息的含义
可以设置多条消息有效性规则,自上而下使用每条规则对当前消息进行判断,如果判断命中,则为当前消息赋予一个“含义”,并进行后续处理
用户上行消息内容:指用户上行消息在上行消息体中的jsonpath
上行消息匹配方式:表示如何为当前上行消息寻找并匹配下行消息。通俗而言,即“查找用户是在回复哪条消息”。
消息id:如果上行消息体中携带了「消息id」,而且是通过gmp自定义webhook下发过的消息的消息id。则会直接找到该消息id对应的下行消息作为对应的下行消息。 需要提供消息id在消息体中的jsonpath
用户id:如果上行消息中携带了用户id,如回复短信中携带了用户手机号,则gmp会找到近三天内对该用户下发的最后一条消息,作为匹配的下行消息。 在此情景下需要提供携带的用户id的id类型,及其在消息体中的jsonpath
需要注意的是,由于用户id匹配涉及到扫描对该用户的所有发送记录,因此当前存在性能问题,每秒的处理qps大约只有20。gmp会对收到的这类上行消息通过消息队列进行异步处理,因此可能会存在上报延迟,这一点需要客户知悉。该问题将在后续版本优化。