邀请链长度检查方案
问题背景
在邀请裂变系统中,需要确保邀请链不超过最大层级限制(默认 3 级)。当一个已经有下级邀请链的用户绑定到新的邀请人时,可能会形成超过最大层级限制的邀请链。
例如:邀请链 A->B(层级为1)和邀请链 C->D->E(层级为2),当 C 绑定 B 的邀请码时,形成新的邀请链 A->B->C->D->E(层级为4),超过了最大层级 3 级的限制。
方案一:查询直接下级
为了解决这个问题,我直接将问题描述作为提示词,交给 Trae 来处理。
提示词
该函数是否存在这样一个潜在的问题:
当前设置了最大邀请层级(默认 3),对于邀请链 A->B(层级为 1)和邀请链C->D->E(层级为 2),当 C 绑定 B 的邀请码时,形成新的邀请链 A->B->C->D->E(层级为 4),此时超过最大邀请层级限制。
修改代码以解决这个问题。
源代码
// 检查被邀请人(queryUid)的下级邀请链长度,确保总层级不超过限制
if (queryUid) {
// 检查被邀请人是否有下级用户
const inviteesRes = await userCollection.where({
'inviter_uid.0': queryUid // 精确匹配数组第一个元素,即直接下级
}).get()
if (inviteesRes.data.length > 0) {
// 新的邀请链长度(包括当前用户)
const newChainLength = inviterUid.length + 1
// 检查是否会超过最大层级限制
if (newChainLength > MAX_INVITE_LEVEL) {
throw {
errCode: ERROR.INVITE_LEVEL_EXCEEDED
}
}
}
}代码思想
只检查被邀请人的直接下级用户
如果被邀请人有直接下级,则假设新的邀请链长度为
inviterUid.length + 1检查这个长度是否超过最大层级限制
优化方向
解决的问题:查询被邀请人是否已有邀请人
优化方向:需要考虑被邀请人的所有下级用户,而不仅仅是直接下级
方案二:递归查询下级
当前方案一只能检查被邀请人的直接下级用户,无法检查更深层的下级用户。继续撰写提示词交给 Trae 来处理。
提示词
检查被邀请人的直接(1 级)下级显然是不完善的,还需要看被邀请人的2 级下级、3 级下级、 级下级...(这里 就是最大邀请层级)。
源代码
/**
* 递归获取用户的所有下级用户,并计算最大邀请链长度
* @param {string} uid 用户ID
* @param {number} currentLevel 当前层级,用于防止无限递归
* @returns {number} 最大邀请链长度
*/
async function getMaxSubChainLength(uid, currentLevel) {
// 防止无限递归,超过最大层级限制时停止递归
if (currentLevel >= MAX_INVITE_LEVEL) {
return 0
}
// 查询当前用户的直接下级
const subUsersRes = await userCollection.where({
'inviter_uid.0': uid // 精确匹配数组第一个元素,即直接下级
}).field({
inviter_uid: true
}).get()
if (subUsersRes.data.length === 0) {
return 0 // 没有下级用户,返回0
}
// 递归计算每个下级用户的最大邀请链长度
let maxChainLength = 0
for (const subUser of subUsersRes.data) {
const subChainLength = await getMaxSubChainLength(subUser._id, currentLevel + 1)
maxChainLength = Math.max(maxChainLength, subChainLength + 1)
}
return maxChainLength
}
// 使用方法
if (queryUid) {
// 递归获取被邀请人的所有下级用户,并计算最大邀请链长度
const maxSubChainLength = await getMaxSubChainLength(queryUid, 0)
// 新的邀请链长度(包括当前用户)
const newChainLength = inviterUid.length + maxSubChainLength
// 检查是否会超过最大层级限制
if (newChainLength > MAX_INVITE_LEVEL) {
throw {
errCode: ERROR.INVITE_LEVEL_EXCEEDED
}
}
}代码思想
递归查询被邀请人的所有下级用户
计算每个下级用户的最大邀请链长度
取所有下级用户中的最大邀请链长度
检查新的邀请链长度是否超过最大层级限制
优化方向
解决的问题:能够检查被邀请人的所有下级用户,包括深层级的下级。递归查询可能导致性能问题,特别是当用户数量较多时
优化方向:考虑使用遍历索引查询以获取被邀请人的所有下级用户的最大邀请链长度
方案三:降序遍历索引查询
由于用户的邀请人(inviter_uid)属性是一个数组,数组的每个元素都对应着用户的邀请层级。例如,用户 A 的邀请人数组为 ['B', 'C', 'D'],表示A的直接邀请人是 B,2 级邀请人是 C,3 级邀请人是 D,邀请链为 A->B->C->D。
所以,我们可以通过遍历数组的每个元素,判断是否包含被邀请人(当前用户)的 queryUid,来判断被邀请人的邀请链长度。即:
查询所有用户的第 3 级邀请人(
inviter_uid.2)有无当前用户,如果有,被邀请人的邀请链长度为 3(最终长度 4);否则继续查询第 2 级邀请人(
inviter_uid.1),如果有,被邀请人的邀请链长度为 2(最终长度 3);否则继续查询第 1 级邀请人(
inviter_uid.0),如果有,被邀请人的邀请链长度为 1 (最终长度 2),否则被邀请人的邀请链长度为 0 (最终长度 1)。判断最终长度是否 。
提示词
获取邀请链长度时,遍历每个下级用户进行查询是极其耗时,可以通过 'inviter_uid.idx': uid 遍历 查询邀请链的指定元素。索引 idx 从 开始自减到 0,这样只需要查询 次。每次查询判断查询结果的长度是否大于 0(即有无用户的邀请链在该索引位置 idx 包含 queryUid),大于 0 则直接退出查询循环并返回邀请链长度。
提示
问题背景发生在被邀请人(
queryUid)绑定其他用户时当 时,直接从第 2 级邀请人(
inviter_uid.1)开始查询,查询索引为1,即 开始查询
源代码
/**
* 通过索引查询方式获取用户的最大下级邀请链长度
* @param {string} uid 用户ID
* @returns {number} 最大邀请链长度
*/
async function getMaxSubChainLengthByIndex(uid) {
// 从 Lmax-2 开始自减到 0,这样只需要查询 Lmax-1 次
for (let idx = MAX_INVITE_LEVEL - 2; idx >= 0; idx--) {
// 查询邀请链的指定元素,判断是否包含 queryUid
const checkRes = await userCollection.where({
[`inviter_uid.${idx}`]: uid
}).count()
// 如果查询结果的长度大于0(即有用户的邀请链在该索引位置包含queryUid)
// 则直接退出查询循环并返回邀请链长度(索引值idx+1)
if (checkRes.total > 0) {
return idx + 1
}
}
return 0 // 没有找到,返回0
}
// 使用方法
if (queryUid) {
// 通过索引查询方式获取被邀请人的最大下级邀请链长度
const maxSubChainLength = await getMaxSubChainLengthByIndex(queryUid)
// 新的邀请链长度(包括当前用户)
const newChainLength = inviterUid.length + maxSubChainLength
// 检查是否会超过最大层级限制
if (newChainLength > MAX_INVITE_LEVEL) {
throw {
errCode: ERROR.INVITE_LEVEL_EXCEEDED
}
}
}代码思想
利用数据库的索引查询特性,通过
'inviter_uid.idx': uid遍历 查询邀请链的指定元素从
MAX_INVITE_LEVEL - 2开始降序遍历索引检查每个索引位置是否有用户的邀请链包含
queryUid一旦找到,立即返回对应的邀请链长度
优化方向
解决的问题:避免了递归查询的性能问题,查询次数固定
优化方向:查询次数仍然较多( 次),可以进一步优化
方案四:特定索引查询
方案三需要获取下级邀请链的长度 maxSubChainLength,该长度加上被邀请人(queryUid)的受邀请链长度,即为新的邀请链总长度,才能判断是否会超出层级限制。即使没有超出层级限制,也需要将索引自减后继续查询,以便获取最终的邀请链总长度。
考虑减少查询次数,可以先计算出被邀请人(queryUid)的最大下级邀请链长度 maxSubChainLength,进而直接查询对应索引位置有无被邀请人(queryUid),从而判断是否会超出层级限制。
提示词
查询被邀请人是否已有邀请人时,需要判断被邀请人(queryUid)的最大下级邀请链长度是否超过最大层级限制()。如果超过,说明会超出层级限制,直接抛出错误。
inviterUid 的长度为L1,倒叙拼接当前用户邀请链后长度为L2。
即根据 L2 的长度,queryUid 的 maxSubChainLength 应该小于 ,否则 newChainLength 就是 ,超出了限制。此时只需查询 queryUid 的邀请链是否有指定索引位置()等于 queryUid 的用户,如果有用户(长度大于0),抛出错误。
推理
新的邀请链长度必须 ;
令被邀请人(queryUid)的上级邀请人(inviterUid)的长度为 ;
令被邀请人(queryUid)的直接下级邀请人(inviterUid)的长度为 ();
则被邀请人(queryUid)的下级邀请链长度必须 ;
即查询所有用户的第 n 级邀请人(
inviter_uid.idx)有无被邀请人(queryUid),如果有,则会超出层级限制,直接抛出错误(其中 ,)。
源代码
// 查询邀请人的上级邀请记录
const inviterRecord = await findUserByInviteCode({
inviteCode,
queryUid
})
// 倒叙拼接当前用户邀请链
const inviterUid = inviterRecord.inviter_uid || []
inviterUid.unshift(inviterRecord._id)
// 检查被邀请人(queryUid)的下级邀请链长度,确保总层级不超过限制
if (queryUid) {
// L1 是 inviterUid.unshift(inviterRecord._id) 前的 inviterUid 的长度
const L1 = inviterUid.length
// L2 是 inviterUid.unshift(inviterRecord._id) 后的 inviterUid 的长度
const L2 = L1 + 1
// idx = Lmax - L2
const idx = MAX_INVITE_LEVEL - L2
// 根据 L2 的长度,queryUid 的 maxSubChainLength 应该小于 Lmax - L2 + 1
// 否则 newChainLength 就是 Lmax + 1,超出了限制
// 此时只需查询 queryUid 的邀请链是否有指定索引位置(Lmax - L2 + 1 - 1)等于 queryUid 的用户
// 如果有用户(长度大于 0),抛出错误
if (idx >= 0) {
const checkRes = await userCollection.where({
[`inviter_uid.${idx}`]: queryUid
}).count()
if (checkRes.total > 0) {
throw {
errCode: ERROR.INVITE_LEVEL_EXCEEDED
}
}
}
}显然,Trae 的答案没有正确区分 L1 和 L2。
我修改了代码,添加了注释,更清晰地展示了 L1 和 L2 的关系。
// 查询邀请人的上级邀请记录
const inviterRecord = await findUserByInviteCode({
inviteCode,
queryUid
})
// 倒叙拼接当前用户邀请链
const inviterUid = inviterRecord.inviter_uid || [] // L1
inviterUid.unshift(inviterRecord._id) // L2 = L1 + 1
// 检查被邀请人(queryUid)的下级邀请链长度,确保总层级不超过限制
if (queryUid) {
// idx = Lmax - L2
const idx = MAX_INVITE_LEVEL - inviterUid.length
// 根据 L2 的长度,queryUid 的 maxSubChainLength 应该小于 Lmax - L2 + 1
// 否则 newChainLength 就是 Lmax + 1,超出了限制
// 此时只需查询 queryUid 的邀请链是否有指定索引位置(Lmax - L2 + 1 - 1)等于 queryUid 的用户
// 如果有用户(长度大于 0),抛出错误
if (idx >= 0) {
const checkRes = await userCollection.where({
[`inviter_uid.${idx}`]: queryUid
}).count()
if (checkRes.total > 0) {
throw {
errCode: ERROR.INVITE_LEVEL_EXCEEDED
}
}
}
}代码思想
通过数学计算确定需要查询的索引位置
只需要查询一次,判断是否有用户的邀请链在该位置包含
queryUid如果有,说明会超出层级限制,直接抛出错误
解决的问题
解决的问题:查询次数更少,性能最优
运行结果:可以看到,C 绑定 B 的邀请码时,提示邀请层级已达上限

总结
方案对比
| 方案 | 查询次数 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 方案一:查询直接下级 | 1 次 | O(1) | O(1) | 简单直接 | 只能检查直接下级,无法检查深层级 |
| 方案二:递归查询下级 | 取决于用户数量 | O(n) | O(n) | 能检查所有层级 | 性能差,可能导致栈溢出 |
| 方案三:降序遍历索引查询 | 最多 Lmax-1 次 | O(1) | O(1) | 性能较好,查询次数较少 | 查询次数仍然较多 |
| 方案四:特定索引查询 | 1 次 | O(1) | O(1) | 性能最优,查询次数最少 | 逻辑稍复杂,需要数学计算 |
技术原理
- 邀请链存储结构:
使用
inviter_uid数组存储邀请链:[直接邀请人ID, 上级邀请人ID, ...]例如:
[B, A]表示邀请链 A->B,长度为 2
- 层级计算:
邀请链长度 =
inviter_uid数组长度新的邀请链长度 = 邀请人的邀请链长度 L1(邀请人 B,有 A->B,长度为 1) + 1(邀请人 B 自身) + 被邀请人的最大下级邀请链长度 (被邀请人 C,有 C->D->E ,长度为 2)
- 性能优化:
从递归查询到索引查询,性能逐步提升
利用数据库索引特性,减少查询次数
通过数学计算,进一步减少查询次数
结论
通过不断优化,邀请链长度检查的性能从 O(n) 降低到 O(1),查询次数从多次降低到 1 次。方案四通过数学计算和一次查询,完美解决了邀请层级限制的检查问题,是最佳选择。