0%

Elixir 微信(WeChat) SDK 使用指南

前言

在国内,腾讯的社交网络及其强大,其社交生态也是百花齐放,而微信是其王牌中的王牌。

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 的刷新流程。

公众号和小程序均可以使用 AppIDAppSecret 调用本接口来获取 access_token

AppIDAppSecret 可在“微信公众平台 - 开发 - 基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。

从官方的说明中,界定出以下两种使用场景

  • 本地调用微信 APIs: 单一调用方

  • 中控服务器调用 APIs:多方同时调用

实际中的使用场景多为 【多方同时调用】 接口,而因为 access_token 的获取和刷新是覆盖的,如果在多方并发调用的情况下同时刷新,会导致前面拿到的 access_token 失效,而导致接口请求失败,所以需要中控服务器来统一管理 获取和刷新 access_token

但为了简单起见,在此先从【本地调用微信 APIs】场景开始,后续再循序渐进,讲述【中控服务器调用 APIs】场景。

安装 WeChat SDK

首先,安装 SDK,如果是已有项目,在 mix.exs 文件中加入依赖:

1
2
3
4
5
def deps do
[
{:wechat, "~> 0.10", hex: :wechat_sdk}
]
end

在此我们走一遍从 0 到 1 的过程,从创建新项目开始

1
mix phx.new wechat_demo --no-ecto --no-gettext --no-dashboard --no-mailer

项目创建完成之后,在 mix.exs 文件中加入依赖:

1
2
3
4
5
6
def deps do
[
{:saxy, "~> 1.4"},
{:wechat, "~> 0.10", hex: :wechat_sdk}
]
end

这里添加了两个依赖库 wechat_sdksaxy 依赖,在后续的 【微信推送消息的接受和处理】章节中,微信推送过来的消息是 xml 格式的,因此需要用到 saxy 库进行解析。

修改好之后执行: mix deps.get 下载新依赖

定义 client 模块

首先需要初始化 SDK 的 client 模块,新建一个 WeChatDemo.Demo 模块:

1
2
3
4
5
6
defmodule WeChatDemo.Demo do
@moduledoc "demo"
use WeChat,
appid: "wx552588fc9207632d", // 填入你的 appID
appsecret: "your-appsecret" // 填入你的 appsecret
end

获取 Access token 接口需要用到两个参数:appIDappsecret

测试号在 测试号管理 页面中查看,正常的公众号请阅读 官方指引文档

定义 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
2
[info] Refresh appid: wx552588fc9207632d, key: access_token succeed, get expires_in: 7200s.
[info] Start Refresh Timer for appid: wx552588fc9207632d, key: access_token, time: 5400000s.

看到上面的日志输出,表示刷新器已运行正常

接口调用

下面来试试接口调用,还是以 获取用户信息 接口来举例

测试号管理 页面中 - 测试号二维码 一栏可以找到你的 openid

OpenID

在关注者与公众号产生消息交互后,公众号可获得关注者的 OpenID(加密后的微信号,每个用户对每个公众号的 OpenID 是唯一的。对于不同公众号,同一用户的 openid 不同)

引用自 link

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
iex(1)> WeChatDemo.Demo.User.user_info("oWP97uGZgZRks50Q7pwRRDIOrLdA")
{:ok,
%Tesla.Env{
__client__: %Tesla.Client{adapter: nil, fun: nil, post: [], pre: []},
__module__: WeChat.Requester.OfficialAccount,
body: %{
"city" => "",
"country" => ",
"groupid" => 0,
"headimgurl" => "",
"language" => "zh_CN",
"nickname" => "",
"openid" => "oWP97uGZgZRks50Q7pwRRDIOrLdA",
"province" => "",
"qr_scene" => 0,
"qr_scene_str" => "",
"remark" => "",
"sex" => 0,
"subscribe" => 1,
"subscribe_scene" => "ADD_SCENE_QR_CODE",
"subscribe_time" => 1570788299,
"tagid_list" => []
},
headers: [
{"connection", "keep-alive"},
{"content-type", "application/json; encoding=utf-8"},
{"date", "Fri, 08 Jul 2022 15:15:37 GMT"},
{"content-length", "286"}
],
method: :get,
opts: [],
query: [
openid: "oWP97uGZgZRks50Q7pwRRDIOrLdA",
access_token: "58_lJTMRIhYm-PpB0jpNQw79DhUrmKeHe3Ac-EoZIpaWRkooZnM3HSUukM1w1lAxQckoW-3sA9Hol6fvlBmEx95Jb84brTvJS_Ey53-bhECnMj2uHPdr3b7jjMZRMKMj3aJVxp_yE9UAdXPaamNETCgAFAJYH"
],
status: 200,
url: "https://api.weixin.qq.com/cgi-bin/user/info"
}}

所有 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
2
3
4
5
6
7
8
defmodule WeChatDemo.Demo do
@moduledoc "demo"
use WeChat,
appid: "wx552588fc9207632d", // 填入你的 appID
appsecret: "your-appsecret", // 填入你的 appsecret
server_role: Application.get_env(:wechat, :server_role, :hub),
storage: Application.get_env(:wechat, :storage, WeChat.Storage.File)
end

对于线上服务器, server_role 需要设为 :hub , 且使用 WeChat.Storage.File 作为 storage(存储器);

作为 中控服务器(Hub),主要的功能为两点

  1. 统一刷新 Access token

  2. hub_client 提供 Access token

  3. 接受微信消息推送

Hub

hub_client 提供 Access token,需要提供一个安全的接口,示例为简单起见,使用 BasicAuth 方式鉴权,修改 router.exs 增加 /hub/expose/:store_id/:store_key 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if Application.compile_env(:wechat, :server_role) == :hub do
pipeline :exposer_auth do
import Plug.BasicAuth, warn: false

opts =
Application.compile_env(:wechat, WeChat.Storage.HttpForHubClient)
|> Keyword.take([:username, :password])

plug :basic_auth, opts
end

scope "/hub/expose", WeChat.Plug do
pipe_through :exposer_auth
get "/:store_id/:store_key", HubExposer, clients: [WeChatDemo.Demo]
end
end

注意 示例使用的 BasicAuth 并不能保护接口的安全,线上服务器请使用更加安全合规的方式鉴权。

HubClient

对于开发环境或者测试环境,server_role 需要设为 :hub_clientstorage 需要设为 WeChat.Storage.HttpForHubClient, 修改 dev.exs

1
2
3
config :wechat,
server_role: :hub_client,
storage: WeChat.Storage.HttpForHubClient

hub_client 模式下,刷新器通过 WeChat.Storage.HttpForHubClient 存储器从 hub 提供的 HTTP 接口中获取 Access token ,且不再调用存储器的存储方法。

因此还需要告知 WeChat.Storage.HttpForHubClient 存储器 hubHTTP 接口路径,修改 config.exs

1
2
3
4
5
host = "https://wx.example.com"
config :wechat, WeChat.Storage.HttpForHubClient,
hub_base_url: "#{host}/hub/expose",
username: "hello",
password: "000999"

测试

完成上述修改之后,将程序打包并部署到服务器,然后在服务器上测试接口

1
2
iex(1)> WeChatDemo.Demo.User.user_info("oWP97uGZgZRks50Q7pwRRDIOrLdA")
{:ok, ...}

服务器环境下调用接口正常,接下来再看看本地开发环境,运行 iex -S mix phx.server 启动应用,测试接口

1
2
iex(1)> WeChatDemo.Demo.User.user_info("oWP97uGZgZRks50Q7pwRRDIOrLdA")
{:ok, ...}

You did it, Good job!

可以试试通过 消息发送接口 给自己发送一条文本消息.

注意 微信为了限制公众号恶意发送消息骚扰用户,所以有一些限制才能调用消息发送接口,具体请看官方文档

网页授权

如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。

引用自 官方文档

在开始之前,需要先设置 网页授权回调域名

关于网页授权回调域名的说明

  1. 在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;
  2. 授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.htmlhttp://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.comhttp://music.qq.comhttp://qq.com 无法进行OAuth2.0鉴权
  3. 如果公众号登录授权给了第三方开发者来进行管理,则不必做任何设置,由第三方代替公众号实现网页授权即可

引用自 官方文档

测试号管理 页面中 -> 体验接口权限表 -> 网页服务 -> 网页帐号-> 网页授权获取用户基本信息 -> 点击修改 -> 授权回调页面域名 -> 把服务器 IP 或者 域名填入即可。

配置

router.exs 增加一个 oauth2_checker pipeline,并应用到需要使用 网页授权 的路由规则上:

1
2
3
4
5
6
7
8
pipeline :oauth2_checker do
plug WeChat.Plug.OAuth2Checker, clients: [WeChatDemo.Demo]
end

scope "/hello/:app" do
pipe_through :oauth2_checker
get "/", PageController, :index
end

在路径 "/hello/:app" 中的 :app , 可以是

  • code_name = demo

  • appid = wx552588fc9207632d

code_name: 可以在定义是设置 client 的 code_name, 如果没有设置,默认为模块名最后一个名称的全小写格式。

因为有WeChat.Plug.OAuth2Checker 的检测,只有授权成功的才能访问到页面,page_controller.ex 被调用时可以用 OAuth2Checker 设置的 session 来获取用户信息:

1
2
3
4
5
6
7
8
9
defmodule WeChatDemoWeb.PageController do
use WeChatDemoWeb, :controller

def index(conn, _params) do
appid = get_session(conn, "appid")
info = get_session(conn, "access_info") |> inspect()
render(conn, "index.html", appid: appid, info: info)
end
end

设置好之后打包部署到服务器

  1. 访问 /wx/demo or /hello/wx552588fc9207632d

  2. 连接转跳到微信的授权页;

  3. 用户的授权;

  4. 微信在用户的授权完成之后,跳转回原路径,并带上一个参数 code

  5. 通过这个参数获取到用户的一些信息, 完成网页授权。

Hub 跳板

上面的配置虽然完成了功能开发,但是实际使用中,会发现有两点问题

  • 无法在开发环境使用网页授权

  • 无法在其他域名或者请他服务器上使用网页授权

有以上需求可以使用 WeChat.Plug.HubSpringboard 模块

在 hub 服务器上增加一条用于跳转的路由规则

1
get "/wx/:app/:env/cb/*callback_path", WeChat.Plug.HubSpringboard, clients: [WeChatDemo.Demo]

修改 config.exs

1
2
3
4
5
6
7
8
9
host = "https://wx.example.com"
oauth2_envcallbacks = %{"dev" => "http://127.0.0.1:4000"}

config :wechat, clients: %{
WeChatDemo.Demo => [
hub_springboard_url: "#{host}/wx/:app/:env/cb/",
oauth2_callbacks: oauth2_env_callbacks
]
}

设置 hub_springboard_url 告知 hub_client 跳转到 中控服务器的地址;

设置 oauth2_env_callbacks 告知 hub 有哪些环境。

设置好之后打包部署到服务器

  1. 访问本地 /wx/demo or /hello/wx552588fc9207632d

  2. 连接转跳到微信的授权页,但 callback 地址为 hub 的跳转路由;

  3. 用户的授权;

  4. 微信在用户的授权完成之后,跳转 hub 的跳转路由;

  5. 跳转路由根据 client & env 获取 回调地址;

  6. 跳回原路径,并带上一个参数 code

  7. 通过这个参数获取到用户的一些信息, 完成网页授权。

env 默认为 dev,如果环境有多个,可以在对应的环境设置,如 QA 环境

1
2
3
4
5
6
config :wechat, :env, :qa

oauth2_env_callbacks = %{
"dev" => "http://127.0.0.1:4000",
"qa" => "http://wx.qa.example.com"
}

JS SDK 设置

微信 JS-SDK 是 微信公众平台 面向网页开发者提供的基于微信内的网页开发工具包。

通过使用微信 JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。

此文档面向网页开发者介绍微信 JS-SDK 如何使用及相关注意事项。

引用自 设置指南

在开始之前,需要先设置 JS接口安全域名,因为微信限制,JS-SDK 仅能在指定的安全域名使能。

测试号管理 页面中 -> JS接口安全域名 -> 点击修改 -> 填入域名: https://wx.example.com

通过 config 接口注入权限验证配置:

1
2
3
4
5
6
7
8
wx.config({
debug: true, // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [] // 必填,需要使用的 JS 接口列表
});

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
2
3
4
5
6
7
defmodule WeChatDemo.Demo do
@moduledoc "demo"
use WeChat,
appid: "wx552588fc9207632d", // 填入你的 appID
appsecret: "your-appsecret", // 填入你的 appsecret
token: "FakEmGXMZg1nxm273F7a" // 填入你的 token
end

修改 router.exs, 增加消息 WebHook 路由规则

1
2
3
forward "/wx/event", WeChat.Plug.EventHandler,
client: WeChatDemo.Demo,
event_handler: &WeChatDemo.WeChatEvent.handle_event/3

event_handler 的值为 3 参数的函数,详细请阅读: 函数定义

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
defmodule WeChatDemo.WeChatEvent do
require Logger
import WeChat.Utils, only: [now_unix: 0]
alias WeChat.ServerMessage.XmlMessage

# 处理 公众号 & 小程序 推送消息
def handle_event(_conn, client, message) do
Logger.info("client: #{inspect(client)} get message: #{inspect(message)}")

case message do
# 文本消息
%{"MsgType" => "text"} ->
openid = message["FromUserName"]
timestamp = now_unix()

reply_text =
case message["Content"] do
"openid" ->
openid

_ ->
"Hello!"
end

reply_msg = XmlMessage.reply_text(openid, message["ToUserName"], timestamp, reply_text)

{:reply, reply_msg, timestamp}

# 订阅消息
%{"MsgType" => "event", "Event" => "subscribe"} ->
timestamp = now_unix()

reply_msg =
XmlMessage.reply_text(
message["FromUserName"],
message["ToUserName"],
timestamp,
"非常感谢您的订阅关注"
)

{:reply, reply_msg, timestamp}

_ ->
:ignore
end
end
end

当服务器接收到微信推送的消息后,SDK 会先 decode xml,然后调用 event_handler 回调函数, 上面的代码实现以下逻辑:

  • 收到 【文本消息】,如果内容是 openid , 返回当前用户 OpenID 的值,其他情况 返回文本消息 Hello!

  • 收到 【关注订阅消息】,返回文本消息 非常感谢您的订阅关注

将上面修改好之后,将代码部署到服务器上,然后扫描 【测试号管理】页面的二维码 进入 测试公众号,发送测试消息: “1”,服务器收到之后打印如下日志:

1
2
3
09:54:34.060 request_id=FwCjiY4DLbmKhfsAAScx [info] POST /wx/event
09:54:34.061 request_id=FwCjiY4DLbmKhfsAAScx [info] client: WeChatDemo.Demo get message: %{"Content" => "1", "CreateTime" => "1657504472", "FromUserName" => "occpC65S9o5FR8WXOt7cLVVRa-bI", "MsgId" => "23729756606043028", "MsgType" => "text", "ToUserName" => "gh_e88a0414dbdf"}
09:54:34.061 request_id=FwCjiY4DLbmKhfsAAScx [info] Sent 200 in 501µs

接入完成!!!

后记

Erlang/Elixir 虽然是小众语言,但并不代表其语言能力有问题,每种语言都有其适用范围和人群,撰写此文也是想为 Elixir 社区贡献一份力量,希望对观看的你有所帮助,如果错误,请不吝指正,感谢!

梦想基金
feng19 微信

微信

feng19 支付宝

支付宝

欢迎关注我的其它发布渠道