首页
/ OmniRoute 游戏化与排行榜系统全解析:本地优先、零阻塞、服务端权威计分的设计与实现

OmniRoute 游戏化与排行榜系统全解析:本地优先、零阻塞、服务端权威计分的设计与实现

2026-09-07 16:27:39作者:尤辰城Agatha

OmniRoute 内置了一套本地优先(local-first)的游戏化层,通过 XP、等级、徽章、连续打卡(streak)、排行榜、代币分享和邀请码等机制激励用户与平台互动。本文以 GAMIFICATION.md 为骨架,逐模块结合当前仓库的真实源码与测试展开:读完后你将掌握该系统从请求管线接入、SQLite 数据层、XP 曲线到反作弊审计的完整调用链,并能复现其核心计算逻辑与 API 契约。

一、系统定位与设计原则

该系统的目标是提升用户参与度和留存:可见的成长(XP、等级、徽章)、社会证明(排行榜)以及经济激励(代币分享、邀请奖励)。所有状态都保存在本地 SQLite 中;与社区服务器的联邦(federation)是可选的、推送式的。

能力 说明
XP 与等级 按动作获得 XP,沿多项式曲线升级
徽章 20+ 成就,覆盖多个类别与稀有度等级
Streak 每日活跃追踪,记录当前/最长连续天数
排行榜 global、weekly、monthly、tokens_shared、contributions 五种范围
代币分享 用户间转账,双式记账(double-entry ledger)
邀请与兑换 推荐码,SHA-256 哈希存储
社区服务器 与其他 OmniRoute 实例联邦
反作弊 服务端计分、限流、z-score 异常检测

五条设计原则(见 GAMIFICATION.md):

  1. Local-first:全部状态在 SQLite,不依赖任何外部服务;
  2. Non-blocking:事件是 fire-and-forget,LLM 响应路径绝不被游戏化逻辑延迟;
  3. Server-authoritative:XP 只在服务端计算,客户端无法虚增分数;
  4. Privacy-respecting:排行榜参与是 opt-in,用户可以隐藏个人资料;
  5. Federation-ready:社区服务器通过带签名的 API 推送分数,同步采用覆盖(overwrite)而非累加。

二、架构:单一事件入口的扇出模型

2.1 请求管线集成点

游戏化只在一个位置接入请求管线——chat 处理器在响应已经发给客户端之后,用 setImmediate 触发 emitGamificationEvent()(fire-and-forget):

Client Request
  → /v1/chat/completions
    → handleChatCore()                      [open-sse/handlers/chatCore.ts]
      → ... (existing pipeline) ...
      → upstream response sent to client
      → setImmediate (fire-and-forget):
        → emitGamificationEvent()           [src/lib/gamification/events.ts]
          → XP 累加 / 等级重算
          → updateStreak()                  [src/lib/gamification/streaks.ts]
          → 徽章检查(streak + action count)
          → updateScore() 排行榜三个范围      [src/lib/gamification/leaderboard.ts]

在当前仓库中,这一钩子的具体实现在 chatCore 游戏化钩子模块事件发射器 是唯一集成点。setImmediate + 全量 try/catch 的组合保证三件事:

  1. 响应完整发出后游戏化才运行;
  2. 游戏化错误绝不上抛到客户端(源码中 catch 分支仅记录日志 events.error);
  3. 事件处理在下一个事件循环 tick 中执行,而不是内联阻塞。

2.2 事件发射器的真实扇出逻辑

events.ts 中的 emitGamificationEvent() 接受 { apiKeyId, action, metadata }action 为如下联合类型之一:

action:
  | "request"
  | "provider_switch"
  | "model_switch"
  | "combo_create"
  | "combo_use"
  | "token_share"
  | "invite_redeem"
  | "daily_login"
  | "radar_supporter"

每个事件的扇出顺序(源码注释编号 1–4):

  1. 授 XP:查 getXpForAction(action) 得分值,调用 db.addXp() 记入 xp_audit_log,再用 calculateLevel() 重算等级,若等级变化则 updateLevel() 并记录 events.level_up 日志;
  2. 更新 streak(仅 request 动作):updateStreak(apiKeyId) 返回新连续天数,按阈值触发对应徽章——>= 365 解锁 unstoppable>= 30 解锁 monthly-master>= 7 解锁 weekly-warrior>= 3 解锁 daily-user
  3. 更新排行榜:对 globalweeklymonthly 三个范围各 updateScore() 一次;若动作是 token_share,额外更新 tokens_shared 范围;
  4. 检查动作计数徽章checkActionCountBadges() 统计该动作在 xp_audit_log 中的累计次数,对照阈值表解锁徽章。

一个特殊的动作是 radar_supporter:它不是 XP/排行榜行为,而是荣誉事件——只调用 checkAndUnlockBadge(apiKeyId, "radar-supporter", false) 后直接返回(调用方仅提供单向身份,日志中也刻意不暴露 apiKeyId)。

2.3 模块依赖图

src/lib/gamification/
  events.ts          ← 入口(由 chatCore 管线调用)
    ├── xp.ts          ← XP 计算与等级解析(纯函数,无 DB 调用)
    ├── streaks.ts     ← 每日连续活跃追踪
    ├── badges.ts      ← 徽章条件评估
    ├── leaderboard.ts ← 分数/排名管理、范围轮转
    ├── antiCheat.ts   ← 分数变更校验与异常查询
    ├── sharing.ts     ← 代币转账(双式记账)
    ├── invites.ts     ← 邀请码/兑换码
    ├── servers.ts     ← 社区服务器联邦
    └── notifications.ts ← SSE 通知流

src/lib/db/gamification.ts   ← 全部 CRUD(8 张表)

src/app/api/gamification/    ← Next.js API 路由

xp.ts 是一个关键的设计细节:文件头注释明确声明“纯函数,无副作用、无 DB 调用——所有有状态逻辑都在持久层”。这使等级曲线可以被独立单测,而 xp.test.ts 正是围绕纯函数行为编写的。

三、数据层:8 张表与领域模块

3.1 数据库表

全部表都在 OmniRoute 主 SQLite 库中,由迁移 060_create_gamification.sql 创建(已确认存在)。WAL 日志继承自 core.ts 的单例 getDbInstance()。迁移文件中实际建表:

leaderboard            user_levels          badge_definitions
user_badges            xp_audit_log         token_ledger
invite_tokens          community_servers

各表职责:

  • leaderboard:(api_key_id, scope, period) 维度存分数;
  • user_levels:每个 key 的累计 XP、当前等级、更新时间;
  • badge_definitions / user_badges:徽章定义(名称、类别、稀有度、图标、描述)与用户已解锁记录(1:N);
  • xp_audit_log:审计流水——每次授 XP、转账、异常检测都会落一行;
  • token_ledger:双式记账转账流水,含幂等键(唯一约束);
  • invite_tokens:邀请码(唯一)+ 令牌 SHA-256 哈希、已用次数、有效期;
  • community_servers:联邦服务器注册记录(名称、URL、token 哈希、状态、上次同步)。

3.2 领域模块的 CRUD 面

db/gamification.ts(614 行)遵循标准 OmniRoute 模式:从 core.ts 引入 getDbInstance(),导出类型化 CRUD 函数,路由处理器中不出现裸 SQL。当前仓库实际导出的函数:

函数 职责
updateScore / getRank / getTopN / getLeaderboardNeighbors 排行榜分数更新、查排名、Top-N 查询、邻位查询
addXp / getXp / updateLevel / getAggregateXp XP 记账、查询、等级更新、聚合统计
unlockBadge / hasBadge / getBadges / getBadgeDefinitions / getAllEarnedBadges 徽章解锁、幂等检查、查询
transferTokens / getBalance / getHistory 双式记账转账、余额、流水
createInviteToken / getInviteByCode / redeemInvite / revokeInvite 邀请码生命周期
connectServer / disconnectServer / listServers / getConnectedServerByKeyHash 联邦服务器管理
rotateLeaderboardScope 周/月排行榜归档轮转

其中 hasBadge()events.ts 中注释 #3472 强调的幂等性关键:getBadges() 因为 INNER JOIN badge_definitions,在定义表未播种时会误报“未获得”,导致每次请求重复发解锁事件;直接查 user_badgeshasBadge() 才可靠。对应的回归测试是 badge-unlock-once-3472.test.ts

四、XP / 等级系统

源码: src/lib/gamification/xp.ts

4.1 等级曲线

到达等级 n 所需增量 XP 遵循多项式曲线:

xp_for_level(n) = floor(100 * n^1.5)

xpForLevel() 的实现与文档一致(level <= 1 返回 0,否则 Math.floor(100 * Math.pow(level, 1.5)))。在此基础上,源码比文档多提供了三层 API:

  • cumulativeXpForLevel(level):从 2 级到目标级累加各级增量,得到累计阈值;
  • calculateLevel(totalXp):等级反解。实现很值得看——先用曲线逆函数得到一个快速初值(Math.floor(((xp * 2.5) / 100) ** 0.4)),再与精确的 floor 累计阈值做双向对账(while 循环向上/向下校正)。这比文档描述的“线性 1..100 遍历”更高效,且对极端大数有 Number.MAX_SAFE_INTEGER 保护;
  • xpToNextLevel(totalXp):计算距离下一等级的剩余 XP,供 Dashboard 进度条使用。

4.2 头衔与等级分层

getLevelTitle()getLevelTier() 将等级映射为头衔和展示分层(源码边界为 >=76 / >=51 / >=26 / >=11,文档表格以 75/50/25/10 近似):

等级区间 头衔 展示分层
76+ Legend diamond
51–75 Master platinum
26–50 Expert gold
11–25 Explorer silver
1–10 Beginner bronze

4.3 XP 奖励表

当前源码中 XP_REWARDSxp.ts)与 events.tsgetXpForAction() 一致的动作分值为:

动作 XP 说明
request 1 每次经 OmniRoute 路由的 API 请求
provider_switch 5 切换到另一个 provider
model_switch 3 切换到另一个模型
combo_create 10 创建新的 combo
combo_use 2 使用 combo 发起请求
token_share 1 每分享 1,000 代币(注释约定)
invite_redeem 50 成功兑换邀请码
daily_login 5 每日活跃(每天一次)
streak_bonus 2 每个连续天(随连续天数放大)
badge_unlock 10 解锁任意徽章

说明:原文档中给出的奖励表(如 badge_earned 25streak_milestone 15referral 50compression_use 2 等)与当前仓库源码存在差异;本文以 xp.tsevents.ts 的实际常量为准。

4.4 授 XP 流程

  1. XP_REWARDS[action] 得到分值;
  2. db.addXp(apiKeyId, action, amount, metadataJSON) 写入 xp_audit_log(metadata 为 JSON 字符串);
  3. getXp(apiKeyId) 取累计值,calculateLevel() 重算等级;
  4. 等级变化时 updateLevel() 落库并打 events.level_up 日志。

通知由调用方/notifications.ts 处理,与计分解耦。

五、徽章系统

源码: src/lib/gamification/badges.ts(552 行)+ db/gamification.ts 中的徽章 CRUD。

5.1 类别与稀有度

类别 说明
usage 使用量里程碑(首个请求、1K/10K/100K 请求)
sharing 代币分享与推荐
contribution 社区贡献(combo 创建、provider 探索)
streak 时间一致性(周战士、月度坚持者)
rare 稀有/隐藏成就

稀有度四档:common(灰)、uncommon(绿)、rare(蓝)、legendary(金)。徽章定义存于 badge_definitions,含名称、类别、稀有度、JSON 条件、描述、图标。

5.2 评估流程(事件驱动)

评估是事件驱动的——每次游戏化事件后运行,但只检查与事件动作对齐的条件,从而保持快速。当前源码中的两条评估路径:

路径 A:streak 徽章(在 events.ts 中,随 request 动作触发):

const streak = await updateStreak(apiKeyId);
if (streak >= 365)      await checkAndUnlockBadge(apiKeyId, "unstoppable");
else if (streak >= 30)  await checkAndUnlockBadge(apiKeyId, "monthly-master");
else if (streak >= 7)   await checkAndUnlockBadge(apiKeyId, "weekly-warrior");
else if (streak >= 3)   await checkAndUnlockBadge(apiKeyId, "daily-user");

路径 B:动作计数徽章checkActionCountBadges()events.ts):按 action 类型统计 xp_audit_log 累计次数,对照阈值表解锁:

const thresholds = {
  request: [
    { id: "first-token",     threshold: 1 },
    { id: "token-consumer",  threshold: 1000 },
    { id: "token-machine",   threshold: 10000 },
    { id: "token-whale",     threshold: 100000 },
  ],
  token_share: [
    { id: "generous",        threshold: 1000 },
    { id: "philanthropist",  threshold: 10000 },
    { id: "token-santa",     threshold: 100000 },
    { id: "community-hero",  threshold: 1000000 },
  ],
};

解锁动作本身是幂等的:checkAndUnlockBadge()hasBadge() 检查,再 unlockBadge(),随后查 badge_definitions 取徽章详情并调 recordBadgeUnlock()notifications.ts)记录一条 SSE toast 通知。原文档列出的“20+ 内建徽章完整清单”在当前仓库中由 badge_definitions 表播种 + 上述阈值表共同承载,评估框架(条件匹配 → 幂等解锁 → 通知)保持不变。

5.3 文档中的条件类型模型

原文档定义了完整的条件类型体系(badge_definitions.criteria 为 JSON):

类型 字段 说明
action_count count 执行动作 N 次(如 1000 次请求)
streak days 保持 N 天连续活跃
unique_count field, n 使用 N 个不同值(如 10 个不同模型)
rank scope, n 达到某排行榜范围第 N 名
first 首个执行该动作的用户
hidden (可变) 未获得前不展示条件

条件 JSON 示例:

{
  "type": "action_count",
  "action": "request",
  "count": 1000
}

六、Streak 连续打卡追踪器

源码: src/lib/gamification/streaks.ts

6.1 数据模型

Streak 不占独立表,而是存入共享的 key_value 表,命名空间为 gamification:streaks(源码常量 NAMESPACE),值为 JSON:

{
  "currentStreak": 8,
  "longestStreak": 12,
  "lastActiveDate": "2026-09-06",
  "streakStartDate": "2026-08-30"
}

(原文档描述为 {current},{longest},{lastDate} 逗号格式;当前实现为 JSON 对象,且额外记录了 streakStartDate。解析函数 parseStreakJson() 对每个字段做类型校验,脏数据一律回落到 emptyStreak()。)

另外两个值得注意的防御:getStreak() / updateStreak() 开头都有 if (isBuildPhase || isCloud) return ... 守卫——Next.js 构建期与云环境下直接返回空值,避免构建阶段触碰 SQLite;读取走 SELECT value FROM key_value WHERE namespace = ? AND key = ?,写入用 INSERT OR REPLACE

6.2 判定逻辑

updateStreak() 的分支:

  1. 读取 streak 记录;
  2. lastActiveDate === today → 今天已计过,原样返回 currentStreak(no-op);
  3. lastActiveDate === yesterdaycurrent += 1
  4. 其他(断档或首次活跃)→ current = 1,并重置 streakStartDate = today
  5. longest = max(newStreak, longest)
  6. 写回并返回新 currentStreak

里程碑徽章(3 / 7 / 30 / 365 天)的触发在 events.ts 中完成,如第五节所述。

6.3 边界情况

  • 时区:一律使用 UTC 日期(new Date().toISOString().split("T")[0])。这是有意为之——单一规范时区可防止“跳时区刷连续天数”;
  • 新用户:无记录时首次请求创建 current=1, longest=1
  • 一天多次请求:同一 UTC 日内只有第一次请求会推进 streak。

七、排行榜引擎

源码: src/lib/gamification/leaderboard.ts

7.1 范围(Scope)

export type LeaderboardScope =
  | "global" | "weekly" | "monthly" | "tokens_shared" | "contributions";
范围 周期 说明
global 全部 累计 XP
weekly 当前 UTC 周(周一至周日) 本周获得 XP
monthly 当前 UTC 月 本月获得 XP
tokens_shared 全部 累计转给他人代币总量
contributions 全部 combo 创建 + provider 使用 + skill 使用等贡献

events.ts 中可以看到每个事件如何落到这些范围:global/weekly/monthly 每次事件都增量更新,tokens_shared 仅在 token_share 动作时更新。

7.2 排名计算与轮转

  • updateScore(apiKeyId, scope, points):原子增量(委托 db/gamification.tsupdateScore);
  • getRank(apiKeyId, scope):查某 key 的排名;
  • getTopN(scope, limit, offset):Top-N 分页(默认 limit 50);
  • getNeighbors(apiKeyId, scope, radius):查询某用户上下的邻位条目,供前端展示“你离上一名还差多少”;
  • rotateScope("weekly" | "monthly"):委托 rotateLeaderboardScope() 执行归档与重置——在周期边界把旧条目复制到归档区并清空当前范围,周榜每周一 00:00 UTC 重置、月榜每月 1 日重置。该轮转在每次 updateScore 路径上检查,新周期的第一个请求触发。

7.3 SSE 实时推送

端点: stream/route.ts

Client → GET /api/gamification/stream
  → SSE 连接建立
  → 服务端立即推送 Top-10 快照
  → 数据变化时推送更新(仅推送有变化的范围)
  → 周期性心跳注释(": heartbeat\n\n")
  → 客户端断开 → 清理监听器

事件格式:

event: leaderboard
data: {"scope":"global","entries":[...]}

event: leaderboard
data: {"scope":"weekly","entries":[...]}

: heartbeat

SSE 管理器按范围跟踪已连接客户端,只有排行榜数据相对上次推送真正发生变化时才发送——避免无意义的重绘。

八、代币分享:双式记账

源码: src/lib/gamification/sharing.ts

8.1 记账约定

每笔转账在 token_ledger 中产生两行:

from_key_id to_key_id amount 含义
Send(流出) 发送方 接收方 +amount 发送方流出
Receive(流入) 接收方 发送方 +amount 接收方流入

余额由 db/gamification.tsgetBalance() 计算:SUM(to_key_id = ?) - SUM(from_key_id = ?),即流入减流出。

8.2 转账流程

transferTokens() 的契约:

export async function transferTokens(
  fromApiKeyId: string,
  toApiKeyId: string,
  amount: number,
  reason?: string,
  idempotencyKey?: string
): Promise<{ success: boolean; idempotencyKey: string; error?: string }>;
  1. 校验amount > 0fromApiKeyId !== toApiKeyId(自转直接返回 "Cannot transfer to yourself");
  2. 幂等idempotencyKey 缺省时用 crypto.randomUUID() 生成;同一 key 重复提交返回缓存结果而非二次记账;
  3. DB 层dbTransfer() 在单个 SQLite 事务内完成余额检查(余额不足则中止)与两行流水插入;
  4. 事件:转账完成后以 token_share 动作发出游戏化事件,触发 XP(1 分)与分享类徽章评估,并更新 tokens_shared 排行榜。

限流约束(文档定义):每分钟每 key 最多 10 笔转账、单笔上限 10,000 代币、每 key 每日转账上限 100,000 代币。

API 契约transfer/route.ts):

// POST /api/gamification/transfer — Request
{
  "to": "recipient-api-key-id",
  "amount": 500,
  "idempotencyKey": "uuid-v4"
}

// Response 200
{
  "success": true,
  "transfer": {
    "id": "txn-uuid",
    "from": "sender-api-key-id",
    "to": "recipient-api-key-id",
    "amount": 500,
    "createdAt": "2026-05-19T12:00:00.000Z"
  },
  "balance": 2500
}

// Response 400 (insufficient funds)
{
  "error": "Insufficient balance",
  "balance": 200,
  "requested": 500
}

九、邀请码与兑换

源码: src/lib/gamification/invites.ts(导出 createInvite / redeemInvite / listInvites / revokeInvite),DB 层为 createInviteToken / getInviteByCode / redeemInvite / revokeInvite

9.1 码与令牌的双轨设计

  • Code:8 位字母数字短码(如 A3K9-X7M2),人类可读,展示给用户;
  • Token:32 字节随机令牌,只存 SHA-256 哈希token_hash 列),用于程序化兑换(如 URL 链接)。
code A3K9X7M2(唯一、有索引)
token_hash SHA-256(raw_token)

原始令牌只在创建时一次性返回给用户,OmniRoute 永不存储或再次展示它——数据库中只留哈希。这使联邦与兑换的鉴权都无法从数据库反推原值。

9.2 自推荐(self-referral)防护

兑换时系统做两项交叉校验:

  1. 该码不属于兑换者自己的 api_key_id
  2. 兑换者未曾兑换过同一推荐者的任何码。

任一校验失败即以清晰错误信息拒绝。

9.3 有效期与次数

  • 默认 max_uses:10(创建时可配置);
  • 默认 expires_at:创建后 30 天;
  • 过期或次数耗尽的码返回 HTTP 410 Gone;
  • 兑换成功触发 invite_redeem 动作,奖励 50 XP(xp.tsinvite_redeem: 50)。

rotate/route.ts 提供 POST /api/gamification/rotate,用于批量轮换邀请令牌密钥。

十、社区服务器联邦

源码: src/lib/gamification/servers.ts(导出 connectServer / disconnectServer / listServers / syncLeaderboard / pushScore / healthCheck)。

10.1 连接流程

社区服务器通过远端实例签发的邀请令牌接入:

  1. 本地实例收到邀请令牌(例如粘贴到 Dashboard);
  2. 调用远端 POST /api/gamification/federation/leaderboard 验证令牌并拉取当前排行榜;
  3. status: connected 存入服务器记录(federation/leaderboard/route.ts 提供拉取端点,federation/score/route.ts 提供分数推送端点)。

10.2 覆盖式同步模型

联邦采用 overwrite sync(覆盖,而非累加):

Local Instance                Community Server
     │                              │
     ├── push score ───────────────►│  POST /federation/score
     │   { api_key_id, score }      │  (server validates token hash)
     │                              │
     ├── pull leaderboard ─────────►│  GET /federation/leaderboard
     │◄── top-N entries ────────────┤  (overwrites local cache)
     │                              │
     └── health check ─────────────►│  GET /federation/health
         (periodic, short timeout)  │

10.3 鉴权与健康监控

联邦请求头:

Authorization: Bearer <raw_token>
X-Federation-Version: 1

远端服务器对令牌做哈希后与 community_servers 表中的行匹配,避免传输已存储的哈希。每条服务器记录跟踪:

字段 说明
status connected / degraded / unreachable
last_sync 上次成功同步的 ISO 时间戳
failures 连续健康检查失败次数

连续失败达到阈值后状态转为 unreachable,同步暂停,直到手动健康检查成功。联邦鉴权行为有专门测试覆盖:federation-auth.test.ts

十一、反作弊:服务端计分、限流与审计

源码: src/lib/gamification/antiCheat.ts(导出 validateScoreChange / getAnomalies)。

11.1 服务端权威计分

所有 XP 计算在服务端 xp.ts / events.ts 中完成。客户端从不提交分数——它们提交动作,服务端计算 XP;leaderboard.score 只可被服务端代码写入。validateScoreChange() 在分数变更路径上执行校验。

11.2 限流

限制 取值 范围
每分钟最大 XP 1,000 每 API key
每分钟最大转账数 10 每 API key
单笔转账上限 10,000 每笔
每日转账上限 100,000 每 API key

限流采用内存滑动窗口(与 open-sse/services/RateLimitManager 同一模式),进程重启后回落到 SQLite 计数。

11.3 z-score 异常检测

对每个 API key 维护每小时 XP 的滚动 7 天窗口;每次授 XP 时:

  1. 计算该用户当前小时 XP 速率;
  2. 计算总体均值与标准差;
  3. z = (user_rate - mean) / stddev
  4. z > 3.0 即标记为异常。

异常写入 xp_audit_logaction = 'anomaly_detected'),管理员通过 GET /api/gamification/anomaliesanomalies/route.ts)与 getAnomalies() 查看。

11.4 审计轨迹

每次授 XP、转账、徽章解锁、异常检测都落 xp_audit_log

字段 说明
api_key_id
action 发生了什么(xp_award、transfer、anomaly…)
xp_awarded 数额(非 XP 事件为 0)
metadata JSON 上下文(动作类型、目标等)
created_at ISO 8601 时间戳

这张表同时是 streak 徽章(动作计数)的数据源——checkActionCountBadges() 直接对 xp_audit_logCOUNT(*)

十二、API 路由全景

所有路由遵循标准 OmniRoute 模式:Route → CORS preflight → Body validation (Zod) → Auth (extractApiKey) → Handler。当前仓库 src/app/api/gamification/ 下的实际路由:

方法 路径 说明 鉴权
GET /api/gamification/leaderboard 排行榜(scope、分页) 可选
POST /api/gamification/leaderboard 强制刷新排行榜缓存 必需
GET /api/gamification/stream SSE 实时排行榜 可选
GET /api/gamification/transfer 转账历史(分页) 必需
POST /api/gamification/transfer 向他人发送代币 必需
GET /api/gamification/invite 我的邀请码列表 必需
POST /api/gamification/invite 生成新邀请码 必需
DELETE /api/gamification/invite 吊销邀请码 必需
POST /api/gamification/invite/redeem 兑换邀请码 必需
GET/POST /api/gamification/badges 徽章定义 / 查询 按方法
GET /api/gamification/badges/earned 已解锁徽章 必需
GET /api/gamification/level XP、等级、头衔 必需
GET /api/gamification/servers 社区服务器列表 必需
POST /api/gamification/servers 连接社区服务器 必需
DELETE /api/gamification/servers 断开社区服务器 必需
POST /api/gamification/federation/score 向远端推送分数 联邦
GET /api/gamification/federation/leaderboard 从远端拉取排行榜 联邦
GET /api/gamification/notifications SSE 徽章/升级通知 必需
GET /api/gamification/anomalies 异常报告(管理员) 管理员
POST /api/gamification/rotate 轮换邀请令牌密钥 必需

排行榜响应示例(GET /api/gamification/leaderboard?scope=weekly&limit=10):

{
  "scope": "weekly",
  "period": "2026-W20",
  "entries": [
    {
      "rank": 1,
      "apiKeyId": "key-uuid",
      "displayName": "User***1234",
      "score": 15230,
      "level": 42,
      "title": "Expert"
    }
  ],
  "total": 847,
  "updatedAt": "2026-05-19T12:00:00.000Z"
}

十三、威胁模型与安全对策

威胁 缓解手段
分数虚增 仅服务端计算 XP;客户端提交动作而非分数
重放攻击 转账幂等键;审计日志去重
转账欺诈 双式记账;原子事务;限流
自推荐 兑换时交叉核对 api_key_id
排行榜操纵 z-score 异常检测;管理员异常面板
联邦令牌窃取 SHA-256 哈希存储;原始令牌仅展示一次
邀请码爆破 兑换端点限流;8 字符熵
显示名 XSS 显示名清洗;排行榜条目转义
哈希计时攻击 crypto.timingSafeEqual 比较令牌哈希

鉴权分层:公开(无鉴权)只有 GET /leaderboardGET /stream 两个只读端点;API key 必需覆盖所有写操作、个人资料、转账、邀请;管理员专属异常面板与审计日志;联邦走独立的 Authorization 原令牌鉴权路径,与存储的 SHA-256 哈希比对。

十四、测试体系

所有游戏化测试位于 tests/unit/gamification/,当前仓库实际包含的测试文件(比文档原表更完整):

测试文件 覆盖
xp.test.ts XP 曲线、累计阈值、等级反解、头衔/分层
badges.test.ts 徽章条件匹配与解锁
badge-unlock-once-3472.test.ts 回归:#3472 解锁事件只发一次
streaks.test.ts streak 逻辑、里程碑、边界情况
leaderboard.test.ts 排名计算、分页、范围轮转
leaderboard-limit-validation.test.ts limit 参数校验
sharing.test.ts 转账、余额、幂等
invites.test.ts 创建、兑换、过期、自推荐拦截
antiCheat.test.ts 限流、z-score、审计日志
events.test.ts 事件发射、扇出、错误隔离
db-gamification.test.ts DB 层 CRUD
federation-auth.test.ts 联邦令牌鉴权
aggregate-profile-3484.test.ts 聚合画像统计

运行方式:

# 全部游戏化测试
node --import tsx/esm --test tests/unit/gamification/*.test.ts

# 单文件
node --import tsx/esm --test tests/unit/gamification/xp.test.ts

覆盖要求(据 CONTRIBUTING.md):分支覆盖率 >= 80%;每个公开函数至少测一次;错误路径(余额不足、码过期、限流)必须覆盖——上表中的 sharing(余额不足/幂等)、invites(过期码)、antiCheat(限流)即对应这些要求。

十五、小结:可复用的设计要点

把文档骨架与当前仓库源码对照后,这套游戏化系统有五个可直接借鉴的工程决策:

  1. 单一入口 + 扇出emitGamificationEvent() 是管线与游戏化之间唯一的接触面,setImmediate + 吞错保证 LLM 热路径零影响;
  2. 纯函数计分核心xp.ts 无副作用,曲线、等级、头衔全部可独立单测;
  3. 复用既有基础设施:streak 借 key_value 表、限流沿用 RateLimitManager 模式、DB 单例继承 WAL 配置,避免为游戏化引入新依赖;
  4. 幂等性贯穿始终hasBadge() 防重复解锁、idempotency_key 防重复转账、审计表承担去重与徽章计数双重职责;
  5. 本地优先 + 可选联邦:默认单实例即可完整运行,联邦只是覆盖式缓存同步与一次性展示令牌的组合,不改变核心数据模型。

如需继续深入,建议按此顺序阅读:events.ts(扇出全貌)→ xp.ts(计分核心)→ db/gamification.ts(持久层契约)→ stream/route.ts(实时推送),再配合 tests/unit/gamification/ 下的对应测试验证行为边界。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388