Invite 邀请归因
Invite 是 oak-general-business 6.1.0 新增的一套通用邀请归因能力。它解决的不是“注册表单怎么画”,而是“一个用户通过谁的邀请入口进入应用,并在后续登录或注册成功后,把这次来源关系可靠地落下来”。
所以阅读这块能力时,最好先把它和 UserEntityGrant、Parasite 分开:
UserEntityGrant解决的是把某个对象上的关系权限分享给别人认领;Parasite解决的是先给一个临时身份入口,后续再唤醒或激活;Invite解决的是邀请来源归因,最终落到邀请人、被邀请 token 和应用之间的关系。
也就是说,Invite 更适合“邀请注册”“邀请好友”“渠道归因”“扫码进入后再登录”这类场景。
主要对象
邀请归因链路里有三个实体:
inviteinviteTouchinviteRelation
invite 是邀请人的邀请身份。当前后端会保证同一个 inviterId + applicationId 下只有一条有效邀请记录,调用 getMyInvite 时如果不存在会自动创建,如果存在但被停用则会重新启用。
inviteTouch 是一次触达记录。用户打开邀请链接、扫邀请二维码,或者从微信分享进入时,只要最终进入公共触达页并调用了 touchInvite,都会先生成一条 touch。它记录:
- 属于哪条
invite - 属于哪个
application - 来源是
webLink、wechatQrCode、wechatPublicScan还是wechatMpShare - 是否已经转化成正式邀请关系
- 可选的
wechatUserId
inviteRelation 是最终归因结果。它记录:
- 邀请人
inviterId - 被邀请人的
inviteeTokenId - 应用
applicationId - 原始 touch
touchId - 生效时间
effectedAt
数据库上会约束 inviteeTokenId + applicationId 唯一,也会约束一个 touchId 只能生成一条关系。因此同一个 token 在同一个应用里不会被重复归因。
一次完整邀请链路
完整流程可以拆成四步。
1. 邀请人生成邀请入口
前端可以直接调用:
const result = await this.features.invite.getMyInvite();
这一步需要当前用户已经登录,因为后端会用当前 userId 作为邀请人。
如果要给指定应用生成邀请入口,也可以调用:
const result = await this.features.invite.getMyInviteByTarget(targetApplicationId);
返回结果里最常用的是:
result.invite.coderesult.landingUrlresult.landingRoute
landingUrl 是对外分享的完整 URL。它不是从旧的 application.config.location 拼出来的,而是通过当前 Application 绑定的 Domain 生成,路径固定走邀请触达页:
/invite/landing?code=...
公共包也提供了 components/my/invite,可以直接展示邀请码、邀请链接、二维码和触达记录。项目里如果只需要一个“我的邀请”入口,优先包这个组件,而不是从零写。
2. 被邀请人打开邀请落地页
模板里已经有:
template/src/pages/frontend/invite/landing
这个页面会挂载 components/invite/landing。组件拿到 code 和 source 后,会调用:
await this.features.invite.touchInvite({
code,
source,
});
后端会校验邀请码是否存在、是否有效、是否属于当前应用,并创建 inviteTouch。如果当前访问者已经登录,并且上下文里有 token,会立刻尝试生成 inviteRelation。
如果访问者还没有登录,features.invite 会把这次 touch 暂存在 localStorage 里,等待后续登录或注册成功。
3. 跳到项目配置的业务落地页
/invite/landing 只是公共触达页,不应该承载业务注册 UI。真正跳到哪里,由当前应用配置决定:
invite: {
landing: {
pathname: '/frontend/login',
props: {}
},
touchTtl: 7
}
其中:
landing.pathname是 touch 成功后跳转的页面;landing.props会透传给跳转;touchTtl是触达记录在前后端保留的天数,不配时默认 7 天。
这个配置在 Application.config.invite 上,web、wechatMp、wechatPublic、native 类型应用都支持。
4. 登录或注册成功后物化邀请关系
当前 features.token 已经和 features.invite 接好了。它在调用登录或注册 aspect 前,会自动取 pending touch:
const inviteTouchId = await this.getInviteTouchId();
下面这些前端方法都会自动带上 inviteTouchId:
features.token.loginByMobile(...)features.token.loginByEmail(...)features.token.loginByAccount(...)features.token.registerByLoginName(...)
后端登录或注册成功后,会通过 materializeInviteRelationForUser 创建 inviteRelation,然后把 touch 标记成 transformed。前端拿到登录结果后,也会清理本地 pending touch。
这里的“自动带上”只覆盖上面列出的公共 token feature 方法。features.token.loginByOAuth(...)、loginWechat(...)、loginWechatMp(...)、loginWechatNative(...) 当前不会读取本地 pending touch 再传 inviteTouchId。项目页面如果使用公共账号、手机、邮箱或登录名注册方法,通常不需要自己传;如果绕过公共 token feature、直接调用后端 aspect,或者自定义了其它登录入口,就要确认这条登录链路是否支持 inviteTouchId。不支持时,应在项目自己的后端登录入口里调用邀请归因物化逻辑,而不是只在前端多传一个无效参数。
微信二维码和扫码来源
创建 invite 时,公共 trigger 会按应用类型自动补邀请二维码。
当前规则是:
wechatPublic服务号应用会生成公众号二维码;wechatMp如果配置了qrCodePrefix,会生成小程序 domain URL 二维码;- 普通
wechatMp会生成小程序码; - 非微信应用不会自动生成微信二维码。
二维码里的页面仍然会指向公共触达页,并带上:
{
code,
source: 'wechatQrCode'
}
wechatQrCode 是当前公共 invite 二维码 trigger 会主动写入的来源。wechatPublicScan 和 wechatMpShare 是 touchInvite 接受的来源值,但需要具体入口显式传入;不要误以为公共包会在所有公众号扫码或小程序分享场景里自动创建 touch。
微信登录还有一个特殊归因能力:loginWechat、loginWechatMp、loginWechatNative 等流程最终会加载 token 信息;如果 token 关联的是 wechatUser,后端会尝试用同一个 wechatUserId 最近一次未转化 touch 来生成 inviteRelation。这个能力的前提是 touch 记录本身带了 wechatUserId。当前公共 /invite/landing 模板只接收 code 和 source,不会自动取得并传入 wechatUserId;普通 web 链接和邀请二维码落地后,主要还是依赖本地 pending touch 加公共 token feature 完成归因。如果项目要做“公众号扫码后不经过浏览器本地缓存也能归因”,需要在自定义微信回调或自定义落地逻辑里显式调用 touchInvite 并传入 wechatUserId。
项目最小接入步骤
如果项目已经接入 oak-general-business,邀请归因通常不需要单独初始化。最小接入重点是下面几项。
1. 同步依赖和生成文件
确认 src/configuration/dependency.ts 已经依赖 oak-general-business,然后执行项目的初始化和依赖生成流程:
npm run project:init
npm run make:dep
生成后的前端运行时会创建 features.invite,并在 features.token 里注入 invite feature。后端的 aspect、trigger、checker 则由 AppLoader 按依赖图合并。
2. 执行 6.1.0 数据库升级
需要执行 oak-general-business/upgrade/6.1.0/04.sql,创建:
inviteinviteTouchinviteRelation
如果项目还没完成 6.1.0 的其它升级,也要同时处理同目录下其它 SQL,尤其是 Application.config.location 迁到 Domain 的升级脚本。
3. 配置 Domain 和 Application invite
getMyInvite 返回的 landingUrl 依赖应用域名。项目至少要保证:
- 当前
Application能通过Domain解析出可访问域名; Application.config.invite.landing.pathname指向一个真实存在的业务页面;- 如果有多个应用共享一个系统域名,需要用
Application.domainId明确绑定。
典型配置类似:
{
type: 'web',
invite: {
landing: {
pathname: '/frontend/login',
props: {
from: 'invite'
}
},
touchTtl: 7
}
}
4. 挂载邀请触达页
如果项目来自当前模板,通常已经有:
src/pages/frontend/invite/landing
如果没有,需要从 oak-general-business/template/src/pages/frontend/invite/landing 补进项目路由,让 /invite/landing 能被访问。
这个页面不要改成注册页。它的职责只是消费 code,创建 inviteTouch,再跳转到 Application.config.invite.landing。
5. 登录注册页使用公共 token feature
业务登录、注册页面尽量使用公共 token feature:
await this.features.token.loginByMobile(mobile, captcha);
await this.features.token.loginByAccount(account, password);
await this.features.token.registerByLoginName(loginName, password);
这样 pending invite touch 会自动随登录或注册请求带到后端。
如果项目自己封了登录组件,也不要直接调用 cache.exec('loginByMobile', ...) 后就结束。要么调用 features.token,要么显式读取:
const inviteTouchId = await this.features.invite.getPendingTouchId();
并把它传给支持 inviteTouchId 的登录 aspect,再在登录成功后清理对应 touch。如果登录 aspect 本身没有这个参数,需要在后端自定义登录逻辑里完成邀请关系物化。
常见坑
把 /invite/landing 当成注册页
/invite/landing 是归因触达页。真正的注册或登录页应该放在 Application.config.invite.landing.pathname。
只配了 landing,没有配 Domain
邀请链接需要完整 URL。landingUrl 会通过 Domain 拼接,如果当前应用找不到启用的 domain,会报应用配置不完整。
自己绕过 features.token
公共 features.token 已经会自动携带 pending touch。项目如果绕过它直接调支持邀请参数的 aspect,就要自己传 inviteTouchId;如果调用的是不支持该参数的 OAuth、微信或自定义登录 aspect,需要在后端扩展登录逻辑,显式完成邀请关系物化。否则登录注册成功后不会落 inviteRelation。
以为所有登录方式都自动携带 pending touch
当前自动携带 inviteTouchId 的是 loginByMobile、loginByEmail、loginByAccount 和 registerByLoginName。OAuth、微信登录或项目自定义登录入口,需要单独确认有没有自己的归因链路。
误以为一个用户可以被多次归因
当前唯一约束是 inviteeTokenId + applicationId。同一个 token 在同一应用里只会归因一次;后续再打开其它邀请链接,最多只会把新的 touch 标记为已转化,不会改写已有归因。
忽略 touch 有效期
默认有效期是 7 天。过期 touch 不会再生成关系。项目如果需要更短或更长的窗口,应该配置 Application.config.invite.touchTtl。
和其它分享能力怎么选
如果只是想知道“这个注册用户是谁邀请来的”,用 Invite。
如果要把某个具体业务对象的权限分享给别人领取,用 UserEntityGrant。
如果要给一个还没正式登录的人先创建临时身份,并让他以这个临时身份进入流程,用 Parasite。
这三个能力可以组合,但不要互相替代。邀请归因应该保持轻量,只记录来源关系;业务权限、临时身份和后续奖励逻辑,应放在各自更合适的对象或项目私有逻辑里。