RankHub

API 目前处于 Early Preview 阶段, 可能存在诸多不完善与错误之处,欢迎加群交流反馈:加入官方交流群(1073434398)

概念:档案、成绩与最佳成绩桶

profile / score / bucket 三层概念,以及跨游戏归一化做到哪、没做到哪:rating 同名不同义、排序只在服务端、游标不透明、能力以 GET /v1/games 为准。

本页讲概念,不讲每个端点的字段细则(那些在游戏档案与成绩读取与写入)。

先记住一句话:平台只归一"形状"和"翻页/排序协议",不归一"数值语义"。 rating 在每个游戏里都叫 rating,但它是各游戏自己的分 —— 不能跨游戏比大小,也不能相加。

三层,各回答一个问题

层回答什么端点
profile(档案)这个用户在这个游戏上是谁GET /v1/games/{game}/profiles、.../{profileId}
score(成绩)一条成绩长什么样.../{profileId}/scores
bucket(最佳成绩桶)这个游戏怎么算"总力".../{profileId}/bests

三层都套在 v1 envelope 里({ code, message, description, data, requestId }),时间 RFC3339、字段 camelCase。

档案(profile)

一条档案 = 一个 RankHub 用户在一个游戏上的一张档案。它的 id 是 profileId(uuid), 这是 RankHub 的标识,游戏平台不认识它。

游戏原生标识放在 player.native 里(userId / friendCode / objectId / osuUserId 等), 两套东西没有换算关系。每个游戏的身份字段与必填组合见 怎么传数据(速查)。

一个用户在同一游戏上可能有多张档案(换过账号、或用不同标识各建过一张),所以列表接口才是列表。

每个游戏的原生身份字段(建档要填什么、哪些必填)见各游戏格式页的「身份字段」。

字段

{
  "profileId": "8d1f4a0e-6c2b-4a1e-9d3f-2b7c5e9a0f11",
  "game": "maimai",
  "player": { "name": "Player", "rating": 15234, "native": { "userId": 12345678 } },
  "isPublic": true,
  "origin": "app",
  "createdByClientId": "cli_7Q2…",
  "syncCapable": false,
  "lastSyncedAt": null,
  "createdAt": "2026-10-21T18:40:00Z",
  "updatedAt": "2026-10-21T18:40:00Z"
}
字段含义注意
player.name显示名没有就是 null
player.rating该游戏的"总分"各游戏语义不同(见下方边界一节)
player.native原生玩家对象只用于展示/调试,别从这里取归一字段
isPublic是否允许别人看这张档案不影响你自己读自己的档案
originuser = 用户自己建;app = 第三方应用建的——
createdByClientId建号应用的公开 client id(cli_…)origin: "user" 时为 null
syncCapable布尔,只读:该档案是否处于自动更新模式别拿它当写权限
lastSyncedAt时间戳,或 null只读

syncCapable 与 origin 无关,也不是写权限:它只影响"档案会不会自动更新"。 你建的档案 syncCapable 为 false,更新靠推成绩;取值一律读字段,不要假设。

「未解绑」与「删除申请」是两件事

未解绑删除申请
是什么档案的状态(已从 RankHub 解绑)一次待用户确认的动作
谁触发用户自己删、或用户批准删除申请后由平台执行应用只能申请;批准权在用户
效果档案从读接口消失,历史数据不丢本身不改数据;批准后才执行解绑

读接口对"不存在 / 不是你的 / 已解绑"统一返回 404 common.notFound,刻意不区分 —— 不要用 404 反推档案是被删了还是解绑了。

成绩(score)

一条成绩 = 一张谱面 + 一次(或最佳一次)游玩结果。平台把跨游戏共有的部分提成顶层字段, 游戏特异的部分一律留在 native:

字段含义谁有
scoreId成绩行 id全部
songId曲目 id(字符串)全部(osu 是谱面 id)
songName曲名大部分
type谱面类型(standard / dx / utage)仅 maimai
difficulty难度标识(字符串,各游戏自己的集合)arcaea / phigros / rizline / milthm / notanote / musedash / chunithm / orzmic
levelIndex难度下标maimai / arcaea / phigros / musedash
level难度标签(13+、IN…)maimai / musedash
achievements达成率 / 准确率大部分(口径随游戏)
score原始得分phigros / arcaea / osu / musedash / rizline / milthm / chunithm / orzmic
rating单曲 rating / potential / rks / pp全部(语义各不相同)
rank评级(sss / EX+ / S…)maimai / arcaea / phigros / osu / chunithm / orzmic
dxScore / dxStarmaimai 的 DX 分数 / DX 星级仅 maimai
fc / fs全连 / 全同步标记(ap/fc/fs…)maimai 两者都有;phigros / rizline / musedash 只有 fc 一侧
playTime游玩时间(RFC3339)maimai / arcaea / osu;其余 null
native游戏原生成绩对象(未归一)全部

逐游戏的「achievements / score / rating 各是什么、量纲多少、native 里有什么」, 见各游戏格式页(也是 GET /v1/games/{game}/format 的内容)。 本页只讲概念,不重复游戏细节。

null 表示「这个游戏没有这个概念」

type 只有 maimai 有、playTime 有的游戏没有。请按 null 渲染占位, 不要当 0 或空字符串处理。跨游戏渲染只用归一字段,游戏特异的地方读 native。

最佳成绩桶(bucket)

旧接口按游戏分成 best50 / best30 / b40 / best100 / best……现在一个端点、一个形状:

{
  "game": "maimai",
  "computedAt": "2026-10-24T02:10:00Z",
  "totals": { "total": 15234, "latest": 7234, "archive": 8000 },
  "buckets": [
    { "key": "latest",  "label": "当期", "total": 7234, "items": [ /* 与 scores 的 items 同形状 */ ] },
    { "key": "archive", "label": "往期", "total": 8000, "items": [ /* … */ ] }
  ]
}
字段含义纪律
totals该游戏的"总力"相关数字,至少含 total(没有总分概念的游戏可能没有它)键集合随游戏不同,服务端算
buckets[].key稳定的桶键按 key 渲染,不要按数组下标猜
buckets[].label展示名(中文,仅建议)你可以自己本地化
buckets[].total该桶合计(桶内各项 rating 之和;无该概念时 null)不是桶内 score 之和
buckets[].items桶内成绩,与 /scores 的 items 同形状——
computedAt本次计算时间曲库更新后历史档案的展示值会变,用它判新鲜度

各游戏的桶(由服务端切,客户端不要重算)

桶键、成员规则与 totals 语义逐游戏不同(maimai 按谱面版本切、phigros 有 ϕ 桶、 rizline 在曲库元数据缺失时会退化、osu 的 key 带模式…)。完整表见 各游戏格式页的「最佳成绩桶」,或 GET /v1/games/{game}/format 的 score.buckets;本页只保留下面三条通用纪律。

三条纪律

  1. 按 key 渲染,label 只作兜底;顺序也按 key,不要按数组位置。
  2. 未知 key 原样展示(不要 default: 丢弃):新游戏、新桶、osu 的模式桶都会新增 key,不会改变已有 key 的含义。
  3. 不要自己按分数重算:桶成员与 total 都是服务端给的。你算出来的数字一旦与平台不一致,用户会先怀疑你。

跨游戏归一化的边界

这一节是"别误用"的清单。

rating 同名,但语义各游戏独立

游戏rating 是什么单曲值谁算能跨游戏比吗
maimai单曲 DX rating(整数)平台(按定数 + 达成率算)不支持
arcaea单曲 potential(小数)平台(按定数 + 分数区间算)不支持
phigros单曲 rks(小数)你随成绩给不支持
milthm / notanote / musedash单曲 reality / nrk / performance你随成绩给不支持
rizline单曲 rks(与 phigros 不同源)你随成绩给不支持
osu单曲 pp你随成绩给不支持
chunithm / orzmic该游戏的 rating 原值随原始成绩对象给不支持

可以做:同一游戏内的排序与比较、同一游戏内的桶求和。 不要做:跨游戏的 rating 排行榜、跨游戏求和/平均、把不同游戏的 rating 放进同一个量纲。

想表达"玩家水平"跨游戏时,请用游戏内的相对位置(如段位、百分位)并明确标注是哪个游戏。

排序只在服务端

scores 的 sortBy 只有 playTime | achievements | rating 三键(order 为 asc/desc)。

  • 三项是"跨游戏同名"的最小交集,不是所有游戏都完整支持;值为 null 的条目不参与比较、排在最后;
  • 不传 sortBy ⇒ 用各游戏的原生默认顺序(不要假设它是 rating 降序);
  • 排序只作用于单张档案的成绩列表 —— 跨游戏混排没有服务端实现,也不要在客户端拼。

difficulty 只是字符串

difficulty 是各游戏原生难度标识的透传(maimai 的 3、arcaea 的 2、phigros 的 IN…), 平台不校验枚举:合法取值集合因游戏而异;传一个该游戏不存在的值通常是空列表,而不是 400。 (个别游戏原生不支持难度过滤,该参数会被忽略。)

分页是不透明游标

列表统一 { items, nextCursor, hasMore }:nextCursor 原样回传,不要解析、不要当 offset; 换 sortBy / order / difficulty 必须从第一页重来。limit 默认 50、上限 100(超限 400)。 参数细节见业务 API 总览 · 分页与批量上限。

没有"跨游戏总榜"

因为没有跨游戏可比的标尺:rating 量纲不同、achievements 口径不同(百分比 / 0~1 / 0~100)、 难度集合不同、曲库完全不同。任何跨游戏排名都必须先由你定义换算规则,并在 UI 上说明。

写入:两个形状,问的是不同问题

POST /v1/games/{game}/profilesPOST .../{profileId}/scores
做什么建一张档案(可带一批成绩)往已有档案推成绩
数据从哪来你直接把玩家的身份与资料填进来你把这一局的成绩填进来
成功201 + 档案对象200 + { rating, applied, ignored }
幂等idempotencyKey(作用域 = 当前用户 + 当前应用),重复提交返回同一张档案靠"只升不降"本身;重放不会改当前成绩
冲突该游戏身份字段已被别的档案占用 ⇒ 409档案不是本人/不存在 ⇒ 404

建号需要的是"身份字段"(同一请求体是超集,字段按游戏生效):具体字段与必填组合见 怎么传数据(速查);字段级定义见 游戏数据格式总览。

maimai / arcaea 沿用各自的既有建号语义,字段比其余游戏多:maimai 要 userId 或 friendCode, arcaea 还需要 character(搭档)非空。这两族以错误信息为准。

建档与更新是两条独立的路

"能建号"和"能自动更新"是两件互不相关的事。

你建档(POST /profiles)
  ├─ 只写身份字段 + name(+ 同请求里可带的 scores)
  ├─ syncCapable = false
  └─ 之后这条档案靠
       ① 你推成绩(POST .../{profileId}/scores)   ← 10 个游戏都能用
你想做的事该用哪条现在能不能
我有玩家在游戏里的身份,替他建一张档案POST /v1/games/{game}/profiles支持 10 个游戏
我算出了这批新成绩,帮他更新POST .../{profileId}/scores支持 10 个游戏(评分值自己给,见下)
让档案自动保持更新用户自己在 RankHub 里开启⛔ 不是应用能力

评分的两个来源

同一批字段名(scores[].rating),在两类游戏里含义完全不同:

maimai / arcaea其余 8 个游戏
单曲 rating平台算(有曲库定数与算分公式),你传了也无效你给:rks / reality / nrk / RL / rks / pp(本地游戏放在 scores[].native 里)
平台做什么算单曲评分 → 重算总分与桶只做"只升不降"合并 + 上界校验;不代算单曲评分
总分/桶平台算平台用你给的单曲值汇总
校验谱面级(曲库存在、物量、定数、组合自洽)通用级(标识非空、数值合理、时间不在未来、native 是对象)

三条纪律:

  1. 别指望平台帮你把评分算对:那 8 个游戏里评分是你给的事实输入,平台只挡畸形值;
  2. 本地游戏(chunithm / orzmic)用 scores[].native 带上原始成绩对象,并用 scores[].bucket 指定落到哪个桶(缺省 best30;chunithm 还可 selection10 / new20,写错桶名 ⇒ 400);
  3. 本地游戏不改"玩家头部":推成绩只写成绩与桶,player / summary 不在写侧。

写入语义(要点)

  • 只升不降:更差的提交不改当前成绩,进 ignored[];但每次提交都会追加成绩历史。
  • 异常写入要用户确认:低频异常 ⇒ 428 game.writeConfirmationRequired,用户确认后原样重放同一份 payload; 令牌过期 / payload 变了 ⇒ 409。批量异常会暂停应用写权限(403 game.writeSuspended)。
  • 删除只能申请:DELETE 直接删会 403 game.deletionConsentRequired;流程、状态与令牌规则见 游戏档案 · 删除档案。

400 / 422 / 428 / 429 的完整语义与重试规则见写入格式。

能力矩阵:以服务端为准

不要把你需要的游戏写死。第一步永远是拉能力矩阵:

curl -sS https://api.rankhub.cc/v1/games -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "code": "common.ok",
  "data": { "games": [
    { "game": "maimai",  "read": true, "pushProfile": true, "pushScores": true, "deletionRequest": true },
    { "game": "phigros", "read": true, "pushProfile": true, "pushScores": true, "deletionRequest": true }
  ] }
}
列含义
read可读档案、成绩、最佳成绩
pushProfile可建号(你填身份字段)
pushScores可逐条推成绩
deletionRequest可提交删除申请

三条纪律:

  1. 别抄成本地常量:能力会变,路径与形状不变;按 false 优雅降级(把入口藏起来);
  2. 每列背后做的事不同:pushProfile 是"填字段建档"(身份字段见上),pushScores 是"推成绩" (评分值口径见上);建档与推成绩是两个动作,建档后 syncCapable 仍是 false;
  3. 能力是"按游戏"的,不是"按档案"的:pushScores: true 仍可能 404(不是你的档案)或 403(停写)。

下一步

本页内容

官方交流群二维码官方交流群问题反馈 · 版本资讯 · 成绩交流群号 1073434398