Repository Wiki
jason5ng32/MyIP

用户身份、成就规则与进度更新

成就系统把应用事件转换为账号成就解锁:前端统一判断登录状态、远端快照同步状态与规则条件,再通过单槽更新队列触发上报;后端代理接口负责读取用户信息和转发成就更新。

目的与范围

本页覆盖 useAchievementEngine 的生命周期与防重逻辑、ACHIEVEMENT_RULES 的全部触发条件,以及用户信息和成就更新两个后端处理器。重点回答:事件何时可解锁成就、初始化竞态如何处理、多个成就如何顺序派发,以及上游请求失败时返回什么。

本页不展开测速、IP 查询、安全检查等业务功能内部实现,它们在这里仅作为事件来源;报告收集与分享也不属于本页范围。登录提供方、登录凭据格式、User.vue 的具体请求实现、成就展示元数据和上游数据库结构未纳入本次源码读取,不能据此推断其协议或持久化保证。运行时没有提供相邻 Wiki 页的准确路径,因此不构造未经确认的跨页链接。

概述

系统有三个不同层次的状态,排查问题时必须区分:

状态源码标识作用
用户已登录store.isSignedIn决定事件是否参与成就判断
远端成就已同步store.userAchievementsSynced避免以初始全未完成状态重复解锁既有成就
单项成就已获得store.userAchievements[slug].achieved过滤已经完成的项目

引擎并不维护百分比或累计计数;它消费事件中的速度、检查数量、国家数量等数据,并判定是否达到解锁条件。账号累计功能使用次数来自 user:info-loaded 的 totalFunctionUses,不是由引擎自行递增。

来源:use-achievement-engine.js、achievement-rules.js。

架构与职责边界

Loading diagram...

Sources: use-achievement-engine.js、get-user-info.js、update-user-achievement.js。

图中刻意不补画 User.vue 到具体 HTTP 接口的调用细节:引擎注释确认它监听单槽并向后端报告,但其请求格式、乐观更新和通知逻辑尚未直接核对。后端的两个处理器均依赖 fetchUpstream 和 logger;已读处理器没有本地数据库读写。

这样的分层让业务组件只需发出领域事件,不必重复编写登录检查、已有成就检查和派发间隔逻辑。规则表描述“什么条件达成”,引擎负责“何时允许派发”,代理处理器负责“如何转交上游”。

成就引擎的真实执行流程

1. 初始化与订阅

useAchievementEngine() 无参数,取得 useMainStore() 后创建闭包内的 queue、draining 和 pendingUntilSynced。随后对每一条规则调用 onAppEvent(rule.event, callback)。

订阅以规则为单位,而不是先对事件去重。例如一次 speedtest:finished 会交给六条测速规则分别判断,因此一次结果可同时满足多个成就。注释要求从 App.vue 的 setup 调用一次;重复实例化会创建独立队列和重复订阅。

2. 事件入口的判断顺序

事件回调严格依次执行:

  1. 未登录则直接返回,不缓存事件。
  2. 存在 when 且条件不满足则返回。
  3. 已登录但远端成就未同步时,将 slug 放入 pendingUntilSynced,暂不派发。
  4. 已同步时,查询对应成就;项目不存在或已完成则返回。
  5. 符合条件的项目进入派发队列。
javascript
1 onAppEvent(rule.event, (payload) => { 2 if (!store.isSignedIn) return; 3 if (rule.when && !rule.when(payload)) return; 4 if (!store.userAchievementsSynced) { 5 pendingUntilSynced.add(rule.slug); 6 return; 7 } 8 const entry = store.userAchievements[rule.slug]; 9 if (!entry || entry.achieved) return; 10 enqueue(rule.slug); 11 }),

Source: use-achievement-engine.js。

这是引擎回调原文节选,不是独立可运行的应用示例。它也揭示了身份边界:isSignedIn 仅是前端资格门控,不等价于后端已验证身份,更不能代替上游授权。

3. 启动阶段的远端快照屏障

启动时 ipinfo:finished、iphistory:updated 等事件可能比远端用户成就快照更早到达。如果直接使用初始未完成状态,账号已有徽章也会再次触发通知和上报。

引擎使用 Set 保存同步前已满足条件的 slug,在 userAchievementsSynced 变为真时重新检查成就是否存在且尚未完成,再加入队列,最后清空集合。监听使用 { flush: 'sync' }。

javascript
1 const pendingUntilSynced = new Set(); 2 watch(() => store.userAchievementsSynced, (synced) => { 3 if (!synced) return; 4 for (const slug of pendingUntilSynced) { 5 const entry = store.userAchievements[slug]; 6 if (entry && !entry.achieved) enqueue(slug); 7 } 8 pendingUntilSynced.clear(); 9 }, { flush: 'sync' });

Source: use-achievement-engine.js。

该集合保存的是“条件曾经成立”的事实,而不是原始 payload。同步完成后不重新运行 when,也不重新验证事件数据;这适合一次性达成事件,但扩展为依赖当前状态的规则时需要注意语义差别。

4. 单槽队列与二次去重

Store 的更新入口只有一个成就槽位。为避免一次事件同时满足多条规则而相互覆盖,引擎以 2 秒间隔派发:

  • enqueue(slug) 用 queue.includes(slug) 去除等待队列中的重复项。
  • 队列空闲时立即调用 drain(),首项不等待 2 秒。
  • drain() 从队首取出项目,派发前再次检查 achieved。
  • 调用 store.setTriggerUpdateAchievements(slug) 后,以 setTimeout 安排下一次处理。
  • 如果队列已空,将 draining 置为假并结束。
javascript
1 const drain = () => { 2 const slug = queue.shift(); 3 if (!slug) { 4 draining = false; 5 return; 6 } 7 draining = true; 8 // Re-check at dispatch time — the achievement may have been unlocked 9 // while this entry waited in the queue. 10 if (!store.userAchievements[slug]?.achieved) { 11 store.setTriggerUpdateAchievements(slug); 12 } 13 setTimeout(drain, TRIGGER_SPACING_MS); 14 };

Source: use-achievement-engine.js。

这是定时派发,不是等待请求确认的串行事务。 引擎不 await 后端响应,也没有由请求成功回调驱动出队。2 秒只解决单槽触发间距,不保证上游请求不重叠或恰好写入一次。

5. 生命周期清理

onScopeDispose 会逐一调用订阅返回的取消函数,注释说明其主要目的是避免 HMR 重跑 setup 时堆积监听器。源码没有保留定时器句柄,也没有在清理时执行 clearTimeout;已经排队的定时派发不能视为已被取消。

来源:use-achievement-engine.js、use-achievement-engine.js。

核心控制流

Loading diagram...

Source: use-achievement-engine.js。

派发前的第二次检查可能跳过已经完成的项目;图中的 setTriggerUpdateAchievements 仅在该检查允许时发生。同步状态保持为假期间,事件只累计到集合,不进入更新槽位。

成就规则参考

规则结构为 { event, slug, when? }。event 是领域事件名,slug 对应成就键;省略 when 表示事件本身足够,但仍受引擎登录与同步门控约束。以下条件按源码表达式列出,不能仅凭徽章名称推断含义。

事件成就 slug条件与边界
speedtest:finishedBarelyEnoughdownloadSpeed >= 100 Mbps
同上RapidPacedownloadSpeed >= 500 Mbps
同上TorrentFlowdownloadSpeed >= 1000 Mbps
同上SteadyGoinguploadSpeed >= 50 Mbps
同上TooFastTooSimpleuploadSpeed >= 200 Mbps
同上SwiftAscentuploadSpeed >= 1000 Mbps
user:info-loadedIAmHuman无附加条件
同上MakingBigNewstotalFunctionUses > 1000,恰好 1000 不满足
censorship:testedItIsOpenblocked 为真值
securitychecklist:progressSurfaceCheckchecked > 0
同上HalfwayTheretotal > 0 且 checked / total > 0.5,正好一半不满足
同上FullySecuredtotal > 0 且 checked === total
whois:lookupCuriousCat`(query
ruletest:finishedCrossingTheWalluniqueIPCount === 8,不是大于等于 8
invisibility:startedJustInCase无附加条件
invisibility:resultHiddenWellproxyScore === 0 && vpnScore === 0
同上SlipUp`proxyScore > 50
preferences:multiple-tests-toggledResourceHog无附加条件
preferences:autorun-changedEnergySaverallAutoRunOff 为真值
shortcut:help-openedCleverTrickery无附加条件
ipinfo:finishedPrettyDirty任一卡片 Number(qualityScore) === 1
同上SqueakyClean任一卡片 Number(qualityScore) === 100
同上WalkWithPenguins任一卡片 `(country_code
iphistory:updatedGlobeTrottercountryCount >= 10
pulse:status-sentHelloWorld无附加条件;注释约定在后端确认写入后发出
connectivity:custom-addedAddSweetAdd无附加条件;注释约定仅手动添加触发,导入列表不发此事件

来源:achievement-rules.js。

阈值与数据类型的实际含义

  • 规则可以叠加。 下载速度达到 1000 Mbps 时,三条下载速度规则同时成立;检查列表全部完成时,也可能同时满足三个进度成就。
  • 不是统一强类型校验。 >=、> 和除法可能发生 JavaScript 类型转换,而 === 不转换类型。调用方应按注释传入数值,避免字符串造成不一致结果。
  • 容错是局部的。 IP 卡片规则使用 p.cards || [],但没有保护 p 本身,也没有验证 cards 是否为数组。Whois 规则保护缺失的 query,但真值非字符串仍可能在 toLowerCase() 处失败。
  • 质量分显式转换。 sign_in_required、quota_exceeded 和缺失值经 Number() 转换不会满足 1 或 100 的精确条件。不要把这些非数值状态解释成有效质量分。
  • 语义取决于生产者。 引擎不会检查 pulse:status-sent 是否真的已获服务端确认,也不会识别手动添加与导入;这些是事件生产方的责任。

使用与扩展示例:纯谓词规则

以下为安全检查规则的原文,展示同一个进度事件如何产生多个独立成就:

javascript
1 // Security checklist progress — payload { checked, total }. 2 { event: 'securitychecklist:progress', slug: 'SurfaceCheck', when: (p) => p.checked > 0 }, 3 { event: 'securitychecklist:progress', slug: 'HalfwayThere', when: (p) => p.total > 0 && p.checked / p.total > 0.5 }, 4 { event: 'securitychecklist:progress', slug: 'FullySecured', when: (p) => p.total > 0 && p.checked === p.total },

Source: achievement-rules.js。

新增规则时,应保持 when 为纯谓词,在事件 payload 中提供判断所需数据,并确保 slug 对应实际成就项。缺少成就项会被正常事件路径和同步补发路径过滤。不要把登录判断、远端快照等待或节流再写入各业务组件,否则同一职责会分散到多个位置。

规则头部注释指出,规则单元测试会检查谓词和 slug 与成就定义的一致性;本次未读取该测试实现,因此这里不声称测试已经运行或覆盖全部边界。来源:achievement-rules.js。

用户身份与后端 API

身份信息的可验证边界

前端登录状态控制是否参与成就评估。后端两个处理器则都读取服务端 IPCHECKING_API_KEY,并将 req.headers 展开到上游请求中。服务端 API key 与用户登录状态不是同一个概念:前者控制代理是否具备调用配置,后者影响前端规则入口。

已读处理器没有显式解析用户标识、验证令牌或检查 store.isSignedIn;用户凭据如何从浏览器传入、上游如何识别账号,以及全局中间件是否另有验证,未从这些实现中确认。不能将客户端门控描述为完整的安全授权机制。

来源:get-user-info.js、update-user-achievement.js。

接口参考

路由检索确认后端注册了 app.get('/api/getuserinfo', getUserinfo) 和 app.put('/api/updateuserachievement', updateUserAchievement)。见 backend-server.js。处理器均为默认导出的 async (req, res) => { ... },通过 res 写入 JSON 响应,没有声明 TypeScript 参数或返回类型。

项目用户信息成就更新
本地接口GET /api/getuserinfoPUT /api/updateuserachievement
上游路径/userinfo?key=…/updateuserachievements?key=…
方法处理未显式传入上游 method;处理器内部不检查请求方法处理器先检查 req.method === 'PUT',上游显式使用 PUT
请求头展开 req.headers展开 req.headers
请求体无显式上游 bodyJSON.stringify(req.body)
成功返回上游 JSON 原样交给 res.json(data)同左
本地数据转换未见用户字段转换未见成就字段转换

来源:get-user-info.js、update-user-achievement.js。

请求体约束不能从错误文字推导。 更新处理器虽然返回 Achievement name is required,实际只检查 req.body 是否为假值,没有检查 name 字段,也没有验证 slug、规则条件、进度或成就是否已存在。因此不能据此给出确定的 { name: ... } 公共请求协议;该结构需进一步核对消费者与上游契约。

成就更新请求的源码示例

javascript
1 const apiResponse = await fetchUpstream(url, { 2 method: 'PUT', 3 headers: { 4 ...req.headers, 5 }, 6 body: JSON.stringify(req.body), 7 });

Source: update-user-achievement.js。

这是代理转发代码,不是浏览器调用样例。它不显式设置 Content-Type,也不在处理器内筛选转发头;若要修改请求格式或增加身份字段,应先核对上游契约与共享请求函数,而不是只修改规则表。

后端请求时序

Loading diagram...

Source: update-user-achievement.js。

配置选项与状态数据

配置/常量类型默认值生效位置与说明
IPCHECKING_API_KEY环境变量字符串处理器未提供默认值两个接口都要求存在,否则返回 500;作为上游查询参数 key
IPCHECKING_API_ENDPOINT环境变量字符串处理器未提供默认值拼接上游 /userinfo 与 /updateuserachievements URL
TRIGGER_SPACING_MSJavaScript number 常量2000成就引擎闭包外常量;不是已实现的环境变量配置

来源:get-user-info.js、update-user-achievement.js、use-achievement-engine.js。

引擎本地数据只有等待数组、同步前集合和布尔派发标记,均存在于本次 composable 调用的内存中;未见持久化或跨标签页共享。远端成就快照通过 Store 中的映射被读取,但映射的全部字段、更新时间、保存格式和账号切换重置逻辑未由已读源码确定。后端只代理 JSON,不定义本地成就实体或数据库事务。

失败模式、边界与并发

接口错误行为

场景已验证行为调试含义
更新处理器收到非 PUT 方法405,Method not allowed这是处理器防御检查;路由层本身也按 PUT 注册
缺少 API key500,API key is missing在发起上游请求前结束
更新请求体为假值400,Achievement name is required空对象和空数组并不属于假值,不能依赖此检查完成 schema 校验
上游返回非 ok抛出包含上游状态码的 Error,随后返回 500不透传上游原始 HTTP 状态或错误体
请求失败或 JSON 解析失败记录结构化错误日志并返回 500JSON 响应中的 error 是异常的 message
上游 URL 构造失败发生在处理器的 try 之外不经过这里的日志与 500 转换分支;最终行为取决于外层错误处理

两个处理器的日志消息分别为 get-user-info upstream request failed 和 update-user-achievement upstream request failed,便于区分读快照失败和更新失败。源码没有在这两个处理器中实现重试。fetchUpstream 的具体超时、重试和头部处理细节未读取,不能从函数名推断其默认值。

来源:get-user-info.js、update-user-achievement.js。

队列不是严格幂等机制

防重有三层:同步前集合去重、等待数组去重、派发前检查 achieved。但已经 shift() 出队的项目不再受 queue.includes 保护。如果远端更新或本地状态变更尚未完成,同一个事件再次出现,仍可能把该成就重新加入等待队列。最终是否重复请求、重复通知或重复写入,还取决于未读取的消费者和上游幂等实现。

此外需要注意:

  • 退出登录后不自动清队列。 入口检查 isSignedIn,但同步监听和 drain() 没有再次检查。不能保证退出登录后不会继续派发已排队项目。
  • 同步迟迟不完成会阻塞补发。 pendingUntilSynced 没有超时、重试或持久化机制;它只等待同步标志为真。
  • 排队后项目被移除仍可能派发。 正常入队路径要求条目存在,但 drain() 使用 !store.userAchievements[slug]?.achieved;条目变为不存在时该表达式仍为真。
  • 规则异常没有局部兜底。 引擎直接调用 rule.when(payload),没有独立 try/catch。异常是否被事件总线隔离需查看其实现,不能保证一条坏 payload 不影响其他订阅。
  • 没有跨实例锁。 每次 composable 调用都有自己的队列;多个实例或多个浏览器标签页之间不共享防重状态。

来源:use-achievement-engine.js。

性能与运维排查

性能特征

规则注册成本与规则数成正比,每条规则注册一个监听。数组排队通过 includes 做线性去重;IP 卡片规则通过 .some() 遍历卡片;同步前则仅保存去重后的 slug,不保留完整 payload。

连续队列按每 2000 ms 处理一项的节奏推进,首项立即派发;即使某项在派发前已完成而被跳过,仍会安排下一次 2 秒后的处理。该策略优先保证单槽触发之间有间隔,而不是追求瞬时吞吐或等待网络完成。来源:use-achievement-engine.js、achievement-rules.js。

推荐排查顺序

  1. 没有解锁: 先检查事件名和 payload 是否匹配规则,特别是严格等于、严格大于和数值类型。
  2. 登录前操作未计入: 检查事件发生时 isSignedIn;未登录事件不会进入待同步集合。
  3. 登录后仍无响应: 检查 userAchievementsSynced 是否变真,以及映射中是否存在该 slug。
  4. 部分成就稍后才出现: 检查是否一次触发多个规则;2 秒派发间隔是预期行为。
  5. 反复触发: 检查本地 achieved 更新时机、重复 composable 初始化及请求延迟;不要仅以队列去重作为成功写入证明。
  6. 接口失败: 检查 API key、endpoint URL 和对应日志;再区分网络失败、非成功状态与 JSON 解析失败。

上述顺序分别对应规则入口、同步屏障、队列和代理接口四个已核对的边界,不依赖未经确认的数据库或 UI 行为。

测试与安全扩展建议

源码检索定位到成就引擎测试 composable-achievement-engine.test.js,以及更新处理器测试 api-handlers.test.js。检索片段显示后者包含非 PUT 返回 405 的用例;本次未读取完整测试文件,也未执行测试,因此不将其他行为描述为已由测试保证。

扩展时建议针对本页已识别的边界增加或核对测试:

  • 阈值恰好相等、略高于阈值、缺失 payload 字段及字符串数值。
  • 事件早于快照、快照已含该成就、同步后集合清空。
  • 一次事件触发多个规则、排队期间外部解锁、同一 slug 在请求未完成前重复出现。
  • 排队期间退出登录、作用域销毁、账号切换和多个引擎实例。
  • 缺少 key、缺少 body、空对象 body、上游非成功响应、无效 JSON 和 URL 构造异常。

若要把“固定间隔派发”升级为可靠更新任务,应在核对 User.vue 与上游协议后再设计确认、重试、幂等键及取消机制;这些是扩展方向,不是当前引擎已经提供的能力。

相关链接