笔者从私域流量出发,论述了不同的业务和场景下如何处理微信私域相关的用户数据,并分享了自己的操作建议。

最近私域流量的概念非常火,所谓私域流量,就是你可以自由反复利用,无需付费,又能随时触达,被沉淀在公众号、微信群、个人微信号、头条号、抖音等自媒体渠道的用户。相对淘宝、京东、百度这些公域流量平台,它属于商家「私有资产」。

大家应该都在微信内用过小程序或者打开过网页,是不是经常发现会弹个窗,要求授权,然后网页上面就可以展示自己的微信头像和昵称了?这个其实就是微信通过接口给开发者提供的识别用户的能力。

做私域流量,就会不可避免地涉及到一个问题,说用户进入了自己的「私域」,但是我们真的能认识用户吗?我们能成功地识别这个用户,并且展示该给这个用户展示的数据吗?微信的 openid 和 unionid 机制其中的复杂内容又有多少人知道呢?我们有很好地利用微信给我们提供的这些机制吗?抖音到底是怎么利用这些接口拿到用户的关系链的呢?

我在网络上搜了很多相关的资料,有一些机制性质的描述,但是非常完整的剖析这个事情,并且做详细最佳实践分享的文章比较少,而且大部分都是从接口角度去分析这个东西。正好最近接触这块内容,就总结了一下,希望帮助后来的人少踩几个坑。

在写这篇稿子的时候,我也考虑到可能不同公司的阶段不同,理论上功能开发要求应该不太一样,所以这篇稿子会论述不同的业务和场景下如何处理微信私域相关的用户数据,希望能抛砖引玉。

一、基础概念知多少

对用户做身份识别和权限限制,本质上是一个用户中心做的事情,计算机系统发展的历史有很久了,古往今来,凡是涉及到用户中心的系统,目的无外乎两个:

一是「身份识别」,能够准确认识这个用户是谁,不会出现来一个用户,系统明明应该认识 TA,但是完全不认识,也不会认错用户,不会把这个用户当成别的人,也不会把别人当成这个用户,也不会允许有人冒充这个用户。最好能够实现物理用户与虚拟账户的一一对应的关系。(在这里不考虑小号的情况)

二是「限权」,用户相关的数据可以全部连带展示,不会丢数据,也不会展示不该展示的数据,不会出现越权的现象。

好的用户体系主要就是做两个事情,身份识别和限权。所以下文所有的内容,都是围绕身份识别和限权这两个底层需要去做的。所有系统做的迭代和改进都只有一个目的——如何让用户身份识别的成本降到最低,如何让系统限权做的最准确。上面这段话是整个用户中心设计的「核心思想」,如果想要打造一个靠谱的用户中心,就务必牢记这些内容。

在正式开始进行最佳实践的探索之旅之前,不妨先简单的了解一下如下几个接口。

1. 微信网页授权(针对用户在微信内打开对应网页)

https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842

在用户端的授权弹窗样式如下图所示:

搞定微信生态内的账户体系,看这篇文章就够了

2. 获取用户列表(针对已经关注了公众号的用户)

https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140840

3. 小程序获取用户信息接口(针对访问小程序的场景)

https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html

在用户端的授权弹窗样式如下图所示:

搞定微信生态内的账户体系,看这篇文章就够了

4. 移动应用微信登录接口(针对App内利用第三方登录访问微信的场景)

https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419317851&token=&lang=zh_CN

在用户端的授权弹窗样式如下图所示:

搞定微信生态内的账户体系,看这篇文章就够了

上述 4 个接口,分别是微信针对微信内浏览网页、已经关注公众号的用户、小程序和 App 提供的获取当前用户的微信相关信息和唯一标识的接口。

四个接口(除了第二个)的流程都会涉及用户授权,用户也可能会拒绝授权,微信的官方文档经常会说一句屁话,就是开发者需要妥善处理用户拒绝的情况,但是从来没说过怎么妥善处理。

四个流程的用户端的交互流程不太一样(主要是授权弹窗的样式和唤起环境不太一样,我觉得各位应该都见过),但是大体都是必须经过用户的授权(非静默),开发者才能拿到比较关键的信息,比如昵称、头像、unionid——一个非常重要的唯一标识。

有的人可能会问,为什么我一个开发者要搞 4 个接口?因为一个开发者有多个公众号,小程序和 App,在微信看来都属于不同的「应用」,微信会给每个「应用」分配一个专属的 Appid,不同的应用类型,接口自然是不同的。

通过「微信网页授权」接口文档我们可以知道,微信会给我方系统返回如下参数:

搞定微信生态内的账户体系,看这篇文章就够了

基于文档可以看到两个和 id 有关的字段,一个叫 openid,一个叫 unionid,这两个 id 是什么概念呢?都是 id,有什么区别?

微信官方的解释是这样的:unionid 机制的作用说明:如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的 unionid 来区分用户的唯一性,因为同一用户,对同一个微信开放平台下的不同应用(移动应用、网站应用和公众帐号),unionid 是相同的。

也就是说,一个公司如果有一个小程序矩阵,同一个用户使用了这 3 个小程序,开发者能够从微信的接口获得到的数据一共有 3 条,3 条数据用户的 openid 是不一致的,但是 unionid 是一致的,openid 和 unionid 都是用户的唯一标示,但是二者唯一性的生成条件是不一致的。

  • openid 的唯一性=用户微信号+应用的 appid(不同的公众号,小程序,appid 都是不同的);
  • unionid 的唯一性=用户微信号+开发者主体(一般是公司)。

值得一提的是,微信内的网页必须要挂靠一个公众号,所以微信网页授权获得的 openid 和对应挂靠公众号粉丝的 openid 是一致的。开发者可以选择将所有的网页都挂靠在一个公众号下面,这样可以减少授权弹窗的次数。

总的来说,微信提供了很全面的生态系统,但是由于微信体系比较庞大,加上其设计理念比较保守,极端重视用户隐私(甚至有些偏执),会给开发者带来比较大的麻烦,所以想要正确地使用其实并不简单。

此外,在下方的具体内容中,会涉及比较多的系统模块的描述,如果读者自己所在的公司还没有实施类似于「微服务」的架构,那么可能还需要对基础设施进行一些改造,但是核心思想也是可以套用的。

二、单Appid场景的微信账户体系建设

假设现在我们加入了一家初创公司,公司的业务非常简单,暂时不涉及到付费业务,只有一个小型的社区,提供一些评论,点赞和收藏的功能。目前只开通了一个公众号,那我们应该怎么设计针对微信的账户体系呢?我们该如何应用 openid 和 unionid 呢?

基于上文提到的“用户体系的核心思想”——既能够实现对于用户身份的精确识别,以及能够识别准确地读取该用户对应的数据,要做的事情就比较清晰了。一般在系统内都会生成一个 userid,用来作为用户在系统内的唯一标示,同时业务方基于这个字段将用户和业务数据关联起来,实现限权。

在微信体系内,unionid 这个字段是可以被认定为识别用户的核心key,所以需要把 userid 和 unionid 做一个关联关系,总结下来,其实是要做如下几件事:

  1. 「快速精确身份识别」——要能够快速的辨识用户,用户识别要准确,不能出现把用户识别错误的情况,也不能出现明明应该认识但是却不认识的情况。交互层面,最好用户操作成本超级低,点一下,甚至不点就能识别;
  2. 「准确限权」——用户相关的数据可以全部连带展示,不会丢数据,也不会展示不该展示的数据。

结合微信提供的接口能力,将上述目标进行翻译,其实要做的事情是这样的:

  • 核心机制一,「将 unionid 和 userid 绑定实现限权」——把微信的unionid和自有系统的 userid 绑定起来,通常需要基于登录行为实现;
  • 核心机制二,「利用 openid 进行快速身份识别」——由于微信内网页可以直接识别用户的 openid,所以需要将 openid 和 unionid 做关联,来实现一种“长效并且精准的 cookie”的效果,这样用户即使更换手机,只要不更改微信号,系统就永远能够以最低成本直接对其进行身份识别;
  • 核心机制三,「交互层面处理用户拒绝授权」——妥善处理用户拒绝授权,所以拿不到 unionid 的情况。

虽然我很讨厌微信这个“妥善处理”的说辞,但是在这种情况下,我真的不知道该怎么总结这项工作。

核心机制一——「将unionid和userid绑定实现限权」的逻辑比较简单,用户在微信内访问系统的页面,触发了「微信网页授权」的弹窗,用户同意授权后,系统拿到了该用户(该微信号)对应的 unionid,然后把这个 unionid 和系统本身的的 userid 关联起来(比如先弹窗,然后要求用户基于手机号和短信验证码登录,也可以做静默注册,看具体业务),然后系统就会有一个【unionid-userid】的数据,两者一一对应。

从此以后,用户只要通过微信环境访问系统系统,就自动种上数据库已经关联的对应 userid 的 cookie,同时限制用户在别的微信号环境内再用这个 userid 进行登录操作。(场景举例:换个微信用相同的手机号尝试登录)

核心机制二——「利用 openid 进行快速身份识别」实现起来也不复杂,「微信网页授权」有一种机制叫做「snsapi_base 为 scope 发起的网页授权」,可以获取进入页面的用户的 openid 的,并且是静默授权。同时由于用户已经允许授权,所以系统拿到了【openid-unionid】的关联关系,系统内的数据就变成了【openid(公众号A)-unionid-userid】结构的数据。

借助数据库里面的数据和「snsapi_base 为 scope 发起的网页授权」的机制,这个用户日后无论在天涯还是海角,只要在微信内打开网页,系统都能认识他。

核心机制三——「交互层面处理用户拒绝授权」,这个事情要处理起来会比较玄学,比较常用的做法是设定一个拦截机制。一般来说可以把强制要求授权弹窗的界面和强制要求登录的流程做在一起的,毕竟这两个性质是很相似的,说穿了就是两种不同的身份识别形式。

哪些操作是必须是登录用户才能做的(比如下单购买、收藏、点赞、评论),就在对应的流程前面加上弹窗。微信网页授权弹窗是可以反复触发的,就和公司自己的开发写的原生登录弹窗一样,也可以在一进入页面的时候判断数据库内有没有对应的数据记录(openid 相同的数据),如果没有就弹窗。

如何具体地设计这些机制,需要产品经理根据自身业务和对转化率的敏感程度自行决定,如果老板觉得下单前拦截用户要求登录很蠢,也可以考虑去掉,但是这样就需要设计「游客身份」的机制,这个在下文会有更加详细的描述。

上面三个核心机制就是做好公众号登录(微信网页授权)的核心逻辑,请各位同学如果要实践的话务必牢记。

在数据存储方面,其实可以选择先存储 openid-unionid 的关联信息,再根据用户后续登录的情况,存储 unionid-userid 的关联信息,具体的逻辑顺序会在下一小节的流程图之中描述中展现。

当然有同学会问,为什么不可以通过「snsapi_base 为 scope 发起的网页授权」直接获取 openid,然后和 userid 关联,为什么要因为获取不到 unionid 就做拦截呢?原因很简单,因为公司会发展,迟早会做小程序,做更多地业务,到时候回过头来再填这个坑,得不偿失。

在拿不到 unionid 的情况下获取到的 openid,只能作为前端用来给游客用户种 cookie 的手段,可以落库,给前端当 cookie 标识位使,但是不要落库到「用户中心」的数据库,否则后面填坑真的是火葬场,虽然会有两个填坑的小技巧就在下面,但是有条件不这么做的同学请务必不要这么搞。

上面所有的设计都是理想情况,在现实生活中,我们还会遇到一些不理想的情况,比如程序员大哥忘记存 unionid 这个字段了。

那怎么办?通过如下接口可以基于用户的 openid 再把 unionid 拿回来。

接口地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140839

理论上来说,这个接口能够拿回来数据的,一定是因为用户做了下面几件事情:

  1. 开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号。
  2. 开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。

上面说的授权登录过该公众号其实就是指「微信网页授权」,这点要喷一下微信官方的文档,一个行为搞两个名词,其实挺烦人的。那么如果用户授权过同主体的小程序会是什么效果呢?官方文档没写,我也不敢乱说。

这个方法不是万能的,假如用户已经把公众号取关了(备注:取关了再关注,openid 和 unionid 不变),系统就拿不回信息了,然后数据库里面就会出现一堆 unionid 字段为空的数据,其实很蛋疼,所以说系统搭建这个事情,有的时候是一步错步步错。但是往往是业务初期,产品经理和开发都不是很在意,给后面人会造成很大的困扰,我会对这个机制如此了解,相信各位读者已经知道我之前踩了多少坑。

如果用户已经取关,为了下次和用户再次相遇时可以拿回数据,就需要前端层面代码在做授权弹窗是否要弹的判断机制里面加上一条,该条 openid 对应的用户数据的 unionid 字段是否为空,如果为空,需要弹窗授权,拿回对应的 unionid。

三、多Appid场景的微信账户体系建设

随着公司业务发展,之前的公众号做的不错,最近小程序风口正胜,公司也希望做一些变现的工作。老板说我们来做个小程序吧,然后再把咱们的电商给做起来,面对新的业务要求,用户中心该做出什么样的改进和变化呢?

在这个情况下,我们肯定要先结构目标,如何在新增小程序的情况下做好系统的用户体系呢?其实说到底,还是要牢记设计用户中心时的核心思想,抽象下来就是要实现下面几个目标:

  1. 「快速精确身份识别」——要能够快速的辨识用户,用户识别要准确,不能出现把用户识别错误的情况,也不能出现明明应该认识但是却不认识的情况。交互层面,最好用户操作成本超级低,点一下,甚至不点就能识别。
  2. 「准确限权」——用户相关的数据可以全部连带展示,不会丢数据,也不会展示不该展示的数据。
  3. 「快速识别自动绑定」——由于有两个软件环境(微信网页和小程序),会涉及到两个环境下的用户数据的绑定与统一,由于会做电商,用户会对“数据丢失”非常敏感,所以用户数据的绑定就格外重要了。

上面三点,一、二两点是固有的要求,第三点是涉及到了多Appid的情况之后新增的要求。

结合微信提供的结果,把上面的目标翻译过来,就是下面几个核心要点:

  • 核心机制一,「将 unionid 和 userid 绑定实现限权」——把微信的unionid和自有系统的userid绑定起来,通常需要基于登录行为实现
  • 核心机制二,「利用 openid 进行快速身份识别」——由于微信网页可以直接识别用户的openid,所以需要将 openid 和 unionid 做关联,来实现一种“长效并且精准的cookie”的效果。
  • 核心机制三,「利用 openid 进行快速身份识别」(小程序场景)——小程序也可以直接获取用户的openid,所以需要将 openid 和 unionid 做关联,来实现一种“长效并且精准的 cookie”的效果。
  • 核心机制四,「利用 unionid 与 userid 的绑定关系,实现快捷登录」——从小程序内获得的 unionid 和在微信网页内获得的 unionid 是一致的,所以二者其一如果在数据库已经建立了 unionid 和 userid 的关联数据,另一方应该可以借助数据库内的数据自动建立联系而不需要用户在做任何类似于「手机号验证码登录」的身份识别操作。
  • 核心机制五,「利用 unionid 与 userid 的绑定关系,实现静默登录」——在某些特定情况下,小程序可以不通过用户授权直接获取 unionid,所以应该要借助这个机制,帮助用户实现无需任何操作即可直接登录的效果。
  • 核心机制六,「交互层面处理用户拒绝授权」——妥善处理用户拒绝授权,所以拿不到 unionid 的情况。

「核心机制一」、「二」、「三」、「六」在上面一小节已经详细描述过就不多说了,我们就来着重说说上面「核心机制四」的和「核心机制五」。

「核心机制四」和「核心机制五」的本质是一样的,本质上都是如下的流程:

搞定微信生态内的账户体系,看这篇文章就够了

一定要说的话,「核心机制四」和「核心机制五」的最大区别就在上述流程图中的黄色区域部分。

在一般情况下,我们需要弹窗请求用户的授权,才能从用户处获得openid和unionid的关联关系,否则就只能得到openid这一数据。特殊情况下,小程序环境内,可以通过接口直接获得用户的unionid和openid的关联关系,具体的描述见文档-unionid获取途径:

https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html

反过来说,如果用户是先在小程序做了登录,再访问微信内网页,也要做上面流程图内的操作,这样才可以最大成本的降低用户十分识别的成本,毕竟很多用户根本分不清小程序和微信内网页的区别是什么,在他们的认知里面,这个服务都是同一个公司提供的,理论上就应该可以自动可用,数据不能丢。

说完了「核心机制四」和「核心机制五」,我再回过头来说「核心机制一」,虽然存在了2条数据,但是其实事情没有变复杂,只要自己不作死。

在上面的一个小节里面,我们是如此描述绑定的逻辑的:

用户在微信内访问系统的页面,触发了「微信网页授权」的弹窗,用户同意授权后,系统拿到了该用户(该微信号)对应的 unionid,然后把这个 unionid 和系统本身的的 userid 关联起来(比如先弹窗,然后要求用户基于手机号和短信验证码登录,也可以做静默注册,看具体业务),然后我们就有一个【unionid-userid】的数据,两者一一对应。

从此以后,用户只要通过微信环境访问系统系统,就自动种上数据库已经关联的对应 userid 的 cookie,同时限制用户在别的微信号环境内再用这个 userid 进行登录操作。(场景举例:换个微信用相同的手机号尝试登录)

其实这个逻辑核心做的事情是两个,第一个是把【unionid-userid】的这个关联关系数据存下来,二是做限制逻辑,保证二者是一一对应的关系。

在加入了小程序之后,这个场景就产生了一些变化,因为 unionid-userid 这样的数据会出现两条,而且一定有先有后,所以后来的一条其实完全可以直接把先前一条的 userid 的数据填过来,就是上面流程图所描述的逻辑。即使一条数据变两条,事情本质是没有什么变化的。

这里面就要格外强调,上面一个小节曾经提到过这样一个观点:

在拿不到 unionid 的情况下获取到的 openid,只能作为前端用来给游客用户种 cookie 的手段,可以落库,给前端当 cookie 标识位使,但是不要落库到【用户中心】的数据库。

如果我们把 unionid 字段为空的数据落库到了「用户中心」的数据库会造成什么问题?不妨举个例子来说明。试下一下这样的场景:一个用户进入小程序,拒绝授权但是登录了手机号 A,然后进入微信网页,也拒绝授权但是登录了手机号 B,我们就会有如下数据。

【openidA-unionid 为空-useridA】

【openidB-unionid 为空-useridB】

假如用户再来访问,在两个环境都允许授权了,会怎么样呢?数据库里面的数据就会被补全,变成如下的样子。

【openidA-unionidA-useridA】

【openidB-unionidA-useridB】

问题来了,如果这个时候公司又开发了一个小程序,这个用户过来访问,允许授权,系统拿到了 openidC 和 unionidA,那该把用户关联到哪个 userid 上面呢?所以我们可以看出来,如果 unionid 字段为空的数据落入了【用户中心】的数据是多么的麻烦,一旦牵扯到两个 appid 系统就立马歇菜了,非常容易出差错,但是如果 unionid 是不允许为空的,那么其实系统的逻辑依旧比较简单,而且很干净。

如果有人不信邪觉得可以通过 appid 或者别的手段去关联,不妨可以试试,在线上运行一段时间,再去 mysql 里面跑数看看,百分百会遇到一个微信号关联两个 userid 的情况,这种时候用户再投诉「订单丢失」的问题,真的是跳进黄河也洗不清。

除了逻辑层面的区别,和上面一小节里纯粹公众号场景处理的方式还有有一个比较大的区别,就是数据库里面理论上会存在这样的两条数据:

【openid(公众号 A)-unionid-userid】

【openid(小程序 B)-unionid-userid】

我会建议把数据结构改造成下面这样的:

【openid(公众号 A)-unionid-userid-appidA】

【openid(小程序 B)-unionid-userid-appidB】

为什么要增加 appid 的字段呢?原因比较简单,一会要做什么消息通知之类的功能,很多时候会基于 appid 这个字段做检索,加上去会很方便。

很多时候一部分没什么经验的开发开发会喜欢用数字枚举值,比如公众号是 1,小程序是 2,但是实际上公司业务发展之后可能会做 7,8 个小程序,枚举值并不能很好地表达业务层面实质性的含义,这是种有问题的抽象方式,所以从一开始就把 Appid 存入系统中会比较科学。

上面描述的逻辑其实已经很复杂了,单纯用文字描述不是很好懂,下面有一个流程图,各位可以抄作业了。需要强调的是,我所在的公司业务场景比这个还要再复杂一些,这个流程图里面的内容是没有在线上验证过的,所以这个流程图是无法保证 100% 正确的。

搞定微信生态内的账户体系,看这篇文章就够了

单独看流程图的话,也并不是非常好懂,所以我在 6 处地方做了 tips,这 6 处的逻辑可能会有点绕,但是都是有非常明确地目的的,缺少其中的某一个步骤,会导致结果产生变动。

【1】这个步骤做去重,主要是针对数据库很可能已经存在了一条一模一样的数据的情况,假如用户清除了 cookie,前端不认识用户,就会把已有的数据传输过来,如果不做去重,下方逻辑判断会有问题。

【2】这个情况如果完全按照上文的要求进行开发,理论上不会出现,但是由于开发不规范很可能会导致类似情况出现,所以特意加上这样的限制逻辑。

【3】为了防止用户试图在别的微信号再次登录已经绑定了微信号的手机号而设计的机制。

【4】再次去重的目的也比较简单,有可能先前数据库里面有数据【openidA-unionidA-userid 为空】,然后接口传过来【openidA-unionidA-useridA】,第一次去重是不会把这两条数据当做重复数据的,但是经过上面的流程处理后,两条数据都会变为【openidA-unionidA-useridA】,所以需要做去重逻辑。

【5】将 alpha 写入数据库的时候,如果去重后还有数据 read_time 为空,说明这是全新的数据,需要直接 insert,典型场景是用户第一次访问我方系统的服务。

【6】这一步就是将最终结果返回给前端的过程,很可能前端只给后端 openid 和 unionid,但是后端通过关联逻辑把 userid 也返回给了前端,前端就可以把 cookie 给种上了。

上面这个流程,严格来说不是一个常规的「登录」流程,更像是一个「数据绑定」的流程,但是我建议所有涉及到微信用户体系的接口,全部封死成一个接口,不要分成「绑定」和「登录」两个接口。

同时,所有涉及到常规登录的地方(比如 App 的账密登录),如果前端可以取到 openid 和 unionid 的 value,都应该再调用一遍上面这个流程的接口。

这么做的原因有三,一是因为这样做也可以实现业务的需求。二是为了减少测试的成本,通过上方的完整流程图就可以看出来其实上面的逻辑说这很简单,其实麻烦得很,如果再区分是绑定场景还是登录场景,测试起来会非常头疼。三是因为,从底层抽象来看,所谓的「数据绑定」本质是是利用用户无法伪造的 openid 来做了一个身份识别,其实也是一种登录,绑定就是登录的一种。

接口的入参主要就是 openid,unionid,userid 三个要素,其中 userid 可以缺省,因为在「核心机制四」和「核心机制五」中提到的自动登录的场景下。我们还拿不到用户的 userid,其实是把 unionid 传给服务端,服务端返回 userid。

四、涉及不登录生单的微信账户体系建设

小程序上线了,用户数量激增,公司拿到了一笔融资,老板有点膨胀,说咱们做 App 吧。

恰好,这时竞争对手的 App 也上线了,他们还支持一个非常吊的功能,就是游客身份也能直接生单购买,不需要在生单前登录,这时老板着急了,要求自家产品也要有一样的功能。

首先,这是一个非常真实的背景,而且根据行业内知名电商公司的实践,有超过 40% 的订单来源于游客生单,所以做的必要时有的,关键的点在于怎么做。在正式开始讨论方案之前,其实我们不妨再次再次回过头来看一下「核心思想」。

用户体系要做的事情是什么?对用户进行身份识别,以及在身份识别的基础上进行限权操作。那么行业内常见的「游客生单购买」设计是不是和这个体系本身是相悖的呢?这个设计是不是太「人格分裂」了?我认为不是。

因为无论是哪家公司,就算它对「游客生单购买」的支持做的再好,也仍然千方百计地在引导用户登录。本质是是因为「游客生单购买」这件事情其实是把 App 内的 cookie 当成了一种身份识别的唯一标示,而我们都知道 cookie 这种东西其实是很容易就会被抹掉的,如果用户用收集的垃圾清理功能清一下缓存,很可能就洗掉了,对应到用户端的感受就是找不到订单,这其实是比较严重的。

需要注意的是,App 内的微信授权和小程序/微信内网页的微信授权,在交互上有比较大的差别。

在小程序/微信内网页获取 openid 时,可以通过类似于「snsapi_base 为 scope 发起的网页授权」直接获取 openid,不需要用户做任何操作。

在获取 unionid 时,也只是唤起微信环境内自带的控件(就是一个弹窗)向用户申请要求授权,交互流程是很简单的,而且任何页面或者任何按钮都可以加上这个触发,其实是非常方便的。

在 App 内,想要获得用户的授权(这是唯一获得 openid 和 unionid 的方式)需要唤醒微信的 App,然后用户在微信内授权后再返回开发者的 App。这意味着这种唤醒操作不能像之前设计的时候那样随意地在任意节点插入,因为之前的环境内,发起授权不过是一个弹窗,阻断性会比较小,而在 App 内则会直接跳出 App,交互上的代价会比较大,频繁弹出容易导致阻断主流程,得不偿失。

一般建议将微信账户的授权做在登录界面处,引导用户绑定。也有一些在微信生态内玩的很溜的公司会选择在「订单列表」,「我的」等页面引导用户绑定,因为这些商家有很多用户都是从微信导流过来的,用户下载 App 之后第一反应肯定是「我在微信里面下的单子跑哪里去了?」。这样的设计可以很好地引导用户。

上面说的都是交互层面的问题,在后端逻辑层面,App 完成可以被当成另一个小程序或者公众号。

说完微信在 App 内的授权,我们再说说「游客生单购买」是什么情况,游客生单购买的技术方案一般有两种。

第一种是基于业务进行设计的「游客生单购买」,简单地说就是业务方的数据库允许在特定情况下,userid 的字段为空。

第二种方案是基于用户中心进行设计的「游客生单购买」,简单地说就是把游客的 cookie 当做身份的唯一标示,比如说利用 cookie 生成一个 userid,传送给后端,然后等用户登录之后,用户中心再做替换,并且广播给业务方。

无论是我自己所在的公司还是请教别的公司的产品经理,一般都认为第二种方案是更好地设计,扩展性会比较强,业务线不需要关心这么复杂 userid 替换的逻辑,可以直接拿过来用,比较符合「小前台,大中台」的设计理念,耦合性比较低。

根据上面第二种方案的逻辑可以看出来,游客生单这件事情,其实在哪里都能做,大部分公司选择在 App 做是因为 App 的 cookie 保存时间一般会比浏览器久一些,用户不会没事去把 App 的缓存给清掉,风险相对可控。

在讨论完本次系统改进的主要方案之后,让我们再回过头来看看这次系统设计的目标,我认为主要是下面的几个目标:

  1. 「快速精确身份识别」——要能够快速的辨识用户,用户识别要准确,不能出现把用户识别错误的情况,也不能出现明明应该认识但是却不认识的情况。交互层面,最好用户操作成本超级低,点一下,甚至不点就能识别。
  2. 「准确限权」——用户相关的数据可以全部连带展示,不会丢数据,也不会展示不该展示的数据。
  3. 「快速识别自动绑定」——由于有多个软件环境(微信网页、小程序、App等),会涉及到多个环境下的用户数据的绑定与统一,由于会做电商,用户会对“数据丢失”非常敏感,所以用户数据的绑定就格外重要了。
  4. 「产品导流后账户绑定相关的交互设计」——由于用户会很大概率先用小程序,再下载 App,所以 App 在核心界面,可能需要引导用户绑定微信账户,这样才能同步数据。(比如用户在小程序以游客身份下单,但是同意了微信网页内授权,来到 App 的订单列表是看不到数据的,需要引导用户绑定微信账户)
  5. 「基于游客身份的自动绑定」——用户在多个微信环境以游客身份生单的关系关联问题,这时用户尽管没有登录,但是基于微信的机制我们仍然能够把他视为同一个人,所以需要做对应的措施。

上面五点,一、二、三点是上文提到的固有的要求,四、五两点是基于新的业务场景提出来的新的要求。

结合微信提供的接口,把上面的要求翻译成具体可执行的措施,就是下面几个核心要点:

  • 核心机制一,「将 unionid 和 userid 绑定实现限权」——把微信的unionid和自有系统的userid绑定起来,通常需要基于登录行为实现
  • 核心机制二,「利用 openid 进行快速身份识别」——由于微信网页可以直接识别用户的openid,所以需要将 openid 和 unionid 做关联,来实现一种“长效并且精准的cookie”的效果。
  • 核心机制三,「利用 openid 进行快速身份识别」(小程序场景)——小程序也可以直接获取用户的openid,所以需要将 openid 和 unionid 做关联,来实现一种“长效并且精准的 cookie”的效果。
  • 核心机制四,「基于交互引导解决App内数据同步问题」在 App 内基于微信授权的交互机制和用户的行为轨迹,在关键页面提示用户「绑定微信账户」。
  • 核心机制五,「利用 unionid 与 userid 的绑定关系,实现快捷登录」——从小程序内获得的 unionid,在微信网页内获得的 unionid 和 App 基于账户绑定获得的 unionid 是一致的,所以三者其一如果在数据库已经建立了 unionid 和 userid 的关联数据,剩余两方应该可以借助数据库内的数据自动建立联系而不需要用户在做任何类似于「手机号验证码登录」的身份识别操作。这个关联逻辑需要对游客身份生成的虚拟 userid 也同样生效。
  • 核心机制六,「利用 unionid 与 userid 的绑定关系,实现静默登录」——在某些特定情况下,小程序可以不通过用户授权直接获取 unionid,所以应该要借助这个机制,帮助用户实现无需任何操作即可直接登录的效果。
  • 核心机制七,「交互层面处理用户拒绝授权」——妥善处理用户拒绝授权,所以拿不到 unionid 的情况。
  • 核心机制八,「用户标识变更后的广播机制」——所有涉及 userid 之间替换的行为,必须广播至所有业务线,不论是虚拟的 userid 被真实登录的 userid 替换,还是虚拟的 userid 之间的替换。

其中「核心机制一」、「二」、「三」、「六」、「七」都是在上文讨论过的,「核心机制四」在本小节的开头部分已经讨论过,我们就来说一下「核心机制五」和「核心机制八」。

我们不妨来对比一下涉及游客生单和不涉及游客生单对于「核心机制五」的设计上的不同要求

【涉及游客生单】

从小程序内获得的 unionid,在微信网页内获得的 unionid 和 App 基于账户绑定获得的 unionid 是一致的,所以三者其一如果在数据库已经建立了 unionid 和 userid 的关联数据,剩余两方应该可以借助数据库内的数据自动建立联系而不需要用户在做任何类似于「手机号验证码登录」的身份识别操作。这个关联逻辑需要对游客身份生成的虚拟 userid 也同样生效。

【不涉及游客生单】

从小程序内获得的 unionid 和在微信网页内获得的 unionid 是一致的,所以二者其一如果在数据库已经建立了 unionid 和 userid 的关联数据,另一方应该可以借助数据库内的数据自动建立联系而不需要用户在做任何类似于「手机号验证码登录」的身份识别操作。

排除增加了 App 这个运行环境之外,最大的不同点在于「这个关联逻辑需要对游客身份生成的虚拟 userid 也同样生效。」这句话。

为什么需要这么做呢?主要是为了满足如下场景:

用户在小程序同意了「用户信息授权」,系统获得了她的 openid 和 unionid,但是她紧接着以游客身份生单,并没有登录。

这个时候用户又在微信内网页做了同样的事情,这个时候系统如果不对她作为游客身份的 userid 做同步,那么她在两个环境的 cookie 是不一样的,生成的虚拟 userid 自然就不同,但是系统基于 unionid 应该是能对用户在不同的地方的数据做整合同步的。

可能有的人会说,竭尽全力帮助用户同步数据会不会让用户觉得「毛骨悚然」,我觉得这是无稽之谈,我做过很多用户访谈,很多人甚至分不清「小程序」和「微信内网页」之间的区别,在他们看来,数据就应该是同步的,丢数据才会让他们毛骨悚然。

基于上面的讨论,我们再次将流程图做修正,就得到了如下的流程图:

搞定微信生态内的账户体系,看这篇文章就够了

与上方的流程图相比,唯一的区别在于判断「userid 是否为空」的逻辑变成了「userid 是不是都是虚拟」,因为在游客生单的机制下,即使用户不登录,也可以获得一个虚拟 userid。

在做 userid 的关联补充的时候,如果系统能够识别用户身份(基于获取到的 unionid),那么即使当前 userid 是虚拟的,数据库中早先用户数据的 userid 也是虚拟的,也需要保持 userid-unionid 一一对应的关系,将现在这个虚拟的 userid 替换成数据库中更早的那一个,这样用户先前的数据就可以在这个环境被读取出来,具体的应用场景在上方已经描述。但是假如其中有一个 userid 是真实的,那么它应该获得最高的优先级。

后记

至此,关于整个微信账户体系如何建设的论述就告一段落了。其实想要打造完整的用户体系还有很多可以做的事情,比如除了微信,是否能支持 QQ 和微博的登录,比如第三方社交账户是否支持解绑(一般大公司的用户中心都做了这个功能),都是我们需要考虑的问题,但是我觉得在前期只需要做好微信就够了。

如果将微博和 QQ 的数据加入进来,那么整体数据结构就会呈现如下的分布:

搞定微信生态内的账户体系,看这篇文章就够了

而整体的产品结构会呈现如下样式:

搞定微信生态内的账户体系,看这篇文章就够了

其实在正式开始写这篇文章之后,才有时间沉下心来再次对逻辑抽象,想出了上文中的流程图。

在那之前我给开发看也好,自己检查也好,思考的逻辑都是一个类似于二叉树的逻辑。但是随着业务复杂度的提高,二叉树的逻辑很明显只能用来做覆盖性测试,不适合用来做开发,这算是在这件事情过程中踩得一个不大不小的坑吧。

搞定微信生态内的账户体系,看这篇文章就够了

我觉得用户体系这个东西发展了这么多年,看上去很简单,但是其实里面深究起来学问也不少,但是总的来说在实际工作的时候,还是要牢牢把握住底层的核心思想,好的用户体系主要就是做两个事情,身份识别和限权。所有的工作都应该围绕身份之别和限权这两个底层需要去做的,其实我们所做的一切,万变不离其宗,就是如何让用户把身份识别的成本降到最低,如何让系统限权做的最准确。

不妨回想一下,以前的用户体系还会区分「注册」和「登录」,现在基本上就是手机短信验证码登录,没注册默认注册,这个变化其实就是上面所提到的核心思想的一个实践方案,而且经过业界各个大厂的实践,基本上可以认为是一种最佳实践方案了。

从个人角度来看,微信这个 openid 和 unionid 的机制非常的蛋疼,微信自己也作出过不少次改进,但是我觉得这仍然是一件非常操蛋而且麻烦的事情。从微信官方的角度来看,他们或许还是出于保护用户隐私不被滥用的角度在做这件事情,而作为开发者也只有适应的份,毕竟这个机制已经在这边了,没啥好说的。

上面文章内的很多想法,大部分我都实践过,效果是符合预期的,但是由于客观条件的限制,有一些想法并没有在生产环境得到过验证,并不能保证 100% 正确,况且不同的业务场景,对于同一件事情会衍生出不同的实践方案,比如本文会强调尽量做到【userid-unionid】一一对应,在实践中有一些公司是允许一个 userid 对应多个 unionid 的,其实仔细想一下,好像也没什么大毛病。

感谢如下的小伙伴给本文提供的支持

  • 十一贝科技-前端工程师-周航
  • 某待业的前端工程师-权金铎
  • 十一贝科技-资深 java 工程师-李锐
  • 厨芯科技-产品经理-邵琨容
  • 虎嗅-编辑-小田一成

文章内的流程图都是使用的 OmniGraffle 和 Xmind 绘制而成,如果有需要源文件或者有一些别的想法,欢迎添加我的微信做进一步交流。