LeanTicket 与微信对接 | LeanCloud Web 应用开发实践(第六期)

Hi,亲爱的用户:

最近我们正以「LeanCloud Web 应用开发实践」为主题,进行一系列的直播及文章分享。并将重写后的 LeanTicket (工单)作为范例,介绍使用 LeanCloud 开发 Web 应用的一些最佳实践,内容包括且不限于:

  1. 使用 LeanCloud 账号系统以及三方 OAuth 授权。
  2. 使用 ACL 保障数据安全。
  3. 使用云引擎的云函数和 hook 函数简化客户端逻辑。
  4. LiveQuery 的使用。
  5. 与微信或其他外部系统对接。
  6. 邮件的发送即接收。
  7. 批量处理/订正数据的实践。
  8. 使用数据缓存提高响应速度,减少存储服务查询次数。

欢迎您加入微信群(文末扫码)进行交流。

第六期课程:LeanTicket 与微信对接

因为客服有时无法保证总是在线,用户难免会有得不到及时响应的担心。为了提高处理效率与用户体验,用户的每一次回复都会通过微信或其他外部系统(如邮件、短信通知)发送给客服人员,来确保问题能及时得到关注。

本期我们来介绍 LeanTicket 如何接入微信,主要内容:

  • 应用接入微信企业账号
  • 发送微信消息到企业成员

讲师

LeanCloud 工程师 陈伟

直播时间

2017 年 9 月 5 日(周二) 20:00

课程地址:https://www.douyu.com/1587969

扫描下方二维码即可加入「LeanCloud Web 应用开发实践」,如有疑问也可加微信「 guixiaoTL」咨询。


往期回顾(末尾文字讲解):

第一期课程:示例应用(LeanTicket)开发环境搭建及功能简介

第二期课程:理清 Web 应用的登录状态

第三期课程:使用 OAuth 接入第三方用户信息

第四期课程:使用 ACL 保护数据安全

第五期课程:使用 LiveQuery 实现多端数据实时同步

视频回放: https://www.bilibili.com/video/av14203697/

微信在 LeanTicket 中的应用场景 00:00

我们将微信消息通知接入 LeanTicket 系统,这样就可以使客服人员更方便的获取工单的一些通知,比如:

  • 新的工单产生
  • 已有工单有了用户新的回复
  • 别的客服将工单转移给自己

提示:本文使用的 LeanTicket 版本 5c228a0

相关链接 01:18

因为工单通知的对象都是公司或者产品的技术支持人员,所以我们直接接入 企业微信 。接入 微信公众平台 方法类似。

LeanTicket 中和微信交互的所有代码都保存在 api/wechat.js 中。

调用微信 API 发送消息 03:33

要调用微信的 API 发送消息或进行其他操作,首先需要配置几个 key:

  • corpid:每个企业都拥有唯一的 corpid,获取此信息可在管理后台「我的企业」-> 「企业信息」下查看(需要有管理员权限)。
  • secret:是企业应用里面用于保障数据安全的「钥匙」,每一个应用都有一个独立的访问密钥,为了保证数据的安全,secret 务必不能泄漏。企业自定义的应用的密钥可以在企业应用的详情里面手动生成。
  • agentid:企业应用的 id,非必需,部分操作需要。

通过上面这些 key,就可以通过 wechat-enterprise-api 初始化得到 api 对象,来调用微信提供的 API(代码):

api = new wechat.API(config.wechatCorpID, config.wechatSecret, config.wechatAgentId,
(cb) => {
  // queryToken
},
(cb) => {
  // saveToken
})

queryTokensaveToken 部分后面「获取微信 API accessToken」部分提到。

LeanTicket 中有几个地方涉及到对微信 API 的调用:

  • 获取企业成员信息
  • 发送微信消息

获取企业成员信息

客服人员可以在个人设置页面看到一个微信账号绑定的下拉列表框,其中会显示企业微信中所有员工名称的列表,客服人员只要选中自己并保存即可完成 LeanTicket 账号与企业微信账号的绑定。

服务端相关 代码

const getUsers = () => {
  // ...
  return api.getDepartmentsAsync()
  .then((data) => {
    if (data.errcode !== 0) {
      throw new Error(`wechat enterprise get departments err: code=${data.errcode}, msg=${data.errmsg}`)
    }
    return Promise.map(data.department, (department) => {
      return api.getDepartmentUsersAsync(department.id, 1, 1)
      .then((data) => {
        if (data.errcode !== 0) {
          throw new Error(`wechat enterprise get department Users err: code=${data.errcode}, msg=${data.errmsg}`)
        }
        return data.userlist
      })
    })
  })
  .then(_.flatten)
  .then((users) => {
    return _.uniqWith(users, _.isEqual)
  })
}

api.getDepartmentsAsync() :获取所有部门列表, 微信 API 文档wechat-enterprise-api 文档

api.getDepartmentUsersAsync(department.id, 1, 1):获取部门成员, 微信 API 文档wechat-enterprise-api 文档

基本流程就是查询所有部门,然后查询每个部门的成员,然后合并去重。

提示:关于 Async 结尾的方法(13:48):因为 wechat-enterprise-api 的 API 是 callback 风格,如果想统一使用 Promise 风格编写代码,可以使用 bluebird 的 promisifyAll 方法包装(代码):

Promise.promisifyAll(wechat.API.prototype)

客户端相关 代码

  componentDidMount() {
    AV.Cloud.run('getWechatEnterpriseUsers', {})
    .then((wechatUsers) => {
      this.setState({wechatUsers})
    })
  }

调用云函数获取企业微信成员列表,并使用 React 展现。

发送微信消息 19:50

账号绑定之后,一旦有事件发生(比如自己负责的新工单产生)就会根据账号绑定关系,向相关微信账号发送消息(代码):

const send = (params) => {
  // ...
  return api.sendAsync({
    touser: params.to
  }, {
    msgtype: 'news',
    news: {
      articles:[
        {
          title: params.title,
          description: params.content,
          url: params.url
        }
      ]
    },
  })
  .catch((err) => {
    // ...
  })
}

api.sendAsync(to, message):发送微信消息。 微信 API 文档wechat-enterprise-api 文档

调用 send 方法的就是一些事件产生,比如:

exports.newTicket = (ticket, from, to) => {
  // ...
  send({
    to: to.get('wechatEnterpriseUserId'),
    title: `${ticket.get('title')} (#${ticket.get('nid')})`,
    content: ticket.get('content'),
    url: common.getTicketUrl(ticket),
  })
}

exports.replyTicket = ({ticket, reply, to}) => {
  // ...
  send({
    to: to.get('wechatEnterpriseUserId'),
    title: `${ticket.get('title')} (#${ticket.get('nid')})`,
    content: reply.get('content'),
    url: common.getTicketUrl(ticket),
  })
}

exports.changeAssignee = (ticket, from ,to) => {
  // ...
  send({
    to: to.get('wechatEnterpriseUserId'),
    title: `${ticket.get('title')} (#${ticket.get('nid')})`,
    content: '...',
    url: common.getTicketUrl(ticket),
  })
}

上面这些方法的调用基本都是在 Ticket 的 hook 中完成。

关于 notify 模块 22:35

api/notify.js 是通知发送的路由,所有需要发通知都调用它的相关方法,它再根据需要发送消息给对应的通道(比如微信、邮件、短信等):

exports.newTicket = (ticket, author, assignee) => {
  return Promise.all([
    mail.newTicket(ticket, author, assignee),
    bearychat.newTicket(ticket, author, assignee),
    wechat.newTicket(ticket, author, assignee),
  ])
}

该模块还可以做一些额外的处理,比如判断是是工作时间来决定通知只发送到办公协同 IM 中,下班时间通知只发送到微信和邮件中。或者一些重复提醒等逻辑的实现。

获取微信 API accessToken 25:54

Ticket 要调用微信的 API,需要提供 accessToken 作为凭证,微信验证 accessToken 合法之后才会执行具体的操作。accessToken 的获取方式见 文档 。获取的 accessToken 有效时间 2 小时,所以没必要每次访问微信 API 都去获取,所以涉及到 accessToken 的缓存问题:

  • 如果是单进程应用,程序只要在内存中使用全局变量缓存 accessToken 即可。
  • 如果是多进程应用,要使多个进程能共享 accessToken,则需要将其保存在一个所有进程都能访问到的存储中,比如保存在 LeanCloud 存储服务的一张数据表里(代码):

  api = new wechat.API(config.wechatCorpID, config.wechatSecret, config.wechatAgentId, (cb) => {
    new AV.Query('Config')
    .equalTo('key', 'wechatToken')
    .descending('createdAt')
    .first({useMasterKey: true})
    .then((token) => {
      if (token && token.createdAt > new Date(new Date().getTime() - 7200000)) {
        cb(null, JSON.parse(token.get('value')))
      } else {
        cb(null, null)
      }
    })
    .catch(cb)
  }, (token, cb) => {
    new AV.Object('Config')
    .setACL(new AV.ACL()) // 任何人无法读取,除非使用 masterKey
    .save({key: 'wechatToken', value: JSON.stringify(token)})
    .then(() => {
      cb()
    })
    .catch(cb)
  })

构造 api 对象时,除了前三个 key 的参数,后面两个一个是「获取现有 token」的回调,另一个是「保存 token」的回调,具体 API 描述见 文档

保存 accessToken 时,我们设置的 ACL 为空实例,即「任何人无法读写」,所以查询的时候我们使用 masterKey 权限进行查询。因为我们不希望任何客户端对 accessToken 可见。

LeanTicket 获取微信消息 33:31

如果希望客服能直接在微信里回复工单,则需要将微信消息发送到 LeanTicket 中,由后者解析并形成具体操作。

要接受微信的消息,需要配置一些信息。在企业的管理端后台,进入需要设置接收消息的目标应用,点击「接收消息」的「设置」页面,可以配置下面的信息:

  • URL:企业应用接收企业微信推送请求的访问协议和地址,支持 http 或 https 协议。
  • Token:自定义的随机字符串,用于生成签名。
  • EncodingAESKey:用于消息体的加密,是 AES 密钥的 Base64 编码。

编辑上面的信息保存时,微信会尝试「ping」请求 URL 相关的地址,如果验证失败会报错,所以需要提前将应用部署到云端。

LeanTicket 相关代码:

// file: api/index.js
router.use('/webhooks/wechat', require('./wechat').router)

// file: api/wechat.js
router.use('/', wechat(wechatConfig, function (req, res, _next) {
  res.status(200).send('ok')
}))

LeanTicket 目前只做了最基本的配置,并未加入实质的功能。

附: 微信 API 文档wechat-enterprise 文档

其他 39:50

微信通知等这类辅助功能依赖一些 key 的配置,对于一个新搭建的应用如果没有提供相关的 key,我们还是希望应用主体功能能正常使用,所以对于微信相关功能是否启用需要有一些额外的判断(代码):

if (wechatConfig.token) {
  api = // ...
  // ...
} else {
    console.log('微信相关信息没有配置,所以微信账号绑定和微信通知功能无法使用。')
  router.use('/', (req, res) => {
    res.status(501).send('Not Implemented')
  })
}
...
const send = (params) => {
  if (api === null) {
    return
  }
  // ...
}

如果不用企业微信,直接用微信公众平台,该如何操作?


在小程序中的客服会话中链接,需要设置如上的url,在leancloud中如何设置?

在企业应用里,小程序没有agentid,还能把小程序中的客户信息接入到企业微信吗?

如果不用企业微信,直接用微信公众平台,该如何操作?

应该是类似,印象中公众平台的功能受限多一些,具体操作还是参考微信公众平台的文档吧,这些细节我忘记了,抱歉。

需要设置如上的url,在leancloud中如何设置?

就是一个微信回调云引擎的 url,应该是将 https://github.com/leancloud/ticket/blob/master/api/wechat.js#L21 这里的 router 地址配置到微信就可以了。