#Arch# 企微客服号后台开发小结

#Arch# 企微客服号后台开发小结

本文将介绍一种通用的企微客服号后台设计与实现的方案(仍在改进ing…


前言

企业微信开放了机器人客服号两种方式对会话进行自定义的功能扩展,本文将介绍一种通用的企微客服号后台设计与实现的方案(仍在改进ing… 需要注意的是如何注册申请以及相关接口规范不在本次的讨论范围中。最终 wework-service 应该具备两种通用能力

  1. 对于企微用户而言:响应多种模式指令,执行指定业务逻辑。
  2. 对于其它服务而言:提供原生or聚合的调用企微接口操作。

整体设计

逻辑架构

分层设计架构图

  • wework-service 内部实现采用的是洋葱模型,通过 gin 框架的中间件灵活集成各类插件,包括:消息解析中间件(decrypt XML)、崩溃恢复中间件(panic recover)、日志记录中间件(log request)、流量控制中间件(rate limit)、权限认证中间件(rbac auth)和消息处理控制器(handle controller)。
  • wework-service 外部依赖主要有微服务和数据库。其它微服务绑定响应的逻辑,数据库主要用于存储流量控制和权限认证的相关配置,缓存主要避免企微对令牌的获取限流。

调用关系

用户交互

用户交互时序图

  • Step 1:企微用户与客服号进行单聊or群聊会话,发送文本or混编消息,包含了指令本身和业务数据
  • Step 2-3:加密消息从客服号回调到 wework-service,此时为了避免重试应该及时返回空包
  • Step 4-6:进行消息解析、流量控制、权限认证和消息处理
  • Step 7-9:消息流转到具体的处理策略当中,通过驱动与 other-service 和企微后台进行交互。
  • Step 10-11企微后台直接通过客服号将结果发送给企微用户

服务交互

服务交互时序图

  • Step 1other-serivce 先自行进行业务的处理
  • Step 2-3other-serivce 请求 wework-serivce 操作企微后台
  • Step 4-5企微后台直接通过客服号将结果发送给企微用户

组件设计

消息解析中间件

加解密工具库https://github.com/sbzhu/weworkapi_golang

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
// ParseCallbackMessage verify, decrypt and unmarshal wework callback request
func ParseCallbackMessage(query *model.WeworkCallbackQueryModel, body []byte) (*model.WeworkXMLRecvMsg, error) {
var err error

// verify and decrypt
weworkSvcToken := app.GetConfig().WeworkSvcToken
weworkSvcEncodingAESKey := app.GetConfig().WeworkSvcEncodingAESKey
crypt := wxbizmsgcrypt.NewWXBizMsgCrypt(weworkSvcToken, weworkSvcEncodingAESKey, "", wxbizmsgcrypt.XmlType)
bts, cryptErr := crypt.DecryptMsg(query.MsgSignature, query.Timestamp, query.Nonce, body)
if cryptErr != nil {
err = fmt.Errorf("verify and decrypt wework callback request failed: %s", cryptErr.ErrMsg)
return nil, err
}

// unmarshal xml data
recv := &model.WeworkXMLRecv{}
unmarshalErr := xml.Unmarshal(bts, recv)
if unmarshalErr != nil {
err = fmt.Errorf("unmarshal xml data of wework callback request failed: %s", unmarshalErr.Error())
return nil, err
}

logrus.Debugf("WeworkXMLRecvMsg: %+v", recv.Msg) // better log in middleware
return &recv.Msg, nil
}

// MessageParserMiddleware middleware to parse callback message
func MessageParserMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
var query model.WeworkCallbackQueryModel
if err := ctx.ShouldBindQuery(&query); err != nil {
logrus.Errorf(err.Error())
ctx.JSON(http.StatusOK, nil)
ctx.Abort()
return
}
body, err := ioutil.ReadAll(ctx.Request.Body)
if err != nil {
logrus.Errorf(err.Error())
ctx.JSON(http.StatusOK, nil)
ctx.Abort()
return
}

msg, err := ParseCallbackMessage(&query, body)
if err != nil {
logrus.Errorf(err.Error())
ctx.JSON(http.StatusOK, nil)
ctx.Abort()
return
}

ctx.Set("msg", msg)
ctx.Next()
}
}

权限认证中间件

  • to be continue…

流量控制中间件

  • to be continue…

消息回调控制器

工厂模式与策略模式

工厂模式

CallStrategyFactory 主要解决多种匹配模式和消息格式问题,以支持通过单聊or群聊和文本or混编的方式响应。getTriggerAndDataFromTextgetTriggerFromMixedMessage/getMediaIdAndPicUrlFromMixedMessage 解决匹配模式问题,TextStrategyFactoryMixedMessageFactory 类解决消息格式问题。

1
2
3
4
// CallbackStrategyFactory
type CallbackStrategyFactory interface {
ProduceStrategy(*trigger.Config, *model.WeworkXMLRecvMsg) strategy.CallbackStrategy
}

策略模式

所有策略必须实现 CallbackStrategy.Handle(CallbackContext),换言之定义策略实现了这个接口就可在工厂中注册,投入实际使用,这样非常灵活。这里辨析一点:Handle 传入参数 ctx 指的是完整的消息(包含发送主体和内容),而 Strategy 固有数据成员则往往仅代表业务数据(可以是紧跟在指令后的文本或图片)。

1
2
3
4
// CallbackStrategy
type CallbackStrategy interface {
Handle(ctx *context.CallbackContext)
}

注意

缓存令牌

由于企微后台对获取令牌的接口做了限流保护,如果每次经 wework-service 的流量都要调用一次接口获取令牌,当流量过大时会影响到 wework-service 的正常工作,考虑到令牌有过期时间,实际上也没必要每次都直接请求。我们可以多加一层缓存,避免被限流了同时提高响应速度

缓存令牌流程图

避免重试

由于企微的客服号在回调后若未及时收到空包,将会在短时间内做持续重试。这种情况必须考虑,否则消息将会重复消费以及增加后台处理负荷

  1. 方法一接收和处理的逻辑分离,亦即同步接收异步处理。这里需要注意的点:错误处理不能再以直接返回的方式做,需要写进日志,必要时还可以消息通知用户。这是一种积极避免重试的手段(防患于未然)。
  2. 方法二消息 id 去重,使用缓存记录一定周期内的所有消息编号,并且设置过期时间,对重复的消息直接忽略不做处理。这是一种悲观避免重试的手段(来之则安之)。

参考文献

  1. 企业微信内部服务号CI助手开发总结
  2. 企业微信客服服务号开发小记
  3. 企业微信内部客服号开发小结
  4. 从零开始搭建你的企业微信客服服务号

# Golang

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×