前言
在国内,腾讯的社交网络及其强大,其社交生态也是百花齐放,而微信是其王牌中的王牌。
Elixir,继承了 Erlang 的衣钵,使用 Ruby 的皮肤,再加上强大的社区,开发出了众多神器,让这个小众语言在全球开始遍地开花。
为结合两者,因此有了此文。
背景知识
阅读本文前,您需要有一定的背景知识
Elixir 知识
微信开发知识
本文是假定读者有一定 elixir & Phoenix 基础的前提下进行编写的,如没有,可以阅读上面的连接进行补充。
微信公众号
公众号介绍
我们日常使用的公众号分为两种:
-
为企业和组织提供更强大的业务服务与用户管理能力,主要偏向服务类交互(功能类似12315,114,银行,提供绑定信息,服务交互的);
适用人群:媒体、企业、政府或其他组织。
群发次数:服务号1个月(按自然月)内可发送4条群发消息。
-
为媒体和个人提供一种新的信息传播方式,主要功能是在微信侧给用户传达资讯;(功能类似报纸杂志,提供新闻信息或娱乐趣事)
适用人群:个人、媒体、企业、政府或其他组织。
群发次数:订阅号(认证用户、非认证用户)1天内可群发1条消息。
两者在接口上大体上基本相同,主要是两者群发消息的次数不一样,另外有部分接口权限有细微区别,在微信客户端显示上服务号和订阅号会有比较大的区别,订阅号 是折叠在【订阅号消息】功能里面,而 服务号 是一个独立项。
WeChat SDK 介绍
Elixir 的 WeChat SDK 目前已在 Github 上 开源,SDK 除了对公众号的支持外,还支持其他微信生态的产品
本文仅限讨论【公众号】开发,其余功能请前往 SDK 文档 阅读
阅读指引
因文章篇幅有限,本文只讲解了以下几个侧重点:
SDK 安装及使用
中控服务器(Hub)模式
网页授权
JS SDK 设置
微信推送消息接受和处理
在开始之前,你还需要准备一个测试号
开通方式:申请公众号测试号
本文配套 demo 代码在此: Github
SDK 安装及使用
Access token
微信的公众平台与目前大部分开放平台的 API 设计思路一致,每个接口都需要附带 Access token
来进行鉴权,Access token
是公众号接口调用的凭据,因此在调用接口前需要先 获取 Access token
公众平台的 API 调用所需的
access_token
的使用及生成方式说明:1、建议公众号开发者使用中控服务器统一获取和刷新
access_token
,其他业务逻辑服务器所使用的access_token
均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token
覆盖而影响业务;2、目前
access_token
的有效期通过返回的expires_in
来传达,目前是7200
秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token
。在刷新过程中,中控服务器可对外继续输出的老access_token
,此时公众平台后台会保证在5分钟内,新老access_token
都可用,这保证了第三方业务的平滑过渡;3、
access_token
的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token
的接口,这样便于业务服务器在 API 调用获知access_token
已超时的情况下,可以触发access_token
的刷新流程。公众号和小程序均可以使用
AppID
和AppSecret
调用本接口来获取access_token
。
AppID
和AppSecret
可在“微信公众平台 - 开发 - 基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。
从官方的说明中,界定出以下两种使用场景
本地调用微信 APIs: 单一调用方
中控服务器调用 APIs:多方同时调用
实际中的使用场景多为 【多方同时调用】 接口,而因为 access_token
的获取和刷新是覆盖的,如果在多方并发调用的情况下同时刷新,会导致前面拿到的 access_token
失效,而导致接口请求失败,所以需要中控服务器来统一管理 获取和刷新 access_token
。
但为了简单起见,在此先从【本地调用微信 APIs】场景开始,后续再循序渐进,讲述【中控服务器调用 APIs】场景。
安装 WeChat SDK
首先,安装 SDK,如果是已有项目,在 mix.exs
文件中加入依赖:
1 | def deps do |
在此我们走一遍从 0 到 1 的过程,从创建新项目开始
1 | mix phx.new wechat_demo --no-ecto --no-gettext --no-dashboard --no-mailer |
项目创建完成之后,在 mix.exs
文件中加入依赖:
1 | def deps do |
这里添加了两个依赖库
wechat_sdk
和saxy
依赖,在后续的 【微信推送消息的接受和处理】章节中,微信推送过来的消息是 xml 格式的,因此需要用到saxy
库进行解析。
修改好之后执行: mix deps.get
下载新依赖
定义 client 模块
首先需要初始化 SDK 的 client 模块,新建一个 WeChatDemo.Demo
模块:
1 | defmodule WeChatDemo.Demo do |
获取 Access token
接口需要用到两个参数:appID
和 appsecret
测试号在 测试号管理 页面中查看,正常的公众号请阅读 官方指引文档。
定义 client
模块之后,可以使用两种调用方式:
以 获取用户信息 接口 为例
调用
client
模块方法:WeChatDemo.Demo.User.user_info(openid)
原生调用方法:
WeChat.User.user_info(WeChatDemo.Demo, openid)
两种方式的返回值是一致的, WeChatDemo.Demo
模块在编译期间会将所有接口模块编译为自己的子模块,并将每个函数中的 client
参数去掉,第一种方式在使用上比第二种便捷,会产生子模块但并无不良副作用,推荐使用第一种方式。
可以通过查看模块
WeChat.Builder.OfficialAccount
中的@both_modules
和@official_account_modules
了解子模块列表如代码中不使用第一种方式,且不希望生成子模块,可以在
use WeChat
时设置gen_sub_module?: false
配置 Access token 自动刷新器
完成 client
模块定义之后,直接调用模块,会发现接口报错: access_token is invalid
无效的 Access token
,因为还缺少了一步:激活 Access token
自动刷新器,请在配置文件 config.exs
中添加:
1 | config :wechat, :refresh_settings, [WeChatDemo.Demo] |
重新编译 mix compile
,运行 iex -S mix phx.server
启动应用
1 | [info] Refresh appid: wx552588fc9207632d, key: access_token succeed, get expires_in: 7200s. |
看到上面的日志输出,表示刷新器已运行正常
接口调用
下面来试试接口调用,还是以 获取用户信息 接口来举例
在 测试号管理 页面中 - 测试号二维码 一栏可以找到你的 openid
OpenID
在关注者与公众号产生消息交互后,公众号可获得关注者的 OpenID(加密后的微信号,每个用户对每个公众号的 OpenID 是唯一的。对于不同公众号,同一用户的 openid 不同)
引用自 link
1 | iex(1)> WeChatDemo.Demo.User.user_info("oWP97uGZgZRks50Q7pwRRDIOrLdA") |
所有 API 的接口返回值类型为 Wechat.response/0
轻松三步,搞定微信接口开发!
以上是【本地调用微信 APIs】 场景的内容,接下来会比较复杂一点
中控服务器(Hub)调用 APIs 场景
要实现中控功能,并让一份代码同时支持多种场景,可以通过在 use WeChat
模块时根据不同的场景设置不同的 server_role
(服务器角色) 和 storage
(Access token
存储器) :
ServerRole (服务器角色)
:client
: 默认,主动刷新Access token
:hub
: 中控服务器,主动刷新Access token
:hub_client
: 逻辑服务器,从hub
获取Access token
Storage (Access token 存储器)
作用:在 Access token
刷新器每次完成刷新之后,会通过 storage
保存 Access token
,当 Access token
刷新器启动的时会从 storage
读取保存的 Access token
以快速启动。
storage 如不填,则默认值为:
WeChat.Storage.File
,将Access token
保存在json
文件中;WeChat.Storage.HttpForHubClient
通过 http 从 Hub 中获取Access token
;如果以上两种存储器并不适用你的情况的话,可以通过实行
WeChat.Storage.Adapter
来自定义存储器。
配置
修改 WeChatDemo.Demo
模块,配置 server_role
& storage
:
1 | defmodule WeChatDemo.Demo do |
对于线上服务器, server_role
需要设为 :hub
, 且使用 WeChat.Storage.File
作为 storage
(存储器);
作为 中控服务器(Hub),主要的功能为两点
统一刷新
Access token
为
hub_client
提供Access token
接受微信消息推送
Hub
为 hub_client
提供 Access token
,需要提供一个安全的接口,示例为简单起见,使用 BasicAuth
方式鉴权,修改 router.exs
增加 /hub/expose/:store_id/:store_key
接口
1 | if Application.compile_env(:wechat, :server_role) == :hub do |
注意 示例使用的 BasicAuth
并不能保护接口的安全,线上服务器请使用更加安全合规的方式鉴权。
HubClient
对于开发环境或者测试环境,server_role
需要设为 :hub_client
, storage
需要设为 WeChat.Storage.HttpForHubClient
, 修改 dev.exs
1 | config :wechat, |
hub_client
模式下,刷新器通过 WeChat.Storage.HttpForHubClient
存储器从 hub
提供的 HTTP
接口中获取 Access token
,且不再调用存储器的存储方法。
因此还需要告知 WeChat.Storage.HttpForHubClient
存储器 hub
的 HTTP
接口路径,修改 config.exs
1 | host = "https://wx.example.com" |
测试
完成上述修改之后,将程序打包并部署到服务器,然后在服务器上测试接口
1 | iex(1)> WeChatDemo.Demo.User.user_info("oWP97uGZgZRks50Q7pwRRDIOrLdA") |
服务器环境下调用接口正常,接下来再看看本地开发环境,运行 iex -S mix phx.server
启动应用,测试接口
1 | iex(1)> WeChatDemo.Demo.User.user_info("oWP97uGZgZRks50Q7pwRRDIOrLdA") |
You did it, Good job!
可以试试通过 消息发送接口 给自己发送一条文本消息.
注意 微信为了限制公众号恶意发送消息骚扰用户,所以有一些限制才能调用消息发送接口,具体请看官方文档。
网页授权
如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。
引用自 官方文档
在开始之前,需要先设置 网页授权回调域名
关于网页授权回调域名的说明
- 在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;
- 授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com 无法进行OAuth2.0鉴权
- 如果公众号登录授权给了第三方开发者来进行管理,则不必做任何设置,由第三方代替公众号实现网页授权即可
引用自 官方文档
在 测试号管理 页面中 -> 体验接口权限表 -> 网页服务 -> 网页帐号-> 网页授权获取用户基本信息 -> 点击修改 -> 授权回调页面域名 -> 把服务器 IP 或者 域名填入即可。
配置
在 router.exs
增加一个 oauth2_checker
pipeline,并应用到需要使用 网页授权 的路由规则上:
1 | pipeline :oauth2_checker do |
在路径 "/hello/:app"
中的 :app
, 可以是
code_name
=demo
appid
=wx552588fc9207632d
code_name: 可以在定义是设置 client 的 code_name, 如果没有设置,默认为模块名最后一个名称的全小写格式。
因为有WeChat.Plug.OAuth2Checker
的检测,只有授权成功的才能访问到页面,page_controller.ex
被调用时可以用 OAuth2Checker
设置的 session
来获取用户信息:
1 | defmodule WeChatDemoWeb.PageController do |
设置好之后打包部署到服务器
访问
/wx/demo
or/hello/wx552588fc9207632d
;连接转跳到微信的授权页;
用户的授权;
微信在用户的授权完成之后,跳转回原路径,并带上一个参数
code
;通过这个参数获取到用户的一些信息, 完成网页授权。
Hub 跳板
上面的配置虽然完成了功能开发,但是实际使用中,会发现有两点问题
无法在开发环境使用网页授权
无法在其他域名或者请他服务器上使用网页授权
有以上需求可以使用 WeChat.Plug.HubSpringboard
模块
在 hub 服务器上增加一条用于跳转的路由规则
1 | get "/wx/:app/:env/cb/*callback_path", WeChat.Plug.HubSpringboard, clients: [WeChatDemo.Demo] |
修改 config.exs
1 | host = "https://wx.example.com" |
设置 hub_springboard_url
告知 hub_client
跳转到 中控服务器的地址;
设置 oauth2_env_callbacks
告知 hub
有哪些环境。
设置好之后打包部署到服务器
访问本地
/wx/demo
or/hello/wx552588fc9207632d
;连接转跳到微信的授权页,但 callback 地址为 hub 的跳转路由;
用户的授权;
微信在用户的授权完成之后,跳转 hub 的跳转路由;
跳转路由根据
client
&env
获取 回调地址;跳回原路径,并带上一个参数
code
;通过这个参数获取到用户的一些信息, 完成网页授权。
env
默认为 dev
,如果环境有多个,可以在对应的环境设置,如 QA 环境
1 | config :wechat, :env, :qa |
JS SDK 设置
微信 JS-SDK 是 微信公众平台 面向网页开发者提供的基于微信内的网页开发工具包。
通过使用微信 JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。
此文档面向网页开发者介绍微信 JS-SDK 如何使用及相关注意事项。
引用自 设置指南
在开始之前,需要先设置 JS接口安全域名,因为微信限制,JS-SDK 仅能在指定的安全域名使能。
在 测试号管理 页面中 -> JS接口安全域名 -> 点击修改 -> 填入域名: https://wx.example.com
通过 config 接口注入权限验证配置:
1 | wx.config({ |
Setup 参数有 5 个必填项,其中,jsApiList
根据业务需要填写即可,
其余 4 个参数可以通过接口 WeChat.WebPage.js_sdk_config/2 生成,然后传递到前端页面,使用 wx.config
注入配置,完成之后,就可以正常使用 JS-SDK
了。
微信推送消息接受和处理
当用户发送消息到公众号时,微信服务器会调用设置的 WebHook,把用户发送的消息传递到 Webhook,请阅读 接入指南。
在开始之前,需要先设置 【接口配置信息】,在 测试号管理 页面中 -> 接口配置信息 -> 修改:
- 填入 URL:
https://wx.example.com/wx/event
- 填入 Token:
FakEmGXMZg1nxm273F7a
(此处填写随机生成的一个 token 即可) - 正式环境还需要填入
encoding_aes_key
,测试号因为是用明文模块过来的,并不涉及到解密 xml,因此忽略即可 - 定义
client
模块时必须设置对应的:encoding_aes_key
&token
值
修改 WeChatDemo.Demo
模块,增加 appsecret
& token
1 | defmodule WeChatDemo.Demo do |
修改 router.exs
, 增加消息 WebHook 路由规则
1 | forward "/wx/event", WeChat.Plug.EventHandler, |
event_handler
的值为 3 参数的函数,详细请阅读: 函数定义
1 | defmodule WeChatDemo.WeChatEvent do |
当服务器接收到微信推送的消息后,SDK 会先 decode xml,然后调用 event_handler
回调函数, 上面的代码实现以下逻辑:
收到 【文本消息】,如果内容是
openid
, 返回当前用户OpenID
的值,其他情况 返回文本消息Hello!
收到 【关注订阅消息】,返回文本消息
非常感谢您的订阅关注
将上面修改好之后,将代码部署到服务器上,然后扫描 【测试号管理】页面的二维码 进入 测试公众号,发送测试消息: “1”,服务器收到之后打印如下日志:
1 | 09:54:34.060 request_id=FwCjiY4DLbmKhfsAAScx [info] POST /wx/event |
接入完成!!!
后记
Erlang/Elixir 虽然是小众语言,但并不代表其语言能力有问题,每种语言都有其适用范围和人群,撰写此文也是想为 Elixir 社区贡献一份力量,希望对观看的你有所帮助,如果错误,请不吝指正,感谢!