游戏档案
在 /v1/games/** 上读档案、推档案建号、看来源标记与同步能力,以及第三方唯一能做的删除动作——提交申请。
一个 RankHub 用户可以绑定多个游戏的档案;每个档案属于一个用户 + 一个游戏档案 ID。
第三方应用读写档案的前提是拿到该用户的 access_token,并且令牌具备 game:read
(或 game:write,写覆盖读)。
档案接口在 /v1/games/** 上
旧文档里的 /v0/games/** 本期只服务 RankHub 自有客户端:第三方令牌打它会得到
403 game.firstPartyOnly。本页所有路径都是 /v1 形状(envelope + camelCase + 游标分页)。
支持的游戏
路径里的 {game} 是小写游戏键,不是控制器名(maimai 的路径是 maimai,不是 maimai-dx)。
| 游戏键 | 读 | 推档案建号 | 推成绩 | 删除申请 | 建档身份字段 |
|---|---|---|---|---|---|
maimai | ✅ | ✅ | ✅ | ✅ | friendCode 或 userId;评分由服务端算 |
arcaea | ✅ | ✅ | ✅ | ✅ | arcaeaId;评分由服务端算 |
phigros | ✅ | ✅ | ✅ | ✅ | objectId(+ 可选 shortId) |
milthm | ✅ | ✅ | ✅ | ✅ | milthmUserId + region |
notanote | ✅ | ✅ | ✅ | ✅ | notanoteUid |
musedash | ✅ | ✅ | ✅ | ✅ | musedashUid |
rizline | ✅ | ✅ | ✅ | ✅ | playerId |
osu | ✅ | ✅ | ✅ | ✅ | osuUserId |
chunithm | ✅ | ✅ | ✅ | ✅ | friendCode(街机好友码,不是 LXNS id) |
orzmic | ✅ | ✅ | ✅ | ✅ | externalId(本地游戏无官方账号体系) |
四列对 10 个游戏全部为真:读、建档、推成绩、申请删除都可用。
- 建档 = 填身份字段:不需要任何会话 / 玩家文件 / 验证码 / 快照,服务端也不会去上游拉取;
- 推成绩 = 填评分值:单曲评分由你在
scores[].rating里给;maimai/arcaea例外, 这两族服务端自己算分。
这张表只是今天的状态,权威是 GET /v1/games 的能力矩阵(见业务 API 总览)。
读取档案
# 能力矩阵:先看这个游戏支不支持写
curl -sS https://api.rankhub.cc/v1/games \
-H "Authorization: Bearer $ACCESS_TOKEN"
# 列表(游标分页,limit 最大 100)
curl -sS "https://api.rankhub.cc/v1/games/maimai/profiles?limit=50" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# 单个档案
curl -sS https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"列表只返回该用户在当前游戏下未解绑的档案,形状是
{ items, nextCursor, hasMore }。未绑定 / 不属于当前用户 / 已解绑的档案一律 404 common.notFound
(不区分"不存在"与"不是你的",避免被拿来探测)。
档案对象(各游戏一致的外壳 + 游戏原生字段进 player.source):
{
"profileId": "8d1f4a0e-6c2b-4a1e-9d3f-2b7c5e9a0f11",
"game": "maimai",
"player": {
"name": "Player",
"rating": 15234,
"source": { "course_rank": 0, "class_rank": 0, "star": 0 }
},
"isPublic": true,
"origin": "app",
"createdByClientId": "cli_7Q2…",
"syncCapable": false,
"lastSyncedAt": null,
"createdAt": "2026-10-21T18:40:00Z",
"updatedAt": "2026-10-21T18:40:00Z"
}| 字段 | 说明 |
|---|---|
profileId | 档案 ID(后续所有 /profiles/{profileId}/** 都用它) |
game | 游戏键 |
player.name / player.rating | 归一后的显示名与该游戏的"总分"(maimai 的 rating、arcaea 的 potential…);没有总分概念时为 null |
player.source | 游戏原生的玩家对象,未归一,仅供展示/调试;字段随游戏与版本增减 |
isPublic | 是否允许出现在公开 / 社交面 |
origin | user = 用户在 RankHub 自己建的;app = 第三方应用建的 |
createdByClientId | 建号应用的公开 client id(cli_…);origin: "user" 时为 null |
syncCapable | 该档案当前是否具备同步凭据(详见下节) |
lastSyncedAt | 最近一次从游戏平台同步的时间;从未同步过为 null |
createdAt / updatedAt | 服务端时间(RFC3339) |
null 不是 0
某游戏没有某个概念时字段是 null(例如没有总分概念的游戏 player.rating 为 null)。
请按 null 渲染占位,不要把它当 0 显示。
来源标记与同步能力
第三方建的档案会在下一次读取时露出两个字段:
origin: "app"+createdByClientId: 告诉用户与你自己"这张档案是谁建的";syncCapable: 该档案当前是否具备同步凭据。
syncCapable 与 origin 无关:它只看"账号级凭据是否在 RankHub 手里"。
- 建档不会写入任何凭据(你能填的只有身份字段与成绩)⇒ 你建的档案默认
syncCapable: false,此后只能靠你推成绩更新(POST .../{profileId}/scores,10 个游戏都支持); - 用户在 RankHub App 里绑定该游戏的账号 / 凭据之后,
syncCapable才变成true, 之后由 App 侧同步刷新 —— 那部分对你不可见,也不需要你参与; - 唯一的例外是"用户此前已经绑过该游戏的账号级凭据":这时你新建的档案可能直接
就是
true(同步能力是账号级的,不是这张档案的)。所以不要假设它一定为false,一律读字段。
各游戏看的是什么:
| 游戏 | syncCapable 的判据 |
|---|---|
arcaea / orzmic | 恒为 false(这两个上游没有可长期持有的同步凭据) |
phigros / musedash / rizline | 看档案行上的密文列(用户在 RankHub 绑定后写入) |
maimai / chunithm / osu / milthm / notanote | 看用户级 external account(绑定后写入) |
不要用 syncCapable 当写权限
syncCapable 只影响"能不能自动同步",不影响"能不能写":false 的档案在 10 个游戏里
都能推成绩(maimai / arcaea 也一样)。
推档案建号
10 个游戏都支持(能力矩阵 pushProfile 全为真)。建档只有一条口径:
调用方自己拿到玩家在该游戏里的身份与资料,直接把字段填进来 —— 服务端只落库,
不发起任何上游请求、不保存任何凭据。
所以这里没有"材料"这一说:你不需要(也不能)传会话 token、玩家文件、短信验证码或完整快照。
需要登录 / 验证码 / 二维码 / 机台中转的那些流程,属于 RankHub App 里的第一方票据式建档
(POST /v1/games/{game}/profiles/create),对你的应用仍然是 403 game.firstPartyOnly。
身份字段
player 是超集:字段按游戏生效,不适用字段被忽略;name 恒必填(展示名)。
每个游戏必须给出下表中的身份字段,缺失按该游戏既有语义返回 400:
| 游戏 | 身份字段(必填其一) |
|---|---|
maimai | friendCode 或 userId(机台侧 id);可选 courseRank / classRank / star |
arcaea | arcaeaId;可选 character / potential / avatar / nameplate |
phigros | objectId(可另给 shortId) |
milthm | milthmUserId + region(缺省 cn) |
notanote | notanoteUid |
musedash | musedashUid |
rizline | playerId |
osu | osuUserId |
chunithm | friendCode(❗ 街机侧身份就是好友码,不是 LXNS 的 id) |
orzmic | externalId(本地游戏没有官方账号体系,值由你与用户约定) |
player.rating 只是"总分提示":服务端会用同一请求里的 scores 重算(没有成绩则 0)。
返回的 player.rating 一律以存量为准,不要把你传进去的数字当成已生效。
例子
# maimai:身份是机台好友码;评分由服务端按成绩算
curl -sS -X POST https://api.rankhub.cc/v1/games/maimai/profiles \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"player": { "name": "Player", "friendCode": "1234567890123" },
"scores": [
{ "songId": 1234, "type": "dx", "levelIndex": 3, "achievements": 100.5125 }
],
"idempotencyKey": "b4c0f0d6-8f6f-4d6a-a0d3-3c6f0c4a11"
}'# phigros:身份是 objectId;单曲评分(rks)由你给
curl -sS -X POST https://api.rankhub.cc/v1/games/phigros/profiles \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"player": { "name": "Player", "objectId": "a1b2c3d4-…" },
"scores": [
{ "songId": "…", "difficulty": "IN", "score": 1000000, "accuracy": 100, "rating": 15.2 }
]
}'建档之后的这张档案
- 成功是
201+ 档案对象(data就是上面那张档案); - 建号会写
origin: "app"与createdByClientId,且不写入同步凭据 ⇒ 默认syncCapable: false(例外见「来源标记与同步能力」)。此后这张档案只能靠你推成绩更新; scores可选,对 10 个游戏都生效(与推成绩走同一套"只升不降"合并,见 成绩读取与写入),受单批条数上限约束(超限400 common.invalidArgument);idempotencyKey的作用域是「当前用户 + 当前应用」:重复提交返回同一张档案; 不传就没有幂等保护;- 重复建号返回
409:同一用户在同一游戏已有未解绑档案、或该身份已被占用时, 按该游戏既有唯一索引语义返回409 common.conflict(个别游戏沿用既有400; 文案也沿用既有服务,如… is already linked)。这不是"重试就能过"—— 正确做法是更新已有档案(推成绩),不要反复新建。
为什么不再需要「材料」
第三方能拿到的只有"你在自己一侧已经看到的数据"。任何需要替用户登录游戏才能拿到的东西 (会话 token、验证码、二维码、机台中转)都不属于开放平台——那类操作留在 RankHub App, 由用户自己在第一方完成。
删除档案:第三方只能申请
第三方应用不能直接删除档案。删除是"用户对自己数据的处置",必须用户本人在 RankHub 里点头。
直接对档案发 DELETE(不管 /v1 还是 /v0 的档案路径)会得到 403 game.deletionConsentRequired。
提交申请
curl -sS -X POST https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID/deletion-requests \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "reason": "用户在我们这里注销了" }'reason 可选(≤ 500 字)。成功是 202 + code: "common.accepted":
{
"code": "common.accepted",
"data": {
"requestId": "pdr_9f2c1e4a77b34d0e",
"status": "pending",
"approveUrl": "rankhub://profiles/deletion?token=pdr_tok_…",
"expiresAt": "2026-10-25T18:40:00Z"
}
}- 平台会给档案 owner 推一条通知,
approveUrl是那条通知里的 deep link(带一次性令牌); - 申请默认 72 小时过期,过期即作废(
status: "expired"),需要重新提交; - 幂等的是"申请",不是"令牌":同一档案 + 同一应用已有
pending申请时,再次 POST 仍返回同一条202,但approveUrl里的令牌会轮换(旧链接立即失效)。 这样重复申请不会堆出第二张待办,同时旧链接也不会长期可用。
查询状态
curl -sS https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID/deletion-requests/$REQUEST_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"code": "common.ok",
"data": {
"requestId": "pdr_9f2c1e4a77b34d0e",
"status": "pending",
"requestedAt": "2026-10-22T18:40:00Z",
"expiresAt": "2026-10-25T18:40:00Z",
"decidedAt": null
}
}status | 含义 |
|---|---|
pending | 等用户处理(可继续轮询;别把轮询做得比一分钟还密) |
approved | 用户同意,档案已软解绑(不再出现在列表里,历史成绩仍保留) |
denied | 用户拒绝,档案不动 |
expired | 超时未处理,作废;需要就重新提交 |
查询只能看到你自己发起的那一条:不存在的 requestId、别人的申请、以及别的应用对同一档案发的申请
一律 404 common.notFound(不区分,避免被拿来探测)。
没有集合查询(一次列出该档案所有申请):请自己保存 requestId。
用户侧的表态(第一方专用)
用户在 RankHub App 里点通知后,客户端会调
POST /v1/games/{game}/profiles/{profileId}/deletion-requests/{requestId}/decision,
body 是 { "decision": "approve" | "deny", "token": "pdr_tok_…" },返回 200 + 该申请的最新状态
(requestId / status / decidedAt 等)。
这条端点不对第三方开放:你的应用拿它去调会得到 403 game.firstPartyOnly,
而且这个限制不受灰度开关影响 —— "删除必须用户同意"是对外承诺,不是灰度里可以临时放宽的行为。
批准后执行的是与用户在 App 里直接删档案完全一致的软解绑。
第一方专用的建档 / 导入 / 同步
下面这些入口都需要用户的游戏平台凭据或设备环境,所以只对 RankHub 自有客户端开放。
第三方令牌调用会得到 403 game.firstPartyOnly(补 scope 也无效)。
⚠️ 注意区分两条看起来相近的路径:POST /v1/games/{game}/profiles(填身份字段,第三方可用)
与 POST /v1/games/{game}/profiles/create(票据式,要替用户登录游戏,第一方专用)。
| 形状 | 用途 |
|---|---|
POST /v1/games/{game}/profiles/create | 票据式建档(多步收集:扫码、验证码、所有权挑战…) |
POST /v1/games/{game}/profiles/import | 从游戏平台导入(异步任务) |
POST /v1/games/{game}/profiles/{profileId}/sync | 同步已有档案(异步任务) |
/v1/tasks/** | 任务查询 / 取消 / 重试 |
/v1/games/maimai/relay/** | maimai 机台中转(App 直连机台) |
各游戏的凭据形态(第三方拿不到,但你需要知道为什么拿不到,好在文档里向用户解释):
| 游戏 | 第一方要怎么拿到数据 |
|---|---|
maimai | 机台好友码 / userId,或经 App 的机台中转 |
arcaea | 好友码 + 所有权挑战 |
phigros | 二维码登录令牌 |
milthm | 玩家文件 v2 / 云端导入 |
musedash | 导入 + 验证码 |
notanote | 云端导入 |
osu | 官方 OAuth 导入 |
rizline | 手机号 + 验证码 |
chunithm | LXNS 导入 |
orzmic | 本地快照导入 |
给你的用户的正确说法是:"要自动同步,请在 RankHub 里绑定这个游戏", 而不是"让用户把密码/验证码填进我们的表单" —— 后者既不安全,平台也不会接受。
公开档案(社交面)
用户把档案设为公开后,其他登录用户可以在 RankHub 社交面读到同一份数据
(GET /v0/social/users/{uid}/game-profiles/{game} 等)。
社交面本期不开放给第三方
/v0/social/** 只服务第一方(谱面评论是唯一的过渡例外)。第三方令牌打这些路径会拿到
403 game.firstPartyOnly —— 你只能读授权用户本人的档案,读不到其他人的公开档案。
需要"看别人档案"的产品形态,本轮请不要基于社交面设计。
写操作会检查账号状态
新建 / 推成绩 / 申请删除都要求账号没有处于注销流程(accountDeletionRequestedAt
或 accountDeletedAt 非空 ⇒ 403)。这类失败不要重试,也不要循环刷 ——
注销中的账号,用户已经明确表示不再使用。