Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Parasite 寄生登录

Parasiteoak-general-business 里一个很特别的设计。它不是普通的用户,也不是普通的 token,而是一种“先寄生在某个对象或流程上,后续再被激活成正式登录态”的中间身份。

这个能力通常出现在这些场景里:

  • 用户还没正式注册,但已经开始参与流程;
  • 需要先发一个临时访问入口给用户;
  • 需要在某个对象上生成一次短期、可回收的临时身份。

主要对象

这一章的核心实体是 Parasite

它定义了:

  • 临时身份属于哪个用户;
  • 作用在哪个 entity/entityId 上;
  • 过期时间和是否允许重复使用;
  • 唤醒后应该跳到哪个页面;
  • 关联生成出来的 token。

从对象结构就能看出来,它更像一个“临时登录入口”而不是“长期账号”。

组件

现成组件主要有:

  • src/components/parasite/detail
  • src/components/parasite/excess
  • src/components/parasite/list
  • src/components/parasite/upsert

其中 detail 组件会在 web 环境下直接生成寄生访问链接,excess 则更像实际落地页。

parasite/upsert 常用参数

这组组件是项目里最常直接包起来用的创建入口,关键参数有:

  • entity
  • entityId
  • relation
  • redirectTo
  • multiple
  • nameLabel
  • nameRequired

它当前的真实流程是:

  1. 先按昵称前缀搜索 shadow 用户
  2. 如果选中了现有 shadow 用户,就直接复用 userId
  3. 如果没选中用户,就创建一个新的 shadow 用户,并自动补一条 userRelation
  4. expiresAttokenLifeLength 都设成“有效期天数换算后的毫秒数”
  5. 创建成功后直接切换到 parasite/detail 展示二维码和链接

也就是说,这个组件不是“只创建 parasite 记录”,而是已经把:

  • 找人
  • 补影子用户
  • 建关系
  • 生成分享入口

这一整段流程串起来了。

parasite/list 适合放在哪里

它的关键参数是:

  • entity
  • entityId
  • nameLabel

真实行为则是:

  • 只看当前 entity + entityId 下的 parasite
  • 默认按创建时间倒序
  • 表格操作里直接暴露 cancelqrcode
  • 点“详情”时会在弹窗里包 parasite/detail

所以这组组件最适合放在:

  • 某个业务对象的后台管理页
  • 某条邀请关系的分享记录页
  • 某个领取流程的二维码管理页

parasite/detail 的可调参数

除了自动生成链接,它还暴露了几组很实用的展示参数:

  • disableDownload
  • size
  • disabled
  • color
  • bgColor

源码里它的真实链接生成方式也值得直接写进文档:

  • web 环境下按 window.location.protocol + hostname + port
  • 自动拼 /parasite/excess?oakId=${parasite.id}

这意味着项目层如果部署域名已经稳定,parasite/detail 生成的链接就可以直接拿去复制、发二维码、放海报。

parasite/excess 的真实职责

这个组件真正干的是“消费寄生入口”,不是单纯展示页面。它进入后会:

  1. 先按 oakId 查 parasite
  2. 非法就标记 illegal
  3. 过期就标记 expired
  4. 先执行 features.token.removeToken()
  5. 再执行 features.token.wakeupParasite(parasite.id!)
  6. 最后按 redirectTo 跳回业务页

而且它在跳转时还会额外把:

  • name
  • parasiteId

一起塞进路由参数。

所以如果项目层想在目标页感知“这是寄生入口进来的”,完全可以直接读 parasiteId

前端入口与 aspect

这一章没有单独的 parasite feature,但它并不是没有前端入口。

真正的唤醒入口在:

  • src/aspects/token.tswakeupParasite
  • features.token.wakeupParasite(...)

也就是说,Parasite 的激活最终仍然走的是 token 体系,而不是自己另起一套登录机制。

后台规则

Parasite 的默认规则主要在:

  • src/checkers/parasite.ts
  • src/triggers/parasite.ts

默认行为包括:

  • 创建时强制检查 expiresAttokenLifeLength 不能为空;
  • 如果是挂到已有 userId 上,对应用户必须还处于 shadow 状态;
  • 过期时,使关联 token 自动失效;
  • 执行 cancel 时,也同步使关联 token 失效。

而在 src/aspects/token.tswakeupParasite(...) 里,还会继续做两层限制:

  • 已经过期的 parasite 不允许再唤醒;
  • 只有 shadow 用户才能被借用身份唤醒。

真正唤醒成功后,创建出来的 token 也不是长期有效的,它会按 tokenLifeLength 计算 disablesAt

此外,src/triggers/user.ts 里还有一条很重要的规则:当用户被正式激活后,会把相关的 parasite 作废。

另外还有一个直接影响业务设计的点:

  • 如果 multiple=falsewakeupParasite(...) 会在创建 token 前先把当前 parasite 标记成失效

这就是一次性寄生入口的真实落地方式。

这说明寄生模式本质上是一段过渡态,而不是长期身份模型。

注入点

这一章的注入点分成两部分:

  • 后端规则通过 ogb0Triggersogb0Checkers 注入;
  • 前端激活入口通过 features.token 暴露。

所以项目层通常不需要自己再做一次“寄生态转正式态”的底层逻辑。

项目中如何接入

Parasite 在项目里通常会拆成两端:

  • 管理端或后台页面,负责创建/查看寄生记录;
  • 消费端页面,负责拿到 oakId 后调用 features.token.wakeupParasite(...) 激活寄生 token。

公共包里已经把这两端组件都写好了:

  • src/components/parasite/list
  • src/components/parasite/detail
  • src/components/parasite/upsert
  • src/components/parasite/excess

所以项目层真正要做的,通常只是把路由接出来。

而且 redirectTo 本身就是实体字段,项目层通常只要在创建时配好:

  • pathname
  • props
  • state

激活成功后,公共组件就会按这组配置跳回业务页。

当前项目里的实际情况

从这次对 haina-busitaicang 的源码检索来看,当前没有看到它们各自落了独立的 parasite 页面壳,更多还是保留了实体、i18n 和公共能力本身。

这说明一件事:

  • Parasite 当前更像一组随时可接入的公共基础能力
  • 真正用不用、落在哪个业务对象上,取决于项目自己有没有邀请/临时访问/借用身份的场景

也就是说,新项目接这章时,不需要去找“现成业务页面”,而是应该按自己的业务流程把公共组件挂出来。

推荐的页面拆法

最稳的接法通常是:

  • 后台对象详情页挂 parasite/list
  • 新建弹窗或侧边抽屉挂 parasite/upsert
  • 分享详情弹窗直接复用 parasite/detail
  • /parasite/excess 路由单独包 parasite/excess

这样:

  • 管理端负责生成入口
  • 消费端负责激活入口

职责会很清晰。

使用示例

1. 生成寄生链接

src/components/parasite/detail/index.ts 会直接把寄生链接组装成:

/parasite/excess?oakId=<parasiteId>

因此项目里最常见的做法,就是在管理台展示这个链接或二维码,让目标用户去消费它。

1.1 创建寄生入口时常用的传参方式

项目层最常见的写法通常像这样:

<ParasiteUpsert
  oakPath="$parasite-upsert"
  entity="yourEntity"
  entityId={entityId}
  relation="viewer"
  redirectTo={{
    pathname: '/frontend/yourPage/detail',
    props: { oakId: entityId },
  }}
  multiple={false}
  nameLabel="访问者名称"
/>

这里最关键的其实不是 UI,而是:

  • relation 要能在当前对象上找到
  • redirectTo 要指向项目里真实存在的页面

2. 在消费页激活寄生 token

src/components/parasite/excess/index.ts 的核心逻辑就是:

const { data: [parasite] } = await this.features.cache.refresh('parasite', {
  data: {
    id: 1,
    expired: 1,
    redirectTo: 1,
    user: { id: 1, nickname: 1 },
  },
  filter: { id: oakId },
});

if (!parasite?.expired) {
  this.features.token.removeToken();
  await this.features.token.wakeupParasite(parasite.id!);
}

公共包里也是先移除当前 token,再唤醒寄生 token,最后按 redirectTo 跳转,这就是项目侧最标准的接入方式。

使用建议

对新手来说,最重要的一点是不要把 Parasite 当成“另一种用户表”。

更准确的理解是:

  • User 代表正式用户;
  • Token 代表正式登录态;
  • Parasite 代表一段可以被唤醒或回收的临时身份流程。

再补四条开发时必须注意的细节:

  • 被复用或新建出来的用户必须是 shadow,否则 checker 和 wakeupParasite(...) 都会直接拒绝。
  • redirectTo.pathname 最好始终写项目真实路由,不要把跳转逻辑散落在消费页里硬编码。
  • 如果希望一个分享入口只能用一次,就把 multiple 设成 false
  • 一旦用户被正式 activate,关联 parasite 会被用户 trigger 自动作废,所以不要把 parasite 当成长期邀请链接。

只要这样分清楚,在设计邀请、领取、临时访问这类功能时,就会顺很多。