裂变功能的技术难点和普通电商页面完全不同,它的根源是一句话:这条链路是跨应用的。用户在我们的页面点分享 → 内容进入微信或短信 → 另一个人在他的设备上点开 → 回到我们的页面。中间经过了不受我们控制的环节,而每一跳都可能丢东西。
第一版的做法很朴素:在各端分别拼接带参数的链接或路径,参数里塞开团 ID、邀请人 ID、活动 ID、渠道标识。问题一个接一个。
第一是参数被截断。小程序的页面路径有长度限制、小程序码的参数字段更短,我们塞了四五个参数之后,某些渠道的参数直接被截掉一部分。表现是用户点进来之后邀请人 ID 是空的,裂变关系断了——他参团了,但邀请人拿不到奖励。这类问题最难查,因为只在参数较长的组合下出现。
解法是不要在端上拼长参数,改成服务端签发一个短标识(分享凭证):分享时把全部参数提交给服务端,服务端存下来并返回一个短串,端上只传这个短串。打开时用短串换回完整参数。这一下就绕开了所有端的长度限制,而且参数结构以后怎么变都不影响端上。
第二是三端的分享能力差异巨大。小程序有原生的分享回调和小程序码;App 要调各平台 SDK;H5 只能复制链接或引导用户用浏览器菜单分享。第一版在业务代码里写了一堆条件编译分支,很快就没人敢改了。解法是抽象成统一的分享接口(传入内容和参数,各端各自实现),业务代码只调这一个接口。
第三是参数确实丢了怎么办。即使用了短标识,仍有场景会丢:H5 分享被用户手动复制时截断了、App 冷启动时参数没被正确接收、某些渠道会重写链接。所以要有多级兜底:页面参数缺失时,依次尝试首次启动参数(App 冷启动的 deferred deeplink)、剪贴板识别(识别我们自己格式的分享码)、服务端最近分享记录(这个用户设备最近是否有关联的分享)。每一级都有失败可能,所以要多级而不是一级。
第四是归因,这一条最容易引发纠纷。「这个新用户是谁带来的」直接关系到推广费用。归因口径必须在设计阶段就定清楚并写进文档:是首次点击归因还是末次点击归因、归因窗口多长、同一个用户被多个人邀请过归给谁。我们踩过——口径没定,运营和推广方各按自己的理解算,结算时数字对不上,扯了很久。
另外分享和参团要分别埋点:「分享出去了」和「有人真的参团」是两件事,只埋一个就说不清是分享环节的问题还是参团环节的问题。
用服务端签发短标识而不是端上拼参数,代价是多一次网络往返和一份服务端存储。端上拼参数零成本、离线可用;短标识需要分享时先请求一次服务端(弱网下会让分享变慢),打开时也要再换一次。但它换来的是「参数长度不再受任何端的限制」和「参数结构可以自由演进」——而参数被截断这类问题只在特定组合下出现、极难排查,一旦发生就是裂变链路直接断掉。取舍依据是:偶发且难查的正确性问题,优先级高于可感知但可缓解的性能问题。缓解手段是分享前预取凭证(用户进入活动页时就先拿好)。
做多级兜底而不是把主路径做到万无一失。另一种思路是深入排查每一个渠道的参数丢失原因并逐个修。但这条链路上有些环节压根不受我们控制(渠道重写链接、用户手动复制时截断、系统的启动参数传递行为),穷举修复是做不完的。多级兜底承认「主路径会失败」,用冗余路径提高整体成功率。代价是实现复杂、而且剪贴板这类兜底手段有隐私观感成本。缓解手段是埋点区分兜底命中与主路径命中,如果某个渠道的兜底命中率异常高,再针对性去修那个渠道。
归因口径定义比归因实现重要得多。技术上实现任何一种归因都不难,难的是让所有相关方对同一个数字有共识。我们的做法是把口径写成文档并让运营与推广方签字确认,包括「同一用户被多人邀请归给谁」这类边角情况。这件事看起来不是技术工作,但它是这个模块唯一会引发商务纠纷的部分,值得在设计阶段花时间。
踩过的坑:参数被截断导致邀请人 ID 丢失,用户参了团但邀请人拿不到奖励。小程序路径长度有限、小程序码参数字段更短,我们塞了四五个参数后某些渠道的参数被截掉一部分。这个问题最难查,因为只在参数较长的组合下出现(活动 ID 长的那批活动才会),前期完全没暴露。是邀请人来投诉「我明明拉了人却没奖励」才发现的。修法是服务端签发短标识。教训是:任何要经过第三方通道传递的数据,长度和字符集都必须按最严格的那一端设计——而且不要靠「测一下应该够」,要主动查各端的限制值。
踩过的坑二:归因口径没定,结算时和推广方数字对不上。我们按「末次点击」算,推广方按「首次点击」算,同一批用户两边算出的归属不同,结算争执了很久,最后要回溯原始埋点逐条核对。修法是口径写进文档并共同确认,同时保留原始埋点以便复算。教训是:涉及钱的口径必须先达成共识再实现——这和金额分摊规则要可解释、指标口径要版本化是同一类问题:只要结果会被用来算钱,定义的清晰度就比实现的优雅度重要。
踩过的坑三:条件编译分支散落在业务代码里,改一处要动三端。分享逻辑在业务代码里写了大量 #ifdef 分支,新增一个渠道要在好几个文件里各加一段,而且三端行为逐渐不一致(某一端漏了埋点)。修法是抽象统一分享接口,差异关在适配层。教训和后端的协议适配层完全一样:把差异收敛在一处,共性只实现一份,否则横切逻辑(埋点、错误处理)一定会写得不一致。
踩过的坑四:分享凭证没有有效期,活动结束后仍能通过旧链接进入。用户点开一个几个月前的分享,进到了一个已结束的活动页,而页面没有处理这个状态,显示成一个可参团但点击报错的页面。修法是凭证带有效期,并在入口处理活动已结束的状态(详见模块三)。教训是:任何对外发出的凭证都要有生命周期,长期有效的凭证既是安全隐患也会让归因窗口失去意义。
没做的部分:没做跨设备的裂变关系补全(用户在 A 设备看到分享、在 B 设备注册)。需要设备指纹或更强的用户识别,隐私成本高,当时接受这部分归因丢失。也没做分享内容的个性化生成(按邀请人和商品动态生成分享图),需要服务端图片合成,排期没排上。
参数丢失率:最该报的数字。按渠道分别报「打开时参数完整的比例」,改造前后对比。必须按渠道分开——不同渠道的丢失原因完全不同,合成一个总数就看不出该修哪里。
兜底命中率:报各级兜底的命中次数。这个数字有双重价值:证明兜底真的在起作用;某个渠道的兜底命中率异常高就说明该渠道的主路径有问题,值得针对性去修。
裂变关系断裂:报「参团成功但无邀请人关联」的订单数(绝对数),改造前后对比。用绝对数不用比率——每一笔都对应一个可能来投诉的邀请人。
归因一致性:报与推广方对账时的差异笔数。这个数字直接反映口径是否达成共识。诚实的表述是「口径统一并保留原始埋点后,对账差异降到可逐条核对的量级」,而不是声称零差异。
分享转化漏斗:报分享 → 打开 → 参团三段的转化。分开埋点的价值就在这里——能看出问题出在哪一段(分享出去没人点,还是点了没人参团)。
分享耗时:因为改成了服务端签发凭证,要主动报分享操作的耗时,说明有预取优化(进入活动页时先拿好凭证),否则会被追问「多一次请求不慢吗」。
不要报「裂变率提升 N%」。裂变效果主要由活动力度和商品决定,不是这个技术模块的成果,报了会被追问归因。也不要报「参数零丢失」——链路上有不受我们控制的环节。正确表述是「按渠道的参数完整率变化、各级兜底命中次数、裂变关系断裂的绝对数下降、归因口径统一后的对账差异」。
拼团页面上有两个持续变化的东西:团里已经有几个人、还剩多少时间。这两个都看起来简单,实际都踩过坑。
先说倒计时,问题比想象的严重。第一版直接用设备本地时间算剩余时间。上线后收到一批很奇怪的反馈:「倒计时都归零了,为什么我朋友那边还显示在拼」、「倒计时还剩半小时,但点参团说活动结束了」。
原因是设备本地时钟不准。这不是罕见情况——有用户手动改过时间、有设备的自动校时失效、有些用户为了别的目的故意把时间调快。我们用本地时间算倒计时,等于把展示的正确性交给了用户的设备。
更糟的是第一版在本地判定「倒计时归零就是结束了」,然后禁用参团按钮。于是时钟快的用户明明还能参团,却被自己的设备挡住了——这是直接的转化损失。
解法有三层。第一是拉服务端时间并记录偏移:首次进页面时拉一次服务端时间,算出本地时钟与服务端的偏移量,之后所有时间计算都加上这个偏移。第二是用单调时钟递推而不是反复读墙钟——因为用户可能在页面开着的时候改系统时间,或者系统触发一次校时,墙钟会跳变,而单调时钟只会平稳增长。第三是到点后不由本地判定结果,而是向服务端确认:倒计时归零只是「提示用户时间到了」,能不能参团必须问服务端。
再说团进度。团里的人数是多人并发变化的。第一版用固定间隔轮询,间隔短了浪费、间隔长了用户看到的人数是旧的——而拼团的心理体验很依赖「看到人数在涨」。改法是长连接订阅为主、退避轮询兜底:有长连接时实时推送,长连接不可用时退避轮询(越久没变化间隔越长)。
成团瞬间是这个模块的并发热点。差最后一个人时,可能好几个人同时点参团。服务端只会让一个成功,而失败的那几个人拿到的必须是可理解的结果。第一版返回的是笼统的「参团失败」,用户完全不知道发生了什么,以为是系统问题。改成明确文案加明确下一步:「这个团已经满了,为你开一个新团」并直接给出操作入口——把一个失败转化成一个新的开始。
最后是前后台切换与息屏恢复。用户切到微信去分享、几分钟后切回来,这期间定时器可能被挂起、长连接可能已断、团状态可能已变。所以回前台要重新校准时间并重新拉一次状态,而不是接着用挂起前的数据继续走。
倒计时用「服务端时间偏移加单调时钟」而不是「定期重新拉服务端时间」。定期重拉更简单也更准,但它意味着倒计时页面要持续打请求,而拼团页的停留时间可能很长(用户等着人来)。偏移加单调时钟只需要一次校准,之后完全本地递推、零请求、且不受墙钟跳变影响。代价是长时间停留后偏移可能有累积误差(设备时钟漂移),缓解手段是在前后台切换、息屏恢复、以及关键操作前重新校准——这几个时机正好覆盖了长时间停留的场景。
本地不做「结束」判定,这一条是从转化损失里学到的。看起来本地判定更快、体验更好(不用等请求)。但它的错误方向是「误挡住本可成功的用户」,而这是不可挽回的损失——用户看到按钮禁用就走了,不会再试。反过来,让他点一下再由服务端拒绝,代价只是一次多余的请求和一个提示。取舍原则是:涉及「能不能操作」的判定,宁可多问一次服务端,也不要用本地状态去挡——这和「超时按不可售处理」的方向相反,因为这里误挡的代价大于误放的代价(误放只是服务端拒绝一次,误挡是直接流失)。
把并发失败转化成「为你开新团」,这是产品和技术的联合设计。技术上只需要在失败响应里区分「团已满」这个原因;产品上的价值是把一个挫败点变成一个新入口。这个改动的收益比我做的任何性能优化都直接,而它的成本只是一个明确的错误码和一段文案。我想强调的是:错误处理的设计不只是「显示正确的错误信息」,而是「给用户一条可走的下一步」。
踩过的坑:用设备本地时间算倒计时,收到「倒计时归零但团还在拼」的反馈。设备时钟不准的用户看到的剩余时间与实际相差很大。更严重的是我们在本地判定「归零就是结束」并禁用了参团按钮,时钟快的用户明明还能参团却被自己的设备挡住了。修法是服务端时间校准加单调时钟递推加到点后问服务端。教训是:客户端时间是不可信输入——这条我在 IM 消息排序上也遇到过(用客户端时间排序,某用户手机快一天导致消息永远排最前),凡是「时间参与了判定」的地方都要先问一句「这个时间是谁给的」。
踩过的坑二:成团瞬间的并发失败返回笼统的「参团失败」。差最后一个人时几个人同时点,失败的用户看到「参团失败」,以为是系统问题,有人反复重试、有人直接走了。修法是区分失败原因并给出「为你开新团」的下一步。教训是:并发场景下的失败是正常业务结果而不是异常,它的文案和后续路径应该被当成正常流程来设计,而不是复用通用的错误提示。
踩过的坑三:切后台再回来,倒计时和团状态都不对。用户切到微信分享完回来,定时器被挂起过,倒计时的剩余时间比实际多;长连接也断了,团进度停在切走前的数字。修法是回前台重新校准时间并重新拉状态。教训是:任何「基于时间推进」或「基于长连接同步」的界面,回前台都不能接着用挂起前的状态继续走——这和 IM 客户端 onShow 立刻探活是同一条。
踩过的坑四:列表里每个团各起一个定时器,页面明显卡顿。团列表一屏几十个团,几十个定时器同时跑,低端机上滚动明显掉帧。修法是共用一个计时器统一驱动所有倒计时,并且剩余时间长时按分钟更新。教训是:定时器是应用级资源,不应该按 UI 元素数量线性增长——这和「长连接是应用级资源不是页面级资源」是同一类判断。
没做的部分:没做「即将成团」的强提醒(差一人时推送给曾浏览过的用户)。有转化价值但涉及推送策略和打扰控制,需要产品先定规则。也没做拼团的社交关系展示(显示团里有哪些好友),需要通讯录或社交关系授权,隐私成本高。
倒计时偏差:报校准后本地倒计时与服务端剩余时间的偏差分布。测法是在客户端定期上报「本地算出的剩余时间」与「服务端返回的剩余时间」的差值,看分布。要说明这个偏差在长时间停留后会累积,所以有多个重新校准的时机。
「倒计时归零但仍可参团」类反馈:报相关用户反馈或工单数量,改造前后对比。工单数是外部可核查的证据,比自述的「时间准了」有说服力。
误挡的转化损失:报「本地判定已结束而服务端实际仍可参团」的次数。改造后这个数应该归零(因为不再本地判定)。改造前这个数字压根测不到——这一点要诚实说明:我们是通过「时钟偏差分布」反推影响面的。
并发失败的处理效果:报「团已满」失败后点击「开新团」的比例。这个数字直接证明「把失败转化成新开始」是有效的,而且它是一个可归因到这个改动的业务指标。
前后台恢复:确定性验证——切后台数分钟后返回,检查倒计时校准到正确值、团进度重新拉取、长连接恢复。三项要分别验。
定时器数量与帧率:报团列表页的定时器数量(从每团一个变为共用一个)与低端机滚动帧率。报「定时器数量与列表长度解耦」这个结构变化,比报一个帧率数字更能说明设计。
不要报「倒计时 100% 准确」。设备时钟漂移是物理存在的,校准只能收敛偏差不能消除。也不要报「成团率提升 N%」——成团率主要由活动力度和商品决定。正确表述是「倒计时偏差的分布与多个重新校准的时机、不再本地判定结束、并发失败的转化路径与其点击率、前后台恢复由用例保证」。
onShow 立刻发心跳探活是完全同一条判断。顺带说一个性能上的坑:团列表一屏几十个团,我们最初每个团各起一个定时器,几十个定时器同时跑,低端机上滚动明显掉帧。修法是共用一个计时器统一驱动所有倒计时,而且剩余时间长时按分钟更新、接近结束才按秒。教训是定时器是应用级资源,不应该按 UI 元素数量线性增长——这和「长连接是应用级资源不是页面级资源」是同一类判断。
这个模块讲的是一个容易被低估的问题:从分享进来的用户,其状态的组合数远超从站内进来的用户。
站内用户已经登录、已经授权、是主动找到这个活动的。而从分享卡片点进来的用户可能:没登录、没授权、第一次用我们的产品、点开时团已经满了、活动昨天就结束了、商品已经下架了——而这些状态会同时出现。
第一版是按需求逐条实现的:产品说「未登录要引导登录」就加一段、说「团满了要提示」就加一段。结果组合场景全漏了。
最典型的一个:「未登录且团已满」。我们的判定顺序是先判登录,于是用户先被拉去登录、授权、绑手机号,一套流程走完之后才被告知「这个团已经满了」。用户白走一趟,投诉很直接。
还有「活动已结束但页面照常渲染」:用户点开一个几个月前的分享,页面正常显示商品和参团按钮,点下去才报错。
所以第一个也是最重要的设计是把入口状态穷举成矩阵,并固定判定顺序。
判定顺序的原则是「从大到小、从不可挽回到可挽回」:先判活动与商品是否有效(这是最大的前提,无效的话后面全都不用判)、再判团的状态(团满了但活动还在,可以开新团)、最后才判登录(因为登录是用户要付出成本的动作,只在确认「这件事真的可以做」之后才要求他付出)。
第二个设计是登录与授权时机后置。第一版进页面就要求登录,分享落地页的转化明显偏低——用户只是被朋友拉来看看,还没决定要不要参与,就先被要求登录授权,很多人直接退了。改成浏览团详情不要求登录,只在「点参团」这个真正需要身份的动作前才要求。
第三个设计是每种异常状态各配一条明确出路,而不是统一报错退回首页。活动结束 → 推荐同类活动;团已满 → 为你开新团;商品下架 → 回商品列表看相似商品。用户是被朋友拉来的,他有意愿,把他退回首页等于浪费这个意愿。
第四个是状态判定收敛为单一函数。同一套状态判定要在多个入口复用(分享卡片、小程序码、短信、站内),各入口各写一份必然分叉——我们就出现过某个入口漏判了「商品下架」。
最后是冷启动的顺序问题。App 冷启动时参数是异步到达的,第一版在参数到之前就开始渲染,结果先渲染了一个「无邀请人」的默认状态,参数到了之后再刷新,用户能看到界面闪一下;更糟的是有时参数到得太晚,我们已经按默认状态发了请求。所以要明确参数接收与首屏渲染的顺序:参数未就绪时显示加载态,就绪后再判定并渲染。
把状态穷举成矩阵,代价是前期设计时间和用例数量。按需求逐条实现更快,每次只写当前这一条。但组合状态是乘法关系——五个二值状态就是三十二种组合,而产品需求文档通常只描述其中几种典型的。矩阵化的价值是把「没被描述到的组合」显式暴露出来,然后决定每一种的行为(很多组合可以归并到同一处理)。取舍依据是:组合爆炸类的问题靠「想到了就处理」一定会漏,必须靠穷举。而且矩阵一旦建立,新增状态时只需在矩阵上补一行/一列,比在散落的判断里找漏洞容易得多。
判定顺序按「用户付出成本的先后」排,而不是按「实现的方便」排。实现上最方便的顺序是先判登录(因为很多接口需要登录态才能查)。但从用户体验看,登录是他要付出成本的动作,让他付出之后再告知「这事做不了」是最糟的顺序。所以我们让活动状态、团状态的查询接口支持未登录访问,从而能在要求登录之前就判完前两层。代价是这些接口要单独做未登录场景的鉴权与限流,缓解手段是这些接口只返回公开信息、不含用户相关数据。这个取舍值得讲,因为它说明「体验上正确的顺序」有时需要接口设计配合。
登录后置的代价是「未登录状态下的功能边界要想清楚」。浏览不要求登录之后,页面上有一部分内容是需要身份的(我参与的团、我的优惠券)。这些要么隐藏、要么显示为「登录后查看」,不能让它们以空值或默认值的形态出现——空值会被用户理解成「我没有」而不是「需要登录」。这是登录后置带来的额外设计工作,不能只改一个判断就以为完事。
踩过的坑:「未登录且团已满」时先弹登录,用户走完登录授权绑手机才被告知团满。判定顺序是先判登录导致的。用户白走一趟,投诉很直接——而且这类用户是被朋友拉来的新用户,第一次体验就是这个,基本不会再来。修法是固定判定顺序,把登录放到最后。教训是:让用户付出成本的步骤要尽可能靠后,在此之前应该把所有「这件事能不能做」的判定做完——这条在注册、授权、支付、实名等所有需要用户付出的环节都成立。
踩过的坑二:进页面就要求登录,分享落地页转化明显偏低。用户只是被朋友拉来看看、还没决定要不要参与,就先被要求登录授权,很多人直接退了。修法是浏览不要求登录,只在点参团前才要求。教训是:转化漏斗里每加一个前置门槛都会损失一批人,而门槛的位置决定了损失的量级——放在「用户还没产生意愿」的位置损失最大,放在「用户已经决定要做」的位置损失最小。
踩过的坑三:某个入口漏判了「商品下架」。状态判定在几个入口各写了一份,短信入口那份漏了商品下架的判断,用户点进去看到一个已下架商品的团,参团时报错。修法是判定收敛为单一函数。教训还是那条:同一个业务判断不能有多份实现——这次的形态是「多个入口各自判状态」,而入口的数量还会继续增加。
踩过的坑四:App 冷启动时参数未到就渲染,界面闪一下而且发了错误的请求。先渲染「无邀请人」的默认状态、参数到了再刷新,用户能看到界面闪;更糟的是有时我们已经按默认状态发了请求,这会污染埋点甚至造成错误的归因。修法是参数未就绪时显示加载态,就绪后再判定并渲染。教训是:异步到达的关键参数不能靠「先渲染再纠正」处理,尤其当渲染会伴随副作用(发请求、埋点)时——纠正得掉界面,纠正不掉已经发出去的副作用。
没做的部分:没做「未登录状态下的完整参团」(先参团后补身份)。产品讨论过,但涉及匿名订单与后续绑定的复杂度,而且风控上有刷单风险,最后维持「参团前必须登录」。也没做入口状态的可视化配置(让运营配每种状态的文案与出路),当时写在代码里,文案调整要发版。
组合状态的覆盖:这一项只能用用例数量与覆盖清单来说明,不能用比率。报状态矩阵有多少个组合、写了多少条用例、哪些组合被归并到同一处理。这个清单本身就是最好的证明,比「我们处理了各种状态」具体得多。
分享落地页转化:报「打开分享 → 参团」的转化率变化,改造前后对比。要说明这个改动是登录后置带来的,并且承认转化还受活动力度影响,所以最好用同一活动改造前后的对比,而不是跨活动比。
「白走一趟」类投诉:报「登录完才发现团满/活动结束」的投诉或工单数,改造后应该归零(因为判定顺序上不可能了)。用绝对数,而且这类用户是新用户,每一个都值钱。
各异常状态的出路点击率:报「活动结束→推荐同类」「团已满→开新团」这些出路的点击率。这个数字直接证明「给出路」比「退回首页」有效,而且能看出哪条出路设计得不够好。
冷启动参数就绪:确定性验证——模拟参数延迟到达,检查首屏不出现默认状态的闪烁、且未按默认参数发出请求。后半句是重点,因为界面闪烁可见、错误请求不可见。
入口一致性:确定性验证——对每个入口(分享卡片、小程序码、短信、站内)分别构造各异常状态,检查行为完全一致。这是收敛为单一函数之后必须固化的用例。
不要报「入口状态处理准确率 100%」。状态是会新增的,而新增状态就意味着新的未覆盖组合。正确表述是「状态矩阵的组合数与用例覆盖、判定顺序的原则、各异常出路的点击率、多入口行为一致由用例保证」,并说明新增状态时要回到矩阵补全组合这个流程约定。
没有匹配的内容,换个关键词试试。
项目拆解 · 拼团与分享裂变(uni-app)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据