深链路由分发复盘:多入口汇入一个中枢、「待执行」单槽,以及分享短链的双层设计

项目 social-app

  • 深链
  • 路由分发
  • 短链分享
  • 通用链接
  • 冷启动

1. 背景与问题

这是一款面向日本市场的语音社交 App。用户从站外或端内到达指定页的路径很多:自定义 scheme 回跳、通用链接(Universal Link)、点击推送、会话里点一条链接、扫码、运营 Banner 内跳,以及页面里通过网页桥接发起的业务跳转。这些入口最终要落到房间、群会话、动态详情、话题、资料或活动页。

路由要同时解决三件事:

  1. 入口虽多,分发只能有一套。 每种入口各自解析、各自跳转,协议一改就要改好几处,也难以统一处理「现在还不能跳」的情况。
  2. 链接到达的时机,往往还不具备跳转条件。 冷启动时用户可能尚未登录,或还没进到首页;也可能正在匹配聊天、网络暂时不可达。解析成功不等于立刻可以 push 页面。
  3. 站外分享既要先给一个可预览的落地页,又要把已安装的用户带回指定页。 动态和话题分享到社交网络时,需要标题、描述和配图;进群、招募卡这类链接,还常常要在进群之后继续打开一个指定页。

2. 整体设计

2.1 多入口汇入一个中枢

系统入口、推送点击、端内点击和扫码,都会把一条地址交给同一个解析与分发流程。Banner 内跳和网页桥接则有一部分直接走业务跳转,不经过短链解析。

入口
否是带内层跳转且需要再拆
缺参或未知类型可识别
否
是
自定义 scheme 回跳通用链接点击推送端内点击或扫码Banner 内跳或网页桥接是否为可解析的业务短链直接业务跳转用内置网页打开解析路径和参数二次解析内层短链写入单槽待执行已登录且已进首页且有网且不在匹配中保留槽位登录成功、进首页、回前台或网络恢复清空槽位并执行

推送点击在进入中枢之前会先分流:带会话信息的,记下来,等首页出现后再进对应会话;其余把载荷当作短链来解析。首页首次出现时的优先级是:先处理推送进会话,再执行待执行的短链动作,都没有才尝试恢复异常退出的房间。网页桥接发起的去大厅匹配、创建招募卡、创建群等,不走短链解析。

运营位跳到话题,被抽进路由中枢。 内跳只处理话题这一种路径,和短链那一套类型分发是分开的。

2.2 类型如何组织

链接的「真身」是一条带路径和查询参数的长链,交给动态链接服务缩短。生成时会带上 iOS 与 Android 的应用标识,以及分享用的预览标题、描述和配图。路径用类型枚举的标识来区分;解析时看 URL 最后一段路径是否包含某个标识,不是精确匹配,也不是一张可插拔的路由表。

类型目前有 23 种,按业务可以分成几组:

分组 典型动作
关系 / 资料 打开用户资料、形象、状态墙、换装邀请
房间 / 活动 进入语音房(可带有效期)、打开活动页
群 加入群聊,并可在进群后再打开指定页
动态社区 动态详情、话题、消息盒子、切换首页 Tab
增长与活动 邀请新用户、试用、活动页、通用的页面分享
其它 明确约定不跳转的占位类型

同一套枚举要同时服务生成、解析和执行,因此对应着几处大型分支:新增一种类型,至少要改枚举、路径标识、生成、解析和执行。查询参数用短键表示用户、房间、群、话题、动态、目标页地址、内层跳转等;群标识和房间邀请时限在参数里用了相近的键,要靠路径类型来区分。预览用的标题、描述和配图不是业务跳转参数,而是生成短链时写入动态链接服务的社交元数据。

2.3 待执行是单槽,不是队列

解析成功后不会立刻跳转,而是写入一个待执行槽位,然后立即尝试执行。槽位只有一格:后一次解析会覆盖前一次。带内层跳转、需要二次解析的那条路径,在匹配前会先把槽位清空,避免用外层的半成品去执行。

执行前要过一组门闸,任一扇没过就保留槽位,等下一次时机再试:

  1. 正在匹配聊天 → 不做。
  2. 未登录 → 只有邀请类(拉新、换装、状态墙等)会提前写入本地,供注册后使用;其它类型不执行,也不清空槽位。
  3. 已登录但还没进入首页 → 不执行。
  4. 网络不可达 → 不执行;网络恢复时再试一次。
  5. 门闸都通过后,先清空槽位再执行,保证同一个动作只进入一次。

重新尝试的时机包括:热启动或已在前台时解析完成后立刻试一次;回到前台时,若已登录且不在「新好友」流程中,再试一次;冷启动进入首页时,按上面的优先级执行;登录成功时,若带着待执行动作,新用户会先走广场、跳过部分自动进房匹配,真正跳转仍等到进入首页之后;网络重连时,若槽位还在,再执行一次。


3. 关键实现

3.1 解析、二次解析,以及降级

解析的输入可能是通用链接、自定义 scheme 回跳,或端内点到的一条短链。端内点击时,如果地址缺少协议头,会先补上网页协议再解析。识别不出类型,或必选参数缺失,就降级用内置网页打开;有一种类型明确约定「保持不动」,解析后不执行跳转。

站外的动态和话题链接是双层的,解析时如果发现地址里带了内层跳转、且不属于「本身也带跳转地址」的招募卡链接,就要取出内层短链做二次解析。招募卡自己也带跳转字段,不能再拆一次,否则会把外层拆错。

3.2 双层短链:先预览,再回 App

动态和话题的站外分享做成双层短链:内层打开 App 并跳到详情,外层是不带打开 App 参数的预览页,内层地址写在跳转字段里;外层链接粘到会话里也能走二次解析。

客户端路由系统预览页动态链接服务分享方客户端路由系统预览页动态链接服务分享方alt[未登录或未进首页-][已在前台且已登录-]alt[已安装并打开 App][未安装或解析失败]生成内层短链 带打开 App 的参数内层短链生成外层 预览地址加内层跳转 不带打开 App 的参数外层短链用户在站外打开外层打开 App冷启动或热启动传入链接必要时二次解析内层写入待执行槽位登录并进入首页后再执行直接跳到话题或动态详情停留预览或引导商店

这样做的原因很直接:如果生成时就带上打开 App 的参数,已安装的用户往往会直接进 App,站外预览页来不及展示标题、描述和配图;把外层的应用参数置空,用户会先落到预览页,再从预览页打开 App,或把同一条外层链接粘到会话里点击。两种路径汇合到同一次二次解析,详情页的分发逻辑只需维护一套。

3.3 门闸与执行(示意,非原始代码)

// 示意,非原始代码
func handleIncomingLink(_ url: Link) {
    let resolved = needsSecondParse(url) ? unwrapInnerLink(url) : url
    guard let action = parse(resolved) else {
        openInWebView(resolved)            // 未知类型或缺参
        return
    }
    pending = action                       // 单槽:后一次覆盖前一次
    tryPerformPending()
}

func needsSecondParse(_ url: Link) -> Bool {
    url.hasInnerRedirect && !url.carriesOwnRedirect
}

func tryPerformPending() {
    guard let action = pending else { return }
    if isMatching { return }               // 槽位保留
    if !isLoggedIn {
        if action.isPreLogin { rememberForSignup(action) }
        return                             // 其它类型保留槽位
    }
    if !hasEnteredHome { return }
    if !networkReachable { return }
    pending = nil                          // 先清空,只执行一次
    dispatch(action)
}

「先清空再执行」是为了防止执行过程中再次触发尝试,把同一个动作跑两遍。代价是:如果执行到一半失败,槽位里已经没有这份动作,不会自动再来一次,要靠具体跳转自己处理失败。

3.4 进群后再打开指定页

群分享、招募卡可以在进群之后继续打开一个页面。执行顺序是:调用进群接口 → 用服务端返回的群标识打开会话(已在该群则提示「你已在该群中」,但仍视为成功)→ 延迟约 0.3 秒 → 优先打开链接自带的目标页;没有则按类型在本地拼出招募卡编辑页或指定活动页。

指定页群会话服务端路由指定页群会话服务端路由延迟约 0.3 秒alt[资料未完成][成功或已在该群]请求进群拒绝引导去完善资料返回群标识用服务端返回的标识打开会话优先打开链接自带的目标页

进群协议补完整:通用的目标页参数,进群后优先打开该页;预览文案可配置;进群后用服务端返回的群标识打开会话;分享者是自己则不关注;已经在群里也要打开目标页。

进群被拒绝且原因是资料未完成时,会弹出引导,让用户去完善资料。

「已在该群也视为成功」这一点很关键。如果只在「刚刚加入」时打开目标页,已经在群里的用户点开同一条招募卡链接,就只会看到一句「你已在该群中」,活动页出不来。把打开目标页放到进群跳转的成功回调里,并稍微延迟,是为了等会话页先完成转场。


4. 边界与失败处理

4.1 房间与群的冲突

进入语音房时,先消掉正在展示的弹层,再按当前所在位置分流:

已过期有效或无时限是否否是取消确定
执行进入房间邀请是否在有效期内提示邀请已失效已经在目标房间吗收起迷你窗并回到房间页已经在另一间房间吗直接进入弹出确认 将退出当前房间留在当前房间先离开再进入

房间分享可以带一个时间戳,用配置的有效时长校验,默认是 1 小时,过期就提示「房间邀请已失效」。进入过程中有进行中标记,避免重复进入。换到另一间房需要确认,是因为这会打断当前的收听或发言。

群这一侧:如果当前就已经在目标群会话里,只提示已在该群,仍回调成功,以便继续打开目标页。切换首页 Tab、跳到聊天 Tab,只在导航栈深度为 1 时执行,栈更深则静默放弃,避免把用户从一层层业务页里硬拉出来。多数跳转会先关掉正在展示的弹层;礼物中心如果栈顶已经是内置网页,会先弹出再打开新的。

4.2 未知类型、缺参,以及匹配中

  • 无法识别的路径、缺少必选参数:降级用内置网页打开,而不是什么都不做。部分活动页会一边当网页打开,一边执行对应的活动动作。
  • 正在匹配聊天:短链跳转和推送进会话都会被拦住,槽位或会话标记保留,避免把匹配流程打断。
  • 未登录时的普通跳转:槽位保留,等登录并进入首页;新用户若带着待执行动作,会从广场进入,避免和自动匹配抢。
  • 生成侧几乎不设最低版本限制。旧客户端遇到无法识别的路径或新参数,会因为匹配不上或缺参而走到内置网页。这是客户端这一侧能看到的兼容方式。

4.3 解析成功但没有跳转:怎么复现、怎么定位、怎么修

链接已经打开了 App,却停在首页或启动流程、没有到达目标页,可以按下面下手。

  1. 怎么复现:分别在未登录、尚未进入首页、飞行模式、以及正处于匹配中时点开同一条短链,再补齐那个缺失的条件。如果只有某一种条件下必现、补齐后会补跳,就可以把范围缩到对应的门闸。若在前一条尚未执行时又点开另一条,只到达后一条的目标页,则是单槽被覆盖,而不是门闸卡住。
  2. 怎么定位:先看待执行槽位有没有被写入、类型和参数对不对。槽位为空时,再看是解析失败走了内置网页,还是执行时已经被清空。槽位还在却不执行,就按门闸逐项核对:是否已登录、是否已进首页、网络是否可达、是否仍在匹配、回到前台时是不是被「新好友」流程拦住。双层短链还要确认做过二次解析:外层预览地址往往对不上业务类型,不拆内层就会被当成普通网页。
  3. 怎么修:门闸拦截且槽位仍在,应保证登录成功、首页首次出现、回到前台、网络恢复时都会再试一次,并且冷启动时不要被「恢复上次房间」抢在短链前面。单槽覆盖导致前一条丢失,是当前设计的取舍,改队列才能同时保留多条,见第 6 节。若二次解析把招募卡这类「本身也带跳转地址」的链接拆错,就要把它们排除在二次解析之外。解析失败则回到路径标识和必选参数,而不是在执行层补洞。

5. 演进

起步 多入口收成一个动作运营位内跳 收进路由双层短链 先预览再回详情进群后再打开指定页资料未完成时引导完善
  • 起步时,路由中枢把自定义 scheme、通用链接、推送等入口收成「先解析成动作,再择机执行」,并用动态链接服务生成和解析短链。类型以枚举加分支的方式逐渐增加。
  • 运营位跳到话题,抽进路由中枢。
  • 动态和话题改为双层短链,先落到预览页再回到详情;外层链接粘到会话里也能打开。
  • 进群和招募卡补上目标页参数、可配置的预览信息、以服务端返回的群标识进群、不关注自己,以及已在群里也要打开目标页。
  • 进群因资料未完成被拒绝时,引导用户去完善资料。

中枢的组织方式没有换成注册表,一直是枚举加大型分支;变化主要发生在分享回流的协议上:从「生成一条能打开 App 的短链」到「预览与回流分层」,再到进群后打开指定页。单槽待执行和门闸也保持原样,说明「先记下、条件满足再跳」这层抽象是够用的,真正变厚的是链路上的业务协议。


5.1 入口、槽位和门闸

入口很多,解析只有一套。

自定义 scheme解析通用链接推送扫码或端内点击一个动作

槽位只有一格,后一次覆盖前一次。

解析成功新的链接覆盖门闸通过后先清空执行
空待执行已跳转

门闸没过就留着,过了只跑一次。

是否否是否是
槽位里有动作正在匹配?留下已登录?已进首页且有网?清空并跳转

双层短链把预览和回 App 拆开。

客户端预览页短链服务分享方客户端预览页短链服务分享方内层,能打开 App外层,只做预览用户从预览页打开二次解析内层

进群成功之后才打开目标页,已经在群里也算成功。

是
资料未完成
点开进群链接请求进群成功或已在群里?打开会话再打开目标页引导去完善资料
起步多入口收成一个动作收进运营位内跳变厚双层短链进群后再打开指定页路由上变厚的部分

6. 复盘与可改进之处

做对了的地方

  • 把多种入口收进同一个解析与分发流程,生成、解析、执行共用一套类型,业务方不用在系统入口里各写一套跳转。
  • 待执行单槽加上登录、首页、网络、匹配这组门闸,把「链接到达」和「页面可以 push」拆开,冷启动和热启动可以走同一套执行函数,只是触发时机不同。
  • 双层短链把站外预览和回 App 详情做成同协议的两层,会话里粘贴外层链接也能汇合到二次解析,详情跳转不必维护两套。
  • 进群后再打开目标页时,以服务端返回的标识为准,并把「已在该群」视为成功,避免老成员点开招募卡时走不进活动页。

还可以做得更好

  1. 用路由表代替巨型分支。 现在新增一种类型要改枚举、标识、生成、解析、执行至少四处,也容易漏掉某一处。更好的做法是让每种类型自己注册:路径标识、必选参数、是否允许登录前记账、以及执行闭包。中枢只负责解析、门闸和调度。解析也不应再对最后一段路径做包含匹配,而是精确匹配或按注册表查找,避免短标识互相包含。
  2. 单槽与队列的取舍。 单槽的好处是实现简单,不会在启动后连跳多个页面,用户看到的是最后一次意图。代价是前一条被静默丢掉:例如先点了动态详情、启动过程中又点了房间邀请,动态就不会再打开。若要保留多条,可以用队列,但必须规定执行顺序、是否去重、以及最多跳几次,避免冷启动被连续 push。一个折中是:同类型覆盖,不同类型入队,并给队列一个很小的上限。
  3. 参数协议要有版本。 群标识和房间时限共用相近的键、二次解析靠「取出内层地址再解析一遍」,都对调用方和解析顺序很敏感。可以给协议加上版本号,字段名按类型分开;二次解析应读取明确的内层字段,而不是截取字符串。旧客户端遇到更高版本或不识别的类型时,仍可降级打开网页,但应是一种写明的兼容策略,而不是恰好走到缺参分支。
  4. 给解析补测试。 路径包含匹配、缺参、内层跳转、招募卡这类不应二次拆分的例外、未登录可记账的类型,都很适合写成纯函数测试,不需要拉起页面。现在这些规则散落在解析和执行里,改一处类型时缺少回归保护。
现在 枚举加大型分支 单槽待执行改为路由表注册评估队列与单槽的取舍参数协议带上版本为解析补单元测试更易演进的路由层