做好用户获取第一步,微信小程序OpenID和UnionID详解 | 世外天堂

做好用户获取第一步,微信小程序OpenID和UnionID详解

OpenID 和 UnionID 是什么
OpenID 是微信提供给开发者的用户唯一标识。然而当开发者拥有多个移动应用、网站应用、和公众账号(包括小程序)时,同一用户、不同应用下的 OpenID 是不一样的。

而同一个微信开放平台账号下的不同应用,用户的 UnionID 是唯一的。

因此,对有多个应用的开发者来讲,只有通过 UnionID 来区分用户的唯一性,才能实现多个应用间的账号打通。

应该使用哪个 ID 登录
理论上讲,当产品有App、小程序、公众号等多种形式时,用 UnionID 是最好的选择,否则会因为同一用户在不同应用下的 OpenID 不一样导致产生多个账号,那处理起来就很麻烦了。

然而 UnionID 并不是那么容易获取的。微信小程序最新的限制是:

必须使用一个专用按钮控件让用户主动点击,否则无法弹出授权弹窗
用户必须点击「允许」同意小程序获取公开信息

以上2步,每一步都会造成一定的用户流失。所以有的开发者会使用 OpenID ,以最大程度的降低用户获取的成本,从而推动注册用户数的快速增长。

拿拼多多来说,用户打开小程序之后会静默获取 OpenID 并生成账号,可以正常使用购物车、历史记录等服务。用户在「个人中心」页面点击「更新资料」时就会触发授权弹窗获取公开信息和 UnionID。

对于不同的业务场景,会有各自最适合的选择。深入研究下微信的机制,或许能有一些启发,在某些场景下可以尝试优化用户获取的路径。

小程序 OpenID 和 UnionID 获取机制
在小程序的官方文档里有一张图解释了小程序调用wx.login接口的登录流程。不过那张图除了开发能看懂,一般人应该都看不懂什么意思。翻译成一般人都能看懂的图,小程序登录流程是这样的:

avatar

首先,开发者可以在小程序中静默调用登录接口,拿到一个凭证
小程序把凭证发送到开发者的服务器上
服务器拿着这个凭证以及小程序密钥向微信接口请求换取 OpenID
微信接口返回 OpenID 给开发者服务器,满足特殊条件时会一并返回 UnionID
开发者服务器创建登录态并返回给小程序,从而完成登录
其中第4点,在满足下面任何一个条件时可以同时获得 UnionID:

如果开发者账号下存在同主体的公众号,并且该用户已经关注了该公众号
如果开发者账号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用
此时获取该用户 UnionID 是不需要用户再次授权的。

除了调用登录接口,用户在小程序中支付完成5分钟内,开发者可以直接通过 getPaidUnionID 接口获取该用户的 UnionID,也不需要用户授权。

总结一下:

OpenID可以无感获取。

而无感获取 UnionID 必须满足以下任一条件:

用户已经关注了同主体的公众号
用户已经授权过同主体的其他应用获取 UnionID
用户刚刚通过小程序完成了支付
否则就必须让用户主动点击按钮并允许获取公开信息后,才可以获得 UnionID。
公开信息有哪些
最后说说微信的用户公开信息(UserInfo)究竟包含哪些信息:

微信昵称
微信头像图片的URL,如果用户没有头像,URL会是空的。如果用户更换了头像,原有头像的URL会失效
用户性别:未知、男性、女性
所在国家
所在省份
所在城市
国家、省份、城市所用的语言:英文、简体中文、繁体中文
与用户信息一并返回的还有一串加密信息,转交给开发者的服务器解密之后,就可以得到用户的 OpenID 和 UnionID 了。

一件比较tricky的事情是,如果只是需要在小程序中展示用户头像和昵称,可以使用 ,微信在渲染小程序的时候会显示用户的头像和昵称。但是此时只是显示出来能被用户看到,开发者并不能拿到用户头像昵称的数据,所以这个时候就不要想什么分享到聊天的时候小程序卡片标题能带上用户昵称了。

1
2
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName"></open-data>

通过除了可以不经过授权直接展示头像、昵称之外,还可以直接展示:

用户性别
用户所在国家
用户所在城市
用户所在省份
用户的语言
群名称(必须是用户曾经分享过小程序的群)

从微信小程序官方发布的公告中我们可获知:小程序体验版、开发版调用 wx.getUserInfo 接口,将无法弹出授权询问框,默认调用失败,需使用 引导用户主动进行授权操作:

1.当用户未授权过,调用该接口将直接报错

2.当用户授权过,可以使用该接口获取用户信息

但在实际开发中我们可能需要弹出授权询问框,因此需要我们自己来写模拟授权弹框(主要是对 的包裹+用户是否是第一次授权判断来显示该页面),代码如下:

1.页面结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<view v-if="isCanUse">
<view>
<view class='header'>
<image src='../../static/img/wx_login.png'></image>
</view>
<view class='content'>
<view>申请获取以下权限</view>
<text>获得你的公开信息(昵称,头像、地区等)</text>
</view>

<button class='bottom' type='primary' open-type="getUserInfo" withCredentials="true" lang="zh_CN" @getuserinfo="wxGetUserInfo">
授权登录
</button>
</view>
</view>
<!-- #endif -->
</view>
</template>

这里的isCanUse是用来记录当前用户是否是第一次授权使用的,wx_login.png图在底部下载获取即可。

2.样式

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
<style>
.header {
margin: 90rpx 0 90rpx 50rpx;
border-bottom: 1px solid #ccc;
text-align: center;
width: 650rpx;
height: 300rpx;
line-height: 450rpx;
}

.header image {
width: 200rpx;
height: 200rpx;
}

.content {
margin-left: 50rpx;
margin-bottom: 90rpx;
}

.content text {
display: block;
color: #9d9d9d;
margin-top: 40rpx;
}

.bottom {
border-radius: 80rpx;
margin: 70rpx 50rpx;
font-size: 35rpx;
}
</style>

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<script>
export default {
data() {
return {
SessionKey: '',
OpenId: '',
nickName: null,
avatarUrl: null,
isCanUse: uni.getStorageSync('isCanUse')||true//默认为true
};
},
methods: {
//第一授权获取用户信息===》按钮触发
wxGetUserInfo() {
let _this = this;
uni.getUserInfo({
provider: 'weixin',
success: function(infoRes) {
let nickName = infoRes.userInfo.nickName; //昵称
let avatarUrl = infoRes.userInfo.avatarUrl; //头像
try {
uni.setStorageSync('isCanUse', false);//记录是否第一次授权 false:表示不是第一次授权
_this.updateUserInfo();
} catch (e) {}
},
fail(res) {}
});
},

      //登录
login() {
let _this = this;
uni.showLoading({
title: '登录中...'
});

// 1.wx获取登录用户code
uni.login({
provider: 'weixin',
success: function(loginRes) {
let code = loginRes.code;
if (!_this.isCanUse) {
//非第一次授权获取用户信息
uni.getUserInfo({
provider: 'weixin',
success: function(infoRes) {
                      //获取用户信息后向调用信息更新方法
let nickName = infoRes.userInfo.nickName; //昵称
let avatarUrl = infoRes.userInfo.avatarUrl; //头像
_this.updateUserInfo();//调用更新信息方法
}
});
}

//2.将用户登录code传递到后台置换用户SessionKey、OpenId等信息
uni.request({
url: '服务器地址',
data: {
code: code,
},
method: 'GET',
header: {
'content-type': 'application/json'
},
success: (res) => {
//openId、或SessionKdy存储//隐藏loading
uni.hideLoading();
}
});
},
});
},
//向后台更新信息
updateUserInfo() {
let _this = this;
uni.request({
url:'url' ,//服务器端地址
data: {
appKey: this.$store.state.appKey,
customerId: _this.customerId,
nickName: _this.nickName,
headUrl: _this.avatarUrl
},
method: 'POST',
header: {
'content-type': 'application/json'
},
success: (res) => {
if (res.data.state == "success") {
uni.reLaunch({//信息更新成功后跳转到小程序首页
url: '/pages/index/index'
});
}
}

});
}
},
onLoad() {//默认加载
this.login();
}
}
</script>